diff options
622 files changed, 6550 insertions, 4518 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 0d1d0c36ce..8a0c55d5d4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -18,27 +18,27 @@ Style/BracesAroundHashParameters: Enabled: true # Align `when` with `case`. -Style/CaseIndentation: +Layout/CaseIndentation: Enabled: true # Align comments with method definitions. -Style/CommentIndentation: +Layout/CommentIndentation: Enabled: true # No extra empty lines. -Style/EmptyLines: +Layout/EmptyLines: Enabled: true # In a regular class definition, no empty lines around the body. -Style/EmptyLinesAroundClassBody: +Layout/EmptyLinesAroundClassBody: Enabled: true # In a regular method definition, no empty lines around the body. -Style/EmptyLinesAroundMethodBody: +Layout/EmptyLinesAroundMethodBody: Enabled: true # In a regular module definition, no empty lines around the body. -Style/EmptyLinesAroundModuleBody: +Layout/EmptyLinesAroundModuleBody: Enabled: true # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. @@ -47,30 +47,30 @@ Style/HashSyntax: # Method definitions after `private` or `protected` isolated calls need one # extra level of indentation. -Style/IndentationConsistency: +Layout/IndentationConsistency: Enabled: true EnforcedStyle: rails # Two spaces, no tabs (for indentation). -Style/IndentationWidth: +Layout/IndentationWidth: Enabled: true -Style/SpaceAfterColon: +Layout/SpaceAfterColon: Enabled: true -Style/SpaceAfterComma: +Layout/SpaceAfterComma: Enabled: true -Style/SpaceAroundEqualsInParameterDefault: +Layout/SpaceAroundEqualsInParameterDefault: Enabled: true -Style/SpaceAroundKeyword: +Layout/SpaceAroundKeyword: Enabled: true -Style/SpaceAroundOperators: +Layout/SpaceAroundOperators: Enabled: true -Style/SpaceBeforeFirstArg: +Layout/SpaceBeforeFirstArg: Enabled: true # Defining a method with parameters needs parentheses. @@ -78,18 +78,18 @@ Style/MethodDefParentheses: Enabled: true # Use `foo {}` not `foo{}`. -Style/SpaceBeforeBlockBraces: +Layout/SpaceBeforeBlockBraces: Enabled: true # Use `foo { bar }` not `foo {bar}`. -Style/SpaceInsideBlockBraces: +Layout/SpaceInsideBlockBraces: Enabled: true # Use `{ a: 1 }` not `{a:1}`. -Style/SpaceInsideHashLiteralBraces: +Layout/SpaceInsideHashLiteralBraces: Enabled: true -Style/SpaceInsideParens: +Layout/SpaceInsideParens: Enabled: true # Check quotes usage according to lint rule below. @@ -98,15 +98,15 @@ Style/StringLiterals: EnforcedStyle: double_quotes # Detect hard tabs, no hard tabs. -Style/Tab: +Layout/Tab: Enabled: true # Blank lines should not have any spaces. -Style/TrailingBlankLines: +Layout/TrailingBlankLines: Enabled: true # No trailing whitespace. -Style/TrailingWhitespace: +Layout/TrailingWhitespace: Enabled: true # Use quotes for string literals when they are enough. diff --git a/.travis.yml b/.travis.yml index 19fc899a8b..91ac7e8e5e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ cache: services: - memcached - - redis addons: postgresql: "9.4" @@ -34,6 +33,7 @@ before_script: # Decodes to e.g. `export VARIABLE=VALUE` - $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX0FDQ0VTU19LRVk9YTAzNTM0M2YtZTkyMi00MGIzLWFhM2MtMDZiM2VhNjM1YzQ4") - $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX1VTRVJOQU1FPXJ1YnlvbnJhaWxz") + - redis-server --bind 127.0.0.1 --port 6379 --requirepass 'password' --daemonize yes script: 'ci/travis.rb' @@ -65,25 +65,21 @@ matrix: env: "GEM=aj:integration" services: - memcached - - redis - rabbitmq - rvm: 2.3.4 env: "GEM=aj:integration" services: - memcached - - redis - rabbitmq - rvm: 2.4.1 env: "GEM=aj:integration" services: - memcached - - redis - rabbitmq - rvm: ruby-head env: "GEM=aj:integration" services: - memcached - - redis - rabbitmq - rvm: 2.3.4 env: @@ -98,17 +94,17 @@ matrix: - "GEM=ar:postgresql POSTGRES=9.2" addons: postgresql: "9.2" - - rvm: jruby-9.1.8.0 + - rvm: jruby-9.1.12.0 jdk: oraclejdk8 env: - "GEM=ap" - - rvm: jruby-9.1.8.0 + - rvm: jruby-9.1.12.0 jdk: oraclejdk8 env: - "GEM=am,amo,aj" allow_failures: - rvm: ruby-head - - rvm: jruby-9.1.8.0 + - rvm: jruby-9.1.12.0 - env: "GEM=ac:integration" fast_finish: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b44486c75a..097e2f2f49 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,6 @@ Changes that are cosmetic in nature and do not add anything substantial to the s * Please read [Contributing to the Rails Documentation](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation). -</br> Ruby on Rails is a volunteer effort. We encourage you to pitch in and [join the team](http://contributors.rubyonrails.org)! Thanks! :heart: :heart: :heart: @@ -16,7 +16,7 @@ gem "rake", ">= 11.1" # be loaded after loading the test library. gem "mocha", "~> 0.14", require: false -gem "capybara", "~> 2.13.0" +gem "capybara", "~> 2.13" gem "rack-cache", "~> 1.2" gem "jquery-rails" @@ -33,9 +33,6 @@ gem "bcrypt", "~> 3.1.11", require: false # sprockets. gem "uglifier", ">= 1.3.0", require: false -# FIXME: Remove this fork after https://github.com/nex3/rb-inotify/pull/49 is fixed. -gem "rb-inotify", github: "matthewd/rb-inotify", branch: "close-handling", require: false - # Explicitly avoid 1.x that doesn't support Ruby 2.4+ gem "json", ">= 2.0.0" @@ -90,7 +87,7 @@ group :cable do end # Add your own local bundler stuff. -local_gemfile = File.dirname(__FILE__) + "/.Gemfile" +local_gemfile = File.expand_path(".Gemfile", __dir__) instance_eval File.read local_gemfile if File.exist? local_gemfile group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 445bab1ca3..9f636b0307 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,14 +7,6 @@ GIT pg (>= 0.17, < 0.20) GIT - remote: https://github.com/matthewd/rb-inotify.git - revision: 90553518d1fb79aedc98a3036c59bd2b6731ac40 - branch: close-handling - specs: - rb-inotify (0.9.7) - ffi (>= 0.5.0) - -GIT remote: https://github.com/matthewd/websocket-client-simple.git revision: e161305f1a466b9398d86df3b1731b03362da91b branch: close-race @@ -25,7 +17,7 @@ GIT GIT remote: https://github.com/rails/arel.git - revision: 437aa3a4bb8ad4f3f4eba299dbb1112852f9c7ac + revision: 67a51c62f4e19390cd8eb408596ca48bb0806362 specs: arel (8.0.0) @@ -91,7 +83,7 @@ PATH GEM remote: https://rubygems.org/ specs: - addressable (2.5.0) + addressable (2.5.1) public_suffix (~> 2.0, >= 2.0.2) amq-protocol (2.1.0) ast (2.3.0) @@ -125,7 +117,7 @@ GEM bunny (2.6.2) amq-protocol (>= 2.0.1) byebug (9.0.6) - capybara (2.13.0) + capybara (2.14.1) addressable mime-types (>= 1.16) nokogiri (>= 1.3.3) @@ -148,10 +140,10 @@ GEM daemons (1.2.4) dalli (2.7.6) dante (0.2.0) - delayed_job (4.1.2) - activesupport (>= 3.0, < 5.1) - delayed_job_active_record (4.1.1) - activerecord (>= 3.0, < 5.1) + delayed_job (4.1.3) + activesupport (>= 3.0, < 5.2) + delayed_job_active_record (4.1.2) + activerecord (>= 3.0, < 5.2) delayed_job (>= 3.0, < 5) em-hiredis (0.3.1) eventmachine (~> 1.0) @@ -207,14 +199,14 @@ GEM ruby_dep (~> 1.2) loofah (2.0.3) nokogiri (>= 1.5.9) - mail (2.6.4) + mail (2.6.5) mime-types (>= 1.16, < 4) metaclass (0.0.4) method_source (0.8.2) mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) - mini_portile2 (2.1.0) + mini_portile2 (2.2.0) minitest (5.3.3) mocha (0.14.0) metaclass (~> 0.0.1) @@ -222,16 +214,17 @@ GEM multi_json (1.12.1) multipart-post (2.0.0) mustache (1.0.3) - mysql2 (0.4.5) - mysql2 (0.4.5-x64-mingw32) - mysql2 (0.4.5-x86-mingw32) + mysql2 (0.4.6) + mysql2 (0.4.6-x64-mingw32) + mysql2 (0.4.6-x86-mingw32) nio4r (2.0.0) - nokogiri (1.7.0.1) - mini_portile2 (~> 2.1.0) - nokogiri (1.7.0.1-x64-mingw32) - mini_portile2 (~> 2.1.0) - nokogiri (1.7.0.1-x86-mingw32) - mini_portile2 (~> 2.1.0) + nokogiri (1.8.0) + mini_portile2 (~> 2.2.0) + nokogiri (1.8.0-x64-mingw32) + mini_portile2 (~> 2.2.0) + nokogiri (1.8.0-x86-mingw32) + mini_portile2 (~> 2.2.0) + parallel (1.11.2) parser (2.4.0.0) ast (~> 2.2) pg (0.19.0) @@ -249,10 +242,10 @@ GEM simple_uuid que (0.12.0) racc (1.4.14) - rack (2.0.1) + rack (2.0.3) rack-cache (1.6.1) rack (>= 0.4) - rack-protection (1.5.3) + rack-protection (2.0.0) rack rack-test (0.6.3) rack (>= 1.0) @@ -261,12 +254,15 @@ GEM nokogiri (~> 1.6) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - rainbow (2.2.1) + rainbow (2.2.2) + rake rake (12.0.0) rb-fsevent (0.9.8) + rb-inotify (0.9.9) + ffi (~> 1.0) rdoc (5.1.0) redcarpet (3.2.3) - redis (3.3.2) + redis (3.3.3) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) resque (1.27.0) @@ -280,7 +276,8 @@ GEM redis (~> 3.3) resque (~> 1.26) rufus-scheduler (~> 3.2) - rubocop (0.48.1) + rubocop (0.49.1) + parallel (~> 1.10) parser (>= 2.3.3.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) @@ -307,11 +304,11 @@ GEM sequel (4.42.1) serverengine (1.5.11) sigdump (~> 0.2.2) - sidekiq (4.2.9) + sidekiq (5.0.0) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) - redis (~> 3.2, >= 3.2.1) + redis (~> 3.3, >= 3.3.3) sigdump (0.2.4) simple_uuid (0.4.0) sinatra (1.0) @@ -364,7 +361,7 @@ GEM websocket-driver (0.6.4) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) - xpath (2.0.0) + xpath (2.1.0) nokogiri (~> 1.3) PLATFORMS @@ -383,7 +380,7 @@ DEPENDENCIES blade blade-sauce_labs_plugin byebug - capybara (~> 2.13.0) + capybara (~> 2.13) coffee-rails dalli (>= 2.2.1) delayed_job @@ -410,7 +407,6 @@ DEPENDENCIES rack-cache (~> 1.2) rails! rake (>= 11.1) - rb-inotify! redcarpet (~> 3.2.3) redis resque @@ -433,4 +429,4 @@ DEPENDENCIES websocket-client-simple! BUNDLED WITH - 1.14.6 + 1.15.1 @@ -1,6 +1,6 @@ require "net/http" -$:.unshift File.expand_path("..", __FILE__) +$:.unshift __dir__ require "tasks/release" require "railties/lib/rails/api/task" diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index f0502c06ff..b1408496a0 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,7 +1,17 @@ +* ActionCable's `redis` adapter allows for other common redis-rb options (`host`, `port`, `db`, `password`) in cable.yml. + + Previously, it accepts only a [redis:// url](https://www.iana.org/assignments/uri-schemes/prov/redis) as an option. + While we can add all of these options to the `url` itself, it is not explicitly documented. This alternative setup + is shown as the first example in the [Redis rubygem](https://github.com/redis/redis-rb#getting-started), which + makes this set of options as sensible as using just the `url`. + + *Marc Rendl Ignacio* + * ActionCable socket errors are now logged to the console Previously any socket errors were ignored and this made it hard to diagnose socket issues (e.g. as discussed in #28362). *Edward Poot* - + + Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actioncable/CHANGELOG.md) for previous changes. diff --git a/actioncable/README.md b/actioncable/README.md index e044f54b45..6946dbefb0 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -409,7 +409,7 @@ application. The recommended basic setup is as follows: ```ruby # cable/config.ru -require ::File.expand_path('../../config/environment', __FILE__) +require_relative '../config/environment' Rails.application.eager_load! run ActionCable.server diff --git a/actioncable/Rakefile b/actioncable/Rakefile index bda8c7b6c8..e21843bb44 100644 --- a/actioncable/Rakefile +++ b/actioncable/Rakefile @@ -3,15 +3,13 @@ require "pathname" require "open3" require "action_cable" -dir = File.dirname(__FILE__) - task default: :test task package: %w( assets:compile assets:verify ) Rake::TestTask.new do |t| t.libs << "test" - t.test_files = Dir.glob("#{dir}/test/**/*_test.rb") + t.test_files = Dir.glob("#{__dir__}/test/**/*_test.rb") t.warning = true t.verbose = true t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) @@ -46,7 +44,7 @@ namespace :assets do desc "Verify compiled Action Cable assets" task :verify do file = "lib/assets/compiled/action_cable.js" - pathname = Pathname.new("#{dir}/#{file}") + pathname = Pathname.new("#{__dir__}/#{file}") print "[verify] #{file} exists " if pathname.exist? @@ -64,8 +62,8 @@ namespace :assets do fail end - print "[verify] #{dir} can be required as a module " - stdout, stderr, status = Open3.capture3("node", "--print", "window = {}; require('#{dir}');") + print "[verify] #{__dir__} can be required as a module " + _, stderr, status = Open3.capture3("node", "--print", "window = {}; require('#{__dir__}');") if status.success? puts "[OK]" else diff --git a/actioncable/actioncable.gemspec b/actioncable/actioncable.gemspec index 6d95f022fa..a72c0d2608 100644 --- a/actioncable/actioncable.gemspec +++ b/actioncable/actioncable.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -18,6 +18,11 @@ Gem::Specification.new do |s| s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*"] s.require_path = "lib" + s.metadata = { + "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actioncable", + "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actioncable/CHANGELOG.md" + } + s.add_dependency "actionpack", version s.add_dependency "nio4r", "~> 2.0" diff --git a/actioncable/bin/test b/actioncable/bin/test index a7beb14b27..470ce93f10 100755 --- a/actioncable/bin/test +++ b/actioncable/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/actioncable/lib/action_cable/channel/periodic_timers.rb b/actioncable/lib/action_cable/channel/periodic_timers.rb index c9daa0bcd3..90c68cfe84 100644 --- a/actioncable/lib/action_cable/channel/periodic_timers.rb +++ b/actioncable/lib/action_cable/channel/periodic_timers.rb @@ -4,8 +4,7 @@ module ActionCable extend ActiveSupport::Concern included do - class_attribute :periodic_timers, instance_reader: false - self.periodic_timers = [] + class_attribute :periodic_timers, instance_reader: false, default: [] after_subscribe :start_periodic_timers after_unsubscribe :stop_periodic_timers diff --git a/actioncable/lib/action_cable/connection/identification.rb b/actioncable/lib/action_cable/connection/identification.rb index c91a1d1fd7..ffab359429 100644 --- a/actioncable/lib/action_cable/connection/identification.rb +++ b/actioncable/lib/action_cable/connection/identification.rb @@ -6,8 +6,7 @@ module ActionCable extend ActiveSupport::Concern included do - class_attribute :identifiers - self.identifiers = Set.new + class_attribute :identifiers, default: Set.new end class_methods do diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb index 03eb6e2ea8..27ae499f29 100644 --- a/actioncable/lib/action_cable/connection/web_socket.rb +++ b/actioncable/lib/action_cable/connection/web_socket.rb @@ -3,7 +3,7 @@ require "websocket/driver" module ActionCable module Connection # Wrap the real socket to minimize the externally-presented API - class WebSocket + class WebSocket # :nodoc: def initialize(env, event_target, event_loop, protocols: ActionCable::INTERNAL[:protocols]) @websocket = ::WebSocket::Driver.websocket?(env) ? ClientSocket.new(env, event_target, event_loop, protocols) : nil end diff --git a/actioncable/lib/action_cable/remote_connections.rb b/actioncable/lib/action_cable/remote_connections.rb index d2856bc6ae..e689fbf21b 100644 --- a/actioncable/lib/action_cable/remote_connections.rb +++ b/actioncable/lib/action_cable/remote_connections.rb @@ -45,7 +45,7 @@ module ActionCable end # Returns all the identifiers that were applied to this connection. - def identifiers + redefine_method :identifiers do server.connection_identifiers end diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb index 419eccd73c..3b3a17a532 100644 --- a/actioncable/lib/action_cable/server/base.rb +++ b/actioncable/lib/action_cable/server/base.rb @@ -10,7 +10,7 @@ module ActionCable include ActionCable::Server::Broadcasting include ActionCable::Server::Connections - cattr_accessor(:config, instance_accessor: true) { ActionCable::Server::Configuration.new } + cattr_accessor :config, instance_accessor: true, default: ActionCable::Server::Configuration.new def self.logger; config.logger; end delegate :logger, to: :config diff --git a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb index ed8f315791..ae71708240 100644 --- a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb +++ b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb @@ -17,11 +17,11 @@ module ActionCable # Overwrite this factory method for EventMachine Redis connections if you want to use a different Redis connection library than EM::Hiredis. # This is needed, for example, when using Makara proxies for distributed Redis. - cattr_accessor(:em_redis_connector) { ->(config) { EM::Hiredis.connect(config[:url]) } } + cattr_accessor :em_redis_connector, default: ->(config) { EM::Hiredis.connect(config[:url]) } # Overwrite this factory method for Redis connections if you want to use a different Redis connection library than Redis. # This is needed, for example, when using Makara proxies for distributed Redis. - cattr_accessor(:redis_connector) { ->(config) { ::Redis.new(url: config[:url]) } } + cattr_accessor :redis_connector, default: ->(config) { ::Redis.new(url: config[:url]) } def initialize(*) ActiveSupport::Deprecation.warn(<<-MSG.squish) diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb index 41a6e55822..225609c236 100644 --- a/actioncable/lib/action_cable/subscription_adapter/redis.rb +++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb @@ -10,7 +10,9 @@ module ActionCable # Overwrite this factory method for redis connections if you want to use a different Redis library than Redis. # This is needed, for example, when using Makara proxies for distributed Redis. - cattr_accessor(:redis_connector) { ->(config) { ::Redis.new(url: config[:url]) } } + cattr_accessor :redis_connector, default: ->(config) do + ::Redis.new(config.slice(:url, :host, :port, :db, :password)) + end def initialize(*) super diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb index 984b78bc9c..80f512c94c 100644 --- a/actioncable/lib/rails/generators/channel/channel_generator.rb +++ b/actioncable/lib/rails/generators/channel/channel_generator.rb @@ -1,7 +1,7 @@ module Rails module Generators class ChannelGenerator < NamedBase - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) argument :actions, type: :array, default: [], banner: "method method" diff --git a/actioncable/test/subscription_adapter/async_test.rb b/actioncable/test/subscription_adapter/async_test.rb index 7bc2e55d40..8a447c7a4f 100644 --- a/actioncable/test/subscription_adapter/async_test.rb +++ b/actioncable/test/subscription_adapter/async_test.rb @@ -1,5 +1,5 @@ require "test_helper" -require_relative "./common" +require_relative "common" class AsyncAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest diff --git a/actioncable/test/subscription_adapter/evented_redis_test.rb b/actioncable/test/subscription_adapter/evented_redis_test.rb index 256458bc24..1c99031ab0 100644 --- a/actioncable/test/subscription_adapter/evented_redis_test.rb +++ b/actioncable/test/subscription_adapter/evented_redis_test.rb @@ -1,6 +1,6 @@ require "test_helper" -require_relative "./common" -require_relative "./channel_prefix" +require_relative "common" +require_relative "channel_prefix" class EventedRedisAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest @@ -54,6 +54,6 @@ class EventedRedisAdapterTest < ActionCable::TestCase end def cable_config - { adapter: "evented_redis", url: "redis://127.0.0.1:6379/12" } + { adapter: "evented_redis", url: "redis://:password@127.0.0.1:6379/12" } end end diff --git a/actioncable/test/subscription_adapter/inline_test.rb b/actioncable/test/subscription_adapter/inline_test.rb index 52bfa00aba..eafa3df2df 100644 --- a/actioncable/test/subscription_adapter/inline_test.rb +++ b/actioncable/test/subscription_adapter/inline_test.rb @@ -1,5 +1,5 @@ require "test_helper" -require_relative "./common" +require_relative "common" class InlineAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest diff --git a/actioncable/test/subscription_adapter/postgresql_test.rb b/actioncable/test/subscription_adapter/postgresql_test.rb index beb6efab28..ada4c2e2bd 100644 --- a/actioncable/test/subscription_adapter/postgresql_test.rb +++ b/actioncable/test/subscription_adapter/postgresql_test.rb @@ -1,5 +1,5 @@ require "test_helper" -require_relative "./common" +require_relative "common" require "active_record" diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb index 4df5e0cbcd..60596dd205 100644 --- a/actioncable/test/subscription_adapter/redis_test.rb +++ b/actioncable/test/subscription_adapter/redis_test.rb @@ -1,13 +1,13 @@ require "test_helper" -require_relative "./common" -require_relative "./channel_prefix" +require_relative "common" +require_relative "channel_prefix" class RedisAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest include ChannelPrefixTest def cable_config - { adapter: "redis", driver: "ruby", url: "redis://127.0.0.1:6379/12" } + { adapter: "redis", driver: "ruby", url: "redis://:password@127.0.0.1:6379/12" } end end @@ -16,3 +16,11 @@ class RedisAdapterTest::Hiredis < RedisAdapterTest super.merge(driver: "hiredis") end end + +class RedisAdapterTest::AlternateConfiguration < RedisAdapterTest + def cable_config + alt_cable_config = super.dup + alt_cable_config.delete(:url) + alt_cable_config.merge(host: "127.0.0.1", port: 6379, db: 12, password: "password") + end +end diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb index a47032753b..5d246c2b76 100644 --- a/actioncable/test/test_helper.rb +++ b/actioncable/test/test_helper.rb @@ -11,7 +11,7 @@ rescue LoadError end # Require all the stubs and models -Dir[File.dirname(__FILE__) + "/stubs/*.rb"].each { |file| require file } +Dir[File.expand_path("stubs/*.rb", __dir__)].each { |file| require file } class ActionCable::TestCase < ActiveSupport::TestCase def wait_for_async diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index e488d867de..9993a11c9d 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1 +1,12 @@ +* Allow Action Mailer classes to configure their delivery job. + + class MyMailer < ApplicationMailer + self.delivery_job = MyCustomDeliveryJob + + ... + end + + *Matthew Mongeau* + + Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionmailer/CHANGELOG.md) for previous changes. diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index e75dae6cf9..ae908ddda7 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -19,6 +19,11 @@ Gem::Specification.new do |s| s.require_path = "lib" s.requirements << "none" + s.metadata = { + "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actionmailer", + "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionmailer/CHANGELOG.md" + } + s.add_dependency "actionpack", version s.add_dependency "actionview", version s.add_dependency "activejob", version diff --git a/actionmailer/bin/test b/actionmailer/bin/test index a7beb14b27..470ce93f10 100755 --- a/actionmailer/bin/test +++ b/actionmailer/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 211190560a..8e59f033d0 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -25,6 +25,7 @@ require "abstract_controller" require "action_mailer/version" # Common Active Support usage in Action Mailer +require "active_support" require "active_support/rails" require "active_support/core_ext/class" require "active_support/core_ext/module/attr_internal" diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 6849f5c0f9..f8aa54bd44 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -459,8 +459,8 @@ module ActionMailer helper ActionMailer::MailHelper - class_attribute :default_params - self.default_params = { + class_attribute :delivery_job, default: ::ActionMailer::DeliveryJob + class_attribute :default_params, default: { mime_version: "1.0", charset: "UTF-8", content_type: "text/plain", diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb index bcc4ef03cf..93ae10fd70 100644 --- a/actionmailer/lib/action_mailer/delivery_methods.rb +++ b/actionmailer/lib/action_mailer/delivery_methods.rb @@ -7,20 +7,13 @@ module ActionMailer extend ActiveSupport::Concern included do - class_attribute :delivery_methods, :delivery_method - # Do not make this inheritable, because we always want it to propagate - cattr_accessor :raise_delivery_errors - self.raise_delivery_errors = true - - cattr_accessor :perform_deliveries - self.perform_deliveries = true - - cattr_accessor :deliver_later_queue_name - self.deliver_later_queue_name = :mailers + cattr_accessor :raise_delivery_errors, default: true + cattr_accessor :perform_deliveries, default: true + cattr_accessor :deliver_later_queue_name, default: :mailers - self.delivery_methods = {}.freeze - self.delivery_method = :smtp + class_attribute :delivery_methods, default: {}.freeze + class_attribute :delivery_method, default: :smtp add_delivery_method :smtp, Mail::SMTP, address: "localhost", diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb index cf7c57e6bf..0b54e12431 100644 --- a/actionmailer/lib/action_mailer/message_delivery.rb +++ b/actionmailer/lib/action_mailer/message_delivery.rb @@ -51,6 +51,14 @@ module ActionMailer # Notifier.welcome(User.first).deliver_later!(wait: 1.hour) # Notifier.welcome(User.first).deliver_later!(wait_until: 10.hours.from_now) # + # By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each + # <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable + # +delivery_job+. + # + # class AccountRegistrationMailer < ApplicationMailer + # self.delivery_job = RegistrationDeliveryJob + # end + # # Options: # # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay @@ -67,6 +75,14 @@ module ActionMailer # Notifier.welcome(User.first).deliver_later(wait: 1.hour) # Notifier.welcome(User.first).deliver_later(wait_until: 10.hours.from_now) # + # By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each + # <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable + # +delivery_job+. + # + # class AccountRegistrationMailer < ApplicationMailer + # self.delivery_job = RegistrationDeliveryJob + # end + # # Options: # # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay. @@ -118,7 +134,8 @@ module ActionMailer "method*, or 3. use a custom Active Job instead of #deliver_later." else args = @mailer_class.name, @action.to_s, delivery_method.to_s, *@args - ::ActionMailer::DeliveryJob.set(options).perform_later(*args) + job = @mailer_class.delivery_job + job.set(options).perform_later(*args) end end end diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb index 87ba743f3d..4f72eca930 100644 --- a/actionmailer/lib/action_mailer/preview.rb +++ b/actionmailer/lib/action_mailer/preview.rb @@ -20,8 +20,7 @@ module ActionMailer mattr_accessor :show_previews, instance_writer: false # :nodoc: - mattr_accessor :preview_interceptors, instance_writer: false - self.preview_interceptors = [ActionMailer::InlinePreviewInterceptor] + mattr_accessor :preview_interceptors, instance_writer: false, default: [ActionMailer::InlinePreviewInterceptor] end module ClassMethods diff --git a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb index 99fe4544f1..bc21b07109 100644 --- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb +++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb @@ -1,7 +1,7 @@ module Rails module Generators class MailerGenerator < NamedBase - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) argument :actions, type: :array, default: [], banner: "method method" diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index a646cbd581..dbfdb07e6e 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -9,7 +9,7 @@ end module Rails def self.root - File.expand_path("../", File.dirname(__FILE__)) + File.expand_path("..", __dir__) end end @@ -28,7 +28,7 @@ ActiveSupport::Deprecation.debug = true # Disable available locale checks to avoid warnings running the test suite. I18n.enforce_available_locales = false -FIXTURE_LOAD_PATH = File.expand_path("fixtures", File.dirname(__FILE__)) +FIXTURE_LOAD_PATH = File.expand_path("fixtures", __dir__) ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH class ActiveSupport::TestCase diff --git a/actionmailer/test/caching_test.rb b/actionmailer/test/caching_test.rb index cff49c8894..e76466439e 100644 --- a/actionmailer/test/caching_test.rb +++ b/actionmailer/test/caching_test.rb @@ -5,7 +5,7 @@ require "mailers/caching_mailer" CACHE_DIR = "test_cache" # Don't change '/../temp/' cavalierly or you might hose something you don't want hosed -FILE_STORE_PATH = File.join(File.dirname(__FILE__), "/../temp/", CACHE_DIR) +FILE_STORE_PATH = File.join(__dir__, "/../temp/", CACHE_DIR) class FragmentCachingMailer < ActionMailer::Base abstract! @@ -21,10 +21,6 @@ class BaseCachingTest < ActiveSupport::TestCase @mailer.perform_caching = true @mailer.cache_store = @store end - - def test_fragment_cache_key - assert_equal "views/what a key", @mailer.fragment_cache_key("what a key") - end end class FragmentCachingTest < BaseCachingTest @@ -126,7 +122,7 @@ class FunctionalFragmentCachingTest < BaseCachingTest assert_match expected_body, email.body.encoded assert_match expected_body, - @store.read("views/caching/#{template_digest("caching_mailer/fragment_cache")}") + @store.read("views/caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache")}/caching") end def test_fragment_caching_in_partials @@ -135,7 +131,7 @@ class FunctionalFragmentCachingTest < BaseCachingTest assert_match(expected_body, email.body.encoded) assert_match(expected_body, - @store.read("views/caching/#{template_digest("caching_mailer/_partial")}")) + @store.read("views/caching_mailer/_partial:#{template_digest("caching_mailer/_partial")}/caching")) end def test_skip_fragment_cache_digesting @@ -185,7 +181,7 @@ class FunctionalFragmentCachingTest < BaseCachingTest end assert_equal "caching_mailer", payload[:mailer] - assert_equal "views/caching/#{template_digest("caching_mailer/fragment_cache")}", payload[:key] + assert_equal [ :views, "caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache")}", :caching ], payload[:key] ensure @mailer.enable_fragment_cache_logging = true end diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb index d864c3acca..799c6144d7 100644 --- a/actionmailer/test/log_subscriber_test.rb +++ b/actionmailer/test/log_subscriber_test.rb @@ -26,7 +26,7 @@ class AMLogSubscriberTest < ActionMailer::TestCase wait assert_equal(1, @logger.logged(:info).size) - assert_match(/Sent mail to system@test.lindsaar.net/, @logger.logged(:info).first) + assert_match(/Sent mail to system@test\.lindsaar\.net/, @logger.logged(:info).first) assert_equal(2, @logger.logged(:debug).size) assert_match(/BaseMailer#welcome: processed outbound mail in [\d.]+ms/, @logger.logged(:debug).first) @@ -36,7 +36,7 @@ class AMLogSubscriberTest < ActionMailer::TestCase end def test_receive_is_notified - fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email") + fixture = File.read(File.expand_path("fixtures/raw_email", __dir__)) TestMailer.receive(fixture) wait assert_equal(1, @logger.logged(:info).size) diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb index c0683be94d..51f10b0bf1 100644 --- a/actionmailer/test/message_delivery_test.rb +++ b/actionmailer/test/message_delivery_test.rb @@ -95,6 +95,19 @@ class MessageDeliveryTest < ActiveSupport::TestCase end end + test "should enqueue the job with the correct delivery job" do + old_delivery_job = DelayedMailer.delivery_job + DelayedMailer.delivery_job = DummyJob + + assert_performed_with(job: DummyJob, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3]) do + @mail.deliver_later + end + + DelayedMailer.delivery_job = old_delivery_job + end + + class DummyJob < ActionMailer::DeliveryJob; end + test "can override the queue when enqueuing mail" do assert_performed_with(job: ActionMailer::DeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3], queue: "another_queue") do @mail.deliver_later(queue: :another_queue) diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 51cec82801..d3d3188d95 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,4 +1,37 @@ -* Add `action_controller_api` and `action_controller_base` load hooks to be called in `ActiveSupport.on_load` +* `driven_by` now registers poltergeist and capybara-webkit + + If driver poltergeist or capybara-webkit is set for System Tests, + `driven_by` will register the driver and set additional options passed via + `:options` param. + + Refer to drivers documentation to learn what options can be passed. + + *Mario Chavez* + +* AEAD encrypted cookies and sessions with GCM + + Encrypted cookies now use AES-GCM which couples authentication and + encryption in one faster step and produces shorter ciphertexts. Cookies + encrypted using AES in CBC HMAC mode will be seamlessly upgraded when + this new mode is enabled via the + `action_dispatch.use_authenticated_cookie_encryption` configuration value. + + *Michael J Coyne* + +* Change the cache key format for fragments to make it easier to debug key churn. The new format is: + + views/template/action.html.erb:7a1156131a6928cb0026877f8b749ac9/projects/123 + ^template path ^template tree digest ^class ^id + + *DHH* + +* Add support for recyclable cache keys with fragment caching. This uses the new versioned entries in the + `ActiveSupport::Cache` stores and relies on the fact that Active Record has split `#cache_key` and `#cache_version` + to support it. + + *DHH* + +* Add `action_controller_api` and `action_controller_base` load hooks to be called in `ActiveSupport.on_load` `ActionController::Base` and `ActionController::API` have differing implementations. This means that the one umbrella hook `action_controller` is not able to address certain situations where a method @@ -10,4 +43,5 @@ *Julian Nadeau* + Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionpack/CHANGELOG.md) for previous changes. diff --git a/actionpack/Rakefile b/actionpack/Rakefile index 31dd1865f9..69408c8aab 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -26,7 +26,7 @@ namespace :test do end task :lines do - load File.expand_path("..", File.dirname(__FILE__)) + "/tools/line_statistics" + load File.expand_path("..", __dir__) + "/tools/line_statistics" files = FileList["lib/**/*.rb"] CodeTools::LineStatistics.new(files).print_loc end diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 2c24a54305..294cc45593 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -19,6 +19,11 @@ Gem::Specification.new do |s| s.require_path = "lib" s.requirements << "none" + s.metadata = { + "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actionpack", + "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionpack/CHANGELOG.md" + } + s.add_dependency "activesupport", version s.add_dependency "rack", "~> 2.0" diff --git a/actionpack/bin/test b/actionpack/bin/test index a7beb14b27..470ce93f10 100755 --- a/actionpack/bin/test +++ b/actionpack/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index e7cb6347a2..dc79820a82 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -14,8 +14,16 @@ module AbstractController # expected to provide their own +render+ method, since rendering means # different things depending on the context. class Base + ## + # Returns the body of the HTTP response sent by the controller. attr_internal :response_body + + ## + # Returns the name of the action this controller is processing. attr_internal :action_name + + ## + # Returns the formats that can be processed by the controller. attr_internal :formats include ActiveSupport::Configurable diff --git a/actionpack/lib/abstract_controller/caching.rb b/actionpack/lib/abstract_controller/caching.rb index 26e3f08bc1..30e3d4426c 100644 --- a/actionpack/lib/abstract_controller/caching.rb +++ b/actionpack/lib/abstract_controller/caching.rb @@ -37,8 +37,7 @@ module AbstractController config_accessor :enable_fragment_cache_logging self.enable_fragment_cache_logging = false - class_attribute :_view_cache_dependencies - self._view_cache_dependencies = [] + class_attribute :_view_cache_dependencies, default: [] helper_method :view_cache_dependencies if respond_to?(:helper_method) end diff --git a/actionpack/lib/abstract_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb index c85b4adba1..14e4a82523 100644 --- a/actionpack/lib/abstract_controller/caching/fragments.rb +++ b/actionpack/lib/abstract_controller/caching/fragments.rb @@ -25,7 +25,10 @@ module AbstractController self.fragment_cache_keys = [] - helper_method :fragment_cache_key if respond_to?(:helper_method) + if respond_to?(:helper_method) + helper_method :fragment_cache_key + helper_method :combined_fragment_cache_key + end end module ClassMethods @@ -62,17 +65,36 @@ module AbstractController # with the specified +key+ value. The key is expanded using # ActiveSupport::Cache.expand_cache_key. def fragment_cache_key(key) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Calling fragment_cache_key directly is deprecated and will be removed in Rails 6.0. + All fragment accessors now use the combined_fragment_cache_key method that retains the key as an array, + such that the caching stores can interrogate the parts for cache versions used in + recyclable cache keys. + MSG + head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } tail = key.is_a?(Hash) ? url_for(key).split("://").last : key ActiveSupport::Cache.expand_cache_key([*head, *tail], :views) end + # Given a key (as described in +expire_fragment+), returns + # a key array suitable for use in reading, writing, or expiring a + # cached fragment. All keys begin with <tt>:views</tt>, + # followed by ENV["RAILS_CACHE_ID"] or ENV["RAILS_APP_VERSION"] if set, + # followed by any controller-wide key prefix values, ending + # with the specified +key+ value. + def combined_fragment_cache_key(key) + head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } + tail = key.is_a?(Hash) ? url_for(key).split("://").last : key + [ :views, (ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]), *head, *tail ].compact + end + # Writes +content+ to the location signified by # +key+ (see +expire_fragment+ for acceptable formats). def write_fragment(key, content, options = nil) return content unless cache_configured? - key = fragment_cache_key(key) + key = combined_fragment_cache_key(key) instrument_fragment_cache :write_fragment, key do content = content.to_str cache_store.write(key, content, options) @@ -85,7 +107,7 @@ module AbstractController def read_fragment(key, options = nil) return unless cache_configured? - key = fragment_cache_key(key) + key = combined_fragment_cache_key(key) instrument_fragment_cache :read_fragment, key do result = cache_store.read(key, options) result.respond_to?(:html_safe) ? result.html_safe : result @@ -96,7 +118,7 @@ module AbstractController # +key+ exists (see +expire_fragment+ for acceptable formats). def fragment_exist?(key, options = nil) return unless cache_configured? - key = fragment_cache_key(key) + key = combined_fragment_cache_key(key) instrument_fragment_cache :exist_fragment?, key do cache_store.exist?(key, options) @@ -123,7 +145,7 @@ module AbstractController # method (or <tt>delete_matched</tt>, for Regexp keys). def expire_fragment(key, options = nil) return unless cache_configured? - key = fragment_cache_key(key) unless key.is_a?(Regexp) + key = combined_fragment_cache_key(key) unless key.is_a?(Regexp) instrument_fragment_cache :expire_fragment, key do if key.is_a?(Regexp) @@ -135,8 +157,7 @@ module AbstractController end def instrument_fragment_cache(name, key) # :nodoc: - payload = instrument_payload(key) - ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", payload) { yield } + ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key)) { yield } end end end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index ce4ecf17cc..e4400e8704 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -1,4 +1,24 @@ module AbstractController + # = Abstract Controller Callbacks + # + # Abstract Controller provides hooks during the life cycle of a controller action. + # Callbacks allow you to trigger logic during this cycle. Available callbacks are: + # + # * <tt>after_action</tt> + # * <tt>append_after_action</tt> + # * <tt>append_around_action</tt> + # * <tt>append_before_action</tt> + # * <tt>around_action</tt> + # * <tt>before_action</tt> + # * <tt>prepend_after_action</tt> + # * <tt>prepend_around_action</tt> + # * <tt>prepend_before_action</tt> + # * <tt>skip_after_action</tt> + # * <tt>skip_around_action</tt> + # * <tt>skip_before_action</tt> + # + # NOTE: Calling the same callback multiple times will overwrite previous callback definitions. + # module Callbacks extend ActiveSupport::Concern @@ -9,7 +29,7 @@ module AbstractController included do define_callbacks :process_action, - terminator: ->(controller, result_lambda) { result_lambda.call if result_lambda.is_a?(Proc); controller.performed? }, + terminator: ->(controller, result_lambda) { result_lambda.call; controller.performed? }, skip_after_callbacks_if_terminated: true end diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index ef3be7af83..2e50637c39 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -5,11 +5,8 @@ module AbstractController extend ActiveSupport::Concern included do - class_attribute :_helpers - self._helpers = Module.new - - class_attribute :_helper_methods - self._helper_methods = Array.new + class_attribute :_helpers, default: Module.new + class_attribute :_helper_methods, default: Array.new end class MissingHelperError < LoadError diff --git a/actionpack/lib/abstract_controller/translation.rb b/actionpack/lib/abstract_controller/translation.rb index 9e3858802a..e4ac95df50 100644 --- a/actionpack/lib/abstract_controller/translation.rb +++ b/actionpack/lib/abstract_controller/translation.rb @@ -13,7 +13,7 @@ module AbstractController path = controller_path.tr("/", ".") defaults = [:"#{path}#{key}"] defaults << options[:default] if options[:default] - options[:default] = defaults + options[:default] = defaults.flatten key = "#{path}.#{action_name}#{key}" end I18n.translate(key, options) diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index d29a5fe68f..d00fcbcd13 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -60,9 +60,9 @@ module ActionController class_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{method}(event) return unless logger.info? && ActionController::Base.enable_fragment_cache_logging - key_or_path = event.payload[:key] || event.payload[:path] + key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path]) human_name = #{method.to_s.humanize.inspect} - info("\#{human_name} \#{key_or_path} (\#{event.duration.round(1)}ms)") + info("\#{human_name} \#{key} (\#{event.duration.round(1)}ms)") end METHOD end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 246644dcbd..96c708f45a 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -208,8 +208,7 @@ module ActionController @_request.reset_session end - class_attribute :middleware_stack - self.middleware_stack = ActionController::MiddlewareStack.new + class_attribute :middleware_stack, default: ActionController::MiddlewareStack.new def self.inherited(base) # :nodoc: base.middleware_stack = middleware_stack.dup diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index eb636fa3f6..0525252c7c 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -7,8 +7,7 @@ module ActionController include Head included do - class_attribute :etaggers - self.etaggers = [] + class_attribute :etaggers, default: [] end module ClassMethods diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb index 798564db96..69c3979a0e 100644 --- a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb +++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb @@ -22,8 +22,7 @@ module ActionController include ActionController::ConditionalGet included do - class_attribute :etag_with_template_digest - self.etag_with_template_digest = true + class_attribute :etag_with_template_digest, default: true ActiveSupport.on_load :action_view, yield: true do etag do |options| diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index 347fbf0e74..24d1097ebe 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -3,8 +3,7 @@ module ActionController #:nodoc: extend ActiveSupport::Concern included do - class_attribute :_flash_types, instance_accessor: false - self._flash_types = [] + class_attribute :_flash_types, instance_accessor: false, default: [] delegate :flash, to: :request add_flash_types(:alert, :notice) diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 476d081239..913a4b9a04 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -53,9 +53,8 @@ module ActionController include AbstractController::Helpers included do - class_attribute :helpers_path, :include_all_helpers - self.helpers_path ||= [] - self.include_all_helpers = true + class_attribute :helpers_path, default: [] + class_attribute :include_all_helpers, default: true end module ClassMethods diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index a89fc1678b..44151c9f71 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -159,8 +159,7 @@ module ActionController end included do - class_attribute :_wrapper_options - self._wrapper_options = Options.from_hash(format: []) + class_attribute :_wrapper_options, default: Options.from_hash(format: []) end module ClassMethods @@ -283,7 +282,7 @@ module ActionController return false unless request.has_content_type? ref = request.content_mime_type.ref - _wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters[_wrapper_key] + _wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters.key?(_wrapper_key) end end end diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 733aca195d..23c21b0501 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -26,8 +26,7 @@ module ActionController RENDERERS = Set.new included do - class_attribute :_renderers - self._renderers = Set.new.freeze + class_attribute :_renderers, default: Set.new.freeze end # Used in <tt>ActionController::Base</tt> diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 7864f9decd..cd6a0c0b98 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -119,8 +119,7 @@ module ActionController # params[:key] # => "value" # params["key"] # => "value" class Parameters - cattr_accessor :permit_all_parameters, instance_accessor: false - self.permit_all_parameters = false + cattr_accessor :permit_all_parameters, instance_accessor: false, default: false cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false @@ -205,8 +204,7 @@ module ActionController # config. For instance: # # config.always_permitted_parameters = %w( controller action format ) - cattr_accessor :always_permitted_parameters - self.always_permitted_parameters = %w( controller action ) + cattr_accessor :always_permitted_parameters, default: %w( controller action ) # Returns a new instance of <tt>ActionController::Parameters</tt>. # Also, sets the +permitted+ attribute to the default value of @@ -247,7 +245,7 @@ module ActionController # oddity: "Heavy stone crab" # }) # params.to_h - # # => ActionController::UnfilteredParameters: unable to convert unfiltered parameters to hash + # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash # # safe_params = params.permit(:name) # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"} @@ -267,7 +265,7 @@ module ActionController # oddity: "Heavy stone crab" # }) # params.to_hash - # # => ActionController::UnfilteredParameters: unable to convert unfiltered parameters to hash + # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash # # safe_params = params.permit(:name) # safe_params.to_hash # => {"name"=>"Senjougahara Hitagi"} @@ -283,6 +281,10 @@ module ActionController # nationality: "Danish" # }) # params.to_query + # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash + # + # safe_params = params.permit(:name, :nationality) + # safe_params.to_query # # => "name=David&nationality=Danish" # # An optional namespace can be passed to enclose key names: @@ -291,7 +293,8 @@ module ActionController # name: "David", # nationality: "Danish" # }) - # params.to_query("user") + # safe_params = params.permit(:name, :nationality) + # safe_params.to_query("user") # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish" # # The string pairs "key=value" that conform the query string @@ -666,8 +669,8 @@ module ActionController # to key. If the key is not found, returns the default value. If the # optional code block is given and the key is not found, pass in the key # and return the result of block. - def delete(key) - convert_value_to_parameters(@parameters.delete(key)) + def delete(key, &block) + convert_value_to_parameters(@parameters.delete(key, &block)) end # Returns a new instance of <tt>ActionController::Parameters</tt> with only diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index e584b84d92..077ab2561f 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -74,7 +74,7 @@ module ActionDispatch PAIR_RE = %r{(#{KV_RE})=(#{KV_RE})} def filtered_query_string # :doc: query_string.gsub(PAIR_RE) do |_| - parameter_filter.filter([[$1, $2]]).first.join("=") + parameter_filter.filter($1 => $2).first.join("=") end end end diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 19f89edbc1..5994a01c78 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -6,8 +6,7 @@ module ActionDispatch extend ActiveSupport::Concern included do - mattr_accessor :ignore_accept_header - self.ignore_accept_header = false + mattr_accessor :ignore_accept_header, default: false end # The MIME type of the HTTP request, such as Mime[:xml]. diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index 889f55a52a..1d2b4b902b 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -54,7 +54,7 @@ module ActionDispatch end def call(original_params, parents = []) - filtered_params = {} + filtered_params = original_params.class.new original_params.each do |key, value| parents.push(key) if deep_regexps diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 2ee52eeb48..3c91677d55 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -81,8 +81,8 @@ module ActionDispatch # :nodoc: LOCATION = "Location".freeze NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304] - cattr_accessor(:default_charset) { "utf-8" } - cattr_accessor(:default_headers) + cattr_accessor :default_charset, default: "utf-8" + cattr_accessor :default_headers include Rack::Response::Helpers # Aliasing these off because AD::Http::Cache::Response defines them. diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 61ba052e45..225272d66e 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -27,14 +27,18 @@ module ActionDispatch @tempfile = hash[:tempfile] raise(ArgumentError, ":tempfile is required") unless @tempfile - @original_filename = hash[:filename] - if @original_filename + if hash[:filename] + @original_filename = hash[:filename].dup + begin @original_filename.encode!(Encoding::UTF_8) rescue EncodingError @original_filename.force_encoding(Encoding::UTF_8) end + else + @original_filename = nil end + @content_type = hash[:type] @headers = hash[:head] end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index b6be48a48b..f902fe36e0 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -7,8 +7,7 @@ module ActionDispatch HOST_REGEXP = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/ PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/ - mattr_accessor :tld_length - self.tld_length = 1 + mattr_accessor :tld_length, default: 1 class << self # Returns the domain part of a host given the domain level. diff --git a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb index d692f6415c..62f052ced6 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb @@ -18,14 +18,6 @@ module ActionDispatch @tt = transition_table end - def simulate(string) - ms = memos(string) { return } - MatchData.new(ms) - end - - alias :=~ :simulate - alias :match :simulate - def memos(string) input = StringScanner.new(string) state = [0] diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb index e1ac2c873e..45aff287b1 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb @@ -82,7 +82,7 @@ module ActionDispatch end def visualizer(paths, title = "FSM") - viz_dir = File.join File.dirname(__FILE__), "..", "visualizer" + viz_dir = File.join __dir__, "..", "visualizer" fsm_js = File.read File.join(viz_dir, "fsm.js") fsm_css = File.read File.join(viz_dir, "fsm.css") erb = File.read File.join(viz_dir, "index.html.erb") diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 7bc15aa6b3..0acbac1d9d 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -10,11 +10,11 @@ module ActionDispatch module VerbMatchers VERBS = %w{ DELETE GET HEAD OPTIONS LINK PATCH POST PUT TRACE UNLINK } VERBS.each do |v| - class_eval <<-eoc - class #{v} - def self.verb; name.split("::").last; end - def self.call(req); req.#{v.downcase}?; end - end + class_eval <<-eoc, __FILE__, __LINE__ + 1 + class #{v} + def self.verb; name.split("::").last; end + def self.call(req); req.#{v.downcase}?; end + end eoc end diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index ffcd875b4d..6d400f3364 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -13,11 +13,13 @@ module ActionDispatch # normalize_path("") # => "/" # normalize_path("/%ab") # => "/%AB" def self.normalize_path(path) + encoding = path.encoding path = "/#{path}" path.squeeze!("/".freeze) path.sub!(%r{/+\Z}, "".freeze) path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase } path = "/" if path == "".freeze + path.force_encoding(encoding) path end @@ -59,11 +61,11 @@ module ActionDispatch end private - def escape(component, pattern) # :doc: + def escape(component, pattern) component.gsub(pattern) { |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII) end - def percent_encode(unsafe) # :doc: + def percent_encode(unsafe) safe = EMPTY.dup unsafe.each_byte { |b| safe << DEC2HEX[b] } safe @@ -84,6 +86,10 @@ module ActionDispatch ENCODER.escape_fragment(fragment.to_s) end + # Replaces any escaped sequences with their unescaped representations. + # + # uri = "/topics?title=Ruby%20on%20Rails" + # unescape_uri(uri) #=> "/topics?title=Ruby on Rails" def self.unescape_uri(uri) ENCODER.unescape_uri(uri) end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index e565c03a8a..533925ebe1 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -43,6 +43,10 @@ module ActionDispatch get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT end + def authenticated_encrypted_cookie_salt + get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT + end + def secret_token get_header Cookies::SECRET_TOKEN end @@ -149,6 +153,7 @@ module ActionDispatch SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze + AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze SECRET_TOKEN = "action_dispatch.secret_token".freeze SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze @@ -207,6 +212,9 @@ module ActionDispatch # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # + # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+ + # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded. + # # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. # # Example: @@ -219,6 +227,8 @@ module ActionDispatch @encrypted ||= if upgrade_legacy_signed_cookies? UpgradeLegacyEncryptedCookieJar.new(self) + elsif upgrade_legacy_hmac_aes_cbc_cookies? + UpgradeLegacyHmacAesCbcCookieJar.new(self) else EncryptedCookieJar.new(self) end @@ -240,6 +250,13 @@ module ActionDispatch def upgrade_legacy_signed_cookies? request.secret_token.present? && request.secret_key_base.present? end + + def upgrade_legacy_hmac_aes_cbc_cookies? + request.secret_key_base.present? && + request.authenticated_encrypted_cookie_salt.present? && + request.encrypted_signed_cookie_salt.present? && + request.encrypted_cookie_salt.present? + end end # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream @@ -415,8 +432,7 @@ module ActionDispatch end end - mattr_accessor :always_write_cookie - self.always_write_cookie = false + mattr_accessor :always_write_cookie, default: false private @@ -576,9 +592,11 @@ module ActionDispatch "Read the upgrade documentation to learn more about this new config option." end - secret = key_generator.generate_key(request.encrypted_cookie_salt || "")[0, ActiveSupport::MessageEncryptor.key_len] - sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || "") - @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) + cipher = "aes-256-gcm" + key_len = ActiveSupport::MessageEncryptor.key_len(cipher) + secret = key_generator.generate_key(request.authenticated_encrypted_cookie_salt || "")[0, key_len] + + @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end private @@ -603,6 +621,32 @@ module ActionDispatch include VerifyAndUpgradeLegacySignedMessage end + # UpgradeLegacyHmacAesCbcCookieJar is used by ActionDispatch::Session::CookieStore + # to upgrade cookies encrypted with AES-256-CBC with HMAC to AES-256-GCM + class UpgradeLegacyHmacAesCbcCookieJar < EncryptedCookieJar + def initialize(parent_jar) + super + + secret = key_generator.generate_key(request.encrypted_cookie_salt || "")[0, ActiveSupport::MessageEncryptor.key_len] + sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || "") + + @legacy_encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) + end + + def decrypt_and_verify_legacy_encrypted_message(name, signed_message) + deserialize(name, @legacy_encryptor.decrypt_and_verify(signed_message)).tap do |value| + self[name] = { value: value } + end + rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage + nil + end + + private + def parse(name, signed_message) + super || decrypt_and_verify_legacy_encrypted_message(name, signed_message) + end + end + def initialize(app) @app = app end diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 1c720c5a8e..336a775880 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -10,7 +10,7 @@ module ActionDispatch # This middleware is responsible for logging exceptions and # showing a debugging page in case the request is local. class DebugExceptions - RESCUES_TEMPLATE_PATH = File.expand_path("../templates", __FILE__) + RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__) class DebugView < ActionView::Base def debug_params(params) diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 397f0a8b92..08b4541d24 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -3,9 +3,7 @@ require "rack/utils" module ActionDispatch class ExceptionWrapper - cattr_accessor :rescue_responses - @@rescue_responses = Hash.new(:internal_server_error) - @@rescue_responses.merge!( + cattr_accessor :rescue_responses, default: Hash.new(:internal_server_error).merge!( "ActionController::RoutingError" => :not_found, "AbstractController::ActionNotFound" => :not_found, "ActionController::MethodNotAllowed" => :method_not_allowed, @@ -21,9 +19,7 @@ module ActionDispatch "Rack::QueryParser::InvalidParameterError" => :bad_request ) - cattr_accessor :rescue_templates - @@rescue_templates = Hash.new("diagnostics") - @@rescue_templates.merge!( + cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!( "ActionView::MissingTemplate" => "missing_template", "ActionController::RoutingError" => "routing_error", "AbstractController::ActionNotFound" => "unknown_action", diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 5d10129d21..fb99f13a1c 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -6,11 +6,11 @@ module ActionDispatch # When initialized, it can accept optional HTTP headers, which will be set # when a response containing a file's contents is delivered. # - # This middleware will render the file specified in `env["PATH_INFO"]` + # This middleware will render the file specified in <tt>env["PATH_INFO"]</tt> # where the base path is in the +root+ directory. For example, if the +root+ - # is set to `public/`, then a request with `env["PATH_INFO"]` of - # `assets/application.js` will return a response with the contents of a file - # located at `public/assets/application.js` if the file exists. If the file + # is set to +public/+, then a request with <tt>env["PATH_INFO"]</tt> of + # +assets/application.js+ will return a response with the contents of a file + # located at +public/assets/application.js+ if the file exists. If the file # does not exist, a 404 "File not Found" response will be returned. class FileHandler def initialize(root, index: "index", headers: {}) @@ -23,8 +23,8 @@ module ActionDispatch # correct read permissions, the return value is a URI-escaped string # representing the filename. Otherwise, false is returned. # - # Used by the `Static` class to check the existence of a valid file - # in the server's `public/` directory (see Static#call). + # Used by the +Static+ class to check the existence of a valid file + # in the server's +public/+ directory (see Static#call). def match?(path) path = ::Rack::Utils.unescape_path path return false unless ::Rack::Utils.valid_path? path @@ -99,7 +99,7 @@ module ActionDispatch # This middleware will attempt to return the contents of a file's body from # disk in the response. If a file is not found on disk, the request will be # delegated to the application stack. This middleware is commonly initialized - # to serve assets from a server's `public/` directory. + # to serve assets from a server's +public/+ directory. # # This middleware verifies the path to ensure that only files # living in the root directory can be rendered. A request cannot diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 16a18a7f25..7662e164b8 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -16,6 +16,7 @@ module ActionDispatch config.action_dispatch.signed_cookie_salt = "signed cookie" config.action_dispatch.encrypted_cookie_salt = "encrypted cookie" config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie" + config.action_dispatch.use_authenticated_cookie_encryption = false config.action_dispatch.perform_deep_munge = true config.action_dispatch.default_headers = { @@ -36,6 +37,8 @@ module ActionDispatch ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses) ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates) + config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie" if config.action_dispatch.use_authenticated_cookie_encryption + config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil? ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 74ba6466cf..3547a8604f 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -101,11 +101,13 @@ module ActionDispatch # Returns keys of the session as Array. def keys + load_for_read! @delegate.keys end # Returns values of the session as Array. def values + load_for_read! @delegate.values end diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index 3615e4b1d8..4f79c4c21e 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -1,8 +1,7 @@ module ActionDispatch class Request class Utils # :nodoc: - mattr_accessor :perform_deep_munge - self.perform_deep_munge = true + mattr_accessor :perform_deep_munge, default: true def self.each_param_value(params, &block) case params diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 74904e3d45..88deee5f5e 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1837,7 +1837,7 @@ module ActionDispatch path_types.fetch(String, []).each do |_path| route_options = options.dup if _path && option_path - raise ArgumentError, "Ambigous route definition. Both :path and the route path where specified as strings." + raise ArgumentError, "Ambiguous route definition. Both :path and the route path where specified as strings." end to = get_to_from_path(_path, to, route_options[:action]) decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints) @@ -2038,8 +2038,8 @@ module ActionDispatch # { controller: "pages", action: "index", subdomain: "www" } # end # - # The return value from the block passed to `direct` must be a valid set of - # arguments for `url_for` which will actually build the URL string. This can + # The return value from the block passed to +direct+ must be a valid set of + # arguments for +url_for+ which will actually build the URL string. This can # be one of the following: # # * A string, which is treated as a generated URL @@ -2058,17 +2058,17 @@ module ActionDispatch # [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ] # end # - # In this instance the `params` object comes from the context in which the the + # In this instance the +params+ object comes from the context in which the the # block is executed, e.g. generating a URL inside a controller action or a view. # If the block is executed where there isn't a params object such as this: # # Rails.application.routes.url_helpers.browse_path # - # then it will raise a `NameError`. Because of this you need to be aware of the + # then it will raise a +NameError+. Because of this you need to be aware of the # context in which you will use your custom URL helper when defining it. # - # NOTE: The `direct` method can't be used inside of a scope block such as - # `namespace` or `scope` and will raise an error if it detects that it is. + # NOTE: The +direct+ method can't be used inside of a scope block such as + # +namespace+ or +scope+ and will raise an error if it detects that it is. def direct(name, options = {}, &block) unless @scope.root? raise RuntimeError, "The direct method can't be used inside a routes scope block" @@ -2078,8 +2078,8 @@ module ActionDispatch end # Define custom polymorphic mappings of models to URLs. This alters the - # behavior of `polymorphic_url` and consequently the behavior of - # `link_to` and `form_for` when passed a model instance, e.g: + # behavior of +polymorphic_url+ and consequently the behavior of + # +link_to+ and +form_for+ when passed a model instance, e.g: # # resource :basket # @@ -2087,8 +2087,8 @@ module ActionDispatch # [:basket] # end # - # This will now generate "/basket" when a `Basket` instance is passed to - # `link_to` or `form_for` instead of the standard "/baskets/:id". + # This will now generate "/basket" when a +Basket+ instance is passed to + # +link_to+ or +form_for+ instead of the standard "/baskets/:id". # # NOTE: This custom behavior only applies to simple polymorphic URLs where # a single model instance is passed and not more complicated forms, e.g: @@ -2105,7 +2105,7 @@ module ActionDispatch # link_to "Profile", @current_user # link_to "Profile", [:admin, @current_user] # - # The first `link_to` will generate "/profile" but the second will generate + # The first +link_to+ will generate "/profile" but the second will generate # the standard polymorphic URL of "/admin/users/1". # # You can pass options to a polymorphic mapping - the arity for the block @@ -2116,11 +2116,11 @@ module ActionDispatch # end # # This generates the URL "/basket#items" because when the last item in an - # array passed to `polymorphic_url` is a hash then it's treated as options + # array passed to +polymorphic_url+ is a hash then it's treated as options # to the URL helper that gets called. # - # NOTE: The `resolve` method can't be used inside of a scope block such as - # `namespace` or `scope` and will raise an error if it detects that it is. + # NOTE: The +resolve+ method can't be used inside of a scope block such as + # +namespace+ or +scope+ and will raise an error if it detects that it is. def resolve(*args, &block) unless @scope.root? raise RuntimeError, "The resolve method can't be used inside a routes scope block" diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index e1f9fc9ecc..68bd6d806b 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -279,6 +279,8 @@ module ActionDispatch if args.size < path_params_size path_params -= controller_options.keys path_params -= result.keys + else + path_params = path_params.dup end inner_options.each_key do |key| path_params.delete(key) diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb index 98fdb36c91..c39a135ce0 100644 --- a/actionpack/lib/action_dispatch/system_test_case.rb +++ b/actionpack/lib/action_dispatch/system_test_case.rb @@ -66,14 +66,18 @@ module ActionDispatch # # To use a headless driver, like Poltergeist, update your Gemfile to use # Poltergeist instead of Selenium and then declare the driver name in the - # +application_system_test_case.rb+ file. In this case you would leave out the +:using+ - # option because the driver is headless. + # +application_system_test_case.rb+ file. In this case, you would leave out + # the +:using+ option because the driver is headless, but you can still use + # +:screen_size+ to change the size of the browser screen, also you can use + # +:options+ to pass options supported by the driver. Please refer to your + # driver documentation to learn about supported options. # # require "test_helper" # require "capybara/poltergeist" # # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase - # driven_by :poltergeist + # driven_by :poltergeist, screen_size: [1400, 1400], options: + # { js_errors: true } # end # # Because <tt>ActionDispatch::SystemTestCase</tt> is a shim between Capybara diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb index 5cf17883f7..1a027f2e23 100644 --- a/actionpack/lib/action_dispatch/system_testing/driver.rb +++ b/actionpack/lib/action_dispatch/system_testing/driver.rb @@ -9,23 +9,42 @@ module ActionDispatch end def use - register if selenium? + register unless rack_test? + setup end private - def selenium? - @name == :selenium + def rack_test? + @name == :rack_test end def register Capybara.register_driver @name do |app| - Capybara::Selenium::Driver.new(app, { browser: @browser }.merge(@options)).tap do |driver| - driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size) + case @name + when :selenium then register_selenium(app) + when :poltergeist then register_poltergeist(app) + when :webkit then register_webkit(app) end end end + def register_selenium(app) + Capybara::Selenium::Driver.new(app, { browser: @browser }.merge(@options)).tap do |driver| + driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size) + end + end + + def register_poltergeist(app) + Capybara::Poltergeist::Driver.new(app, @options.merge(window_size: @screen_size)) + end + + def register_webkit(app) + Capybara::Webkit::Driver.new(app, Capybara::Webkit::Configuration.to_hash.merge(@options)).tap do |driver| + driver.resize_window(*@screen_size) + end + end + def setup Capybara.current_driver = @name end diff --git a/actionpack/lib/action_dispatch/system_testing/server.rb b/actionpack/lib/action_dispatch/system_testing/server.rb index 4a214ef713..89ca6944d9 100644 --- a/actionpack/lib/action_dispatch/system_testing/server.rb +++ b/actionpack/lib/action_dispatch/system_testing/server.rb @@ -3,6 +3,12 @@ require "rack/handler/puma" module ActionDispatch module SystemTesting class Server # :nodoc: + class << self + attr_accessor :silence_puma + end + + self.silence_puma = false + def run register setup @@ -11,7 +17,12 @@ module ActionDispatch private def register Capybara.register_server :rails_puma do |app, port, host| - Rack::Handler::Puma.run(app, Port: port, Threads: "0:1") + Rack::Handler::Puma.run( + app, + Port: port, + Threads: "0:1", + Silent: self.class.silence_puma + ) end end diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb index 187ba2cc5f..f03f0d4299 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 @@ -2,7 +2,12 @@ module ActionDispatch module SystemTesting module TestHelpers module SetupAndTeardown # :nodoc: - DEFAULT_HOST = "127.0.0.1" + DEFAULT_HOST = "http://127.0.0.1" + + def host!(host) + super + Capybara.app_host = host + end def before_setup host! DEFAULT_HOST diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 2416c58817..f16647fac8 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -338,8 +338,7 @@ module ActionDispatch @integration_session = nil end - %w(get post patch put head delete cookies assigns - xml_http_request xhr get_via_redirect post_via_redirect).each do |method| + %w(get post patch put head delete cookies assigns follow_redirect!).each do |method| define_method(method) do |*args| # reset the html_document variable, except for cookies/assigns calls unless method == "cookies" || method == "assigns" diff --git a/actionpack/test/abstract/translation_test.rb b/actionpack/test/abstract/translation_test.rb index 0c4071df8d..4893144905 100644 --- a/actionpack/test/abstract/translation_test.rb +++ b/actionpack/test/abstract/translation_test.rb @@ -62,6 +62,7 @@ module AbstractController def test_default_translation @controller.stub :action_name, :index do assert_equal "bar", @controller.t("one.two") + assert_equal "baz", @controller.t(".twoz", default: ["baz", :twoz]) end end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 4185ce1a1f..bd118b46be 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -1,6 +1,6 @@ -$:.unshift(File.dirname(__FILE__) + "/lib") -$:.unshift(File.dirname(__FILE__) + "/fixtures/helpers") -$:.unshift(File.dirname(__FILE__) + "/fixtures/alternate_helpers") +$:.unshift File.expand_path("lib", __dir__) +$:.unshift File.expand_path("fixtures/helpers", __dir__) +$:.unshift File.expand_path("fixtures/alternate_helpers", __dir__) require "active_support/core_ext/kernel/reporting" @@ -56,7 +56,7 @@ ActiveSupport::Deprecation.debug = true # Disable available locale checks to avoid warnings running the test suite. I18n.enforce_available_locales = false -FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), "fixtures") +FIXTURE_LOAD_PATH = File.join(__dir__, "fixtures") SharedTestRoutes = ActionDispatch::Routing::RouteSet.new @@ -156,7 +156,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase end def with_autoload_path(path) - path = File.join(File.dirname(__FILE__), "fixtures", path) + path = File.join(__dir__, "fixtures", path) if ActiveSupport::Dependencies.autoload_paths.include?(path) yield else diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index 9ab152fc5c..73aab5848b 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -83,7 +83,7 @@ class ActionPackAssertionsController < ActionController::Base end def render_file_absolute_path - render file: File.expand_path("../../../README.rdoc", __FILE__) + render file: File.expand_path("../../README.rdoc", __dir__) end def render_file_relative_path diff --git a/actionpack/test/controller/api/data_streaming_test.rb b/actionpack/test/controller/api/data_streaming_test.rb index f15b78d102..e6419b9adf 100644 --- a/actionpack/test/controller/api/data_streaming_test.rb +++ b/actionpack/test/controller/api/data_streaming_test.rb @@ -1,7 +1,7 @@ require "abstract_unit" module TestApiFileUtils - def file_path() File.expand_path(__FILE__) end + def file_path() __FILE__ end def file_data() @data ||= File.open(file_path, "rb") { |f| f.read } end end diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index fa8d9dc09a..c86dcafee5 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -4,7 +4,7 @@ require "lib/controller/fake_models" CACHE_DIR = "test_cache" # Don't change '/../temp/' cavalierly or you might hose something you don't want hosed -FILE_STORE_PATH = File.join(File.dirname(__FILE__), "/../temp/", CACHE_DIR) +FILE_STORE_PATH = File.join(__dir__, "../temp/", CACHE_DIR) class FragmentCachingMetalTestController < ActionController::Metal abstract! @@ -26,10 +26,6 @@ class FragmentCachingMetalTest < ActionController::TestCase @controller.request = @request @controller.response = @response end - - def test_fragment_cache_key - assert_equal "views/what a key", @controller.fragment_cache_key("what a key") - end end class CachingController < ActionController::Base @@ -43,6 +39,8 @@ class FragmentCachingTestController < CachingController end class FragmentCachingTest < ActionController::TestCase + ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version) + def setup super @store = ActiveSupport::Cache::MemoryStore.new @@ -53,12 +51,25 @@ class FragmentCachingTest < ActionController::TestCase @controller.params = @params @controller.request = @request @controller.response = @response + + @m1v1 = ModelWithKeyAndVersion.new("model/1", "1") + @m1v2 = ModelWithKeyAndVersion.new("model/1", "2") + @m2v1 = ModelWithKeyAndVersion.new("model/2", "1") + @m2v2 = ModelWithKeyAndVersion.new("model/2", "2") end def test_fragment_cache_key - assert_equal "views/what a key", @controller.fragment_cache_key("what a key") - assert_equal "views/test.host/fragment_caching_test/some_action", - @controller.fragment_cache_key(controller: "fragment_caching_test", action: "some_action") + assert_deprecated do + assert_equal "views/what a key", @controller.fragment_cache_key("what a key") + assert_equal "views/test.host/fragment_caching_test/some_action", + @controller.fragment_cache_key(controller: "fragment_caching_test", action: "some_action") + end + end + + def test_combined_fragment_cache_key + assert_equal [ :views, "what a key" ], @controller.combined_fragment_cache_key("what a key") + assert_equal [ :views, "test.host/fragment_caching_test/some_action" ], + @controller.combined_fragment_cache_key(controller: "fragment_caching_test", action: "some_action") end def test_read_fragment_with_caching_enabled @@ -72,6 +83,12 @@ class FragmentCachingTest < ActionController::TestCase assert_nil @controller.read_fragment("name") end + def test_read_fragment_with_versioned_model + @controller.write_fragment([ "stuff", @m1v1 ], "hello") + assert_equal "hello", @controller.read_fragment([ "stuff", @m1v1 ]) + assert_nil @controller.read_fragment([ "stuff", @m1v2 ]) + end + def test_fragment_exist_with_caching_enabled @store.write("views/name", "value") assert @controller.fragment_exist?("name") @@ -198,7 +215,7 @@ CACHED assert_equal expected_body, @response.body assert_equal "This bit's fragment cached", - @store.read("views/test.host/functional_caching/fragment_cached/#{template_digest("functional_caching/fragment_cached")}") + @store.read("views/functional_caching/fragment_cached:#{template_digest("functional_caching/fragment_cached")}/fragment") end def test_fragment_caching_in_partials @@ -207,7 +224,7 @@ CACHED assert_match(/Old fragment caching in a partial/, @response.body) assert_match("Old fragment caching in a partial", - @store.read("views/test.host/functional_caching/html_fragment_cached_with_partial/#{template_digest("functional_caching/_partial")}")) + @store.read("views/functional_caching/_partial:#{template_digest("functional_caching/_partial")}/test.host/functional_caching/html_fragment_cached_with_partial")) end def test_skipping_fragment_cache_digesting @@ -237,7 +254,7 @@ CACHED assert_match(/Some inline content/, @response.body) assert_match(/Some cached content/, @response.body) assert_match("Some cached content", - @store.read("views/test.host/functional_caching/inline_fragment_cached/#{template_digest("functional_caching/inline_fragment_cached")}")) + @store.read("views/functional_caching/inline_fragment_cached:#{template_digest("functional_caching/inline_fragment_cached")}/test.host/functional_caching/inline_fragment_cached")) end def test_fragment_cache_instrumentation @@ -264,7 +281,7 @@ CACHED assert_equal expected_body, @response.body assert_equal "<p>ERB</p>", - @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached")}") + @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment") end def test_xml_formatted_fragment_caching @@ -275,7 +292,7 @@ CACHED assert_equal expected_body, @response.body assert_equal " <p>Builder</p>\n", - @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached")}") + @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment") end def test_fragment_caching_with_variant @@ -286,7 +303,7 @@ CACHED assert_equal expected_body, @response.body assert_equal "<p>PHONE</p>", - @store.read("views/test.host/functional_caching/formatted_fragment_cached_with_variant/#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}") + @store.read("views/functional_caching/formatted_fragment_cached_with_variant:#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}/fragment") end private @@ -412,7 +429,7 @@ class CollectionCacheTest < ActionController::TestCase def test_collection_fetches_cached_views get :index assert_equal 1, @controller.partial_rendered_times - assert_customer_cached "david/1", "david, 1" + assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/david/1") get :index assert_equal 1, @controller.partial_rendered_times @@ -444,14 +461,8 @@ class CollectionCacheTest < ActionController::TestCase def test_caching_with_callable_cache_key get :index_with_callable_cache_key - assert_customer_cached "cached_david", "david, 1" + assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/cached_david") end - - private - def assert_customer_cached(key, content) - assert_match content, - ActionView::PartialRenderer.collection_cache.read("views/#{key}/7c228ab609f0baf0b1f2367469210937") - end end class FragmentCacheKeyTestController < CachingController @@ -470,11 +481,21 @@ class FragmentCacheKeyTest < ActionController::TestCase @controller.cache_store = @store end - def test_fragment_cache_key + def test_combined_fragment_cache_key @controller.account_id = "123" - assert_equal "views/v1/123/what a key", @controller.fragment_cache_key("what a key") + assert_equal [ :views, "v1", "123", "what a key" ], @controller.combined_fragment_cache_key("what a key") @controller.account_id = nil - assert_equal "views/v1//what a key", @controller.fragment_cache_key("what a key") + assert_equal [ :views, "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key") + end + + def test_combined_fragment_cache_key_with_envs + ENV["RAILS_APP_VERSION"] = "55" + assert_equal [ :views, "55", "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key") + + ENV["RAILS_CACHE_ID"] = "66" + assert_equal [ :views, "66", "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key") + ensure + ENV["RAILS_CACHE_ID"] = ENV["RAILS_APP_VERSION"] = nil end end diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb index 4c6a772062..03dbd63614 100644 --- a/actionpack/test/controller/helper_test.rb +++ b/actionpack/test/controller/helper_test.rb @@ -1,6 +1,6 @@ require "abstract_unit" -ActionController::Base.helpers_path = File.expand_path("../../fixtures/helpers", __FILE__) +ActionController::Base.helpers_path = File.expand_path("../fixtures/helpers", __dir__) module Fun class GamesController < ActionController::Base @@ -48,7 +48,7 @@ end class HelpersPathsController < ActionController::Base paths = ["helpers2_pack", "helpers1_pack"].map do |path| - File.join(File.expand_path("../../fixtures", __FILE__), path) + File.join(File.expand_path("../fixtures", __dir__), path) end $:.unshift(*paths) @@ -61,7 +61,7 @@ class HelpersPathsController < ActionController::Base end class HelpersTypoController < ActionController::Base - path = File.expand_path("../../fixtures/helpers_typo", __FILE__) + path = File.expand_path("../fixtures/helpers_typo", __dir__) $:.unshift(path) self.helpers_path = path end @@ -178,7 +178,7 @@ class HelperTest < ActiveSupport::TestCase end def test_all_helpers_with_alternate_helper_dir - @controller_class.helpers_path = File.expand_path("../../fixtures/alternate_helpers", __FILE__) + @controller_class.helpers_path = File.expand_path("../fixtures/alternate_helpers", __dir__) # Reload helpers @controller_class._helpers = Module.new diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 57f58fd835..cb282d4330 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -335,6 +335,18 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end end + def test_redirect_reset_html_document + with_test_route_set do + get "/redirect" + previous_html_document = html_document + + follow_redirect! + + assert_response :ok + refute_same previous_html_document, html_document + end + end + def test_xml_http_request_get with_test_route_set do get "/get", xhr: true @@ -1091,7 +1103,7 @@ class IntegrationFileUploadTest < ActionDispatch::IntegrationTest end def self.fixture_path - File.dirname(__FILE__) + "/../fixtures/multipart" + File.expand_path("../fixtures/multipart", __dir__) end routes.draw do diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 581081dd07..bfb47b90d5 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -152,7 +152,7 @@ module ActionController end def write_sleep_autoload - path = File.join(File.dirname(__FILE__), "../fixtures") + path = File.expand_path("../fixtures", __dir__) ActiveSupport::Dependencies.autoload_paths << path response.headers["Content-Type"] = "text/event-stream" diff --git a/actionpack/test/controller/mime/accept_format_test.rb b/actionpack/test/controller/mime/accept_format_test.rb index a22fa39051..d1c4dbfef7 100644 --- a/actionpack/test/controller/mime/accept_format_test.rb +++ b/actionpack/test/controller/mime/accept_format_test.rb @@ -29,7 +29,7 @@ class StarStarMimeControllerTest < ActionController::TestCase end class AbstractPostController < ActionController::Base - self.view_paths = File.dirname(__FILE__) + "/../../fixtures/post_test/" + self.view_paths = File.expand_path("../../fixtures/post_test", __dir__) end # For testing layouts which are set automatically diff --git a/actionpack/test/controller/new_base/render_file_test.rb b/actionpack/test/controller/new_base/render_file_test.rb index 6d651e0104..4491dd96ed 100644 --- a/actionpack/test/controller/new_base/render_file_test.rb +++ b/actionpack/test/controller/new_base/render_file_test.rb @@ -2,15 +2,15 @@ require "abstract_unit" module RenderFile class BasicController < ActionController::Base - self.view_paths = File.dirname(__FILE__) + self.view_paths = __dir__ def index - render file: File.join(File.dirname(__FILE__), *%w[.. .. fixtures test hello_world]) + render file: File.expand_path("../../fixtures/test/hello_world", __dir__) end def with_instance_variables @secret = "in the sauce" - render file: File.join(File.dirname(__FILE__), "../../fixtures/test/render_file_with_ivar") + render file: File.expand_path("../../fixtures/test/render_file_with_ivar", __dir__) end def relative_path @@ -25,11 +25,11 @@ module RenderFile def pathname @secret = "in the sauce" - render file: Pathname.new(File.dirname(__FILE__)).join(*%w[.. .. fixtures test dot.directory render_file_with_ivar]) + render file: Pathname.new(__dir__).join(*%w[.. .. fixtures test dot.directory render_file_with_ivar]) end def with_locals - path = File.join(File.dirname(__FILE__), "../../fixtures/test/render_file_with_locals") + path = File.expand_path("../../fixtures/test/render_file_with_locals", __dir__) render file: path, locals: { secret: "in the sauce" } end end diff --git a/actionpack/test/controller/new_base/render_implicit_action_test.rb b/actionpack/test/controller/new_base/render_implicit_action_test.rb index 796283466a..c5fc8e15e1 100644 --- a/actionpack/test/controller/new_base/render_implicit_action_test.rb +++ b/actionpack/test/controller/new_base/render_implicit_action_test.rb @@ -6,7 +6,7 @@ module RenderImplicitAction "render_implicit_action/simple/hello_world.html.erb" => "Hello world!", "render_implicit_action/simple/hyphen-ated.html.erb" => "Hello hyphen-ated!", "render_implicit_action/simple/not_implemented.html.erb" => "Not Implemented" - ), ActionView::FileSystemResolver.new(File.expand_path("../../../controller", __FILE__))] + ), ActionView::FileSystemResolver.new(File.expand_path("../../controller", __dir__))] def hello_world() end end diff --git a/actionpack/test/controller/parameters/mutators_test.rb b/actionpack/test/controller/parameters/mutators_test.rb index e61bbdbe13..3fe7340782 100644 --- a/actionpack/test/controller/parameters/mutators_test.rb +++ b/actionpack/test/controller/parameters/mutators_test.rb @@ -25,6 +25,27 @@ class ParametersMutatorsTest < ActiveSupport::TestCase assert_not @params.delete(:person).permitted? end + test "delete returns the value when the key is present" do + assert_equal "32", @params[:person].delete(:age) + end + + test "delete removes the entry when the key present" do + @params[:person].delete(:age) + assert_not @params[:person].key?(:age) + end + + test "delete returns nil when the key is not present" do + assert_nil @params[:person].delete(:first_name) + end + + test "delete returns the value of the given block when the key is not present" do + assert_equal "David", @params[:person].delete(:first_name) { "David" } + end + + test "delete yields the key to the given block when the key is not present" do + assert_equal "first_name: David", @params[:person].delete(:first_name) { |k| "#{k}: David" } + end + test "delete_if retains permitted status" do @params.permit! assert @params.delete_if { |k| k == "person" }.permitted? diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb index 2a41d57b26..4cbb28ef60 100644 --- a/actionpack/test/controller/params_wrapper_test.rb +++ b/actionpack/test/controller/params_wrapper_test.rb @@ -170,6 +170,14 @@ class ParamsWrapperTest < ActionController::TestCase end end + def test_no_double_wrap_if_key_exists_and_value_is_nil + with_default_wrapper_options do + @request.env["CONTENT_TYPE"] = "application/json" + post :parse, params: { "user" => nil } + assert_parameters("user" => nil) + end + end + def test_nested_params with_default_wrapper_options do @request.env["CONTENT_TYPE"] = "application/json" diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 3a0a0a8bde..17d834d55f 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -257,7 +257,7 @@ end module TemplateModificationHelper private def modify_template(name) - path = File.expand_path("../../fixtures/#{name}.erb", __FILE__) + path = File.expand_path("../fixtures/#{name}.erb", __dir__) original = File.read(path) File.write(path, "#{original} Modified!") ActionView::LookupContext::DetailsKey.clear @@ -287,9 +287,9 @@ class ExpiresInRenderTest < ActionController::TestCase def test_dynamic_render_with_file # This is extremely bad, but should be possible to do. - assert File.exist?(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")) + assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__)) response = get :dynamic_render_with_file, params: { id: '../\\../test/abstract_unit.rb' } - assert_equal File.read(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")), + assert_equal File.read(File.expand_path("../../test/abstract_unit.rb", __dir__)), response.body end @@ -306,16 +306,16 @@ class ExpiresInRenderTest < ActionController::TestCase end def test_dynamic_render - assert File.exist?(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")) + assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__)) assert_raises ActionView::MissingTemplate do get :dynamic_render, params: { id: '../\\../test/abstract_unit.rb' } end end def test_permitted_dynamic_render_file_hash - assert File.exist?(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")) + assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__)) response = get :dynamic_render_permit, params: { id: { file: '../\\../test/abstract_unit.rb' } } - assert_equal File.read(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")), + assert_equal File.read(File.expand_path("../../test/abstract_unit.rb", __dir__)), response.body end diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 9e6b975fe2..e265c6c49c 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -2,7 +2,7 @@ require "abstract_unit" module TestFileUtils def file_name() File.basename(__FILE__) end - def file_path() File.expand_path(__FILE__) end + def file_path() __FILE__ end def file_data() @data ||= File.open(file_path, "rb") { |f| f.read } end end diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index 3a4307b64b..677e2ddded 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -122,7 +122,7 @@ XML end def test_send_file - send_file(File.expand_path(__FILE__)) + send_file(__FILE__) end def redirect_to_same_controller @@ -780,7 +780,7 @@ XML end end - FILES_DIR = File.dirname(__FILE__) + "/../fixtures/multipart" + FILES_DIR = File.expand_path("../fixtures/multipart", __dir__) READ_BINARY = "rb:binary" READ_PLAIN = "r:binary" @@ -855,7 +855,7 @@ XML end def test_fixture_file_upload_ignores_fixture_path_given_full_path - TestCaseTest.stub :fixture_path, File.dirname(__FILE__) do + TestCaseTest.stub :fixture_path, __dir__ do uploaded_file = fixture_file_upload("#{FILES_DIR}/ruby_on_rails.jpg", "image/jpg") assert_equal File.open("#{FILES_DIR}/ruby_on_rails.jpg", READ_PLAIN).read, uploaded_file.read end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 664faa31bb..e5646de82e 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -288,8 +288,7 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SALT, iterations: 2) @request.env["action_dispatch.signed_cookie_salt"] = - @request.env["action_dispatch.encrypted_cookie_salt"] = - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT + @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] = SALT @request.host = "www.nextangle.com" end @@ -531,9 +530,7 @@ class CookiesTest < ActionController::TestCase get :set_encrypted_cookie cookies = @controller.send :cookies assert_not_equal "bar", cookies[:foo] - assert_raise TypeError do - cookies.signed[:foo] - end + assert_nil cookies.signed[:foo] assert_equal "bar", cookies.encrypted[:foo] end @@ -542,9 +539,7 @@ class CookiesTest < ActionController::TestCase get :set_encrypted_cookie cookies = @controller.send :cookies assert_not_equal "bar", cookies[:foo] - assert_raises TypeError do - cookies.signed[:foo] - end + assert_nil cookies.signed[:foo] assert_equal "bar", cookies.encrypted[:foo] end @@ -553,9 +548,7 @@ class CookiesTest < ActionController::TestCase get :set_encrypted_cookie cookies = @controller.send :cookies assert_not_equal "bar", cookies[:foo] - assert_raises ::JSON::ParserError do - cookies.signed[:foo] - end + assert_nil cookies.signed[:foo] assert_equal "bar", cookies.encrypted[:foo] end @@ -564,9 +557,7 @@ class CookiesTest < ActionController::TestCase get :set_wrapped_encrypted_cookie cookies = @controller.send :cookies assert_not_equal "wrapped: bar", cookies[:foo] - assert_raises ::JSON::ParserError do - cookies.signed[:foo] - end + assert_nil cookies.signed[:foo] assert_equal "wrapped: bar", cookies.encrypted[:foo] end @@ -577,38 +568,16 @@ class CookiesTest < ActionController::TestCase assert_equal "bar was dumped and loaded", cookies.encrypted[:foo] end - def test_encrypted_cookie_using_custom_digest - @request.env["action_dispatch.cookies_digest"] = "SHA256" - get :set_encrypted_cookie - cookies = @controller.send :cookies - assert_not_equal "bar", cookies[:foo] - assert_equal "bar", cookies.encrypted[:foo] - - sign_secret = @request.env["action_dispatch.key_generator"].generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) - - sha1_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: "SHA1") - sha256_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: "SHA256") - - assert_raises(ActiveSupport::MessageVerifier::InvalidSignature) do - sha1_verifier.verify(cookies[:foo]) - end - - assert_nothing_raised do - sha256_verifier.verify(cookies[:foo]) - end - end - def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json @request.env["action_dispatch.cookies_serializer"] = :hybrid - key_generator = @request.env["action_dispatch.key_generator"] - encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] - encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] - secret = key_generator.generate_key(encrypted_cookie_salt) - sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal) - marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: Marshal).encrypt_and_sign("bar") - @request.headers["Cookie"] = "foo=#{marshal_value}" + marshal_value = encryptor.encrypt_and_sign("bar") + @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape marshal_value}" get :get_encrypted_cookie @@ -616,40 +585,28 @@ class CookiesTest < ActionController::TestCase assert_not_equal "bar", cookies[:foo] assert_equal "bar", cookies.encrypted[:foo] - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON) - assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) + json_encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) + assert_not_nil @response.cookies["foo"] + assert_equal "bar", json_encryptor.decrypt_and_verify(@response.cookies["foo"]) end def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_value @request.env["action_dispatch.cookies_serializer"] = :hybrid - key_generator = @request.env["action_dispatch.key_generator"] - encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] - encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] - secret = key_generator.generate_key(encrypted_cookie_salt) - sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) - json_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON).encrypt_and_sign("bar") - @request.headers["Cookie"] = "foo=#{json_value}" - - get :get_encrypted_cookie + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) - cookies = @controller.send :cookies - assert_not_equal "bar", cookies[:foo] - assert_equal "bar", cookies.encrypted[:foo] - - assert_nil @response.cookies["foo"] - end - - def test_compat_encrypted_cookie_using_64_byte_key - # Cookie generated with 64 bytes secret - message = ["566d4e75536d686e633246564e6b493062557079626c566d51574d30515430394c53315665564a694e4563786555744f57537454576b396a5a31566a626e52525054303d2d2d34663234333330623130623261306163363562316266323335396164666364613564643134623131"].pack("H*") - @request.headers["Cookie"] = "foo=#{message}" + json_value = encryptor.encrypt_and_sign("bar") + @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape json_value}" get :get_encrypted_cookie cookies = @controller.send :cookies assert_not_equal "bar", cookies[:foo] assert_equal "bar", cookies.encrypted[:foo] + assert_nil @response.cookies["foo"] end @@ -813,10 +770,10 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) - sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end @@ -842,8 +799,6 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.cookies_serializer"] = :json @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee" - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar") @@ -852,10 +807,10 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) - sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end @@ -881,8 +836,6 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.cookies_serializer"] = :hybrid @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee" - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar") @@ -891,10 +844,10 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) - sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end @@ -920,8 +873,6 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.cookies_serializer"] = :hybrid @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee" - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate("bar") @@ -930,10 +881,10 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) - sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end @@ -959,6 +910,89 @@ class CookiesTest < ActionController::TestCase assert_nil @response.cookies["foo"] end + def test_legacy_hmac_aes_cbc_encrypted_marshal_cookie_is_upgraded_to_authenticated_encrypted_cookie + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + @request.env["action_dispatch.encrypted_cookie_salt"] = + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT + + key_generator = @request.env["action_dispatch.key_generator"] + encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] + encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] + secret = key_generator.generate_key(encrypted_cookie_salt) + sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: Marshal).encrypt_and_sign("bar") + + @request.headers["Cookie"] = "foo=#{marshal_value}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + aead_cipher = "aes-256-gcm" + aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len(aead_cipher)] + aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: aead_cipher, serializer: Marshal) + + assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_hmac_aes_cbc_encrypted_json_cookie_is_upgraded_to_authenticated_encrypted_cookie + @request.env["action_dispatch.cookies_serializer"] = :json + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + @request.env["action_dispatch.encrypted_cookie_salt"] = + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT + + key_generator = @request.env["action_dispatch.key_generator"] + encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] + encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] + secret = key_generator.generate_key(encrypted_cookie_salt) + sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON).encrypt_and_sign("bar") + + @request.headers["Cookie"] = "foo=#{marshal_value}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + aead_cipher = "aes-256-gcm" + aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len(aead_cipher)] + aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: aead_cipher, serializer: JSON) + + assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_hmac_aes_cbc_encrypted_cookie_using_64_byte_key_is_upgraded_to_authenticated_encrypted_cookie + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + @request.env["action_dispatch.encrypted_cookie_salt"] = + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT + + # Cookie generated with 64 bytes secret + message = ["566d4e75536d686e633246564e6b493062557079626c566d51574d30515430394c53315665564a694e4563786555744f57537454576b396a5a31566a626e52525054303d2d2d34663234333330623130623261306163363562316266323335396164666364613564643134623131"].pack("H*") + @request.headers["Cookie"] = "foo=#{message}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + cipher = "aes-256-gcm" + + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal) + + assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + def test_cookie_with_all_domain_option get :set_cookie_with_domain assert_response :success diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index 01c5ff1429..e7e8c82974 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -21,7 +21,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest end end - FIXTURE_PATH = File.dirname(__FILE__) + "/../../fixtures/multipart" + FIXTURE_PATH = File.expand_path("../../fixtures/multipart", __dir__) def teardown TestController.last_request_parameters = nil diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index 311b80ea0a..228135c547 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -54,6 +54,11 @@ module ActionDispatch assert_equal %w[rails adequate], s.keys end + def test_keys_with_deferred_loading + s = Session.create(store_with_data, req, {}) + assert_equal %w[sample_key], s.keys + end + def test_values s = Session.create(store, req, {}) s["rails"] = "ftw" @@ -61,6 +66,11 @@ module ActionDispatch assert_equal %w[ftw awesome], s.values end + def test_values_with_deferred_loading + s = Session.create(store_with_data, req, {}) + assert_equal %w[sample_value], s.values + end + def test_clear s = Session.create(store, req, {}) s["rails"] = "ftw" @@ -113,6 +123,14 @@ module ActionDispatch def delete_session(env, id, options); 123; end }.new end + + def store_with_data + Class.new { + def load_session(env); [1, { "sample_key" => "sample_value" }]; end + def session_exists?(env); true; end + def delete_session(env, id, options); 123; end + }.new + end end class SessionIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb index 1169bf0cdb..6721a388c1 100644 --- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb @@ -107,7 +107,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest query = [ "customers[boston][first][name]=David", "something_else=blah", - "logo=#{File.expand_path(__FILE__)}" + "logo=#{__FILE__}" ].join("&") expected = { "customers" => { @@ -118,7 +118,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest } }, "something_else" => "blah", - "logo" => File.expand_path(__FILE__), + "logo" => __FILE__, } assert_parses expected, query end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 2f9228a62d..899b27b962 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -110,8 +110,8 @@ class RequestIP < BaseRequestTest request.remote_ip } assert_match(/IP spoofing attack/, e.message) - assert_match(/HTTP_X_FORWARDED_FOR="1.1.1.1"/, e.message) - assert_match(/HTTP_CLIENT_IP="2.2.2.2"/, e.message) + assert_match(/HTTP_X_FORWARDED_FOR="1\.1\.1\.1"/, e.message) + assert_match(/HTTP_CLIENT_IP="2\.2\.2\.2"/, e.message) end test "remote ip with spoof detection disabled" do @@ -1098,6 +1098,19 @@ class RequestParameterFilter < BaseRequestTest end end + test "parameter filter should maintain hash with indifferent access" do + test_hashes = [ + [{ "foo" => "bar" }.with_indifferent_access, ["blah"]], + [{ "foo" => "bar" }.with_indifferent_access, []] + ] + + test_hashes.each do |before_filter, filter_words| + parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_words) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, + parameter_filter.filter(before_filter) + end + end + test "filtered_parameters returns params filtered" do request = stub_request( "action_dispatch.request.parameters" => { diff --git a/actionpack/test/dispatch/routing/route_set_test.rb b/actionpack/test/dispatch/routing/route_set_test.rb index ace35dda53..d6ecbda092 100644 --- a/actionpack/test/dispatch/routing/route_set_test.rb +++ b/actionpack/test/dispatch/routing/route_set_test.rb @@ -138,6 +138,15 @@ module ActionDispatch assert_equal "/a/users/1", url_helpers.user_path(1, foo: "a") end + test "implicit path components consistently return the same result" do + draw do + resources :users, to: SimpleApp.new("foo#index") + end + assert_equal "/users/1.json", url_helpers.user_path(1, :json) + assert_equal "/users/1.json", url_helpers.user_path(1, format: :json) + assert_equal "/users/1.json", url_helpers.user_path(1, :json) + end + private def draw(&block) @set.draw(&block) diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index d64917e0d3..32cd78e492 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -4419,7 +4419,7 @@ class TestInvalidUrls < ActionDispatch::IntegrationTest end end - test "invalid UTF-8 encoding returns a 400 Bad Request" do + test "invalid UTF-8 encoding is treated as ASCII 8BIT encode" do with_routing do |set| set.draw do get "/bar/:id", to: redirect("/foo/show/%{id}") @@ -4435,19 +4435,19 @@ class TestInvalidUrls < ActionDispatch::IntegrationTest end get "/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :not_found get "/foo/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :not_found get "/foo/show/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :ok get "/bar/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :redirect get "/foobar/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :ok end end end diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb index 814e1d707b..4a1b971da5 100644 --- a/actionpack/test/dispatch/system_testing/driver_test.rb +++ b/actionpack/test/dispatch/system_testing/driver_test.rb @@ -15,7 +15,21 @@ class DriverTest < ActiveSupport::TestCase assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) end - test "selenium? returns false if driver is poltergeist" do - assert_not ActionDispatch::SystemTesting::Driver.new(:poltergeist).send(:selenium?) + test "initializing the driver with a poltergeist" do + driver = ActionDispatch::SystemTesting::Driver.new(:poltergeist, screen_size: [1400, 1400], options: { js_errors: false }) + assert_equal :poltergeist, driver.instance_variable_get(:@name) + assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) + assert_equal ({ js_errors: false }), driver.instance_variable_get(:@options) + end + + test "initializing the driver with a webkit" do + driver = ActionDispatch::SystemTesting::Driver.new(:webkit, screen_size: [1400, 1400], options: { skip_image_loading: true }) + assert_equal :webkit, driver.instance_variable_get(:@name) + assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) + assert_equal ({ skip_image_loading: true }), driver.instance_variable_get(:@options) + end + + test "rack_test? returns false if driver is poltergeist" do + assert_not ActionDispatch::SystemTesting::Driver.new(:poltergeist).send(:rack_test?) end end diff --git a/actionpack/test/dispatch/system_testing/system_test_case_test.rb b/actionpack/test/dispatch/system_testing/system_test_case_test.rb index 33d98f924f..8f90e45f5f 100644 --- a/actionpack/test/dispatch/system_testing/system_test_case_test.rb +++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb @@ -19,3 +19,15 @@ class SetDriverToSeleniumTest < DrivenBySeleniumWithChrome assert_equal :selenium, Capybara.current_driver end end + +class SetHostTest < DrivenByRackTest + test "sets default host" do + assert_equal "http://127.0.0.1", Capybara.app_host + end + + test "overrides host" do + host! "http://example.com" + + assert_equal "http://example.com", Capybara.app_host + end +end diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb index 51680216e4..0074d2a314 100644 --- a/actionpack/test/dispatch/uploaded_file_test.rb +++ b/actionpack/test/dispatch/uploaded_file_test.rb @@ -13,6 +13,12 @@ module ActionDispatch assert_equal "foo", uf.original_filename end + def test_filename_is_different_object + file_str = "foo" + uf = Http::UploadedFile.new(filename: file_str, tempfile: Object.new) + assert_not_equal file_str.object_id , uf.original_filename.object_id + end + def test_filename_should_be_in_utf_8 uf = Http::UploadedFile.new(filename: "foo", tempfile: Object.new) assert_equal "UTF-8", uf.original_filename.encoding.to_s diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb index 9b88fa1f5a..dfcd423978 100644 --- a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb +++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb @@ -1,3 +1,3 @@ <body> -<%= cache do %><p>ERB</p><% end %> +<%= cache("fragment") do %><p>ERB</p><% end %> </body> diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder index efdcc28e0f..6599579740 100644 --- a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder +++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder @@ -1,5 +1,5 @@ xml.body do - cache do + cache("fragment") do xml.p "Builder" end end diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb index e523b74ae3..abf7017ce6 100644 --- a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb +++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb @@ -1,3 +1,3 @@ <body> -<%= cache do %><p>PHONE</p><% end %> +<%= cache("fragment") do %><p>PHONE</p><% end %> </body> diff --git a/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb b/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb index fa5e6bd318..1148d83ad7 100644 --- a/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb +++ b/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb @@ -1,3 +1,3 @@ Hello -<%= cache do %>This bit's fragment cached<% end %> +<%= cache "fragment" do %>This bit's fragment cached<% end %> <%= 'Ciao' %> diff --git a/actionpack/test/journey/gtg/transition_table_test.rb b/actionpack/test/journey/gtg/transition_table_test.rb index c7315c0338..889640fdd7 100644 --- a/actionpack/test/journey/gtg/transition_table_test.rb +++ b/actionpack/test/journey/gtg/transition_table_test.rb @@ -35,25 +35,25 @@ module ActionDispatch def test_simulate_gt sim = simulator_for ["/foo", "/bar"] - assert_match sim, "/foo" + assert_match_route sim, "/foo" end def test_simulate_gt_regexp sim = simulator_for [":foo"] - assert_match sim, "foo" + assert_match_route sim, "foo" end def test_simulate_gt_regexp_mix sim = simulator_for ["/get", "/:method/foo"] - assert_match sim, "/get" - assert_match sim, "/get/foo" + assert_match_route sim, "/get" + assert_match_route sim, "/get/foo" end def test_simulate_optional sim = simulator_for ["/foo(/bar)"] - assert_match sim, "/foo" - assert_match sim, "/foo/bar" - assert_no_match sim, "/foo/" + assert_match_route sim, "/foo" + assert_match_route sim, "/foo/bar" + assert_no_match_route sim, "/foo/" end def test_match_data @@ -65,11 +65,11 @@ module ActionDispatch sim = GTG::Simulator.new tt - match = sim.match "/get" - assert_equal [paths.first], match.memos + memos = sim.memos "/get" + assert_equal [paths.first], memos - match = sim.match "/get/foo" - assert_equal [paths.last], match.memos + memos = sim.memos "/get/foo" + assert_equal [paths.last], memos end def test_match_data_ambiguous @@ -86,8 +86,8 @@ module ActionDispatch builder = GTG::Builder.new ast sim = GTG::Simulator.new builder.transition_table - match = sim.match "/articles/new" - assert_equal [paths[1], paths[3]], match.memos + memos = sim.memos "/articles/new" + assert_equal [paths[1], paths[3]], memos end private @@ -109,6 +109,14 @@ module ActionDispatch def simulator_for(paths) GTG::Simulator.new tt(paths) end + + def assert_match_route(simulator, path) + assert simulator.memos(path), "Simulator should match #{path}." + end + + def assert_no_match_route(simulator, path) + assert_not simulator.memos(path) { nil }, "Simulator should not match #{path}." + end end end end diff --git a/actionpack/test/journey/router/utils_test.rb b/actionpack/test/journey/router/utils_test.rb index b77bf6628a..74277a4325 100644 --- a/actionpack/test/journey/router/utils_test.rb +++ b/actionpack/test/journey/router/utils_test.rb @@ -31,6 +31,11 @@ module ActionDispatch def test_normalize_path_uppercase assert_equal "/foo%AAbar%AAbaz", Utils.normalize_path("/foo%aabar%aabaz") end + + def test_normalize_path_maintains_string_encoding + path = "/foo%AAbar%AAbaz".b + assert_equal Encoding::ASCII_8BIT, Utils.normalize_path(path).encoding + end end end end diff --git a/actionpack/test/lib/controller/fake_models.rb b/actionpack/test/lib/controller/fake_models.rb index b768553e7a..ff37d85ed8 100644 --- a/actionpack/test/lib/controller/fake_models.rb +++ b/actionpack/test/lib/controller/fake_models.rb @@ -26,6 +26,10 @@ Customer = Struct.new(:name, :id) do def persisted? id.present? end + + def cache_key + "#{name}/#{id}" + end end Post = Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) do diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 1fc38e76b2..e618183129 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,6 +1,25 @@ -* Update distance_of_time_in_words helper to display better error messages +* Fix issues with scopes and engine on `current_page?` method. + + Fixes #29401. + + *Nikita Savrov* + +* Generate field ids in `collection_check_boxes` and `collection_radio_buttons`. + + This makes sure that the labels are linked up with the fields. + + Fixes #29014. + + *Yuji Yaginuma* + +* Add `:json` type to `auto_discovery_link_tag` to support [JSON Feeds](https://jsonfeed.org/version/1) + + *Mike Gunderloy* + +* Update `distance_of_time_in_words` helper to display better error messages for bad input. *Jay Hayes* + Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/actionview/CHANGELOG.md) for previous changes. diff --git a/actionview/Rakefile b/actionview/Rakefile index de588ad25c..0fc38e8db4 100644 --- a/actionview/Rakefile +++ b/actionview/Rakefile @@ -2,8 +2,6 @@ require "rake/testtask" require "fileutils" require "open3" -dir = File.dirname(__FILE__) - desc "Default Task" task default: :test @@ -95,7 +93,7 @@ namespace :assets do desc "Verify compiled Action View assets" task :verify do file = "lib/assets/compiled/rails-ujs.js" - pathname = Pathname.new("#{dir}/#{file}") + pathname = Pathname.new("#{__dir__}/#{file}") print "[verify] #{file} exists " if pathname.exist? @@ -113,13 +111,13 @@ namespace :assets do fail end - print "[verify] #{dir} can be required as a module " + print "[verify] #{__dir__} can be required as a module " js = <<-JS window = { Event: class {} } class Element {} - require('#{dir}') + require('#{__dir__}') JS - stdout, stderr, status = Open3.capture3("node", "--print", js) + _, stderr, status = Open3.capture3("node", "--print", js) if status.success? puts "[OK]" else @@ -130,7 +128,7 @@ namespace :assets do end task :lines do - load File.expand_path("..", File.dirname(__FILE__)) + "/tools/line_statistics" + load File.join(File.expand_path("..", __dir__), "/tools/line_statistics") files = FileList["lib/**/*.rb"] CodeTools::LineStatistics.new(files).print_loc end diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index cfaa5007a1..48e79c53ca 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -19,6 +19,11 @@ Gem::Specification.new do |s| s.require_path = "lib" s.requirements << "none" + s.metadata = { + "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actionview", + "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionview/CHANGELOG.md" + } + s.add_dependency "activesupport", version s.add_dependency "builder", "~> 3.1" diff --git a/actionview/app/assets/javascripts/README.md b/actionview/app/assets/javascripts/README.md index 92f3e8a3b3..f321b9f720 100644 --- a/actionview/app/assets/javascripts/README.md +++ b/actionview/app/assets/javascripts/README.md @@ -30,16 +30,27 @@ Run `yarn add rails-ujs` to install the rails-ujs package. Usage ------------ -Require `rails-ujs` into your application.js manifest. +Require `rails-ujs` in your application.js manifest. ```javascript //= require rails-ujs ``` +Usage with yarn +------------ + +When using with the Webpacker gem or your preferred JavaScript bundler, just +add the following to your main JS file and compile. + +```javascript +import Rails from 'rails-ujs'; +Rails.start() +``` + How to run tests ------------ -Run `bundle exec rake ujs:server` first, and then run the web tests by visiting [[http://localhost:4567]] in your browser. +Run `bundle exec rake ujs:server` first, and then run the web tests by visiting http://localhost:4567 in your browser. ## License rails-ujs is released under the [MIT License](MIT-LICENSE). diff --git a/actionview/app/assets/javascripts/rails-ujs/start.coffee b/actionview/app/assets/javascripts/rails-ujs/start.coffee index 5746a22287..55595ac96f 100644 --- a/actionview/app/assets/javascripts/rails-ujs/start.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/start.coffee @@ -9,7 +9,7 @@ } = Rails # For backward compatibility -if jQuery? and not jQuery.rails +if jQuery? and jQuery.ajax? and not jQuery.rails jQuery.rails = Rails jQuery.ajaxPrefilter (options, originalOptions, xhr) -> CSRFProtection(xhr) unless options.crossDomain diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee index 26df7b9a3f..a653d3af3d 100644 --- a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee @@ -14,7 +14,7 @@ AcceptHeaders = Rails.ajax = (options) -> options = prepareOptions(options) xhr = createXHR options, -> - response = processResponse(xhr.response, xhr.getResponseHeader('Content-Type')) + response = processResponse(xhr.response ? xhr.responseText, xhr.getResponseHeader('Content-Type')) if xhr.status // 100 == 2 options.success?(response, xhr.statusText, xhr) else diff --git a/actionview/bin/test b/actionview/bin/test index a7beb14b27..470ce93f10 100755 --- a/actionview/bin/test +++ b/actionview/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb index ca586f1d52..99c5b831b5 100644 --- a/actionview/lib/action_view.rb +++ b/actionview/lib/action_view.rb @@ -92,5 +92,5 @@ end require "active_support/core_ext/string/output_safety" ActiveSupport.on_load(:i18n) do - I18n.load_path << "#{File.dirname(__FILE__)}/action_view/locale/en.yml" + I18n.load_path << File.expand_path("action_view/locale/en.yml", __dir__) end diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index 5387174467..1808553239 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -140,30 +140,25 @@ module ActionView #:nodoc: include Helpers, ::ERB::Util, Context # Specify the proc used to decorate input tags that refer to attributes with errors. - cattr_accessor :field_error_proc - @@field_error_proc = Proc.new { |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe } + cattr_accessor :field_error_proc, default: Proc.new { |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe } # How to complete the streaming when an exception occurs. # This is our best guess: first try to close the attribute, then the tag. - cattr_accessor :streaming_completion_on_exception - @@streaming_completion_on_exception = %("><script>window.location = "/500.html"</script></html>) + cattr_accessor :streaming_completion_on_exception, default: %("><script>window.location = "/500.html"</script></html>) # Specify whether rendering within namespaced controllers should prefix # the partial paths for ActiveModel objects with the namespace. # (e.g., an Admin::PostsController would render @post using /admin/posts/_post.erb) - cattr_accessor :prefix_partial_path_with_controller_namespace - @@prefix_partial_path_with_controller_namespace = true + cattr_accessor :prefix_partial_path_with_controller_namespace, default: true # Specify default_formats that can be rendered. cattr_accessor :default_formats # Specify whether an error should be raised for missing translations - cattr_accessor :raise_on_missing_translations - @@raise_on_missing_translations = false + cattr_accessor :raise_on_missing_translations, default: false # Specify whether submit_tag should automatically disable on click - cattr_accessor :automatically_disable_submit_tag - @@automatically_disable_submit_tag = true + cattr_accessor :automatically_disable_submit_tag, default: true class_attribute :_routes class_attribute :logger @@ -207,6 +202,7 @@ module ActionView #:nodoc: @view_renderer = ActionView::Renderer.new(lookup_context) end + @cache_hit = {} assign(assigns) assign_controller(controller) _prepare_context diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 750f96f29e..c21fe782c6 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -122,9 +122,9 @@ module ActionView end # Returns a link tag that browsers and feed readers can use to auto-detect - # an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or - # <tt>:atom</tt>. Control the link options in url_for format using the - # +url_options+. You can modify the LINK tag itself in +tag_options+. + # an RSS, Atom, or JSON feed. The +type+ can be <tt>:rss</tt> (default), + # <tt>:atom</tt>, or <tt>:json</tt>. Control the link options in url_for format + # using the +url_options+. You can modify the LINK tag itself in +tag_options+. # # ==== Options # @@ -138,6 +138,8 @@ module ActionView # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" /> # auto_discovery_link_tag(:atom) # # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" /> + # auto_discovery_link_tag(:json) + # # => <link rel="alternate" type="application/json" title="JSON" href="http://www.currenthost.com/controller/action" /> # auto_discovery_link_tag(:rss, {action: "feed"}) # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" /> # auto_discovery_link_tag(:rss, {action: "feed"}, {title: "My RSS"}) @@ -147,8 +149,8 @@ module ActionView # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"}) # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" /> def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {}) - if !(type == :rss || type == :atom) && tag_options[:type].blank? - raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.") + if !(type == :rss || type == :atom || type == :json) && tag_options[:type].blank? + raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss, :atom, or :json.") end tag( diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 15ab7e304f..b7c7324f31 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -8,10 +8,9 @@ module ActionView # fragments, and so on. This method takes a block that contains # the content you wish to cache. # - # The best way to use this is by doing key-based cache expiration - # on top of a cache store like Memcached that'll automatically - # kick out old entries. For more on key-based expiration, see: - # http://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works + # The best way to use this is by doing recyclable key-based cache expiration + # on top of a cache store like Memcached or Redis that'll automatically + # kick out old entries. # # When using this method, you list the cache dependency as the name of the cache, like so: # @@ -23,10 +22,14 @@ module ActionView # This approach will assume that when a new topic is added, you'll touch # the project. The cache key generated from this call will be something like: # - # views/projects/123-20120806214154/7a1156131a6928cb0026877f8b749ac9 - # ^class ^id ^updated_at ^template tree digest + # views/template/action.html.erb:7a1156131a6928cb0026877f8b749ac9/projects/123 + # ^template path ^template tree digest ^class ^id # - # The cache is thus automatically bumped whenever the project updated_at is touched. + # This cache key is stable, but it's combined with a cache version derived from the project + # record. When the project updated_at is touched, the #cache_version changes, even + # if the key stays stable. This means that unlike a traditional key-based cache expiration + # approach, you won't be generating cache trash, unused keys, simply because the dependent + # record is updated. # # If your template cache depends on multiple sources (try to avoid this to keep things simple), # you can name all these dependencies as part of an array: @@ -211,16 +214,19 @@ module ActionView end end - attr_reader :cache_hit # :nodoc: - private def fragment_name_with_digest(name, virtual_path) virtual_path ||= @virtual_path + if virtual_path name = controller.url_for(name).split("://").last if name.is_a?(Hash) - digest = Digestor.digest name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies - [ name, digest ] + + if digest = Digestor.digest(name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies).presence + [ "#{virtual_path}:#{digest}", name ] + else + [ virtual_path, name ] + end else name end @@ -228,10 +234,10 @@ module ActionView def fragment_for(name = {}, options = nil, &block) if content = read_fragment_for(name, options) - @cache_hit = true + @view_renderer.cache_hits[@virtual_path] = :hit if defined?(@view_renderer) content else - @cache_hit = false + @view_renderer.cache_hits[@virtual_path] = :miss if defined?(@view_renderer) write_fragment_for(name, options, &block) end end diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 3eafe0028e..4b2561e53d 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -474,7 +474,7 @@ module ActionView end private :apply_form_for_options! - mattr_accessor(:form_with_generates_remote_forms) { true } + mattr_accessor :form_with_generates_remote_forms, default: true # Creates a form tag based on mixing URLs, scopes, or models. # @@ -1606,14 +1606,15 @@ module ActionView include ModelNaming # The methods which wrap a form helper call. - class_attribute :field_helpers - self.field_helpers = [:fields_for, :fields, :label, :text_field, :password_field, - :hidden_field, :file_field, :text_area, :check_box, - :radio_button, :color_field, :search_field, - :telephone_field, :phone_field, :date_field, - :time_field, :datetime_field, :datetime_local_field, - :month_field, :week_field, :url_field, :email_field, - :number_field, :range_field] + class_attribute :field_helpers, default: [ + :fields_for, :fields, :label, :text_field, :password_field, + :hidden_field, :file_field, :text_area, :check_box, + :radio_button, :color_field, :search_field, + :telephone_field, :phone_field, :date_field, + :time_field, :datetime_field, :datetime_local_field, + :month_field, :week_field, :url_field, :email_field, + :number_field, :range_field + ] attr_accessor :object_name, :object, :options @@ -2317,8 +2318,6 @@ module ActionView end ActiveSupport.on_load(:action_view) do - cattr_accessor(:default_form_builder, instance_writer: false, instance_reader: false) do - ::ActionView::Helpers::FormBuilder - end + cattr_accessor :default_form_builder, instance_writer: false, instance_reader: false, default: ::ActionView::Helpers::FormBuilder end end diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index aa420c4b66..0895533a60 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -149,7 +149,7 @@ module ActionView end value = options.fetch(:selected) { value(object) } - select = content_tag("select", add_options(option_tags, options, value), html_options.except!("skip_default_ids", "allow_method_names_outside_object")) + select = content_tag("select", add_options(option_tags, options, value), html_options) if html_options["multiple"] && options.fetch(:include_hidden, true) tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "") + select diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb index 7252d4f2d9..e02b7bdb2e 100644 --- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -10,6 +10,7 @@ module ActionView def check_box(extra_html_options = {}) html_options = extra_html_options.merge(@input_html_options) html_options[:multiple] = true + html_options[:skip_default_ids] = false @template_object.check_box(@object_name, @method_name, html_options, @value, nil) end end diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb index a5f72af9ff..f085a5fb73 100644 --- a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb +++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb @@ -9,6 +9,7 @@ module ActionView class RadioButtonBuilder < Builder # :nodoc: def radio_button(extra_html_options = {}) html_options = extra_html_options.merge(@input_html_options) + html_options[:skip_default_ids] = false @template_object.radio_button(@object_name, @method_name, @value, html_options) end end diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb index 667c7e945a..380f7a8c4e 100644 --- a/actionview/lib/action_view/helpers/tags/select.rb +++ b/actionview/lib/action_view/helpers/tags/select.rb @@ -6,7 +6,7 @@ module ActionView @choices = block_given? ? template_object.capture { yield || "" } : choices @choices = @choices.to_a if @choices.is_a?(Range) - @html_options = html_options + @html_options = html_options.except(:skip_default_ids, :allow_method_names_outside_object) super(object_name, method_name, template_object, options) end @@ -33,7 +33,7 @@ module ActionView # [nil, []] # { nil => [] } def grouped_choices? - !@choices.empty? && @choices.first.respond_to?(:last) && Array === @choices.first.last + !@choices.blank? && @choices.first.respond_to?(:last) && Array === @choices.first.last end end end diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 47ed41a129..cc928f2b7a 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -11,8 +11,7 @@ module ActionView include TagHelper included do - mattr_accessor :debug_missing_translation - self.debug_missing_translation = true + mattr_accessor :debug_missing_translation, default: true end # Delegates to <tt>I18n#translate</tt> but also performs three additional diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index a6857101b9..b78c367921 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -552,7 +552,10 @@ module ActionView request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path request_uri = URI.parser.unescape(request_uri).force_encoding(Encoding::BINARY) - url_string.chomp!("/") if url_string.start_with?("/") && url_string != "/" + if url_string.start_with?("/") && url_string != "/" + url_string.chomp!("/") + request_uri.chomp!("/") + end if %r{^\w+://}.match?(url_string) url_string == "#{request.protocol}#{request.host_with_port}#{request_uri}" diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index 81feb90486..ab8409e8d0 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -204,9 +204,9 @@ module ActionView include ActionView::Rendering included do - class_attribute :_layout, :_layout_conditions, instance_accessor: false - self._layout = nil - self._layout_conditions = {} + class_attribute :_layout, instance_accessor: false + class_attribute :_layout_conditions, instance_accessor: false, default: {} + _write_layout_method end diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index d03e1a51b8..ab8ec0aa42 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -25,7 +25,7 @@ module ActionView message = " Rendered #{from_rails_root(event.payload[:identifier])}" message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] message << " (#{event.duration.round(1)}ms)" - message << " #{cache_message(event.payload)}" if event.payload.key?(:cache_hit) + message << " #{cache_message(event.payload)}" unless event.payload[:cache_hit].nil? message end end @@ -73,9 +73,10 @@ module ActionView end def cache_message(payload) # :doc: - if payload[:cache_hit] + case payload[:cache_hit] + when :hit "[cache hit]" - else + when :miss "[cache miss]" end end diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index f385a7cd04..b7dbb38369 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -14,11 +14,9 @@ module ActionView class LookupContext #:nodoc: attr_accessor :prefixes, :rendered_format - mattr_accessor :fallbacks - @@fallbacks = FallbackFileSystemResolver.instances + mattr_accessor :fallbacks, default: FallbackFileSystemResolver.instances - mattr_accessor :registered_details - self.registered_details = [] + mattr_accessor :registered_details, default: [] def self.register_detail(name, &block) registered_details << name diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 647b15ea94..1f8f997a2d 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -344,7 +344,7 @@ module ActionView end content = layout.render(view, locals) { content } if layout - payload[:cache_hit] = view.cache_hit + payload[:cache_hit] = view.view_renderer.cache_hits[@template.virtual_path] content end end diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index 1fbe209200..32663fb80d 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -5,7 +5,7 @@ module ActionView included do # Fallback cache store if Action View is used without Rails. # Otherwise overridden in Railtie to use Rails.cache. - mattr_accessor(:collection_cache) { ActiveSupport::Cache::MemoryStore.new } + mattr_accessor :collection_cache, default: ActiveSupport::Cache::MemoryStore.new end private @@ -38,7 +38,7 @@ module ActionView end def expanded_cache_key(key) - key = @view.fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path)) + key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path)) key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. end diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb index 2a3b89aebf..bcdeb85d30 100644 --- a/actionview/lib/action_view/renderer/renderer.rb +++ b/actionview/lib/action_view/renderer/renderer.rb @@ -46,5 +46,9 @@ module ActionView def render_partial(context, options, &block) #:nodoc: PartialRenderer.new(@lookup_context).render(context, options, block) end + + def cache_hits # :nodoc: + @cache_hits ||= {} + end end end diff --git a/actionview/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb index e99b921cb7..67ad78133d 100644 --- a/actionview/lib/action_view/template/handlers/builder.rb +++ b/actionview/lib/action_view/template/handlers/builder.rb @@ -1,9 +1,7 @@ module ActionView module Template::Handlers class Builder - # Default format used by Builder. - class_attribute :default_format - self.default_format = :xml + class_attribute :default_format, default: :xml def call(template) require_engine @@ -14,7 +12,6 @@ module ActionView end private - def require_engine # :doc: @required ||= begin require "builder" diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 58c7fd1a88..48c2e22a89 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -9,16 +9,13 @@ module ActionView # Specify trim mode for the ERB compiler. Defaults to '-'. # See ERB documentation for suitable values. - class_attribute :erb_trim_mode - self.erb_trim_mode = "-" + class_attribute :erb_trim_mode, default: "-" # Default implementation used. - class_attribute :erb_implementation - self.erb_implementation = Erubi + class_attribute :erb_implementation, default: Erubi # Do not escape templates of these mime types. - class_attribute :escape_whitelist - self.escape_whitelist = ["text/plain"] + class_attribute :escape_whitelist, default: ["text/plain"] ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*") diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index d3905b5f23..75ea4d31f5 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -125,8 +125,7 @@ module ActionView end end - cattr_accessor :caching - self.caching = true + cattr_accessor :caching, default: true class << self alias :caching? :caching diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index ae4fec4337..80403799ab 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -71,7 +71,7 @@ module ActionView def helper_method(*methods) # Almost a duplicate from ActionController::Helpers methods.flatten.each do |method| - _helpers.module_eval <<-end_eval + _helpers.module_eval <<-end_eval, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def current_user(*args, &block) _test_case.send(%(#{method}), *args, &block) # _test_case.send(%(current_user), *args, &block) end # end diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb index f0fe6831fa..938f0fc17f 100644 --- a/actionview/lib/action_view/view_paths.rb +++ b/actionview/lib/action_view/view_paths.rb @@ -3,9 +3,7 @@ module ActionView extend ActiveSupport::Concern included do - class_attribute :_view_paths - self._view_paths = ActionView::PathSet.new - _view_paths.freeze + class_attribute :_view_paths, default: ActionView::PathSet.new.freeze end delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=, diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index dde66a7ba0..a7d706c5e1 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -1,8 +1,8 @@ -$:.unshift(File.dirname(__FILE__) + "/lib") -$:.unshift(File.dirname(__FILE__) + "/fixtures/helpers") -$:.unshift(File.dirname(__FILE__) + "/fixtures/alternate_helpers") +$:.unshift File.expand_path("lib", __dir__) +$:.unshift File.expand_path("fixtures/helpers", __dir__) +$:.unshift File.expand_path("fixtures/alternate_helpers", __dir__) -ENV["TMPDIR"] = File.join(File.dirname(__FILE__), "tmp") +ENV["TMPDIR"] = File.expand_path("tmp", __dir__) require "active_support/core_ext/kernel/reporting" @@ -47,7 +47,7 @@ I18n.backend.store_translations "da", {} I18n.backend.store_translations "pt-BR", {} ORIGINAL_LOCALES = I18n.available_locales.map(&:to_s).sort -FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), "fixtures") +FIXTURE_LOAD_PATH = File.expand_path("fixtures", __dir__) module RenderERBUtils def view @@ -133,7 +133,7 @@ class BasicController def config @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config).tap do |config| # VIEW TODO: View tests should not require a controller - public_dir = File.expand_path("../fixtures/public", __FILE__) + public_dir = File.expand_path("fixtures/public", __dir__) config.assets_dir = public_dir config.javascripts_dir = "#{public_dir}/javascripts" config.stylesheets_dir = "#{public_dir}/stylesheets" @@ -196,7 +196,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase end def with_autoload_path(path) - path = File.join(File.dirname(__FILE__), "fixtures", path) + path = File.join(File.expand_path("fixtures", __dir__), path) if ActiveSupport::Dependencies.autoload_paths.include?(path) yield else diff --git a/actionview/test/actionpack/abstract/abstract_controller_test.rb b/actionview/test/actionpack/abstract/abstract_controller_test.rb index a2cd3deb58..8f65a61493 100644 --- a/actionview/test/actionpack/abstract/abstract_controller_test.rb +++ b/actionview/test/actionpack/abstract/abstract_controller_test.rb @@ -42,7 +42,7 @@ module AbstractController super end - append_view_path File.expand_path(File.join(File.dirname(__FILE__), "views")) + append_view_path File.expand_path("views", __dir__) end class Me2 < RenderingController @@ -152,7 +152,7 @@ module AbstractController class OverridingLocalPrefixes < AbstractController::Base include AbstractController::Rendering include ActionView::Rendering - append_view_path File.expand_path(File.join(File.dirname(__FILE__), "views")) + append_view_path File.expand_path("views", __dir__) def index render diff --git a/actionview/test/actionpack/abstract/helper_test.rb b/actionview/test/actionpack/abstract/helper_test.rb index 83237518d7..13922e4485 100644 --- a/actionview/test/actionpack/abstract/helper_test.rb +++ b/actionview/test/actionpack/abstract/helper_test.rb @@ -1,6 +1,6 @@ require "abstract_unit" -ActionController::Base.helpers_path = File.expand_path("../../../fixtures/helpers", __FILE__) +ActionController::Base.helpers_path = File.expand_path("../../fixtures/helpers", __dir__) module AbstractController module Testing @@ -51,7 +51,7 @@ module AbstractController class AbstractInvalidHelpers < AbstractHelpers include ActionController::Helpers - path = File.expand_path("../../../fixtures/helpers_missing", __FILE__) + path = File.expand_path("../../fixtures/helpers_missing", __dir__) $:.unshift(path) self.helpers_path = path end diff --git a/actionview/test/actionpack/controller/capture_test.rb b/actionview/test/actionpack/controller/capture_test.rb index f0ae609845..cc3a23c60c 100644 --- a/actionview/test/actionpack/controller/capture_test.rb +++ b/actionview/test/actionpack/controller/capture_test.rb @@ -2,7 +2,7 @@ require "abstract_unit" require "active_support/logger" class CaptureController < ActionController::Base - self.view_paths = [ File.dirname(__FILE__) + "/../../fixtures/actionpack" ] + self.view_paths = [ File.expand_path("../../fixtures/actionpack", __dir__) ] def self.controller_name; "test"; end def self.controller_path; "test"; end diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index b79835ff34..b3e0329f57 100644 --- a/actionview/test/actionpack/controller/layout_test.rb +++ b/actionview/test/actionpack/controller/layout_test.rb @@ -5,7 +5,7 @@ require "active_support/core_ext/array/extract_options" # method has access to the view_paths array when looking for a layout to automatically assign. old_load_paths = ActionController::Base.view_paths -ActionController::Base.view_paths = [ File.dirname(__FILE__) + "/../../fixtures/actionpack/layout_tests/" ] +ActionController::Base.view_paths = [ File.expand_path("../../fixtures/actionpack/layout_tests", __dir__) ] class LayoutTest < ActionController::Base def self.controller_path; "views" end @@ -96,7 +96,7 @@ class StreamingLayoutController < LayoutTest end class AbsolutePathLayoutController < LayoutTest - layout File.expand_path(File.expand_path(__FILE__) + "/../../../fixtures/actionpack/layout_tests/layouts/layout_test") + layout File.expand_path("../../fixtures/actionpack/layout_tests/layouts/layout_test", __dir__) end class HasOwnLayoutController < LayoutTest @@ -117,7 +117,7 @@ end class PrependsViewPathController < LayoutTest def hello - prepend_view_path File.dirname(__FILE__) + "/../../fixtures/actionpack/layout_tests/alt/" + prepend_view_path File.expand_path("../../fixtures/actionpack/layout_tests/alt", __dir__) render layout: "alt" end end diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index 51ec8899b1..6528169312 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -56,7 +56,7 @@ class TestController < ApplicationController end def hello_world_file - render file: File.expand_path("../../../fixtures/actionpack/hello", __FILE__), formats: [:html] + render file: File.expand_path("../../fixtures/actionpack/hello", __dir__), formats: [:html] end # :ported: @@ -125,7 +125,7 @@ class TestController < ApplicationController # :ported: def render_file_with_instance_variables @secret = "in the sauce" - path = File.join(File.dirname(__FILE__), "../../fixtures/test/render_file_with_ivar") + path = File.expand_path("../../fixtures/test/render_file_with_ivar", __dir__) render file: path end @@ -142,21 +142,21 @@ class TestController < ApplicationController def render_file_using_pathname @secret = "in the sauce" - render file: Pathname.new(File.dirname(__FILE__)).join("..", "..", "fixtures", "test", "dot.directory", "render_file_with_ivar") + render file: Pathname.new(__dir__).join("..", "..", "fixtures", "test", "dot.directory", "render_file_with_ivar") end def render_file_from_template @secret = "in the sauce" - @path = File.expand_path(File.join(File.dirname(__FILE__), "../../fixtures/test/render_file_with_ivar")) + @path = File.expand_path("../../fixtures/test/render_file_with_ivar", __dir__) end def render_file_with_locals - path = File.join(File.dirname(__FILE__), "../../fixtures/test/render_file_with_locals") + path = File.expand_path("../../fixtures/test/render_file_with_locals", __dir__) render file: path, locals: { secret: "in the sauce" } end def render_file_as_string_with_locals - path = File.expand_path(File.join(File.dirname(__FILE__), "../../fixtures/test/render_file_with_locals")) + path = File.expand_path("../../fixtures/test/render_file_with_locals", __dir__) render file: path, locals: { secret: "in the sauce" } end diff --git a/actionview/test/active_record_unit.rb b/actionview/test/active_record_unit.rb index 7f94b7ebb4..901c0e2b3e 100644 --- a/actionview/test/active_record_unit.rb +++ b/actionview/test/active_record_unit.rb @@ -13,7 +13,7 @@ end # Try to grab AR unless defined?(ActiveRecord) && defined?(FixtureSet) begin - PATH_TO_AR = "#{File.dirname(__FILE__)}/../../activerecord/lib" + PATH_TO_AR = File.expand_path("../../activerecord/lib", __dir__) raise LoadError, "#{PATH_TO_AR} doesn't exist" unless File.directory?(PATH_TO_AR) $LOAD_PATH.unshift PATH_TO_AR require "active_record" @@ -58,13 +58,13 @@ class ActiveRecordTestConnector # Load actionpack sqlite3 tables def load_schema - File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(";").each do |sql| + File.read(File.expand_path("fixtures/db_definitions/sqlite.sql", __dir__)).split(";").each do |sql| ActiveRecord::Base.connection.execute(sql) unless sql.blank? end end def require_fixture_models - Dir.glob(File.dirname(__FILE__) + "/fixtures/*.rb").each { |f| require f } + Dir.glob(File.expand_path("fixtures/*.rb", __dir__)).each { |f| require f } end end end diff --git a/actionview/test/activerecord/controller_runtime_test.rb b/actionview/test/activerecord/controller_runtime_test.rb index 590559f592..1cec5072c0 100644 --- a/actionview/test/activerecord/controller_runtime_test.rb +++ b/actionview/test/activerecord/controller_runtime_test.rb @@ -67,7 +67,7 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase wait assert_equal 2, @logger.logged(:info).size - assert_match(/\(Views: [\d.]+ms \| ActiveRecord: 0.0ms\)/, @logger.logged(:info)[1]) + assert_match(/\(Views: [\d.]+ms \| ActiveRecord: 0\.0ms\)/, @logger.logged(:info)[1]) end def test_log_with_active_record_when_post diff --git a/actionview/test/activerecord/relation_cache_test.rb b/actionview/test/activerecord/relation_cache_test.rb index 43f7242ee9..d12c426586 100644 --- a/actionview/test/activerecord/relation_cache_test.rb +++ b/actionview/test/activerecord/relation_cache_test.rb @@ -4,13 +4,17 @@ class RelationCacheTest < ActionView::TestCase tests ActionView::Helpers::CacheHelper def setup - @virtual_path = "path" + view_paths = ActionController::Base.view_paths + lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"]) + @view_renderer = ActionView::Renderer.new(lookup_context) + @virtual_path = "path" + controller.cache_store = ActiveSupport::Cache::MemoryStore.new end def test_cache_relation_other cache(Project.all) { concat("Hello World") } - assert_equal "Hello World", controller.cache_store.read("views/projects-#{Project.count}/") + assert_equal "Hello World", controller.cache_store.read("views/path/projects-#{Project.count}") end def view_cache_dependencies; end diff --git a/actionview/test/fixtures/comments/empty.html+grid.erb b/actionview/test/fixtures/comments/empty.html+grid.erb new file mode 100644 index 0000000000..dc3fa32a81 --- /dev/null +++ b/actionview/test/fixtures/comments/empty.html+grid.erb @@ -0,0 +1 @@ +<h1>No Comment</h1> diff --git a/actionview/test/fixtures/test/_cached_nested_cached_customer.erb b/actionview/test/fixtures/test/_cached_nested_cached_customer.erb new file mode 100644 index 0000000000..01bf025cd3 --- /dev/null +++ b/actionview/test/fixtures/test/_cached_nested_cached_customer.erb @@ -0,0 +1,3 @@ +<% cache cached_customer do %> + <%= render partial: "test/cached_customer", locals: { cached_customer: cached_customer } %> +<% end %> diff --git a/actionview/test/fixtures/test/_nested_cached_customer.erb b/actionview/test/fixtures/test/_nested_cached_customer.erb new file mode 100644 index 0000000000..f43adc94c9 --- /dev/null +++ b/actionview/test/fixtures/test/_nested_cached_customer.erb @@ -0,0 +1 @@ +<%= render partial: "test/cached_customer", locals: { cached_customer: cached_customer } %> diff --git a/actionview/test/fixtures/test/_partial_with_variants.html+grid.erb b/actionview/test/fixtures/test/_partial_with_variants.html+grid.erb new file mode 100644 index 0000000000..225363c8c3 --- /dev/null +++ b/actionview/test/fixtures/test/_partial_with_variants.html+grid.erb @@ -0,0 +1 @@ +<h1>Partial with variants</h1> diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 07a6452cc1..6093a4e660 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -53,6 +53,7 @@ class AssetTagHelperTest < ActionView::TestCase %(auto_discovery_link_tag) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />), %(auto_discovery_link_tag(:rss)) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />), %(auto_discovery_link_tag(:atom)) => %(<link href="http://www.example.com" rel="alternate" title="ATOM" type="application/atom+xml" />), + %(auto_discovery_link_tag(:json)) => %(<link href="http://www.example.com" rel="alternate" title="JSON" type="application/json" />), %(auto_discovery_link_tag(:rss, :action => "feed")) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />), %(auto_discovery_link_tag(:rss, "http://localhost/feed")) => %(<link href="http://localhost/feed" rel="alternate" title="RSS" type="application/rss+xml" />), %(auto_discovery_link_tag(:rss, "//localhost/feed")) => %(<link href="//localhost/feed" rel="alternate" title="RSS" type="application/rss+xml" />), @@ -709,13 +710,13 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase def test_should_wildcard_asset_host @controller.config.asset_host = "http://a%d.example.com" - assert_match(%r(http://a[0123].example.com), compute_asset_host("foo")) + assert_match(%r(http://a[0123]\.example\.com), compute_asset_host("foo")) end def test_should_wildcard_asset_host_between_zero_and_four @controller.config.asset_host = "http://a%d.example.com" - assert_match(%r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_path("xml.png")) - assert_match(%r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_url("xml.png")) + assert_match(%r(http://a[0123]\.example\.com/collaboration/hieraki/images/xml\.png), image_path("xml.png")) + assert_match(%r(http://a[0123]\.example\.com/collaboration/hieraki/images/xml\.png), image_url("xml.png")) end def test_asset_host_without_protocol_should_be_protocol_relative diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb index 1245a1a966..7304b769a4 100644 --- a/actionview/test/template/atom_feed_helper_test.rb +++ b/actionview/test/template/atom_feed_helper_test.rb @@ -301,8 +301,8 @@ class AtomFeedTest < ActionController::TestCase with_restful_routing(:scrolls) do get :index, params: { id: "feed_with_atomPub_namespace" } assert_match %r{xml:lang="en-US"}, @response.body - assert_match %r{xmlns="http://www.w3.org/2005/Atom"}, @response.body - assert_match %r{xmlns:app="http://www.w3.org/2007/app"}, @response.body + assert_match %r{xmlns="http://www\.w3\.org/2005/Atom"}, @response.body + assert_match %r{xmlns:app="http://www\.w3\.org/2007/app"}, @response.body end end @@ -319,7 +319,7 @@ class AtomFeedTest < ActionController::TestCase with_restful_routing(:scrolls) do get :index, params: { id: "feed_with_xml_processing_instructions" } assert_match %r{<\?xml-stylesheet [^\?]*type="text/css"}, @response.body - assert_match %r{<\?xml-stylesheet [^\?]*href="t.css"}, @response.body + assert_match %r{<\?xml-stylesheet [^\?]*href="t\.css"}, @response.body end end @@ -334,7 +334,7 @@ class AtomFeedTest < ActionController::TestCase def test_feed_xhtml with_restful_routing(:scrolls) do get :index, params: { id: "feed_with_xhtml_content" } - assert_match %r{xmlns="http://www.w3.org/1999/xhtml"}, @response.body + assert_match %r{xmlns="http://www\.w3\.org/1999/xhtml"}, @response.body assert_select "summary", text: /Something Boring/ assert_select "summary", text: /after 2/ end diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index a259752d6a..b667303318 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -138,11 +138,19 @@ class DateHelperTest < ActionView::TestCase assert_equal "10 minutes", distance_of_time_in_words(Time.at(600), 0) end - def test_distance_in_words_with_mathn_required - # test we avoid Integer#/ (redefined by mathn) - silence_warnings { require "mathn" } + def test_distance_in_words_doesnt_use_the_quotient_operator + rubinius_skip "Date is written in Ruby and relies on Fixnum#/" + jruby_skip "Date is written in Ruby and relies on Fixnum#/" + + klass = RUBY_VERSION > "2.4" ? Integer : Fixnum + + # Make sure that we avoid {Integer,Fixnum}#/ (redefined by mathn) + klass.send :private, :/ + from = Time.utc(2004, 6, 6, 21, 45, 0) assert_distance_of_time_in_words(from) + ensure + klass.send :public, :/ end def test_time_ago_in_words_passes_include_seconds diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index e225c3de09..de04f3f25d 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -14,7 +14,7 @@ class FixtureTemplate end class FixtureFinder < ActionView::LookupContext - FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor" + FIXTURES_DIR = File.expand_path("../fixtures/digestor", __dir__) def initialize(details = {}) super(ActionView::PathSet.new(["digestor", "digestor/api"]), details, []) diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index bff0643fb0..ecdd5ce672 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -403,9 +403,9 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input type='hidden' name='post[active]' value='' />" \ - "<input name='post[active]' type='radio' value='true' />" \ + "<input name='post[active]' type='radio' value='true' id='post_active_true' />" \ "<label for='post_active_true'>true</label>" \ - "<input checked='checked' name='post[active]' type='radio' value='false' />" \ + "<input checked='checked' name='post[active]' type='radio' value='false' id='post_active_false' />" \ "<label for='post_active_false'>false</label>" end @@ -426,10 +426,10 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input type='hidden' name='post[active]' value='' />" \ "<label for='post_active_true'>" \ - "<input name='post[active]' type='radio' value='true' />" \ + "<input name='post[active]' type='radio' value='true' id='post_active_true' />" \ "true</label>" \ "<label for='post_active_false'>" \ - "<input checked='checked' name='post[active]' type='radio' value='false' />" \ + "<input checked='checked' name='post[active]' type='radio' value='false' id='post_active_false' />" \ "false</label>" end @@ -452,10 +452,10 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input type='hidden' name='post[active]' value='' />" \ "<label for='post_active_true'>" \ - "<input name='post[active]' type='radio' value='true' />" \ + "<input name='post[active]' type='radio' value='true' id='post_active_true' />" \ "true</label>" \ "<label for='post_active_false'>" \ - "<input checked='checked' name='post[active]' type='radio' value='false' />" \ + "<input checked='checked' name='post[active]' type='radio' value='false' id='post_active_false' />" \ "false</label>" \ "<input name='post[id]' type='hidden' value='1' />" end @@ -473,9 +473,9 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input type='hidden' name='post[1][active]' value='' />" \ - "<input name='post[1][active]' type='radio' value='true' />" \ + "<input name='post[1][active]' type='radio' value='true' id='post_1_active_true' />" \ "<label for='post_1_active_true'>true</label>" \ - "<input checked='checked' name='post[1][active]' type='radio' value='false' />" \ + "<input checked='checked' name='post[1][active]' type='radio' value='false' id='post_1_active_false' />" \ "<label for='post_1_active_false'>false</label>" end @@ -492,11 +492,11 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input name='post[tag_ids][]' type='hidden' value='' />" \ - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' id='post_tag_ids_1' />" \ "<label for='post_tag_ids_1'>Tag 1</label>" \ - "<input name='post[tag_ids][]' type='checkbox' value='2' />" \ + "<input name='post[tag_ids][]' type='checkbox' value='2' id='post_tag_ids_2' />" \ "<label for='post_tag_ids_2'>Tag 2</label>" \ - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' id='post_tag_ids_3' />" \ "<label for='post_tag_ids_3'>Tag 3</label>" end @@ -517,13 +517,13 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input name='post[tag_ids][]' type='hidden' value='' />" \ "<label for='post_tag_ids_1'>" \ - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' id='post_tag_ids_1' />" \ "Tag 1</label>" \ "<label for='post_tag_ids_2'>" \ - "<input name='post[tag_ids][]' type='checkbox' value='2' />" \ + "<input name='post[tag_ids][]' type='checkbox' value='2' id='post_tag_ids_2' />" \ "Tag 2</label>" \ "<label for='post_tag_ids_3'>" \ - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' id='post_tag_ids_3' />" \ "Tag 3</label>" end @@ -547,13 +547,13 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input name='post[tag_ids][]' type='hidden' value='' />" \ "<label for='post_tag_ids_1'>" \ - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' id='post_tag_ids_1' />" \ "Tag 1</label>" \ "<label for='post_tag_ids_2'>" \ - "<input name='post[tag_ids][]' type='checkbox' value='2' />" \ + "<input name='post[tag_ids][]' type='checkbox' value='2' id='post_tag_ids_2' />" \ "Tag 2</label>" \ "<label for='post_tag_ids_3'>" \ - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' id='post_tag_ids_3' />" \ "Tag 3</label>" \ "<input name='post[id]' type='hidden' value='1' />" end @@ -572,7 +572,7 @@ class FormWithActsLikeFormForTest < FormWithTest expected = whole_form("/posts") do "<input name='post[1][tag_ids][]' type='hidden' value='' />" \ - "<input checked='checked' name='post[1][tag_ids][]' type='checkbox' value='1' />" \ + "<input checked='checked' name='post[1][tag_ids][]' type='checkbox' value='1' id='post_1_tag_ids_1' />" \ "<label for='post_1_tag_ids_1'>Tag 1</label>" end @@ -878,24 +878,6 @@ class FormWithActsLikeFormForTest < FormWithTest assert_dom_equal expected, output_buffer end - def test_form_with_with_namespace - skip "Do namespaces still make sense?" - form_for(@post, namespace: "namespace") do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end - - expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do - "<input name='post[title]' type='text' value='Hello World' />" \ - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ - "<input name='post[secret]' type='hidden' value='0' />" \ - "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" - end - - assert_dom_equal expected, output_buffer - end - def test_submit_with_object_as_new_record_and_locale_strings with_locale :submit do @post.persisted = false diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb index 258dcdb806..3247f20ba7 100644 --- a/actionview/test/template/form_options_helper_test.rb +++ b/actionview/test/template/form_options_helper_test.rb @@ -6,6 +6,15 @@ class Map < Hash end end +class CustomEnumerable + include Enumerable + + def each + yield "one" + yield "two" + end +end + class FormOptionsHelperTest < ActionView::TestCase tests ActionView::Helpers::FormOptionsHelper @@ -904,6 +913,14 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_with_enumerable + @post = Post.new + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"><option value=\"one\">one</option>\n<option value=\"two\">two</option></select>", + select("post", "category", CustomEnumerable.new) + ) + end + def test_collection_select @post = Post.new @post.author_name = "Babe" diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb index 7f358add7e..4c9f84f277 100644 --- a/actionview/test/template/log_subscriber_test.rb +++ b/actionview/test/template/log_subscriber_test.rb @@ -8,11 +8,14 @@ class AVLogSubscriberTest < ActiveSupport::TestCase def setup super - view_paths = ActionController::Base.view_paths + + view_paths = ActionController::Base.view_paths lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"]) - renderer = ActionView::Renderer.new(lookup_context) - @view = ActionView::Base.new(renderer, {}) + renderer = ActionView::Renderer.new(lookup_context) + @view = ActionView::Base.new(renderer, {}) + ActionView::LogSubscriber.attach_to :action_view + unless Rails.respond_to?(:root) @defined_root = true def Rails.root; :defined_root; end # Minitest `stub` expects the method to be defined. @@ -21,7 +24,9 @@ class AVLogSubscriberTest < ActiveSupport::TestCase def teardown super + ActiveSupport::LogSubscriber.log_subscribers.clear + # We need to undef `root`, RenderTestCases don't want this to be defined Rails.instance_eval { undef :root } if @defined_root end @@ -39,7 +44,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase def set_view_cache_dependencies def @view.view_cache_dependencies; []; end - def @view.fragment_cache_key(*); "ahoy `controller` dependency"; end + def @view.combined_fragment_cache_key(*); "ahoy `controller` dependency"; end end def test_render_file_template @@ -103,9 +108,9 @@ class AVLogSubscriberTest < ActiveSupport::TestCase set_view_cache_dependencies set_cache_controller - @view.render(partial: "test/cached_customer", locals: { cached_customer: Customer.new("david") }) # Second render should hit cache. @view.render(partial: "test/cached_customer", locals: { cached_customer: Customer.new("david") }) + @view.render(partial: "test/cached_customer", locals: { cached_customer: Customer.new("david") }) wait assert_equal 2, @logger.logged(:info).size @@ -113,6 +118,46 @@ class AVLogSubscriberTest < ActiveSupport::TestCase end end + def test_render_uncached_outer_partial_with_inner_cached_partial_wont_mix_cache_hits_or_misses + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + set_view_cache_dependencies + set_cache_controller + + @view.render(partial: "test/nested_cached_customer", locals: { cached_customer: Customer.new("Stan") }) + wait + *, cached_inner, uncached_outer = @logger.logged(:info) + assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache miss\]/, cached_inner) + assert_match(/Rendered test\/_nested_cached_customer\.erb \(.*?ms\)$/, uncached_outer) + + # Second render hits the cache for the _cached_customer partial. Outer template's log shouldn't be affected. + @view.render(partial: "test/nested_cached_customer", locals: { cached_customer: Customer.new("Stan") }) + wait + *, cached_inner, uncached_outer = @logger.logged(:info) + assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache hit\]/, cached_inner) + assert_match(/Rendered test\/_nested_cached_customer\.erb \(.*?ms\)$/, uncached_outer) + end + end + + def test_render_cached_outer_partial_with_cached_inner_partial + Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do + set_view_cache_dependencies + set_cache_controller + + @view.render(partial: "test/cached_nested_cached_customer", locals: { cached_customer: Customer.new("Stan") }) + wait + *, cached_inner, cached_outer = @logger.logged(:info) + assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache miss\]/, cached_inner) + assert_match(/Rendered test\/_cached_nested_cached_customer\.erb (.*) \[cache miss\]/, cached_outer) + + # One render: inner partial skipped, because the outer has been cached. + assert_difference -> { @logger.logged(:info).size }, +1 do + @view.render(partial: "test/cached_nested_cached_customer", locals: { cached_customer: Customer.new("Stan") }) + wait + end + assert_match(/Rendered test\/_cached_nested_cached_customer\.erb (.*) \[cache hit\]/, @logger.logged(:info).last) + end + end + def test_render_partial_with_cache_hitted_and_missed Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do set_view_cache_dependencies diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index 412948719c..9999607067 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -10,8 +10,8 @@ module RenderTestCases @view = Class.new(ActionView::Base) do def view_cache_dependencies; end - def fragment_cache_key(key) - ActiveSupport::Cache.expand_cache_key(key, :views) + def combined_fragment_cache_key(key) + [ :views, key ] end end.new(paths, @assigns) @@ -27,7 +27,7 @@ module RenderTestCases def test_render_without_options e = assert_raises(ArgumentError) { @view.render() } - assert_match(/You invoked render but did not give any of (.+) option./, e.message) + assert_match(/You invoked render but did not give any of (.+) option\./, e.message) end def test_render_file @@ -83,6 +83,10 @@ module RenderTestCases assert_equal "<h1>Kein Kommentar</h1>", @view.render(template: "comments/empty", locale: [:de]) end + def test_render_template_with_variants + assert_equal "<h1>No Comment</h1>\n", @view.render(template: "comments/empty", variants: :grid) + end + def test_render_file_with_handlers assert_equal "<h1>No Comment</h1>\n", @view.render(file: "comments/empty", handlers: [:builder]) assert_equal "<h1>No Comment</h1>\n", @view.render(file: "comments/empty", handlers: :builder) @@ -134,7 +138,7 @@ module RenderTestCases end def test_render_file_with_full_path - template_path = File.join(File.dirname(__FILE__), "../fixtures/test/hello_world") + template_path = File.expand_path("../fixtures/test/hello_world", __dir__) assert_equal "Hello world!", @view.render(file: template_path) end @@ -156,7 +160,7 @@ module RenderTestCases end def test_render_outside_path - assert File.exist?(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")) + assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__)) assert_raises ActionView::MissingTemplate do @view.render(template: "../\\../test/abstract_unit.rb") end @@ -170,6 +174,10 @@ module RenderTestCases assert_equal "partial html", @view.render(partial: "test/partial") end + def test_render_partial_with_variants + assert_equal "<h1>Partial with variants</h1>\n", @view.render(partial: "test/partial_with_variants", variants: :grid) + end + def test_render_partial_with_selected_format assert_equal "partial html", @view.render(partial: "test/partial", formats: :html) assert_equal "partial js", @view.render(partial: "test/partial", formats: [:js]) @@ -253,7 +261,7 @@ module RenderTestCases def test_render_sub_template_with_errors e = assert_raises(ActionView::Template::Error) { @view.render(template: "test/sub_template_raise") } assert_match %r!method.*doesnt_exist!, e.message - assert_match %r{Trace of template inclusion: .*test/sub_template_raise.html.erb}, e.sub_template_message + assert_match %r{Trace of template inclusion: .*test/sub_template_raise\.html\.erb}, e.sub_template_message assert_equal "1", e.line_number assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name end @@ -710,6 +718,6 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase private def cache_key(*names, virtual_path) digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: [] - @view.fragment_cache_key([ *names, digest ]) + @view.combined_fragment_cache_key([ "#{virtual_path}:#{digest}", *names ]) end end diff --git a/actionview/test/template/resolver_patterns_test.rb b/actionview/test/template/resolver_patterns_test.rb index 43e3f21076..8e21f4b828 100644 --- a/actionview/test/template/resolver_patterns_test.rb +++ b/actionview/test/template/resolver_patterns_test.rb @@ -2,7 +2,7 @@ require "abstract_unit" class ResolverPatternsTest < ActiveSupport::TestCase def setup - path = File.expand_path("../../fixtures/", __FILE__) + path = File.expand_path("../fixtures", __dir__) pattern = ":prefix/{:formats/,}:action{.:formats,}{+:variants,}{.:handlers,}" @resolver = ActionView::FileSystemResolver.new(path, pattern) end diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 6adfa95dd1..bdedbeba92 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -7,8 +7,7 @@ class UrlHelperTest < ActiveSupport::TestCase # In those cases, we'll set up a simple mock attr_accessor :controller, :request - cattr_accessor :request_forgery - self.request_forgery = false + cattr_accessor :request_forgery, default: false routes = ActionDispatch::Routing::RouteSet.new routes.draw do @@ -16,6 +15,10 @@ class UrlHelperTest < ActiveSupport::TestCase get "/other" => "foo#other" get "/article/:id" => "foo#article", :as => :article get "/category/:category" => "foo#category" + + scope :engine do + get "/" => "foo#bar" + end end include ActionView::Helpers::UrlHelper @@ -522,10 +525,10 @@ class UrlHelperTest < ActiveSupport::TestCase assert current_page?("http://www.example.com/?order=desc&page=1") end - def test_current_page_with_not_get_verb - @request = request_for_url("/events", method: :post) + def test_current_page_with_scope_that_match + @request = request_for_url("/engine/") - assert !current_page?("/events") + assert current_page?("/engine") end def test_current_page_with_escaped_params @@ -554,6 +557,12 @@ class UrlHelperTest < ActiveSupport::TestCase assert current_page?("/posts/") end + def test_current_page_with_not_get_verb + @request = request_for_url("/events", method: :post) + + assert !current_page?("/events") + end + def test_link_unless_current @request = request_for_url("/") @@ -615,8 +624,8 @@ class UrlHelperTest < ActiveSupport::TestCase def test_mail_to_with_special_characters assert_dom_equal( - %{<a href="mailto:%23%21%24%25%26%27%2A%2B-%2F%3D%3F%5E_%60%7B%7D%7C%7E@example.org">#!$%&'*+-/=?^_`{}|~@example.org</a>}, - mail_to("#!$%&'*+-/=?^_`{}|~@example.org") + %{<a href="mailto:%23%21%24%25%26%27%2A%2B-%2F%3D%3F%5E_%60%7B%7D%7C@example.org">#!$%&'*+-/=?^_`{}|@example.org</a>}, + mail_to("#!$%&'*+-/=?^_`{}|@example.org") ) end diff --git a/actionview/test/ujs/config.ru b/actionview/test/ujs/config.ru index 48b7a4b53a..213a41127a 100644 --- a/actionview/test/ujs/config.ru +++ b/actionview/test/ujs/config.ru @@ -1,4 +1,4 @@ -$LOAD_PATH.unshift File.expand_path("..", __FILE__) +$LOAD_PATH.unshift __dir__ require "server" run UJS::Server diff --git a/activejob/.gitignore b/activejob/.gitignore deleted file mode 100644 index b3aaf55871..0000000000 --- a/activejob/.gitignore +++ /dev/null @@ -1 +0,0 @@ -test/dummy diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index ddfb926e02..77dfdefc05 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -4,4 +4,5 @@ *Steven Bull* + Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activejob/CHANGELOG.md) for previous changes. diff --git a/activejob/Rakefile b/activejob/Rakefile index 41ff76135e..dd03ab0b8f 100644 --- a/activejob/Rakefile +++ b/activejob/Rakefile @@ -1,8 +1,7 @@ require "rake/testtask" #TODO: add qu back to the list after it support Rails 5.1 -#TODO: add delayed_job back to the list after it support Rails 5.1 -ACTIVEJOB_ADAPTERS = %w(async inline que queue_classic resque sidekiq sneakers sucker_punch backburner test) +ACTIVEJOB_ADAPTERS = %w(async inline delayed_job que queue_classic resque sidekiq sneakers sucker_punch backburner test) ACTIVEJOB_ADAPTERS.delete("queue_classic") if defined?(JRUBY_VERSION) task default: :test @@ -44,9 +43,8 @@ namespace :test do namespace :isolated do task adapter => "test:env:#{adapter}" do - dir = File.dirname(__FILE__) - Dir.glob("#{dir}/test/cases/**/*_test.rb").all? do |file| - sh(Gem.ruby, "-w", "-I#{dir}/lib", "-I#{dir}/test", file) + Dir.glob("#{__dir__}/test/cases/**/*_test.rb").all? do |file| + sh(Gem.ruby, "-w", "-I#{__dir__}/lib", "-I#{__dir__}/test", file) end || raise("Failures") end end diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec index 2547e91262..bdf23223d1 100644 --- a/activejob/activejob.gemspec +++ b/activejob/activejob.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -18,6 +18,11 @@ Gem::Specification.new do |s| s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*"] s.require_path = "lib" + s.metadata = { + "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/activejob", + "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activejob/CHANGELOG.md" + } + s.add_dependency "activesupport", version s.add_dependency "globalid", ">= 0.3.6" end diff --git a/activejob/bin/test b/activejob/bin/test index a7beb14b27..470ce93f10 100755 --- a/activejob/bin/test +++ b/activejob/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb index d5b17de8b5..9aebc880a5 100644 --- a/activejob/lib/active_job/callbacks.rb +++ b/activejob/lib/active_job/callbacks.rb @@ -4,7 +4,7 @@ module ActiveJob # = Active Job Callbacks # # Active Job provides hooks during the life cycle of a job. Callbacks allow you - # to trigger logic during the life cycle of a job. Available callbacks are: + # to trigger logic during this cycle. Available callbacks are: # # * <tt>before_enqueue</tt> # * <tt>around_enqueue</tt> @@ -13,6 +13,8 @@ module ActiveJob # * <tt>around_perform</tt> # * <tt>after_perform</tt> # + # NOTE: Calling the same callback multiple times will overwrite previous callback definitions. + # module Callbacks extend ActiveSupport::Concern include ActiveSupport::Callbacks diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index 548ec89ee2..e3e63f227e 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -80,6 +80,7 @@ module ActiveJob { "job_class" => self.class.name, "job_id" => job_id, + "provider_job_id" => provider_job_id, "queue_name" => queue_name, "priority" => priority, "arguments" => serialize_arguments(arguments), diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb index f46d5c68a8..ddc4915fd3 100644 --- a/activejob/lib/active_job/logging.rb +++ b/activejob/lib/active_job/logging.rb @@ -8,7 +8,7 @@ module ActiveJob extend ActiveSupport::Concern included do - cattr_accessor(:logger) { ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT)) } + cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT)) around_enqueue do |_, block, _| tag_logger do diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb index 9dae80ffc2..b22d8b8347 100644 --- a/activejob/lib/active_job/queue_adapter.rb +++ b/activejob/lib/active_job/queue_adapter.rb @@ -7,6 +7,7 @@ module ActiveJob extend ActiveSupport::Concern included do + class_attribute :_queue_adapter_name, instance_accessor: false, instance_predicate: false class_attribute :_queue_adapter, instance_accessor: false, instance_predicate: false self.queue_adapter = :async end @@ -19,11 +20,15 @@ module ActiveJob _queue_adapter end + def queue_adapter_name + _queue_adapter_name + end + # Specify the backend queue provider. The default queue adapter # is the +:async+ queue. See QueueAdapters for more # information. def queue_adapter=(name_or_adapter_or_class) - self._queue_adapter = interpret_adapter(name_or_adapter_or_class) + interpret_adapter(name_or_adapter_or_class) end private @@ -31,16 +36,24 @@ module ActiveJob def interpret_adapter(name_or_adapter_or_class) case name_or_adapter_or_class when Symbol, String - ActiveJob::QueueAdapters.lookup(name_or_adapter_or_class).new + assign_adapter(name_or_adapter_or_class.to_s, + ActiveJob::QueueAdapters.lookup(name_or_adapter_or_class).new) else if queue_adapter?(name_or_adapter_or_class) - name_or_adapter_or_class + adapter_name = "#{name_or_adapter_or_class.class.name.demodulize.remove('Adapter').underscore}" + assign_adapter(adapter_name, + name_or_adapter_or_class) else raise ArgumentError end end end + def assign_adapter(adapter_name, queue_adapter) + self._queue_adapter_name = adapter_name + self._queue_adapter = queue_adapter + end + QUEUE_ADAPTER_METHODS = [:enqueue, :enqueue_at].freeze def queue_adapter?(object) diff --git a/activejob/lib/active_job/queue_name.rb b/activejob/lib/active_job/queue_name.rb index 352cf62424..6f521a1c1a 100644 --- a/activejob/lib/active_job/queue_name.rb +++ b/activejob/lib/active_job/queue_name.rb @@ -4,8 +4,8 @@ module ActiveJob # Includes the ability to override the default queue name and prefix. module ClassMethods - mattr_accessor(:queue_name_prefix) - mattr_accessor(:default_queue_name) { "default" } + mattr_accessor :queue_name_prefix + mattr_accessor :default_queue_name, default: "default" # Specifies the name of the queue to process the job on. # @@ -32,11 +32,8 @@ module ActiveJob end included do - class_attribute :queue_name, instance_accessor: false - class_attribute :queue_name_delimiter, instance_accessor: false - - self.queue_name = default_queue_name - self.queue_name_delimiter = "_" # set default delimiter to '_' + class_attribute :queue_name, instance_accessor: false, default: default_queue_name + class_attribute :queue_name_delimiter, instance_accessor: false, default: "_" end # Returns the name of the queue the job will be run on. diff --git a/activejob/lib/active_job/queue_priority.rb b/activejob/lib/active_job/queue_priority.rb index b02202fcc8..399d7a135a 100644 --- a/activejob/lib/active_job/queue_priority.rb +++ b/activejob/lib/active_job/queue_priority.rb @@ -4,7 +4,7 @@ module ActiveJob # Includes the ability to override the default queue priority. module ClassMethods - mattr_accessor(:default_priority) + mattr_accessor :default_priority # Specifies the priority of the queue to create the job with. # @@ -27,9 +27,7 @@ module ActiveJob end included do - class_attribute :priority, instance_accessor: false - - self.priority = default_priority + class_attribute :priority, instance_accessor: false, default: default_priority end # Returns the priority that the job will be created with diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb index 50476a2e50..474f181f65 100644 --- a/activejob/lib/rails/generators/job/job_generator.rb +++ b/activejob/lib/rails/generators/job/job_generator.rb @@ -12,7 +12,7 @@ module Rails # :nodoc: hook_for :test_framework def self.default_generator_root - File.dirname(__FILE__) + __dir__ end def create_job_file diff --git a/activejob/test/adapters/delayed_job.rb b/activejob/test/adapters/delayed_job.rb index 5f0ee2418c..98e41c0c36 100644 --- a/activejob/test/adapters/delayed_job.rb +++ b/activejob/test/adapters/delayed_job.rb @@ -1,6 +1,6 @@ ActiveJob::Base.queue_adapter = :delayed_job -$LOAD_PATH << File.dirname(__FILE__) + "/../support/delayed_job" +$LOAD_PATH << File.expand_path("../support/delayed_job", __dir__) Delayed::Worker.delay_jobs = false Delayed::Worker.backend = :test diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb index 3f2e300dfa..c737557ece 100644 --- a/activejob/test/cases/job_serialization_test.rb +++ b/activejob/test/cases/job_serialization_test.rb @@ -44,4 +44,12 @@ class JobSerializationTest < ActiveSupport::TestCase job.deserialize({}) assert_equal "en", job.locale end + + test "serialize stores provider_job_id" do + job = HelloJob.new + assert_nil job.serialize["provider_job_id"] + + job.provider_job_id = "some value set by adapter" + assert_equal job.provider_job_id, job.serialize["provider_job_id"] + end end diff --git a/activejob/test/cases/queue_adapter_test.rb b/activejob/test/cases/queue_adapter_test.rb index 9611b0909b..8368107bdf 100644 --- a/activejob/test/cases/queue_adapter_test.rb +++ b/activejob/test/cases/queue_adapter_test.rb @@ -25,14 +25,19 @@ class QueueAdapterTest < ActiveJob::TestCase base_queue_adapter = ActiveJob::Base.queue_adapter child_job_one = Class.new(ActiveJob::Base) + assert_equal child_job_one.queue_adapter_name, ActiveJob::Base.queue_adapter_name + child_job_one.queue_adapter = :stub_one assert_not_equal ActiveJob::Base.queue_adapter, child_job_one.queue_adapter + assert_equal "stub_one", child_job_one.queue_adapter_name assert_kind_of ActiveJob::QueueAdapters::StubOneAdapter, child_job_one.queue_adapter child_job_two = Class.new(ActiveJob::Base) child_job_two.queue_adapter = :stub_two + assert_equal "stub_two", child_job_two.queue_adapter_name + assert_kind_of ActiveJob::QueueAdapters::StubTwoAdapter, child_job_two.queue_adapter assert_kind_of ActiveJob::QueueAdapters::StubOneAdapter, child_job_one.queue_adapter, "child_job_one's queue adapter should remain unchanged" assert_equal base_queue_adapter, ActiveJob::Base.queue_adapter, "ActiveJob::Base's queue adapter should remain unchanged" diff --git a/activejob/test/support/delayed_job/delayed/backend/test.rb b/activejob/test/support/delayed_job/delayed/backend/test.rb index 98d731ff1e..68288c062b 100644 --- a/activejob/test/support/delayed_job/delayed/backend/test.rb +++ b/activejob/test/support/delayed_job/delayed/backend/test.rb @@ -19,8 +19,7 @@ module Delayed include Delayed::Backend::Base - cattr_accessor :id - self.id = 0 + cattr_accessor :id, default: 0 def initialize(hash = {}) self.attempts = 0 diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb index 29a5691f30..14fe3c9adc 100644 --- a/activejob/test/support/integration/dummy_app_template.rb +++ b/activejob/test/support/integration/dummy_app_template.rb @@ -5,7 +5,7 @@ end rails_command("db:migrate") initializer "activejob.rb", <<-CODE -require "#{File.expand_path("../jobs_manager.rb", __FILE__)}" +require "#{File.expand_path("jobs_manager.rb", __dir__)}" JobsManager.current_manager.setup CODE diff --git a/activejob/test/support/integration/helper.rb b/activejob/test/support/integration/helper.rb index 626b932cce..545b62752e 100644 --- a/activejob/test/support/integration/helper.rb +++ b/activejob/test/support/integration/helper.rb @@ -7,7 +7,7 @@ require "rails/generators/rails/app/app_generator" require "tmpdir" dummy_app_path = Dir.mktmpdir + "/dummy" -dummy_app_template = File.expand_path("../dummy_app_template.rb", __FILE__) +dummy_app_template = File.expand_path("dummy_app_template.rb", __dir__) args = Rails::Generators::ARGVScrubber.new(["new", dummy_app_path, "--skip-gemfile", "--skip-bundle", "--skip-git", "--skip-spring", "-d", "sqlite3", "--skip-javascript", "--force", "--quiet", "--template", dummy_app_template]).prepare! diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index cdba0cee12..2916e5eabb 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,30 +1,35 @@ +* Fix regression in numericality validator when comparing Decimal and Float input + values with more scale than the schema. + + *Bradley Priest* + * Fix methods `#keys`, `#values` in `ActiveModel::Errors`. Change `#keys` to only return the keys that don't have empty messages. Change `#values` to only return the not empty values. - Example: - - # Before - person = Person.new - person.errors.keys # => [] - person.errors.values # => [] - person.errors.messages # => {} - person.errors[:name] # => [] - person.errors.messages # => {:name => []} - person.errors.keys # => [:name] - person.errors.values # => [[]] - - # After - person = Person.new - person.errors.keys # => [] - person.errors.values # => [] - person.errors.messages # => {} - person.errors[:name] # => [] - person.errors.messages # => {:name => []} - person.errors.keys # => [] - person.errors.values # => [] + Example: + + # Before + person = Person.new + person.errors.keys # => [] + person.errors.values # => [] + person.errors.messages # => {} + person.errors[:name] # => [] + person.errors.messages # => {:name => []} + person.errors.keys # => [:name] + person.errors.values # => [[]] + + # After + person = Person.new + person.errors.keys # => [] + person.errors.values # => [] + person.errors.messages # => {} + person.errors[:name] # => [] + person.errors.messages # => {:name => []} + person.errors.keys # => [] + person.errors.values # => [] *bogdanvlviv* diff --git a/activemodel/Rakefile b/activemodel/Rakefile index c7f97a4258..d60f6d9997 100644 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -1,14 +1,12 @@ require "rake/testtask" -dir = File.dirname(__FILE__) - task default: :test task :package Rake::TestTask.new do |t| t.libs << "test" - t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb") + t.test_files = Dir.glob("#{__dir__}/test/cases/**/*_test.rb") t.warning = true t.verbose = true t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) @@ -16,8 +14,8 @@ end namespace :test do task :isolated do - Dir.glob("#{dir}/test/**/*_test.rb").all? do |file| - sh(Gem.ruby, "-w", "-I#{dir}/lib", "-I#{dir}/test", file) + Dir.glob("#{__dir__}/test/**/*_test.rb").all? do |file| + sh(Gem.ruby, "-w", "-I#{__dir__}/lib", "-I#{__dir__}/test", file) end || raise("Failures") end end diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index fd715f6ba9..18a35678f1 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -18,5 +18,10 @@ Gem::Specification.new do |s| s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.rdoc", "lib/**/*"] s.require_path = "lib" + s.metadata = { + "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/activemodel", + "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activemodel/CHANGELOG.md" + } + s.add_dependency "activesupport", version end diff --git a/activemodel/bin/test b/activemodel/bin/test index a7beb14b27..470ce93f10 100755 --- a/activemodel/bin/test +++ b/activemodel/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 2389c858d5..a2892e9ea9 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -68,5 +68,5 @@ module ActiveModel end ActiveSupport.on_load(:i18n) do - I18n.load_path << File.dirname(__FILE__) + "/active_model/locale/en.yml" + I18n.load_path << File.expand_path("active_model/locale/en.yml", __dir__) end diff --git a/activemodel/lib/active_model/attribute_assignment.rb b/activemodel/lib/active_model/attribute_assignment.rb index ee130df989..aa931119ff 100644 --- a/activemodel/lib/active_model/attribute_assignment.rb +++ b/activemodel/lib/active_model/attribute_assignment.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/hash/keys" module ActiveModel @@ -19,10 +21,10 @@ module ActiveModel # cat = Cat.new # cat.assign_attributes(name: "Gorby", status: "yawning") # cat.name # => 'Gorby' - # cat.status => 'yawning' + # cat.status # => 'yawning' # cat.assign_attributes(status: "sleeping") # cat.name # => 'Gorby' - # cat.status => 'sleeping' + # cat.status # => 'sleeping' def assign_attributes(new_attributes) if !new_attributes.respond_to?(:stringify_keys) raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." @@ -42,8 +44,9 @@ module ActiveModel end def _assign_attribute(k, v) - if respond_to?("#{k}=") - public_send("#{k}=", v) + setter = :"#{k}=" + if respond_to?(setter) + public_send(setter, v) else raise UnknownAttributeError.new(self, k) end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index b5c0b43b61..b3b39bf7ae 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -68,9 +68,8 @@ module ActiveModel CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/ included do - class_attribute :attribute_aliases, :attribute_method_matchers, instance_writer: false - self.attribute_aliases = {} - self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new] + class_attribute :attribute_aliases, instance_writer: false, default: {} + class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ] end module ClassMethods diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index eac2761433..835e6f7716 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -56,6 +56,9 @@ module ActiveModel # # Would only create the +after_create+ and +before_create+ callback methods in # your class. + # + # NOTE: Calling the same callback multiple times will overwrite previous callback definitions. + # module Callbacks def self.extended(base) #:nodoc: base.class_eval do diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 9853cf38fe..9ac56526a2 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -47,7 +47,7 @@ module ActiveModel # :method: <=> # # :call-seq: - # ==(other) + # <=>(other) # # Equivalent to <tt>String#<=></tt>. # diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index a9d92eb92a..205b84ddb4 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -10,8 +10,7 @@ module ActiveModel included do extend ActiveModel::Naming - class_attribute :include_root_in_json, instance_writer: false - self.include_root_in_json = false + class_attribute :include_root_in_json, instance_writer: false, default: false end # Returns a hash representing the model. Some configuration can be diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index d460068830..ae1d69f685 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -49,8 +49,7 @@ module ActiveModel private :validation_context= define_callbacks :validate, scope: :name - class_attribute :_validators, instance_writer: false - self._validators = Hash.new { |h, k| h[k] = [] } + class_attribute :_validators, instance_writer: false, default: Hash.new { |h, k| h[k] = [] } end module ClassMethods @@ -147,6 +146,9 @@ module ActiveModel # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a +true+ or +false+ # value. + # + # NOTE: Calling +validate+ multiple times on the same method will overwrite previous definitions. + # def validate(*args, &block) options = args.extract_options! @@ -432,4 +434,4 @@ module ActiveModel end end -Dir[File.dirname(__FILE__) + "/validations/*.rb"].each { |file| require file } +Dir[File.expand_path("validations/*.rb", __dir__)].each { |file| require file } diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index b82c85ddf4..fb053a4c4e 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -36,7 +36,9 @@ module ActiveModel return end - unless raw_value.is_a?(Numeric) + if raw_value.is_a?(Numeric) + value = raw_value + else value = parse_raw_value_as_a_number(raw_value) end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 0ce5935f3a..a8b958e974 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -18,7 +18,6 @@ module ActiveModel # validates :first_name, length: { maximum: 30 } # validates :age, numericality: true # validates :username, presence: true - # validates :username, uniqueness: true # # The power of the +validates+ method comes when using custom validators # and default validators in one call for a given attribute. @@ -34,7 +33,7 @@ module ActiveModel # include ActiveModel::Validations # attr_accessor :name, :email # - # validates :name, presence: true, uniqueness: true, length: { maximum: 100 } + # validates :name, presence: true, length: { maximum: 100 } # validates :email, presence: true, email: true # end # @@ -94,7 +93,7 @@ module ActiveModel # Example: # # validates :password, presence: true, confirmation: true, if: :password_required? - # validates :token, uniqueness: true, strict: TokenGenerationException + # validates :token, length: 24, strict: TokenLengthException # # # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+, +:strict+ diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 98234e9b6b..6c11981e1d 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -82,7 +82,7 @@ module ActiveModel # end # # It can be useful to access the class that is using that validator when there are prerequisites such - # as an +attr_accessor+ being present. This class is accessible via +options[:class]+ in the constructor. + # as an +attr_accessor+ being present. This class is accessible via <tt>options[:class]</tt> in the constructor. # To setup your validator override the constructor. # # class MyValidator < ActiveModel::Validator @@ -97,7 +97,7 @@ module ActiveModel # Returns the kind of the validator. # # PresenceValidator.kind # => :presence - # UniquenessValidator.kind # => :uniqueness + # AcceptanceValidator.kind # => :acceptance def self.kind @kind ||= name.split("::").last.underscore.chomp("_validator").to_sym unless anonymous? end @@ -109,8 +109,8 @@ module ActiveModel # Returns the kind for this validator. # - # PresenceValidator.new.kind # => :presence - # UniquenessValidator.new.kind # => :uniqueness + # PresenceValidator.new(attributes: [:username]).kind # => :presence + # AcceptanceValidator.new(attributes: [:terms]).kind # => :acceptance def kind self.class.kind end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index a229b4eaa2..7e8ca51f25 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,4 +1,87 @@ -* Quote database name in db:create grant statement (when database_user does not have access to create the database). +* Fix eager loading to respect `store_full_sti_class` setting. + + *Ryuta Kamizono* + +* Query cache was unavailable when entering the ActiveRecord::Base.cache block + without being connected. + + *Tsukasa Oishi* + +* Previously, when building records using a `has_many :through` association, + if the child records were deleted before the parent was saved, they would + still be persisted. Now, if child records are deleted before the parent is saved + on a `has_many :through` association, the child records will not be persisted. + + *Tobias Kraze* + +* Merging two relations representing nested joins no longer transforms the joins of + the merged relation into LEFT OUTER JOIN. Example to clarify: + + ``` + Author.joins(:posts).merge(Post.joins(:comments)) + # Before the change: + #=> SELECT ... FROM authors INNER JOIN posts ON ... LEFT OUTER JOIN comments ON... + + # After the change: + #=> SELECT ... FROM authors INNER JOIN posts ON ... INNER JOIN comments ON... + ``` + + TODO: Add to the Rails 5.2 upgrade guide + + *Maxime Handfield Lapointe* + +* `ActiveRecord::Persistence#touch` does not work well when optimistic locking enabled and + `locking_column`, without default value, is null in the database. + + *bogdanvlviv* + +* Fix destroying existing object does not work well when optimistic locking enabled and + `locking column` is null in the database. + + *bogdanvlviv* + +* Use bulk INSERT to insert fixtures for better performance. + + *Kir Shatrov* + +* Prevent making bind param if casted value is nil. + + *Ryuta Kamizono* + +* Deprecate passing arguments and block at the same time to `count` and `sum` in `ActiveRecord::Calculations`. + + *Ryuta Kamizono* + +* Loading model schema from database is now thread-safe. + + Fixes #28589. + + *Vikrant Chaudhary*, *David Abdemoulaie* + +* Add `ActiveRecord::Base#cache_version` to support recyclable cache keys via the new versioned entries + in `ActiveSupport::Cache`. This also means that `ActiveRecord::Base#cache_key` will now return a stable key + that does not include a timestamp any more. + + NOTE: This feature is turned off by default, and `#cache_key` will still return cache keys with timestamps + until you set `ActiveRecord::Base.cache_versioning = true`. That's the setting for all new apps on Rails 5.2+ + + *DHH* + +* Respect `SchemaDumper.ignore_tables` in rake tasks for databases structure dump + + *Rusty Geldmacher*, *Guillermo Iguaran* + +* Add type caster to `RuntimeReflection#alias_name` + + Fixes #28959. + + *Jon Moss* + +* Deprecate `supports_statement_cache?`. + + *Ryuta Kamizono* + +* Quote database name in `db:create` grant statement (when database user does not have access to create the database). *Rune Philosof* diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 7be3d851f1..fe5f9d1071 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,7 +1,7 @@ require "rake/testtask" -require File.expand_path(File.dirname(__FILE__)) + "/test/config" -require File.expand_path(File.dirname(__FILE__)) + "/test/support/config" +require_relative "test/config" +require_relative "test/support/config" def run_without_aborting(*tasks) errors = [] @@ -134,7 +134,7 @@ task drop_postgresql_databases: "db:postgresql:drop" task rebuild_postgresql_databases: "db:postgresql:rebuild" task :lines do - load File.expand_path("..", File.dirname(__FILE__)) + "/tools/line_statistics" + load File.expand_path("../tools/line_statistics", __dir__) files = FileList["lib/active_record/**/*.rb"] CodeTools::LineStatistics.new(files).print_loc end diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 0b37e9076c..a626a1f21b 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -21,6 +21,11 @@ Gem::Specification.new do |s| s.extra_rdoc_files = %w(README.rdoc) s.rdoc_options.concat ["--main", "README.rdoc"] + s.metadata = { + "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/activerecord", + "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activerecord/CHANGELOG.md" + } + s.add_dependency "activesupport", version s.add_dependency "activemodel", version diff --git a/activerecord/bin/test b/activerecord/bin/test index 3a9547e5c1..ab69f4f603 100755 --- a/activerecord/bin/test +++ b/activerecord/bin/test @@ -1,7 +1,7 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" module Minitest def self.plugin_active_record_options(opts, options) diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 96b8545dfc..29f49c6195 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -177,5 +177,5 @@ ActiveSupport.on_load(:active_record) do end ActiveSupport.on_load(:i18n) do - I18n.load_path << File.dirname(__FILE__) + "/active_record/locale/en.yml" + I18n.load_path << File.expand_path("active_record/locale/en.yml", __dir__) end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index e52c2004f3..f05a122544 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -342,7 +342,7 @@ module ActiveRecord # | | belongs_to | # generated methods | belongs_to | :polymorphic | has_one # ----------------------------------+------------+--------------+--------- - # other(force_reload=false) | X | X | X + # other | X | X | X # other=(other) | X | X | X # build_other(attributes={}) | X | | X # create_other(attributes={}) | X | | X @@ -352,7 +352,7 @@ module ActiveRecord # | | | has_many # generated methods | habtm | has_many | :through # ----------------------------------+-------+----------+---------- - # others(force_reload=false) | X | X | X + # others | X | X | X # others=(other,other,...) | X | X | X # other_ids | X | X | X # other_ids=(id,id,...) | X | X | X @@ -1187,7 +1187,7 @@ module ActiveRecord # +collection+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>. # - # [collection(force_reload = false)] + # [collection] # Returns an array of all the associated objects. # An empty array is returned if none are found. # [collection<<(object, ...)] @@ -1276,7 +1276,7 @@ module ActiveRecord # Scope examples: # has_many :comments, -> { where(author_id: 1) } # has_many :employees, -> { joins(:address) } - # has_many :posts, ->(post) { where("max_post_length > ?", post.length) } + # has_many :posts, ->(blog) { where("max_post_length > ?", blog.max_post_length) } # # === Extensions # @@ -1407,7 +1407,7 @@ module ActiveRecord # +association+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>. # - # [association(force_reload = false)] + # [association] # Returns the associated object. +nil+ is returned if none is found. # [association=(associate)] # Assigns the associate object, extracts the primary key, sets it as the foreign key, @@ -1443,7 +1443,7 @@ module ActiveRecord # Scope examples: # has_one :author, -> { where(comment_id: 1) } # has_one :employer, -> { joins(:company) } - # has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) } + # has_one :latest_post, ->(blog) { where("created_at > ?", blog.enabled_at) } # # === Options # @@ -1539,7 +1539,7 @@ module ActiveRecord # +association+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>. # - # [association(force_reload = false)] + # [association] # Returns the associated object. +nil+ is returned if none is found. # [association=(associate)] # Assigns the associate object, extracts the primary key, and sets it as the foreign key. @@ -1573,7 +1573,7 @@ module ActiveRecord # Scope examples: # belongs_to :firm, -> { where(id: 2) } # belongs_to :user, -> { joins(:friends) } - # belongs_to :level, ->(level) { where("game_level > ?", level.current) } + # belongs_to :level, ->(game) { where("game_level > ?", game.current_level) } # # === Options # @@ -1701,7 +1701,7 @@ module ActiveRecord # +collection+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>. # - # [collection(force_reload = false)] + # [collection] # Returns an array of all the associated objects. # An empty array is returned if none are found. # [collection<<(object, ...)] @@ -1769,9 +1769,8 @@ module ActiveRecord # # Scope examples: # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) } - # has_and_belongs_to_many :categories, ->(category) { - # where("default_category = ?", category.name) - # } + # has_and_belongs_to_many :categories, ->(post) { + # where("default_category = ?", post.default_category) # # === Extensions # diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 3963008a76..104de4f69d 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -4,23 +4,21 @@ module ActiveRecord module Associations # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency class AliasTracker # :nodoc: - attr_reader :aliases - - def self.create(connection, initial_table, type_caster) + def self.create(connection, initial_table) aliases = Hash.new(0) aliases[initial_table] = 1 - new connection, aliases, type_caster + new(connection, aliases) end - def self.create_with_joins(connection, initial_table, joins, type_caster) + def self.create_with_joins(connection, initial_table, joins) if joins.empty? - create(connection, initial_table, type_caster) + create(connection, initial_table) else aliases = Hash.new { |h, k| h[k] = initial_count_for(connection, k, joins) } aliases[initial_table] = 1 - new connection, aliases, type_caster + new(connection, aliases) end end @@ -53,17 +51,16 @@ module ActiveRecord end # table_joins is an array of arel joins which might conflict with the aliases we assign here - def initialize(connection, aliases, type_caster) + def initialize(connection, aliases) @aliases = aliases @connection = connection - @type_caster = type_caster end - def aliased_table_for(table_name, aliased_name) + def aliased_table_for(table_name, aliased_name, type_caster) if aliases[table_name].zero? # If it's zero, we can have our table_name aliases[table_name] = 1 - Arel::Table.new(table_name, type_caster: @type_caster) + Arel::Table.new(table_name, type_caster: type_caster) else # Otherwise, we need to use an alias aliased_name = @connection.table_alias_for(aliased_name) @@ -76,10 +73,15 @@ module ActiveRecord else aliased_name end - Arel::Table.new(table_name, type_caster: @type_caster).alias(table_alias) + Arel::Table.new(table_name, type_caster: type_caster).alias(table_alias) end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + attr_reader :aliases + private def truncate(name) diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 1cb2b2d7c6..44cf1f8915 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -94,7 +94,7 @@ module ActiveRecord # actually gets built. def association_scope if klass - @association_scope ||= AssociationScope.scope(self, klass.connection) + @association_scope ||= AssociationScope.scope(self) end end @@ -133,6 +133,16 @@ module ActiveRecord AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all) end + def extensions + extensions = klass.default_extensions | reflection.extensions + + if scope = reflection.scope + extensions |= klass.unscoped.instance_exec(owner, &scope).extensions + end + + extensions + end + # Loads the \target if needed and returns it. # # This method is abstract in the sense that it relies on +find_target+, @@ -152,14 +162,6 @@ module ActiveRecord reset end - def interpolate(sql, record = nil) - if sql.respond_to?(:to_proc) - owner.instance_exec(record, &sql) - else - sql - end - end - # We can't dump @reflection since it contains the scope proc def marshal_dump ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] } @@ -274,7 +276,7 @@ module ActiveRecord end # Returns true if statement cache should be skipped on the association reader. - def skip_statement_cache? + def skip_statement_cache?(scope) reflection.has_scope? || scope.eager_loading? || klass.scope_attributes? || diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 120d75416c..6ef225b725 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -1,8 +1,8 @@ module ActiveRecord module Associations class AssociationScope #:nodoc: - def self.scope(association, connection) - INSTANCE.scope(association, connection) + def self.scope(association) + INSTANCE.scope(association) end def self.create(&block) @@ -16,15 +16,15 @@ module ActiveRecord INSTANCE = create - def scope(association, connection) + def scope(association) klass = association.klass reflection = association.reflection scope = klass.unscoped owner = association.owner - alias_tracker = AliasTracker.create connection, association.klass.table_name, klass.type_caster + alias_tracker = AliasTracker.create(klass.connection, klass.table_name) chain_head, chain_tail = get_chain(reflection, association, alias_tracker) - scope.extending! Array(reflection.options[:extend]) + scope.extending! reflection.extensions add_constraints(scope, owner, reflection, chain_head, chain_tail) end @@ -112,7 +112,11 @@ module ActiveRecord runtime_reflection = Reflection::RuntimeReflection.new(reflection, association) previous_reflection = runtime_reflection reflection.chain.drop(1).each do |refl| - alias_name = tracker.aliased_table_for(refl.table_name, refl.alias_candidate(name)) + alias_name = tracker.aliased_table_for( + refl.table_name, + refl.alias_candidate(name), + refl.klass.type_caster + ) proxy = ReflectionProxy.new(refl, alias_name) previous_reflection.next = proxy previous_reflection = proxy @@ -138,7 +142,7 @@ module ActiveRecord # Exclude the scope of the association itself, because that # was already merged in the #scope method. reflection.constraints.each do |scope_chain_item| - item = eval_scope(reflection.klass, table, scope_chain_item, owner) + item = eval_scope(reflection, table, scope_chain_item, owner) if scope_chain_item == refl.scope scope.merge! item.except(:where, :includes) @@ -159,9 +163,8 @@ module ActiveRecord scope end - def eval_scope(klass, table, scope, owner) - predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) - ActiveRecord::Relation.create(klass, table, predicate_builder).instance_exec(owner, &scope) + def eval_scope(reflection, table, scope, owner) + reflection.build_scope(table).instance_exec(owner, &scope) end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 62c944fce3..bbf3dbb75e 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -30,7 +30,8 @@ module ActiveRecord reload end - CollectionProxy.create(klass, self) + @proxy ||= CollectionProxy.create(klass, self) + @proxy.reset_scope end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -43,10 +44,7 @@ module ActiveRecord if loaded? target.pluck(reflection.association_primary_key) else - @association_ids ||= ( - column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - scope.pluck(column) - ) + @association_ids ||= scope.pluck(reflection.association_primary_key) end end @@ -299,13 +297,14 @@ module ActiveRecord private def find_target - return scope.to_a if skip_statement_cache? + scope = self.scope + return scope.to_a if skip_statement_cache?(scope) conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do StatementCache.create(conn) { |params| as = AssociationScope.create { params.bind } - target_scope.merge as.scope(self, conn) + target_scope.merge!(as.scope(self)) } end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 74a4d515c2..d77fcaf668 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -31,6 +31,9 @@ module ActiveRecord def initialize(klass, association) #:nodoc: @association = association super klass, klass.arel_table, klass.predicate_builder + + extensions = association.extensions + extend(*extensions) if extensions.any? end def target @@ -1084,9 +1087,8 @@ module ActiveRecord # person.pets(true) # fetches pets from the database # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload - @scope = nil proxy_association.reload - self + reset_scope end # Unloads the association. Returns +self+. @@ -1106,9 +1108,14 @@ module ActiveRecord # person.pets # fetches pets from the database # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reset - @scope = nil proxy_association.reset proxy_association.reset_scope + reset_scope + end + + def reset_scope # :nodoc: + @offsets = {} + @scope = nil self end @@ -1121,19 +1128,6 @@ module ActiveRecord delegate(*delegate_methods, to: :scope) - module DelegateExtending # :nodoc: - private - def method_missing(method, *args, &block) - extending_values = association_scope.extending_values - if extending_values.any? && (extending_values - self.class.included_modules).any? - self.class.include(*extending_values) - public_send(method, *args, &block) - else - super - end - end - end - private def find_nth_with_limit(index, limit) @@ -1154,17 +1148,9 @@ module ActiveRecord @association.find_from_target? end - def association_scope - @association.association_scope - end - def exec_queries load_target end - - def respond_to_missing?(method, _) - association_scope.respond_to?(method) || super - end end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 53ffb3b68d..2fd20b4368 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -109,6 +109,11 @@ module ActiveRecord record end + def remove_records(existing_records, records, method) + super + delete_through_records(records) + end + def target_reflection_has_associated_record? !(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?) end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 8995b1e352..bc66194aef 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -93,7 +93,7 @@ module ActiveRecord # joins # => [] # def initialize(base, associations, joins, eager_loading: true) - @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins, base.type_caster) + @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins) @eager_loading = eager_loading tree = self.class.make_tree associations @join_root = JoinBase.new base, build(tree, base) @@ -104,22 +104,17 @@ module ActiveRecord join_root.drop(1).map!(&:reflection) end - def join_constraints(outer_joins, join_type) + def join_constraints(joins_to_add, join_type) joins = join_root.children.flat_map { |child| - - if join_type == Arel::Nodes::OuterJoin - make_left_outer_joins join_root, child - else - make_inner_joins join_root, child - end + make_join_constraints(join_root, child, join_type) } - joins.concat outer_joins.flat_map { |oj| + joins.concat joins_to_add.flat_map { |oj| if join_root.match? oj.join_root walk join_root, oj.join_root else oj.join_root.children.flat_map { |child| - make_outer_joins oj.join_root, child + make_join_constraints(oj.join_root, child, join_type) } end } @@ -175,34 +170,23 @@ module ActiveRecord end def make_outer_joins(parent, child) - tables = table_aliases_for(parent, child) - join_type = Arel::Nodes::OuterJoin - info = make_constraints parent, child, tables, join_type - - [info] + child.children.flat_map { |c| make_outer_joins(child, c) } - end - - def make_left_outer_joins(parent, child) - tables = child.tables join_type = Arel::Nodes::OuterJoin - info = make_constraints parent, child, tables, join_type - - [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) } + make_join_constraints(parent, child, join_type, true) end - def make_inner_joins(parent, child) - tables = child.tables - join_type = Arel::Nodes::InnerJoin - info = make_constraints parent, child, tables, join_type + def make_join_constraints(parent, child, join_type, aliasing = false) + tables = aliasing ? table_aliases_for(parent, child) : child.tables + info = make_constraints(parent, child, tables, join_type) - [info] + child.children.flat_map { |c| make_inner_joins(child, c) } + [info] + child.children.flat_map { |c| make_join_constraints(child, c, join_type, aliasing) } end def table_aliases_for(parent, node) node.reflection.chain.map { |reflection| alias_tracker.aliased_table_for( reflection.table_name, - table_alias_for(reflection, parent, reflection != node.reflection) + table_alias_for(reflection, parent, reflection != node.reflection), + reflection.klass.type_caster ) } end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 97cfec0302..005410d598 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -34,34 +34,10 @@ module ActiveRecord table = tables.shift klass = reflection.klass - join_keys = reflection.join_keys - key = join_keys.key - foreign_key = join_keys.foreign_key + join_scope = reflection.join_scope(table, foreign_table, foreign_klass) - constraint = build_constraint(klass, table, key, foreign_table, foreign_key) - - predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) - scope_chain_items = reflection.join_scopes(table, predicate_builder) - klass_scope = reflection.klass_join_scope(table, predicate_builder) - - scope_chain_items.concat [klass_scope].compact - - rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| - left.merge right - end - - if rel && !rel.arel.constraints.empty? - binds += rel.bound_attributes - constraint = constraint.and rel.arel.constraints - end - - if reflection.type - value = foreign_klass.base_class.name - column = klass.columns_hash[reflection.type.to_s] - - binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name)) - constraint = constraint.and klass.arel_attribute(reflection.type, table).eq(Arel::Nodes::BindParam.new) - end + binds.concat join_scope.bound_attributes + constraint = join_scope.arel.constraints joins << table.create_join(table, table.create_on(constraint), join_type) @@ -72,34 +48,6 @@ module ActiveRecord JoinInformation.new joins, binds end - # Builds equality condition. - # - # Example: - # - # class Physician < ActiveRecord::Base - # has_many :appointments - # end - # - # If I execute `Physician.joins(:appointments).to_a` then - # klass # => Physician - # table # => #<Arel::Table @name="appointments" ...> - # key # => physician_id - # foreign_table # => #<Arel::Table @name="physicians" ...> - # foreign_key # => id - # - def build_constraint(klass, table, key, foreign_table, foreign_key) - constraint = table[key].eq(foreign_table[foreign_key]) - - if klass.finder_needs_type_condition? - constraint = table.create_and([ - constraint, - klass.send(:type_condition, table) - ]) - end - - constraint - end - def table tables.first end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 61cec5403a..80c9fde5d1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -22,10 +22,6 @@ module ActiveRecord @children = children end - def name - reflection.name - end - def match?(other) self.class == other.class end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 9f77f38b35..208d1b2670 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -147,7 +147,7 @@ module ActiveRecord def preloaders_for_one(association, records, scope) grouped_records(association, records).flat_map do |reflection, klasses| klasses.map do |rhs_klass, rs| - loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope) + loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope) loader.run self loader end @@ -159,6 +159,7 @@ module ActiveRecord records.each do |record| next unless record assoc = record.association(association) + next unless assoc.klass klasses = h[assoc.reflection] ||= {} (klasses[assoc.klass] ||= []) << record end @@ -180,20 +181,11 @@ module ActiveRecord end end - class NullPreloader # :nodoc: - def self.new(klass, owners, reflection, preload_scope); self; end - def self.run(preloader); end - def self.preloaded_records; []; end - def self.owners; []; end - end - # Returns a class containing the logic needed to load preload the data # and attach it to a relation. For example +Preloader::Association+ or # +Preloader::HasManyThrough+. The class returned implements a `run` method # that accepts a preloader. - def preloader_for(reflection, owners, rhs_klass) - return NullPreloader unless rhs_klass - + def preloader_for(reflection, owners) if owners.first.association(reflection.name).loaded? return AlreadyLoaded end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 4072d19380..63ef3f2d8c 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -51,11 +51,10 @@ module ActiveRecord raise NotImplementedError end - def options - reflection.options - end - private + def options + reflection.options + end def associated_records_by_owner(preloader) records = load_records do |record| diff --git a/activerecord/lib/active_record/associations/preloader/belongs_to.rb b/activerecord/lib/active_record/associations/preloader/belongs_to.rb index 38e231826c..c20145770f 100644 --- a/activerecord/lib/active_record/associations/preloader/belongs_to.rb +++ b/activerecord/lib/active_record/associations/preloader/belongs_to.rb @@ -3,7 +3,7 @@ module ActiveRecord class Preloader class BelongsTo < SingularAssociation #:nodoc: def association_key_name - reflection.options[:primary_key] || klass && klass.primary_key + options[:primary_key] || klass && klass.primary_key end def owner_key_name diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 9d44a02021..8b954138cd 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -16,15 +16,13 @@ module ActiveRecord through_scope) through_records = owners.map do |owner| - association = owner.association through_reflection.name - - center = target_records_from_association(association) + center = owner.association(through_reflection.name).target [owner, Array(center)] end reset_association owners, through_reflection.name - middle_records = through_records.flat_map { |(_, rec)| rec } + middle_records = through_records.flat_map(&:last) preloaders = preloader.preload(middle_records, source_reflection.name, @@ -43,9 +41,7 @@ module ActiveRecord records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles| rhs_records = middles.flat_map { |r| - association = r.association source_reflection.name - - target_records_from_association(association) + r.association(source_reflection.name).target }.compact # Respect the order on `reflection_scope` if it exists, else use the natural order. @@ -69,7 +65,7 @@ module ActiveRecord def reset_association(owners, association_name) should_reset = (through_scope != through_reflection.klass.unscoped) || - (reflection.options[:source_type] && through_reflection.collection?) + (options[:source_type] && through_reflection.collection?) # Don't cache the association - we would only be caching a subset if should_reset @@ -98,10 +94,6 @@ module ActiveRecord scope end - - def target_records_from_association(association) - association.loaded? ? association.target : association.reader - end end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 91580a28d0..f8bbe4c2ed 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -36,13 +36,14 @@ module ActiveRecord end def find_target - return scope.take if skip_statement_cache? + scope = self.scope + return scope.take if skip_statement_cache?(scope) conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do StatementCache.create(conn) { |params| as = AssociationScope.create { params.bind } - target_scope.merge(as.scope(self, conn)).limit(1) + target_scope.merge!(as.scope(self)).limit(1) } end @@ -63,6 +64,10 @@ module ActiveRecord end def _create_record(attributes, raise_error = false) + unless owner.persisted? + raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" + end + record = build_record(attributes) yield(record) if block_given? saved = record.save diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index c39e9ce4c5..5bc8527745 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -3,8 +3,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :attribute_type_decorations, instance_accessor: false # :internal: - self.attribute_type_decorations = TypeDecorator.new + class_attribute :attribute_type_decorations, instance_accessor: false, default: TypeDecorator.new # :internal: end module ClassMethods # :nodoc: diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index ebe06566cc..83c61fad19 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -62,7 +62,6 @@ module ActiveRecord super(attribute_names) @attribute_methods_generated = true end - true end def undefine_attribute_methods # :nodoc: diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index bd5003d63a..76987fb8f4 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -14,8 +14,7 @@ module ActiveRecord raise "You cannot include Dirty after Timestamp" end - class_attribute :partial_writes, instance_writer: false - self.partial_writes = true + class_attribute :partial_writes, instance_writer: false, default: true after_create { changes_internally_applied } after_update { changes_internally_applied } diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 2f32caa257..b9b2acff37 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -8,17 +8,14 @@ module ActiveRecord # Returns this record's primary key value wrapped in an array if one is # available. def to_key - sync_with_transaction_state key = id [key] if key end # Returns the primary key value. def id - if pk = self.class.primary_key - sync_with_transaction_state - _read_attribute(pk) - end + sync_with_transaction_state + _read_attribute(self.class.primary_key) if self.class.primary_key end # Sets the primary key value. @@ -57,16 +54,12 @@ module ActiveRecord end module ClassMethods - def define_method_attribute(attr_name) - super + ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database).to_set - if attr_name == primary_key && attr_name != "id" - generated_attribute_methods.send(:alias_method, :id, primary_key) - end + def instance_method_already_implemented?(method_name) + super || primary_key && ID_ATTRIBUTE_METHODS.include?(method_name) end - ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database).to_set - def dangerous_attribute_method?(method_name) super && !ID_ATTRIBUTE_METHODS.include?(method_name) end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 321d039ed4..1f1efe8812 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -54,14 +54,10 @@ module ActiveRecord extend ActiveSupport::Concern included do - mattr_accessor :time_zone_aware_attributes, instance_writer: false - self.time_zone_aware_attributes = false + mattr_accessor :time_zone_aware_attributes, instance_writer: false, default: false - class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false - self.skip_time_zone_conversion_for_attributes = [] - - class_attribute :time_zone_aware_types, instance_writer: false - self.time_zone_aware_types = [:datetime, :time] + class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false, default: [] + class_attribute :time_zone_aware_types, instance_writer: false, default: [ :datetime, :time ] end module ClassMethods diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index fe0e01db28..75c5a1a600 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -35,11 +35,15 @@ module ActiveRecord attr_name.to_s end - write_attribute_with_type_cast(name, value, true) + name = self.class.primary_key if name == "id".freeze && self.class.primary_key + @attributes.write_from_user(name, value) + value end def raw_write_attribute(attr_name, value) # :nodoc: - write_attribute_with_type_cast(attr_name, value, false) + name = attr_name.to_s + @attributes.write_cast_value(name, value) + value end private @@ -47,19 +51,6 @@ module ActiveRecord def attribute=(attribute_name, value) write_attribute(attribute_name, value) end - - def write_attribute_with_type_cast(attr_name, value, should_type_cast) - attr_name = attr_name.to_s - attr_name = self.class.primary_key if attr_name == "id" && self.class.primary_key - - if should_type_cast - @attributes.write_from_user(attr_name, value) - else - @attributes.write_cast_value(attr_name, value) - end - - value - end end end end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 66b278219a..01f9d815d5 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -64,7 +64,7 @@ module ActiveRecord end def deep_dup - dup.tap do |copy| + self.class.allocate.tap do |copy| copy.instance_variable_set(:@attributes, attributes.deep_dup) end end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 75f5ba3a96..475b9beec4 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -6,8 +6,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal: - self.attributes_to_define_after_schema_loads = {} + class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false, default: {} # :internal: end module ClassMethods diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 607c54e481..70f0e2af8e 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -140,8 +140,7 @@ module ActiveRecord included do Associations::Builder::Association.extensions << AssociationBuilderExtension - mattr_accessor :index_nested_attribute_errors, instance_writer: false - self.index_nested_attribute_errors = false + mattr_accessor :index_nested_attribute_errors, instance_writer: false, default: false end module ClassMethods # :nodoc: @@ -216,13 +215,7 @@ module ActiveRecord method = :validate_single_association end - define_non_cyclic_method(validation_method) do - send(method, reflection) - # TODO: remove the following line as soon as the return value of - # callbacks is ignored, that is, returning `false` does not - # display a deprecation warning or halts the callback chain. - true - end + define_non_cyclic_method(validation_method) { send(method, reflection) } validate validation_method after_validation :_ensure_no_duplicate_errors end @@ -369,7 +362,6 @@ module ActiveRecord # association whether or not the parent was a new record before saving. def before_save_collection_association @new_record_before_save = new_record? - true end def after_save_collection_association @@ -389,7 +381,7 @@ module ActiveRecord autosave = reflection.options[:autosave] # reconstruct the scope now that we know the owner's id - association.reset_scope if association.respond_to?(:reset_scope) + association.reset_scope if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) if autosave diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index 1e1de1863a..8b937b6703 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -20,7 +20,7 @@ module ActiveRecord subquery_alias = "subquery_for_cache_key" subquery_column = "#{subquery_alias}.#{timestamp_column}" subquery = query.arel.as(subquery_alias) - arel = Arel::SelectManager.new(query.engine).project(select_values % subquery_column).from(subquery) + arel = Arel::SelectManager.new(subquery).project(select_values % subquery_column) else query = collection.unscope(:order) query.select_values = [select_values % column] diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 769f488469..af5314c1d6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -137,9 +137,10 @@ module ActiveRecord # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ - def supports_statement_cache? - false + def supports_statement_cache? # :nodoc: + true end + deprecate :supports_statement_cache? # Runs the given block in a database transaction, and returns the result # of the block. @@ -295,6 +296,9 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). + # Most of adapters should implement `insert_fixtures` that leverages bulk SQL insert. + # We keep this method to provide fallback + # for databases like sqlite that do not support bulk inserts. def insert_fixture(fixture, table_name) fixture = fixture.stringify_keys @@ -307,16 +311,52 @@ module ActiveRecord raise Fixture::FixtureError, %(table "#{table_name}" has no column named #{name.inspect}.) end end - key_list = fixture.keys.map { |name| quote_column_name(name) } - value_list = binds.map(&:value_for_database).map do |value| - begin - quote(value) - rescue TypeError - quote(YAML.dump(value)) + + table = Arel::Table.new(table_name) + + values = binds.map do |bind| + value = with_yaml_fallback(bind.value_for_database) + [table[bind.name], value] + end + + manager = Arel::InsertManager.new + manager.into(table) + manager.insert(values) + execute manager.to_sql, "Fixture Insert" + end + + # Inserts a set of fixtures into the table. Overridden in adapters that require + # something beyond a simple insert (eg. Oracle). + def insert_fixtures(fixtures, table_name) + return if fixtures.empty? + + columns = schema_cache.columns_hash(table_name) + + values = fixtures.map do |fixture| + fixture = fixture.stringify_keys + + unknown_columns = fixture.keys - columns.keys + if unknown_columns.any? + raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.) + end + + columns.map do |name, column| + if fixture.key?(name) + type = lookup_cast_type_from_column(column) + bind = Relation::QueryAttribute.new(name, fixture[name], type) + with_yaml_fallback(bind.value_for_database) + else + Arel.sql("DEFAULT") + end end end - execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", "Fixture Insert" + table = Arel::Table.new(table_name) + manager = Arel::InsertManager.new + manager.into(table) + columns.each_key { |column| manager.columns << table[column] } + manager.values = manager.create_values_list(values) + execute manager.to_sql, "Fixtures Insert" end def empty_insert_statement_value @@ -380,6 +420,17 @@ module ActiveRecord end [relation, binds] end + + # Fixture value is quoted by Arel, however scalar values + # are not quotable. In this case we want to convert + # the column value to YAML. + def with_yaml_fallback(value) + if value.is_a?(Hash) || value.is_a?(Array) + YAML.dump(value) + else + value + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index e53ba4e666..33695c0537 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -123,6 +123,7 @@ module ActiveRecord # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such # queries should not be cached. def locked?(arel) + arel = arel.arel if arel.is_a?(Relation) arel.respond_to?(:locked) && arel.locked end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index f0c0fbab6c..61233dcc51 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -10,8 +10,15 @@ module ActiveRecord value = id_value_for_database(value) if value.is_a?(Base) if value.respond_to?(:quoted_id) + at = value.method(:quoted_id).source_location + at &&= " at %s:%d" % at + + owner = value.method(:quoted_id).owner.to_s + klass = value.class.to_s + klass += "(#{owner})" unless owner == klass + ActiveSupport::Deprecation.warn \ - "Using #quoted_id is deprecated and will be removed in Rails 5.2." + "Defining #quoted_id is deprecated and will be ignored in Rails 5.2. (defined on #{klass}#{at})" return value.quoted_id 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 a4fecc4a8e..93f4529202 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -55,7 +55,7 @@ module ActiveRecord create_sql << "(#{statements.join(', ')})" if statements.present? add_table_options!(create_sql, table_options(o)) - create_sql << " AS #{@conn.to_sql(o.as)}" if o.as + create_sql << " AS #{to_sql(o.as)}" if o.as create_sql end @@ -114,6 +114,11 @@ module ActiveRecord sql end + def to_sql(sql) + sql = sql.to_sql if sql.respond_to?(:to_sql) + sql + 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 46d7f84efd..a30fbe0e05 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -146,7 +146,7 @@ module ActiveRecord end def polymorphic_options - as_options(polymorphic) + as_options(polymorphic).merge(null: options[:null]) end def index_options 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 13629dee7f..22d7791dec 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -188,6 +188,8 @@ module ActiveRecord # The name of the primary key, if one is to be added automatically. # Defaults to +id+. If <tt>:id</tt> is false, then this option is ignored. # + # If an array is passed, a composite primary key will be created. + # # Note that Active Record models will automatically detect their # primary key. This can be avoided by using # {self.primary_key=}[rdoc-ref:AttributeMethods::PrimaryKey::ClassMethods#primary_key=] on the model @@ -241,6 +243,23 @@ module ActiveRecord # label varchar # ) # + # ====== Create a composite primary key + # + # create_table(:orders, primary_key: [:product_id, :client_id]) do |t| + # t.belongs_to :product + # t.belongs_to :client + # end + # + # generates: + # + # CREATE TABLE order ( + # product_id integer NOT NULL, + # client_id integer NOT NULL + # ); + # + # ALTER TABLE ONLY "orders" + # ADD CONSTRAINT orders_pkey PRIMARY KEY (product_id, client_id); + # # ====== Do not add a primary key column # # create_table(:categories_suppliers, id: false) do |t| @@ -493,8 +512,7 @@ module ActiveRecord # * <tt>:default</tt> - # The column's default value. Use +nil+ for +NULL+. # * <tt>:null</tt> - - # Allows or disallows +NULL+ values in the column. This option could - # have been named <tt>:null_allowed</tt>. + # Allows or disallows +NULL+ values in the column. # * <tt>:precision</tt> - # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. # * <tt>:scale</tt> - @@ -992,7 +1010,7 @@ module ActiveRecord def dump_schema_information #:nodoc: versions = ActiveRecord::SchemaMigration.all_versions - insert_versions_sql(versions) + insert_versions_sql(versions) if versions.any? end def initialize_schema_migrations_table # :nodoc: @@ -1280,9 +1298,10 @@ module ActiveRecord end def foreign_key_name(table_name, options) - identifier = "#{table_name}_#{options.fetch(:column)}_fk" - hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) options.fetch(:name) do + identifier = "#{table_name}_#{options.fetch(:column)}_fk" + hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) + "fk_rails_#{hashed_identifier}" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index c742799aab..c42e80ea2c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -29,8 +29,7 @@ module ActiveRecord # to your application.rb file: # # ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false - class_attribute :emulate_booleans - self.emulate_booleans = true + class_attribute :emulate_booleans, default: true NATIVE_DATABASE_TYPES = { primary_key: "bigint auto_increment PRIMARY KEY", @@ -76,12 +75,6 @@ module ActiveRecord true end - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - def supports_index_sort_order? !mariadb? && version >= "8.0.1" end @@ -533,8 +526,25 @@ module ActiveRecord index.using == :btree || super end + def insert_fixtures(*) + without_sql_mode("NO_AUTO_VALUE_ON_ZERO") { super } + end + private + def without_sql_mode(mode) + result = execute("SELECT @@SESSION.sql_mode") + current_mode = result.first[0] + return yield unless current_mode.include?(mode) + + sql_mode = "REPLACE(@@sql_mode, '#{mode}', '')" + execute("SET @@SESSION.sql_mode = #{sql_mode}") + yield + ensure + sql_mode = "CONCAT(@@sql_mode, ',#{mode}')" + execute("SET @@SESSION.sql_mode = #{sql_mode}") + end + def initialize_type_map(m) super @@ -550,7 +560,7 @@ module ActiveRecord m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1) m.register_type %r(^float)i, Type::Float.new(limit: 24) m.register_type %r(^double)i, Type::Float.new(limit: 53) - m.register_type %r(^json)i, MysqlJson.new + m.register_type %r(^json)i, Type::Json.new register_integer_type m, %r(^bigint)i, limit: 8 register_integer_type m, %r(^int)i, limit: 4 @@ -720,16 +730,14 @@ module ActiveRecord # MySQL is too stupid to create a temporary table for use subquery, so we have # to give it some prompting in the form of a subsubquery. Ugh! def subquery_for(key, select) - subsubselect = select.clone - subsubselect.projections = [key] + subselect = select.clone + subselect.projections = [key] # Materialize subquery by adding distinct # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' - subsubselect.distinct unless select.limit || select.offset || select.orders.any? + subselect.distinct unless select.limit || select.offset || select.orders.any? - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(key.name) - subselect.from subsubselect.as("__active_record_temp") + Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key.name)) end def supports_rename_index? @@ -846,12 +854,7 @@ module ActiveRecord end end - class MysqlJson < Type::Internal::AbstractJson # :nodoc: - def changed_in_place?(raw_old_value, new_value) - # Normalization is required because MySQL JSON data format includes - # the space between the elements. - super(serialize(deserialize(raw_old_value)), new_value) - end + class MysqlJson < Type::Json # :nodoc: end class MysqlString < Type::String # :nodoc: @@ -874,7 +877,6 @@ module ActiveRecord end end - ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2) ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb index 8c67a7a80b..9f1021456b 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -13,15 +13,6 @@ module ActiveRecord result end - # Returns an array of arrays containing the field values. - # Order is the same as that returned by +columns+. - def select_rows(arel, name = nil, binds = []) # :nodoc: - select_result(arel, name, binds) do |result| - @connection.next_result while @connection.more_results? - result.to_a - end - end - # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been @@ -58,16 +49,6 @@ module ActiveRecord @connection.last_id end - def select_result(arel, name, binds) - arel, binds = binds_from_relation(arel, binds) - sql = to_sql(arel, binds) - if without_prepared_statement?(binds) - execute_and_free(sql, name) { |result| yield result } - else - exec_stmt_and_free(sql, name, binds, cache_stmt: true) { |_, result| yield result } - end - end - def exec_stmt_and_free(sql, name, binds, cache_stmt: false) # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been # made since we established the connection diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb index f9e1e046ea..fc57e41fc9 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -45,6 +45,13 @@ module ActiveRecord indexes end + def remove_column(table_name, column_name, type = nil, options = {}) + if foreign_key_exists?(table_name, column: column_name) + remove_foreign_key(table_name, column: column_name) + end + super + end + def internal_string_options_for_primary_key super.tap do |options| if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) && (mariadb? || version < "8.0.0") diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 705e6063dc..ac5efbebeb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -7,30 +7,6 @@ module ActiveRecord PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", binds)) end - def select_value(arel, name = nil, binds = []) # :nodoc: - select_result(arel, name, binds) do |result| - result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0 - end - end - - def select_values(arel, name = nil, binds = []) # :nodoc: - select_result(arel, name, binds) do |result| - if result.nfields > 0 - result.column_values(0) - else - [] - end - end - end - - # Executes a SELECT query and returns an array of rows. Each row is an - # array of field values. - def select_rows(arel, name = nil, binds = []) # :nodoc: - select_result(arel, name, binds) do |result| - result.values - end - end - # The internal PostgreSQL identifier of the money data type. MONEY_COLUMN_TYPE_OID = 790 #:nodoc: # The internal PostgreSQL identifier of the BYTEA data type. @@ -175,14 +151,6 @@ module ActiveRecord def suppress_composite_primary_key(pk) pk unless pk.is_a?(Array) end - - def select_result(arel, name, binds) - arel, binds = binds_from_relation(arel, binds) - sql = to_sql(arel, binds) - execute_and_clear(sql, name, binds) do |result| - yield result - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index dbc879ffd4..3c706c27c4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Json < Type::Internal::AbstractJson + class Json < Type::Json # :nodoc: end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb index 87391b5dc7..a1fec289d4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -2,20 +2,10 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Jsonb < Json # :nodoc: + class Jsonb < Type::Json # :nodoc: def type :jsonb end - - def changed_in_place?(raw_old_value, new_value) - # Postgres does not preserve insignificant whitespaces when - # round-tripping jsonb columns. This causes some false positives for - # the comparison here. Therefore, we need to parse and re-dump the - # raw value here to ensure the insignificant whitespaces are - # consistent with our encoder's output. - raw_old_value = serialize(deserialize(raw_old_value)) - super(raw_old_value, new_value) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index da8d0c6992..44eb666965 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -62,7 +62,7 @@ module ActiveRecord def quote_default_expression(value, column) # :nodoc: if value.is_a?(Proc) value.call - elsif column.type == :uuid && value.include?("()") + elsif column.type == :uuid && /\(\)/.match?(value) value # Does not quote function default values for UUID columns elsif column.respond_to?(:array?) value = type_cast_from_column(column, value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 5b483ad4ab..cb45d7ba6b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -186,17 +186,17 @@ module ActiveRecord # Returns the current database encoding format. def encoding - select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns the current database collation. def collation - select_value("SELECT datcollate FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT datcollate FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns the current database ctype. def ctype - select_value("SELECT datctype FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT datctype FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns an array of schema names. @@ -290,9 +290,17 @@ module ActiveRecord if pk && sequence quoted_sequence = quote_table_name(sequence) + max_pk = select_value("select MAX(#{quote_column_name pk}) from #{quote_table_name(table)}") + if max_pk.nil? + if postgresql_version >= 100000 + minvalue = select_value("SELECT seqmin from pg_sequence where seqrelid = '#{quoted_sequence}'::regclass") + else + minvalue = select_value("SELECT min_value FROM #{quoted_sequence}") + end + end select_value(<<-end_sql, "SCHEMA") - SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) + SELECT setval('#{quoted_sequence}', #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false}) end_sql end end @@ -377,14 +385,15 @@ module ActiveRecord clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) - if seq && seq.identifier == "#{table_name}_#{pk}_seq" - new_seq = "#{new_name}_#{pk}_seq" + if pk idx = "#{table_name}_pkey" new_idx = "#{new_name}_pkey" - execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}" execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}" + if seq && seq.identifier == "#{table_name}_#{pk}_seq" + new_seq = "#{new_name}_#{pk}_seq" + execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}" + end end - rename_table_indexes(table_name, new_name) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 033d81916e..5287dd6a51 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -121,12 +121,6 @@ module ActiveRecord include PostgreSQL::DatabaseStatements include PostgreSQL::ColumnDumper - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - def supports_index_sort_order? true end @@ -464,7 +458,7 @@ module ActiveRecord m.register_type "bytea", OID::Bytea.new m.register_type "point", OID::Point.new m.register_type "hstore", OID::Hstore.new - m.register_type "json", OID::Json.new + m.register_type "json", Type::Json.new m.register_type "jsonb", OID::Jsonb.new m.register_type "cidr", OID::Cidr.new m.register_type "inet", OID::Inet.new @@ -849,7 +843,6 @@ module ActiveRecord ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql) ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :postgresql) ActiveRecord::Type.register(:inet, OID::Inet, adapter: :postgresql) - ActiveRecord::Type.register(:json, OID::Json, adapter: :postgresql) ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql) ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql) ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index e2c05ccc4e..ee2faf43b5 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -105,12 +105,6 @@ module ActiveRecord sqlite_version >= "3.8.0" end - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - def requires_reloading? true end @@ -355,6 +349,12 @@ module ActiveRecord end end + def insert_fixtures(rows, table_name) + rows.each do |row| + insert_fixture(row, table_name) + end + end + private def table_structure(table_name) diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 2ede92feff..b8fbb489b6 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionHandling - RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] } + RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence } DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" } # Establishes the connection to the database. Accepts a hash as input where diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 8f78330d4a..198c712abc 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -56,8 +56,7 @@ module ActiveRecord # :singleton-method: # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling # dates and times from the database. This is set to :utc by default. - mattr_accessor :default_timezone, instance_writer: false - self.default_timezone = :utc + mattr_accessor :default_timezone, instance_writer: false, default: :utc ## # :singleton-method: @@ -67,16 +66,14 @@ module ActiveRecord # ActiveRecord::Schema file which can be loaded into any database that # supports migrations. Use :ruby if you want to have different database # adapters for, e.g., your development and test environments. - mattr_accessor :schema_format, instance_writer: false - self.schema_format = :ruby + mattr_accessor :schema_format, instance_writer: false, default: :ruby ## # :singleton-method: # Specifies if an error should be raised if the query has an order being # ignored when doing batch queries. Useful in applications where the # scope being ignored is error-worthy, rather than a warning. - mattr_accessor :error_on_ignored_order, instance_writer: false - self.error_on_ignored_order = false + mattr_accessor :error_on_ignored_order, instance_writer: false, default: false def self.error_on_ignored_order_or_limit ActiveSupport::Deprecation.warn(<<-MSG.squish) @@ -101,8 +98,7 @@ module ActiveRecord ## # :singleton-method: # Specify whether or not to use timestamps for migration versions - mattr_accessor :timestamped_migrations, instance_writer: false - self.timestamped_migrations = true + mattr_accessor :timestamped_migrations, instance_writer: false, default: true ## # :singleton-method: @@ -110,8 +106,7 @@ module ActiveRecord # db:migrate rake task. This is true by default, which is useful for the # development environment. This should ideally be false in the production # environment where dumping schema is rarely needed. - mattr_accessor :dump_schema_after_migration, instance_writer: false - self.dump_schema_after_migration = true + mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true ## # :singleton-method: @@ -120,8 +115,7 @@ module ActiveRecord # schema_search_path are dumped. Use :all to dump all schemas regardless # of schema_search_path, or a string of comma separated schemas for a # custom list. - mattr_accessor :dump_schemas, instance_writer: false - self.dump_schemas = :schema_search_path + mattr_accessor :dump_schemas, instance_writer: false, default: :schema_search_path ## # :singleton-method: @@ -130,7 +124,6 @@ module ActiveRecord # be used to identify queries which load thousands of records and # potentially cause memory bloat. mattr_accessor :warn_on_records_fetched_greater_than, instance_writer: false - self.warn_on_records_fetched_greater_than = nil mattr_accessor :maintain_test_schema, instance_accessor: false diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 0ab03b2ab3..12ef58a941 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -95,8 +95,7 @@ module ActiveRecord module Enum def self.extended(base) # :nodoc: - base.class_attribute(:defined_enums, instance_writer: false) - base.defined_enums = {} + base.class_attribute(:defined_enums, instance_writer: false, default: {}) end def inherited(base) # :nodoc: @@ -154,11 +153,12 @@ module ActiveRecord definitions.each do |name, values| # statuses = { } enum_values = ActiveSupport::HashWithIndifferentAccess.new - name = name.to_sym + name = name.to_s # def self.statuses() statuses end - detect_enum_conflict!(name, name.to_s.pluralize, true) - klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values } + detect_enum_conflict!(name, name.pluralize, true) + singleton_class.send(:define_method, name.pluralize) { enum_values } + defined_enums[name] = enum_values detect_enum_conflict!(name, name) detect_enum_conflict!(name, "#{name}=") @@ -170,7 +170,7 @@ module ActiveRecord _enum_methods_module.module_eval do pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index - pairs.each do |value, i| + pairs.each do |label, value| if enum_prefix == true prefix = "#{name}_" elsif enum_prefix @@ -182,23 +182,23 @@ module ActiveRecord suffix = "_#{enum_suffix}" end - value_method_name = "#{prefix}#{value}#{suffix}" - enum_values[value] = i + value_method_name = "#{prefix}#{label}#{suffix}" + enum_values[label] = value + label = label.to_s - # def active?() status == 0 end + # def active?() status == "active" end klass.send(:detect_enum_conflict!, name, "#{value_method_name}?") - define_method("#{value_method_name}?") { self[attr] == value.to_s } + define_method("#{value_method_name}?") { self[attr] == label } - # def active!() update! status: :active end + # def active!() update!(status: 0) end klass.send(:detect_enum_conflict!, name, "#{value_method_name}!") define_method("#{value_method_name}!") { update!(attr => value) } - # scope :active, -> { where status: 0 } + # scope :active, -> { where(status: 0) } klass.send(:detect_enum_conflict!, name, value_method_name, true) klass.scope value_method_name, -> { where(attr => value) } end end - defined_enums[name.to_s] = enum_values end end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 18fac5af1b..60d4fb70e0 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -105,7 +105,7 @@ module ActiveRecord class WrappedDatabaseException < StatementInvalid end - # Raised when a record cannot be inserted because it would violate a uniqueness constraint. + # Raised when a record cannot be inserted or updated because it would violate a uniqueness constraint. class RecordNotUnique < WrappedDatabaseException end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index c19216702c..e9acb8acae 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -492,8 +492,7 @@ module ActiveRecord end end - cattr_accessor :all_loaded_fixtures - self.all_loaded_fixtures = {} + cattr_accessor :all_loaded_fixtures, default: {} class ClassCache def initialize(class_names, config) @@ -568,9 +567,7 @@ module ActiveRecord end table_rows.each do |fixture_set_name, rows| - rows.each do |row| - conn.insert_fixture(row, fixture_set_name) - end + conn.insert_fixtures(rows, fixture_set_name) end # Cap primary key sequences to max(pk). @@ -878,20 +875,12 @@ module ActiveRecord included do class_attribute :fixture_path, instance_writer: false - class_attribute :fixture_table_names - class_attribute :fixture_class_names - class_attribute :use_transactional_tests - class_attribute :use_instantiated_fixtures # true, false, or :no_instances - class_attribute :pre_loaded_fixtures - class_attribute :config - - self.fixture_table_names = [] - self.use_instantiated_fixtures = false - self.pre_loaded_fixtures = false - self.config = ActiveRecord::Base - - self.fixture_class_names = {} - self.use_transactional_tests = true + class_attribute :fixture_table_names, default: [] + class_attribute :fixture_class_names, default: {} + class_attribute :use_transactional_tests, default: true + class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances + class_attribute :pre_loaded_fixtures, default: false + class_attribute :config, default: ActiveRecord::Base end module ClassMethods diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index fbdaeaae51..5776807507 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -38,8 +38,7 @@ module ActiveRecord included do # Determines whether to store the full constant name including namespace when using STI. # This is true, by default. - class_attribute :store_full_sti_class, instance_writer: false - self.store_full_sti_class = true + class_attribute :store_full_sti_class, instance_writer: false, default: true end module ClassMethods @@ -217,7 +216,7 @@ module ActiveRecord def subclass_from_attributes(attrs) attrs = attrs.to_h if attrs.respond_to?(:permitted?) if attrs.is_a?(Hash) - subclass_name = attrs.with_indifferent_access[inheritance_column] + subclass_name = attrs[inheritance_column] || attrs[inheritance_column.to_sym] if subclass_name.present? find_sti_class(subclass_name) diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 8e71b60b29..cf954852bc 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -7,12 +7,19 @@ module ActiveRecord included do ## # :singleton-method: - # Indicates the format used to generate the timestamp in the cache key. - # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. + # Indicates the format used to generate the timestamp in the cache key, if + # versioning is off. Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. # # This is +:usec+, by default. - class_attribute :cache_timestamp_format, instance_writer: false - self.cache_timestamp_format = :usec + class_attribute :cache_timestamp_format, instance_writer: false, default: :usec + + ## + # :singleton-method: + # 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. + class_attribute :cache_versioning, instance_writer: false, default: false end # Returns a +String+, which Action Pack uses for constructing a URL to this @@ -42,35 +49,65 @@ module ActiveRecord id && id.to_s # Be sure to stringify the id for routes end - # Returns a cache key that can be used to identify this record. + # Returns a stable cache key that can be used to identify this record. # # Product.new.cache_key # => "products/new" - # Product.find(5).cache_key # => "products/5" (updated_at not available) - # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) + # Product.find(5).cache_key # => "products/5" # - # You can also pass a list of named timestamps, and the newest in the list will be - # used to generate the key: + # If ActiveRecord::Base.cache_versioning is turned off, as it was in Rails 5.1 and earlier, + # the cache key will also include a version. # - # Person.find(5).cache_key(:updated_at, :last_reviewed_at) + # Product.cache_versioning = false + # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) def cache_key(*timestamp_names) if new_record? "#{model_name.cache_key}/new" else - timestamp = if timestamp_names.any? - max_updated_column_timestamp(timestamp_names) + if cache_version && timestamp_names.none? + "#{model_name.cache_key}/#{id}" else - max_updated_column_timestamp - end + timestamp = if timestamp_names.any? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Specifying a timestamp name for #cache_key has been deprecated in favor of + the explicit #cache_version method that can be overwritten. + MSG - if timestamp - timestamp = timestamp.utc.to_s(cache_timestamp_format) - "#{model_name.cache_key}/#{id}-#{timestamp}" - else - "#{model_name.cache_key}/#{id}" + max_updated_column_timestamp(timestamp_names) + else + max_updated_column_timestamp + end + + if timestamp + timestamp = timestamp.utc.to_s(cache_timestamp_format) + "#{model_name.cache_key}/#{id}-#{timestamp}" + else + "#{model_name.cache_key}/#{id}" + end end end end + # Returns a cache version that can be used together with the cache key to form + # a recyclable caching scheme. By default, the #updated_at column is used for the + # cache_version, but this method can be overwritten to return something else. + # + # Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to + # +false+ (which it is by default until Rails 6.0). + def cache_version + if cache_versioning && timestamp = try(:updated_at) + timestamp.utc.to_s(:usec) + end + end + + # Returns a cache key along with the version. + def cache_key_with_version + if version = cache_version + "#{cache_key}-#{version}" + else + cache_key + end + end + module ClassMethods # Defines your model's +to_param+ method to generate "pretty" URLs # using +method_name+, which can be any attribute or method that diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 78ce9f8291..522da6a571 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -51,8 +51,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :lock_optimistically, instance_writer: false - self.lock_optimistically = true + class_attribute :lock_optimistically, instance_writer: false, default: true end def locking_enabled? #:nodoc: @@ -63,8 +62,8 @@ module ActiveRecord def increment_lock lock_col = self.class.locking_column - previous_lock_value = send(lock_col).to_i - send(lock_col + "=", previous_lock_value + 1) + previous_lock_value = send(lock_col) + send("#{lock_col}=", previous_lock_value + 1) end def _create_record(attribute_names = self.attribute_names, *) @@ -108,7 +107,8 @@ module ActiveRecord # If something went wrong, revert the locking_column value. rescue Exception - send(lock_col + "=", previous_lock_value.to_i) + send("#{lock_col}=", previous_lock_value.to_i) + raise end end @@ -128,7 +128,7 @@ module ActiveRecord if locking_enabled? locking_column = self.class.locking_column - relation = relation.where(locking_column => _read_attribute(locking_column)) + relation = relation.where(locking_column => read_attribute_before_type_cast(locking_column)) end relation diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 54216caaaf..14e0f5bff7 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,3 +1,5 @@ +require "monitor" + module ActiveRecord module ModelSchema extend ActiveSupport::Concern @@ -128,30 +130,19 @@ module ActiveRecord included do mattr_accessor :primary_key_prefix_type, instance_writer: false - class_attribute :table_name_prefix, instance_writer: false - self.table_name_prefix = "" - - class_attribute :table_name_suffix, instance_writer: false - self.table_name_suffix = "" - - class_attribute :schema_migrations_table_name, instance_accessor: false - self.schema_migrations_table_name = "schema_migrations" - - class_attribute :internal_metadata_table_name, instance_accessor: false - self.internal_metadata_table_name = "ar_internal_metadata" - - class_attribute :protected_environments, instance_accessor: false - self.protected_environments = ["production"] - - class_attribute :pluralize_table_names, instance_writer: false - self.pluralize_table_names = true - - class_attribute :ignored_columns, instance_accessor: false - self.ignored_columns = [].freeze + class_attribute :table_name_prefix, instance_writer: false, default: "" + class_attribute :table_name_suffix, instance_writer: false, default: "" + class_attribute :schema_migrations_table_name, instance_accessor: false, default: "schema_migrations" + class_attribute :internal_metadata_table_name, instance_accessor: false, default: "ar_internal_metadata" + class_attribute :protected_environments, instance_accessor: false, default: [ "production" ] + class_attribute :pluralize_table_names, instance_writer: false, default: true + class_attribute :ignored_columns, instance_accessor: false, default: [].freeze self.inheritance_column = "type" delegate :type_for_attribute, to: :class + + initialize_load_schema_monitor end # Derives the join table name for +first_table+ and +second_table+. The @@ -377,7 +368,7 @@ module ActiveRecord # default values when instantiating the Active Record object for this table. def column_defaults load_schema - _default_attributes.to_hash + @column_defaults ||= _default_attributes.to_hash end def _default_attributes # :nodoc: @@ -435,15 +426,31 @@ module ActiveRecord initialize_find_by_cache end + protected + + def initialize_load_schema_monitor + @load_schema_monitor = Monitor.new + end + private + def inherited(child_class) + super + child_class.initialize_load_schema_monitor + end + def schema_loaded? - defined?(@columns_hash) && @columns_hash + defined?(@schema_loaded) && @schema_loaded end def load_schema - unless schema_loaded? + return if schema_loaded? + @load_schema_monitor.synchronize do + return if defined?(@columns_hash) && @columns_hash + load_schema! + + @schema_loaded = true end end @@ -466,10 +473,12 @@ module ActiveRecord @attribute_types = nil @content_columns = nil @default_attributes = nil + @column_defaults = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column @attributes_builder = nil @columns = nil @columns_hash = nil + @schema_loaded = false @attribute_names = nil @yaml_encoder = nil direct_descendants.each do |descendant| diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 01ecd79b8f..917bc76993 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -10,8 +10,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :nested_attributes_options, instance_writer: false - self.nested_attributes_options = {} + class_attribute :nested_attributes_options, instance_writer: false, default: {} end # = Active Record Nested Attributes @@ -458,7 +457,7 @@ module ActiveRecord end unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) - raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" + raise ArgumentError, "Hash or Array expected for attribute `#{association_name}`, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" end check_record_limit!(options[:limit], attributes_collection) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index f652c7c3a1..b2dba5516e 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -526,7 +526,7 @@ module ActiveRecord if locking_enabled? locking_column = self.class.locking_column - scope = scope.where(locking_column => _read_attribute(locking_column)) + scope = scope.where(locking_column => read_attribute_before_type_cast(locking_column)) changes[locking_column] = increment_lock end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index ec246e97bc..e4c2e1f86f 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -5,20 +5,20 @@ module ActiveRecord # Enable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def cache(&block) - if connected? - connection.cache(&block) - else + if configurations.empty? yield + else + connection.cache(&block) end end # Disable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def uncached(&block) - if connected? - connection.uncached(&block) - else + if configurations.empty? yield + else + connection.uncached(&block) end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index c4a22398f0..b16e178358 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -8,7 +8,7 @@ module ActiveRecord delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all delegate :find_each, :find_in_batches, :in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, - :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, + :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, :having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all delegate :pluck, :ids, to: :all diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 6274996ab8..af6473d250 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -3,8 +3,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_attr_readonly, instance_accessor: false - self._attr_readonly = [] + class_attribute :_attr_readonly, instance_accessor: false, default: [] end module ClassMethods diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 47b561840f..73761ed7ed 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -8,10 +8,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_reflections, instance_writer: false - class_attribute :aggregate_reflections, instance_writer: false - self._reflections = {} - self.aggregate_reflections = {} + class_attribute :_reflections, instance_writer: false, default: {} + class_attribute :aggregate_reflections, instance_writer: false, default: {} end def self.create(macro, name, scope, options, ar) @@ -173,7 +171,7 @@ module ActiveRecord JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: def join_keys - get_join_keys klass + @join_keys ||= get_join_keys(klass) end # Returns a list of scopes that should be applied for this Reflection @@ -187,10 +185,30 @@ module ActiveRecord end deprecate :scope_chain + def join_scope(table, foreign_table, foreign_klass) + predicate_builder = predicate_builder(table) + scope_chain_items = join_scopes(table, predicate_builder) + klass_scope = klass_join_scope(table, predicate_builder) + + key = join_keys.key + foreign_key = join_keys.foreign_key + + klass_scope.where!(table[key].eq(foreign_table[foreign_key])) + + if klass.finder_needs_type_condition? + klass_scope.where!(klass.send(:type_condition, table)) + end + + if type + klass_scope.where!(type => foreign_klass.base_class.sti_name) + end + + scope_chain_items.inject(klass_scope, &:merge!) + end + def join_scopes(table, predicate_builder) # :nodoc: if scope - [ActiveRecord::Relation.create(klass, table, predicate_builder) - .instance_exec(&scope)] + [build_scope(table, predicate_builder).instance_exec(&scope)] else [] end @@ -199,20 +217,15 @@ module ActiveRecord def klass_join_scope(table, predicate_builder) # :nodoc: if klass.current_scope klass.current_scope.clone.tap { |scope| - scope.joins_values = [] + scope.joins_values = scope.left_outer_joins_values = [].freeze } else - relation = ActiveRecord::Relation.create( - klass, - table, - predicate_builder, - ) - klass.send(:build_default_scope, relation) + klass.default_scoped(build_scope(table, predicate_builder)) end end def constraints - chain.map(&:scopes).flatten + chain.flat_map(&:scopes) end def counter_cache_column @@ -289,7 +302,19 @@ module ActiveRecord JoinKeys.new(join_pk(association_klass), join_fk) end + def build_scope(table, predicate_builder = predicate_builder(table)) + Relation.create(klass, table, predicate_builder) + end + + protected + def actual_source_reflection # FIXME: this is a horrible name + self + end + private + def predicate_builder(table) + PredicateBuilder.new(TableMetadata.new(klass, table)) + end def join_pk(_) foreign_key @@ -581,11 +606,9 @@ module ActiveRecord seed + [self] end - protected - - def actual_source_reflection # FIXME: this is a horrible name - self - end + def extensions + Array(options[:extend]) + end private @@ -755,7 +778,6 @@ module ActiveRecord # Holds all the metadata about a :through association as it was specified # in the Active Record class. class ThroughReflection < AbstractReflection #:nodoc: - attr_reader :delegate_reflection delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :type, :get_join_keys, to: :source_reflection @@ -981,19 +1003,23 @@ module ActiveRecord collect_join_reflections(seed + [self]) end - def collect_join_reflections(seed) - a = source_reflection.add_as_source seed - if options[:source_type] - through_reflection.add_as_polymorphic_through self, a - else - through_reflection.add_as_through a + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + attr_reader :delegate_reflection + + def actual_source_reflection # FIXME: this is a horrible name + source_reflection.actual_source_reflection end - end private - - def actual_source_reflection # FIXME: this is a horrible name - source_reflection.send(:actual_source_reflection) + def collect_join_reflections(seed) + a = source_reflection.add_as_source seed + if options[:source_type] + through_reflection.add_as_polymorphic_through self, a + else + through_reflection.add_as_through a + end end def primary_key(klass) @@ -1101,7 +1127,7 @@ module ActiveRecord end def alias_name - Arel::Table.new(table_name) + Arel::Table.new(table_name, type_caster: klass.type_caster) end def all_includes; yield; end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 5775eda5a5..133c1a6318 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -2,7 +2,7 @@ module ActiveRecord # = Active Record \Relation class Relation MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, - :order, :joins, :left_joins, :left_outer_joins, :references, + :order, :joins, :left_outer_joins, :references, :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, @@ -18,6 +18,7 @@ module ActiveRecord attr_reader :table, :klass, :loaded, :predicate_builder alias :model :klass alias :loaded? :loaded + alias :locked? :lock_value def initialize(klass, table, predicate_builder, values = {}) @klass = klass @@ -269,8 +270,7 @@ module ActiveRecord # Returns true if there are no records. def empty? return @records.empty? if loaded? - - limit_value == 0 || !exists? + !exists? end # Returns true if there are no records. @@ -404,9 +404,9 @@ module ActiveRecord # # Note: Updating a large number of records will run an # UPDATE query for each record, which may cause a performance - # issue. So if it is not needed to run callbacks for each update, it is - # preferred to use #update_all for updating all records using - # a single query. + # issue. When running callbacks is not needed for each record update, + # it is preferred to use #update_all for updating all records + # in a single query. def update(id = :all, attributes) if id.is_a?(Array) id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 76031515fd..13a2c3f511 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -30,14 +30,14 @@ module ActiveRecord # end # # ==== Options - # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000. # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when - # an order is present in the relation. + # an order is present in the relation. # # Limits are honored, and if present there is no requirement for the batch - # size, it can be less than, equal, or greater than the limit. + # size: it can be less than, equal to, or greater than the limit. # # The options +start+ and +finish+ are especially useful if you want # multiple workers dealing with the same processing queue. You can make @@ -89,14 +89,14 @@ module ActiveRecord # To be yielded each record one by one, use #find_each instead. # # ==== Options - # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000. # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when - # an order is present in the relation. + # an order is present in the relation. # # Limits are honored, and if present there is no requirement for the batch - # size, it can be less than, equal, or greater than the limit. + # size: it can be less than, equal to, or greater than the limit. # # The options +start+ and +finish+ are especially useful if you want # multiple workers dealing with the same processing queue. You can make @@ -140,9 +140,9 @@ module ActiveRecord # If you do not provide a block to #in_batches, it will return a # BatchEnumerator which is enumerable. # - # Person.in_batches.with_index do |relation, batch_index| + # Person.in_batches.each_with_index do |relation, batch_index| # puts "Processing relation ##{batch_index}" - # relation.each { |relation| relation.delete_all } + # relation.delete_all # end # # Examples of calling methods on the returned BatchEnumerator object: @@ -152,12 +152,12 @@ module ActiveRecord # Person.in_batches.each_record(&:party_all_night!) # # ==== Options - # * <tt>:of</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false. + # * <tt>:of</tt> - Specifies the size of the batch. Defaults to 1000. + # * <tt>:load</tt> - Specifies if the relation should be loaded. Defaults to false. # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when - # an order is present in the relation. + # an order is present in the relation. # # Limits are honored, and if present there is no requirement for the batch # size, it can be less than, equal, or greater than the limit. @@ -186,7 +186,7 @@ module ActiveRecord # # NOTE: It's not possible to set the order. That is automatically set to # ascending on the primary key ("id ASC") to make the batch ordering - # consistent. Therefore the primary key must be orderable, e.g an integer + # consistent. Therefore the primary key must be orderable, e.g. an integer # or a string. # # NOTE: By its nature, batch processing is subject to race conditions if diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 9cabd1af13..24b8be0242 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -37,7 +37,16 @@ module ActiveRecord # Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil) - return super() if block_given? + if block_given? + unless column_name.nil? + ActiveSupport::Deprecation.warn \ + "When `count' is called with a block, it ignores other arguments. " \ + "This behavior is now deprecated and will result in an ArgumentError in Rails 5.3." + end + + return super() + end + calculate(:count, column_name) end @@ -73,7 +82,16 @@ module ActiveRecord # # Person.sum(:age) # => 4562 def sum(column_name = nil) - return super() if block_given? + if block_given? + unless column_name.nil? + ActiveSupport::Deprecation.warn \ + "When `sum' is called with a block, it ignores other arguments. " \ + "This behavior is now deprecated and will result in an ArgumentError in Rails 5.3." + end + + return super() + end + calculate(:sum, column_name) end @@ -293,7 +311,7 @@ module ActiveRecord relation.group_values = group_fields relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes) + calculated_data = @klass.connection.select_all(relation.arel, nil, relation.bound_attributes) if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } @@ -368,9 +386,8 @@ module ActiveRecord relation.select_values = [aliased_column] subquery = relation.arel.as(subquery_alias) - sm = Arel::SelectManager.new relation.engine select_value = operation_over_aggregate_column(column_alias, "count", distinct) - sm.project(select_value).from(subquery) + Arel::SelectManager.new(subquery).project(select_value) end end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 257ae04ff4..8b4dd25689 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -25,8 +25,6 @@ module ActiveRecord def inherited(child_class) child_class.initialize_relation_delegate_cache - delegate = child_class.relation_delegate_class(ActiveRecord::Associations::CollectionProxy) - delegate.include ActiveRecord::Associations::CollectionProxy::DelegateExtending super end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index a1459c87c6..df8909379f 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -147,8 +147,7 @@ module ActiveRecord def last(limit = nil) return find_last(limit) if loaded? || limit_value - result = limit(limit) - result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key + result = ordered_relation.limit(limit) result = result.reverse_order! limit ? result.reverse : result.first @@ -307,14 +306,16 @@ module ActiveRecord MSG end - return false if !conditions + return false if !conditions || limit_value == 0 + + relation = self unless eager_loading? + relation ||= apply_join_dependency(self, construct_join_dependency(eager_loading: false)) - relation = apply_join_dependency(self, construct_join_dependency(eager_loading: false)) return false if ActiveRecord::NullRelation === relation relation = construct_relation_for_exists(relation, conditions) - connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false + connection.select_value(relation.arel, "#{name} Exists", relation.bound_attributes) ? true : false rescue ::RangeError false end @@ -375,8 +376,7 @@ module ActiveRecord if ActiveRecord::NullRelation === relation [] else - arel = relation.arel - rows = connection.select_all(arel, "SQL", relation.bound_attributes) + rows = connection.select_all(relation.arel, "SQL", relation.bound_attributes) join_dependency.instantiate(rows, aliases) end end @@ -423,9 +423,8 @@ module ActiveRecord "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values) relation = relation.except(:select).select(values).distinct! - arel = relation.arel - id_rows = @klass.connection.select_all(arel, "SQL", relation.bound_attributes) + id_rows = @klass.connection.select_all(relation.arel, "SQL", relation.bound_attributes) id_rows.map { |row| row[primary_key] } end @@ -533,11 +532,7 @@ module ActiveRecord if loaded? records[index, limit] || [] else - relation = if order_values.empty? && primary_key - order(arel_attribute(primary_key).asc) - else - self - end + relation = ordered_relation if limit_value.nil? || index < limit_value relation = relation.offset(offset_index + index) unless index.zero? @@ -552,11 +547,7 @@ module ActiveRecord if loaded? records[-index] else - relation = if order_values.empty? && primary_key - order(arel_attribute(primary_key).asc) - else - self - end + relation = ordered_relation relation.to_a[-index] # TODO: can be made more performant on large result sets by @@ -570,5 +561,13 @@ module ActiveRecord def find_last(limit) limit ? records.last(limit) : records.last end + + def ordered_relation + if order_values.empty? && primary_key + order(arel_attribute(primary_key).asc) + else + self + end + end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 183fe91c05..7dea5deec5 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,12 +1,3 @@ -require "active_record/relation/predicate_builder/array_handler" -require "active_record/relation/predicate_builder/base_handler" -require "active_record/relation/predicate_builder/basic_object_handler" -require "active_record/relation/predicate_builder/range_handler" -require "active_record/relation/predicate_builder/relation_handler" - -require "active_record/relation/predicate_builder/association_query_value" -require "active_record/relation/predicate_builder/polymorphic_array_value" - module ActiveRecord class PredicateBuilder # :nodoc: delegate :resolve_column_aliases, to: :table @@ -116,21 +107,26 @@ module ActiveRecord first = value.begin last = value.end unless first.respond_to?(:infinite?) && first.infinite? - binds << build_bind_param(column_name, first) + binds << build_bind_attribute(column_name, first) first = Arel::Nodes::BindParam.new end unless last.respond_to?(:infinite?) && last.infinite? - binds << build_bind_param(column_name, last) + binds << build_bind_attribute(column_name, last) last = Arel::Nodes::BindParam.new end result[column_name] = RangeHandler::RangeWithBinds.new(first, last, value.exclude_end?) + when value.is_a?(Relation) + binds.concat(value.bound_attributes) else if can_be_bound?(column_name, value) - result[column_name] = Arel::Nodes::BindParam.new - binds << build_bind_param(column_name, value) - elsif value.is_a?(Relation) - binds.concat(value.bound_attributes) + bind_attribute = build_bind_attribute(column_name, value) + if value.is_a?(StatementCache::Substitute) || !bind_attribute.value_for_database.nil? + result[column_name] = Arel::Nodes::BindParam.new + binds << bind_attribute + else + result[column_name] = nil + end end end end @@ -173,8 +169,17 @@ module ActiveRecord end end - def build_bind_param(column_name, value) + def build_bind_attribute(column_name, value) Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) end end end + +require "active_record/relation/predicate_builder/array_handler" +require "active_record/relation/predicate_builder/base_handler" +require "active_record/relation/predicate_builder/basic_object_handler" +require "active_record/relation/predicate_builder/range_handler" +require "active_record/relation/predicate_builder/relation_handler" + +require "active_record/relation/predicate_builder/association_query_value" +require "active_record/relation/predicate_builder/polymorphic_array_value" diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb index 2fe0f81cab..3e19646ae5 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb @@ -1,8 +1,6 @@ module ActiveRecord class PredicateBuilder class AssociationQueryValue # :nodoc: - attr_reader :associated_table, :value - def initialize(associated_table, value) @associated_table = associated_table @value = value @@ -12,6 +10,11 @@ module ActiveRecord [associated_table.association_foreign_key.to_s => ids] end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + attr_reader :associated_table, :value + private def ids case value diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb index 9bb2f8c8dc..7029ae5f47 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb @@ -1,8 +1,6 @@ module ActiveRecord class PredicateBuilder class PolymorphicArrayValue # :nodoc: - attr_reader :associated_table, :values - def initialize(associated_table, values) @associated_table = associated_table @values = values @@ -17,6 +15,11 @@ module ActiveRecord end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + attr_reader :associated_table, :values + private def type_to_ids_mapping default_hash = Hash.new { |hsh, key| hsh[key] = [] } diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 1178dec706..d44f6fd572 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -248,7 +248,7 @@ module ActiveRecord return super() end - raise ArgumentError, "Call this with at least one field" if fields.empty? + raise ArgumentError, "Call `select' with at least one field" if fields.empty? spawn._select!(*fields) end @@ -1100,14 +1100,16 @@ module ActiveRecord end VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC, - "asc", "desc", "ASC", "DESC"] # :nodoc: + "asc", "desc", "ASC", "DESC"].to_set # :nodoc: def validate_order_args(args) args.each do |arg| next unless arg.is_a?(Hash) arg.each do |_key, value| - raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \ - "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value) + unless VALID_DIRECTIONS.include?(value) + raise ArgumentError, + "Direction \"#{value}\" is invalid. Valid directions are: #{VALID_DIRECTIONS.to_a.inspect}" + end end end end @@ -1120,7 +1122,7 @@ module ActiveRecord validate_order_args(order_args) references = order_args.grep(String) - references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! + references.map! { |arg| arg =~ /^\W?(\w+)\W?\./ && $1 }.compact! references!(references) if references.any? # if a symbol is given we prepend the quoted table name @@ -1165,7 +1167,7 @@ module ActiveRecord end end - STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having] + STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope] def structurally_incompatible_values_for_or(other) STRUCTURAL_OR_METHODS.reject do |method| get_value(method) == other.get_value(method) diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index 04bee73e8f..b862dd56a5 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -57,7 +57,7 @@ module ActiveRecord else column = klass.column_for_attribute(attribute) - binds << predicate_builder.send(:build_bind_param, attribute, value) + binds << predicate_builder.send(:build_bind_attribute, attribute, value) value = Arel::Nodes::BindParam.new predicate = if options[:case_sensitive] diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 94d63765c9..66a2846f3a 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -11,10 +11,9 @@ module ActiveRecord ## # :singleton-method: # A list of tables which should not be dumped to the schema. - # Acceptable values are strings as well as regexp. - # This setting is only used if ActiveRecord::Base.schema_format == :ruby - cattr_accessor :ignore_tables - @@ignore_tables = [] + # Acceptable values are strings as well as regexp if ActiveRecord::Base.schema_format == :ruby. + # Only strings are accepted if ActiveRecord::Base.schema_format == :sql. + cattr_accessor :ignore_tables, default: [] class << self def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base) diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 2daa48859a..70b2693b28 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -5,11 +5,8 @@ module ActiveRecord included do # Stores the default scope for the class. - class_attribute :default_scopes, instance_writer: false, instance_predicate: false - class_attribute :default_scope_override, instance_writer: false, instance_predicate: false - - self.default_scopes = [] - self.default_scope_override = nil + class_attribute :default_scopes, instance_writer: false, instance_predicate: false, default: [] + class_attribute :default_scope_override, instance_writer: false, instance_predicate: false, default: nil end module ClassMethods @@ -110,7 +107,11 @@ module ActiveRecord if default_scope_override # The user has defined their own default scope method, so call that - evaluate_default_scope { default_scope } + evaluate_default_scope do + if scope = default_scope + (base_rel ||= relation).merge(scope) + end + end elsif default_scopes.any? base_rel ||= relation evaluate_default_scope do diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 27cdf8cb7e..388f471bf5 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -29,13 +29,15 @@ module ActiveRecord end end - def default_scoped # :nodoc: - scope = build_default_scope + def default_scoped(scope = relation) # :nodoc: + build_default_scope(scope) || scope + end - if scope - relation.spawn.merge!(scope) + def default_extensions # :nodoc: + if scope = current_scope || build_default_scope + scope.extensions else - relation + [] end end @@ -156,17 +158,17 @@ module ActiveRecord if body.respond_to?(:to_proc) singleton_class.send(:define_method, name) do |*args| - scope = all.scoping { instance_exec(*args, &body) } + scope = all + scope = scope.scoping { instance_exec(*args, &body) || scope } scope = scope.extending(extension) if extension - - scope || all + scope end else singleton_class.send(:define_method, name) do |*args| - scope = all.scoping { body.call(*args) } + scope = all + scope = scope.scoping { body.call(*args) || scope } scope = scope.extending(extension) if extension - - scope || all + scope end end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 45110a79cd..ba686fc562 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -71,9 +71,9 @@ module ActiveRecord @tasks[pattern] = task end - register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks) - register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks) - register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks) + register_task(/mysql/, "ActiveRecord::Tasks::MySQLDatabaseTasks") + register_task(/postgresql/, "ActiveRecord::Tasks::PostgreSQLDatabaseTasks") + register_task(/sqlite/, "ActiveRecord::Tasks::SQLiteDatabaseTasks") def db_dir @db_dir ||= Rails.application.config.paths["db"].first @@ -288,11 +288,11 @@ module ActiveRecord private def class_for_adapter(adapter) - key = @tasks.keys.detect { |pattern| adapter[pattern] } - unless key + _key, task = @tasks.each_pair.detect { |pattern, _task| adapter[pattern] } + unless task raise DatabaseNotSupported, "Rake tasks not supported by '#{adapter}' adapter" end - @tasks[key] + task.is_a?(String) ? task.constantize : task end def each_current_configuration(environment) diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index c05f0a8fbb..c25d87dd3e 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -59,8 +59,14 @@ module ActiveRecord args.concat(["--no-data"]) args.concat(["--routines"]) args.concat(["--skip-comments"]) - args.concat(Array(extra_flags)) if extra_flags + + ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + if ignore_tables.any? + args += ignore_tables.map { |table| "--ignore-table=#{configuration['database']}.#{table}" } + end + args.concat(["#{configuration['database']}"]) + args.unshift(*extra_flags) if extra_flags run_cmd("mysqldump", args, "dumping") end @@ -69,7 +75,7 @@ module ActiveRecord args = prepare_command_options args.concat(["--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}]) args.concat(["--database", "#{configuration['database']}"]) - args.concat(Array(extra_flags)) if extra_flags + args.unshift(*extra_flags) if extra_flags run_cmd("mysql", args, "loading") end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index f1af90c1e8..7f1a768d8b 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -66,6 +66,12 @@ module ActiveRecord "--schema=#{part.strip}" end end + + ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + if ignore_tables.any? + args += ignore_tables.flat_map { |table| ["-T", table] } + end + args << configuration["database"] run_cmd("pg_dump", args, "dumping") remove_sql_header_comments(filename) diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index 1f756c2979..01562b21e9 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -36,9 +36,18 @@ module ActiveRecord end def structure_dump(filename, extra_flags) - dbfile = configuration["database"] - flags = extra_flags.join(" ") if extra_flags - `sqlite3 #{flags} #{dbfile} .schema > #{filename}` + args = [] + args.concat(Array(extra_flags)) if extra_flags + args << configuration["database"] + + ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + if ignore_tables.any? + condition = ignore_tables.map { |table| connection.quote(table) }.join(", ") + args << "SELECT sql FROM sqlite_master WHERE tbl_name NOT IN (#{condition}) ORDER BY tbl_name, type DESC, name" + else + args << ".schema" + end + run_cmd("sqlite3", args, filename) end def structure_load(filename, extra_flags) @@ -56,6 +65,17 @@ module ActiveRecord def root @root end + + def run_cmd(cmd, args, out) + fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, out: out) + end + + def run_cmd_error(cmd, args) + msg = "failed to execute:\n" + msg << "#{cmd} #{args.join(' ')}\n\n" + msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" + msg + end end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 09d8d1cdd4..55f3a194a9 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -43,8 +43,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :record_timestamps - self.record_timestamps = true + class_attribute :record_timestamps, default: true end def initialize_dup(other) # :nodoc: diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 4f632660a8..9ed6c95bf9 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,11 +1,11 @@ require "active_model/type" -require "active_record/type/internal/abstract_json" require "active_record/type/internal/timezone" require "active_record/type/date" require "active_record/type/date_time" require "active_record/type/decimal_without_scale" +require "active_record/type/json" require "active_record/type/time" require "active_record/type/text" require "active_record/type/unsigned_integer" @@ -69,6 +69,7 @@ module ActiveRecord register(:decimal, Type::Decimal, override: false) register(:float, Type::Float, override: false) register(:integer, Type::Integer, override: false) + register(:json, Type::Json, override: false) register(:string, Type::String, override: false) register(:text, Type::Text, override: false) register(:time, Type::Time, override: false) diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb deleted file mode 100644 index e19c5a14da..0000000000 --- a/activerecord/lib/active_record/type/internal/abstract_json.rb +++ /dev/null @@ -1,33 +0,0 @@ -module ActiveRecord - module Type - module Internal # :nodoc: - class AbstractJson < ActiveModel::Type::Value # :nodoc: - include ActiveModel::Type::Helpers::Mutable - - def type - :json - end - - def deserialize(value) - if value.is_a?(::String) - ::ActiveSupport::JSON.decode(value) rescue nil - else - value - end - end - - def serialize(value) - if value.nil? - nil - else - ::ActiveSupport::JSON.encode(value) - end - end - - def accessor - ActiveRecord::Store::StringKeyedHashAccessor - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/json.rb b/activerecord/lib/active_record/type/json.rb new file mode 100644 index 0000000000..c4732fe388 --- /dev/null +++ b/activerecord/lib/active_record/type/json.rb @@ -0,0 +1,28 @@ +module ActiveRecord + module Type + class Json < ActiveModel::Type::Value + include ActiveModel::Type::Helpers::Mutable + + def type + :json + end + + def deserialize(value) + return value unless value.is_a?(::String) + ActiveSupport::JSON.decode(value) rescue nil + end + + def serialize(value) + ActiveSupport::JSON.encode(value) unless value.nil? + end + + def changed_in_place?(raw_old_value, new_value) + deserialize(raw_old_value) != new_value + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + end + end +end diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb index 68fca44e3b..a79b8eafea 100644 --- a/activerecord/lib/rails/generators/active_record.rb +++ b/activerecord/lib/rails/generators/active_record.rb @@ -10,7 +10,7 @@ module ActiveRecord # Set the current directory as base for the inherited generators. def self.base_root - File.dirname(__FILE__) + __dir__ end end end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 601d575c0e..a1fb6427f9 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -220,7 +220,7 @@ module ActiveRecord def test_select_all_with_legacy_binds post = Post.create!(title: "foo", body: "bar") expected = @connection.select_all("SELECT * FROM posts WHERE id = #{post.id}") - result = @connection.select_all("SELECT * FROM posts WHERE id = #{Arel::Nodes::BindParam.new.to_sql}", nil, [[nil, post.id]]) + result = @connection.select_all("SELECT * FROM posts WHERE id = #{bind_param.to_sql}", nil, [[nil, post.id]]) assert_equal expected.to_hash, result.to_hash end end diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb index 8826ad7fd1..e4a6ed5482 100644 --- a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -48,7 +48,7 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase test "schema dump includes collation" do output = dump_table_schema("charset_collations") - assert_match %r{t.string\s+"string_ascii_bin",\s+collation: "ascii_bin"$}, output - assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+collation: "ucs2_unicode_ci"$}, output + assert_match %r{t\.string\s+"string_ascii_bin",\s+collation: "ascii_bin"$}, output + assert_match %r{t\.text\s+"text_ucs2_unicode_ci",\s+collation: "ucs2_unicode_ci"$}, output end end diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb index 6954006003..26c69edc7b 100644 --- a/activerecord/test/cases/adapters/mysql2/json_test.rb +++ b/activerecord/test/cases/adapters/mysql2/json_test.rb @@ -1,195 +1,22 @@ require "cases/helper" -require "support/schema_dumping_helper" +require "cases/json_shared_test_cases" if ActiveRecord::Base.connection.supports_json? class Mysql2JSONTest < ActiveRecord::Mysql2TestCase - include SchemaDumpingHelper + include JSONSharedTestCases self.use_transactional_tests = false - class JsonDataType < ActiveRecord::Base - self.table_name = "json_data_type" - - store_accessor :settings, :resolution - end - def setup - @connection = ActiveRecord::Base.connection - begin - @connection.create_table("json_data_type") do |t| - t.json "payload" - t.json "settings" - end + super + @connection.create_table("json_data_type") do |t| + t.json "payload" + t.json "settings" end end - def teardown - @connection.drop_table :json_data_type, if_exists: true - JsonDataType.reset_column_information - end - - def test_column - column = JsonDataType.columns_hash["payload"] - assert_equal :json, column.type - assert_equal "json", column.sql_type - - type = JsonDataType.type_for_attribute("payload") - assert_not type.binary? - end - - def test_change_table_supports_json - @connection.change_table("json_data_type") do |t| - t.json "users" + private + def column_type + :json end - JsonDataType.reset_column_information - column = JsonDataType.columns_hash["users"] - assert_equal :json, column.type - end - - def test_schema_dumping - output = dump_table_schema("json_data_type") - assert_match(/t\.json\s+"settings"/, output) - end - - def test_cast_value_on_write - x = JsonDataType.new payload: { "string" => "foo", :symbol => :bar } - assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast) - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload) - x.save - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) - end - - def test_type_cast_json - type = JsonDataType.type_for_attribute("payload") - - data = "{\"a_key\":\"a_value\"}" - hash = type.deserialize(data) - assert_equal({ "a_key" => "a_value" }, hash) - assert_equal({ "a_key" => "a_value" }, type.deserialize(data)) - - assert_equal({}, type.deserialize("{}")) - assert_equal({ "key" => nil }, type.deserialize('{"key": null}')) - assert_equal({ "c" => "}", '"a"' => 'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) - end - - def test_rewrite - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - x.payload = { '"a\'' => "b" } - assert x.save! - end - - def test_select - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - assert_equal({ "k" => "v" }, x.payload) - end - - def test_select_multikey - @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')| - x = JsonDataType.first - assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload) - end - - def test_null_json - @connection.execute "insert into json_data_type (payload) VALUES(null)" - x = JsonDataType.first - assert_nil(x.payload) - end - - def test_select_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - assert_equal(["v0", { "k1" => "v1" }], x.payload) - end - - def test_select_nil_json_after_create - json = JsonDataType.create(payload: nil) - x = JsonDataType.where(payload: nil).first - assert_equal(json, x) - end - - def test_select_nil_json_after_update - json = JsonDataType.create(payload: "foo") - x = JsonDataType.where(payload: nil).first - assert_nil(x) - - json.update_attributes payload: nil - x = JsonDataType.where(payload: nil).first - assert_equal(json.reload, x) - end - - def test_rewrite_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - x.payload = ["v1", { "k2" => "v2" }, "v3"] - assert x.save! - end - - def test_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - x.save! - x = JsonDataType.first - assert_equal "320×480", x.resolution - - x.resolution = "640×1136" - x.save! - - x = JsonDataType.first - assert_equal "640×1136", x.resolution - end - - def test_duplication_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = x.dup - assert_equal "320×480", y.resolution - end - - def test_yaml_round_trip_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = YAML.load(YAML.dump(x)) - assert_equal "320×480", y.resolution - end - - def test_changes_in_place - json = JsonDataType.new - assert_not json.changed? - - json.payload = { "one" => "two" } - assert json.changed? - assert json.payload_changed? - - json.save! - assert_not json.changed? - - json.payload["three"] = "four" - assert json.payload_changed? - - json.save! - json.reload - - assert_equal({ "one" => "two", "three" => "four" }, json.payload) - assert_not json.changed? - end - - def test_assigning_string_literal - json = JsonDataType.create(payload: "foo") - assert_equal "foo", json.payload - end - - def test_assigning_number - json = JsonDataType.create(payload: 1.234) - assert_equal 1.234, json.payload - end - - def test_assigning_boolean - json = JsonDataType.create(payload: true) - assert_equal true, json.payload - end end end diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb deleted file mode 100644 index 2c778b1150..0000000000 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ /dev/null @@ -1,151 +0,0 @@ -require "cases/helper" - -# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with -# reserved word names (ie: group, order, values, etc...) -class Mysql2ReservedWordTest < ActiveRecord::Mysql2TestCase - class Group < ActiveRecord::Base - Group.table_name = "group" - belongs_to :select - has_one :values - end - - class Select < ActiveRecord::Base - Select.table_name = "select" - has_many :groups - end - - class Values < ActiveRecord::Base - Values.table_name = "values" - end - - class Distinct < ActiveRecord::Base - Distinct.table_name = "distinct" - has_and_belongs_to_many :selects - has_many :values, through: :groups - end - - def setup - @connection = ActiveRecord::Base.connection - - # we call execute directly here (and do similar below) because ActiveRecord::Base#create_table() - # will fail with these table names if these test cases fail - - create_tables_directly "group" => "id int auto_increment primary key, `order` varchar(255), select_id int", - "select" => "id int auto_increment primary key", - "values" => "id int auto_increment primary key, group_id int", - "distinct" => "id int auto_increment primary key", - "distinct_select" => "distinct_id int, select_id int" - end - - teardown do - drop_tables_directly ["group", "select", "values", "distinct", "distinct_select", "order"] - end - - # create tables with reserved-word names and columns - def test_create_tables - assert_nothing_raised { - @connection.create_table :order do |t| - t.column :group, :string - end - } - end - - # rename tables with reserved-word names - def test_rename_tables - assert_nothing_raised { @connection.rename_table(:group, :order) } - end - - # alter column with a reserved-word name in a table with a reserved-word name - def test_change_columns - assert_nothing_raised { @connection.change_column_default(:group, :order, "whatever") } - #the quoting here will reveal any double quoting issues in change_column's interaction with the column method in the adapter - assert_nothing_raised { @connection.change_column("group", "order", :Int, default: 0) } - assert_nothing_raised { @connection.rename_column(:group, :order, :values) } - end - - # introspect table with reserved word name - def test_introspect - assert_nothing_raised { @connection.columns(:group) } - assert_nothing_raised { @connection.indexes(:group) } - end - - #fixtures - self.use_instantiated_fixtures = true - self.use_transactional_tests = false - - #activerecord model class with reserved-word table name - def test_activerecord_model - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - x = nil - assert_nothing_raised { x = Group.new } - x.order = "x" - assert_nothing_raised { x.save } - x.order = "y" - assert_nothing_raised { x.save } - assert_nothing_raised { Group.find_by_order("y") } - assert_nothing_raised { Group.find(1) } - end - - # has_one association with reserved-word table name - def test_has_one_associations - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - v = nil - assert_nothing_raised { v = Group.find(1).values } - assert_equal 2, v.id - end - - # belongs_to association with reserved-word table name - def test_belongs_to_associations - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - gs = nil - assert_nothing_raised { gs = Select.find(2).groups } - assert_equal gs.length, 2 - assert(gs.collect(&:id).sort == [2, 3]) - end - - # has_and_belongs_to_many with reserved-word table name - def test_has_and_belongs_to_many - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - s = nil - assert_nothing_raised { s = Distinct.find(1).selects } - assert_equal s.length, 2 - assert(s.collect(&:id).sort == [1, 2]) - end - - # activerecord model introspection with reserved-word table and column names - def test_activerecord_introspection - assert_nothing_raised { Group.table_exists? } - assert_nothing_raised { Group.columns } - end - - # Calculations - def test_calculations_work_with_reserved_words - assert_nothing_raised { Group.count } - end - - def test_associations_work_with_reserved_words - assert_nothing_raised { Select.all.merge!(includes: [:groups]).to_a } - end - - #the following functions were added to DRY test cases - - private - # custom fixture loader, uses FixtureSet#create_fixtures and appends base_path to the current file's path - def create_test_fixtures(*fixture_names) - ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) - end - - # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name - def drop_tables_directly(table_names, connection = @connection) - table_names.each do |name| - connection.drop_table name, if_exists: true - end - end - - # custom create table, uses execute on connection to create a table, note: escapes table_name, does NOT escape columns - def create_tables_directly(tables, connection = @connection) - tables.each do |table_name, column_properties| - connection.execute("CREATE TABLE `#{table_name}` ( #{column_properties} )") - end - end -end diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb index a0823be143..71dcfaa241 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -58,9 +58,9 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase test "schema dump includes unsigned option" do schema = dump_table_schema "unsigned_types" - assert_match %r{t.integer\s+"unsigned_integer",\s+unsigned: true$}, schema - assert_match %r{t.bigint\s+"unsigned_bigint",\s+unsigned: true$}, schema - assert_match %r{t.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema - assert_match %r{t.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema + assert_match %r{t\.integer\s+"unsigned_integer",\s+unsigned: true$}, schema + assert_match %r{t\.bigint\s+"unsigned_bigint",\s+unsigned: true$}, schema + assert_match %r{t\.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema + assert_match %r{t\.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema end end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index c78c6178ff..121c62dadf 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -191,6 +191,12 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase assert_equal(PgArray.last.tags, tag_values) end + def test_insert_fixtures + tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"] + @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays") + assert_equal(PgArray.last.tags, tag_values) + end + def test_attribute_for_inspect_for_array_field record = PgArray.new { |a| a.ratings = (1..10).to_a } assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]", record.attribute_for_inspect(:ratings)) diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index 99175e8091..539c90f0bc 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -96,7 +96,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase end def test_write_binary - data = File.read(File.join(File.dirname(__FILE__), "..", "..", "..", "assets", "example.log")) + data = File.read(File.join(__dir__, "..", "..", "..", "assets", "example.log")) assert(data.size > 1) record = ByteaDataType.create(payload: data) assert_not record.new_record? diff --git a/activerecord/test/cases/adapters/postgresql/collation_test.rb b/activerecord/test/cases/adapters/postgresql/collation_test.rb index b39e298a5d..a603221d8f 100644 --- a/activerecord/test/cases/adapters/postgresql/collation_test.rb +++ b/activerecord/test/cases/adapters/postgresql/collation_test.rb @@ -47,7 +47,7 @@ class PostgresqlCollationTest < ActiveRecord::PostgreSQLTestCase test "schema dump includes collation" do output = dump_table_schema("postgresql_collations") - assert_match %r{t.string\s+"string_c",\s+collation: "C"$}, output - assert_match %r{t.text\s+"text_posix",\s+collation: "POSIX"$}, output + assert_match %r{t\.string\s+"string_c",\s+collation: "C"$}, output + assert_match %r{t\.text\s+"text_posix",\s+collation: "POSIX"$}, output end end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index c52d9e37cc..32afe331fa 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -31,15 +31,21 @@ module ActiveRecord end def test_encoding - assert_not_nil @connection.encoding + assert_queries(1) do + assert_not_nil @connection.encoding + end end def test_collation - assert_not_nil @connection.collation + assert_queries(1) do + assert_not_nil @connection.collation + end end def test_ctype - assert_not_nil @connection.ctype + assert_queries(1) do + assert_not_nil @connection.ctype + end end def test_default_client_min_messages @@ -127,8 +133,8 @@ module ActiveRecord if ActiveRecord::Base.connection.prepared_statements def test_statement_key_is_logged - bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new) - @connection.exec_query("SELECT $1::integer", "SQL", [bind], prepare: true) + binds = [bind_attribute(nil, 1)] + @connection.exec_query("SELECT $1::integer", "SQL", binds, prepare: true) name = @subscriber.payloads.last[:statement_name] assert name res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)") diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 7493bce4fb..d79fbccf47 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -7,14 +7,14 @@ class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase def test_explain_for_one_query explain = Developer.where(id: 1).explain - assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\$1 \[\["id", 1\]\]|1)), explain + assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain assert_match %(QUERY PLAN), explain end def test_explain_with_eager_loading explain = Developer.where(id: 1).includes(:audit_logs).explain assert_match %(QUERY PLAN), explain - assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\$1 \[\["id", 1\]\]|1)), explain + assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain end end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index c1f3a4ae2c..3b6840a1c9 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -93,8 +93,6 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase end def test_empty_string_assignment - assert_nothing_raised { PostgresqlPoint.new(x: "") } - p = PostgresqlPoint.new(x: "") assert_nil p.x end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index d4e627001c..6aa60630c2 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -1,223 +1,36 @@ require "cases/helper" -require "support/schema_dumping_helper" +require "cases/json_shared_test_cases" module PostgresqlJSONSharedTestCases - include SchemaDumpingHelper - - class JsonDataType < ActiveRecord::Base - self.table_name = "json_data_type" - - store_accessor :settings, :resolution - end + include JSONSharedTestCases def setup - @connection = ActiveRecord::Base.connection - begin - @connection.create_table("json_data_type") do |t| - t.public_send column_type, "payload", default: {} # t.json 'payload', default: {} - t.public_send column_type, "settings" # t.json 'settings' - t.public_send column_type, "objects", array: true # t.json 'objects', array: true - end - rescue ActiveRecord::StatementInvalid - skip "do not test on PostgreSQL without #{column_type} type." + super + @connection.create_table("json_data_type") do |t| + t.public_send column_type, "payload", default: {} # t.json 'payload', default: {} + t.public_send column_type, "settings" # t.json 'settings' + t.public_send column_type, "objects", array: true # t.json 'objects', array: true end - end - - def teardown - @connection.drop_table :json_data_type, if_exists: true - JsonDataType.reset_column_information - end - - def test_column - column = JsonDataType.columns_hash["payload"] - assert_equal column_type, column.type - assert_equal column_type.to_s, column.sql_type - assert_not column.array? - - type = JsonDataType.type_for_attribute("payload") - assert_not type.binary? + rescue ActiveRecord::StatementInvalid + skip "do not test on PostgreSQL without #{column_type} type." end def test_default @connection.add_column "json_data_type", "permissions", column_type, default: { "users": "read", "posts": ["read", "write"] } - JsonDataType.reset_column_information + klass.reset_column_information assert_equal({ "users" => "read", "posts" => ["read", "write"] }, JsonDataType.column_defaults["permissions"]) assert_equal({ "users" => "read", "posts" => ["read", "write"] }, JsonDataType.new.permissions) - ensure - JsonDataType.reset_column_information - end - - def test_change_table_supports_json - @connection.transaction do - @connection.change_table("json_data_type") do |t| - t.public_send column_type, "users", default: "{}" # t.json 'users', default: '{}' - end - JsonDataType.reset_column_information - column = JsonDataType.columns_hash["users"] - assert_equal column_type, column.type - - raise ActiveRecord::Rollback # reset the schema change - end - ensure - JsonDataType.reset_column_information - end - - def test_schema_dumping - output = dump_table_schema("json_data_type") - assert_match(/t\.#{column_type.to_s}\s+"payload",\s+default: {}/, output) - end - - def test_cast_value_on_write - x = JsonDataType.new payload: { "string" => "foo", :symbol => :bar } - assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast) - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload) - x.save - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) end def test_deserialize_with_array - x = JsonDataType.new(objects: ["foo" => "bar"]) + x = klass.new(objects: ["foo" => "bar"]) assert_equal ["foo" => "bar"], x.objects x.save! assert_equal ["foo" => "bar"], x.objects x.reload assert_equal ["foo" => "bar"], x.objects end - - def test_type_cast_json - type = JsonDataType.type_for_attribute("payload") - - data = "{\"a_key\":\"a_value\"}" - hash = type.deserialize(data) - assert_equal({ "a_key" => "a_value" }, hash) - assert_equal({ "a_key" => "a_value" }, type.deserialize(data)) - - assert_equal({}, type.deserialize("{}")) - assert_equal({ "key" => nil }, type.deserialize('{"key": null}')) - assert_equal({ "c" => "}", '"a"' => 'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) - end - - def test_rewrite - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - x.payload = { '"a\'' => "b" } - assert x.save! - end - - def test_select - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - assert_equal({ "k" => "v" }, x.payload) - end - - def test_select_multikey - @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')| - x = JsonDataType.first - assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload) - end - - def test_null_json - @connection.execute "insert into json_data_type (payload) VALUES(null)" - x = JsonDataType.first - assert_nil(x.payload) - end - - def test_select_nil_json_after_create - json = JsonDataType.create(payload: nil) - x = JsonDataType.where(payload: nil).first - assert_equal(json, x) - end - - def test_select_nil_json_after_update - json = JsonDataType.create(payload: "foo") - x = JsonDataType.where(payload: nil).first - assert_nil(x) - - json.update_attributes payload: nil - x = JsonDataType.where(payload: nil).first - assert_equal(json.reload, x) - end - - def test_select_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - assert_equal(["v0", { "k1" => "v1" }], x.payload) - end - - def test_rewrite_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - x.payload = ["v1", { "k2" => "v2" }, "v3"] - assert x.save! - end - - def test_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - x.save! - x = JsonDataType.first - assert_equal "320×480", x.resolution - - x.resolution = "640×1136" - x.save! - - x = JsonDataType.first - assert_equal "640×1136", x.resolution - end - - def test_duplication_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = x.dup - assert_equal "320×480", y.resolution - end - - def test_yaml_round_trip_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = YAML.load(YAML.dump(x)) - assert_equal "320×480", y.resolution - end - - def test_changes_in_place - json = JsonDataType.new - assert_not json.changed? - - json.payload = { "one" => "two" } - assert json.changed? - assert json.payload_changed? - - json.save! - assert_not json.changed? - - json.payload["three"] = "four" - assert json.payload_changed? - - json.save! - json.reload - - assert_equal({ "one" => "two", "three" => "four" }, json.payload) - assert_not json.changed? - end - - def test_assigning_string_literal - json = JsonDataType.create(payload: "foo") - assert_equal "foo", json.payload - end - - def test_assigning_number - json = JsonDataType.create(payload: 1.234) - assert_equal 1.234, json.payload - end - - def test_assigning_boolean - json = JsonDataType.create(payload: true) - assert_equal true, json.payload - end end class PostgresqlJSONTest < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 003e6e62e7..76e0ad60fe 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -202,8 +202,8 @@ module ActiveRecord string = @connection.quote("foo") @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - bind = Relation::QueryAttribute.new("id", 1, Type::Value.new) - result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, [bind]) + binds = [bind_attribute("id", 1)] + result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, binds) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -217,8 +217,8 @@ module ActiveRecord string = @connection.quote("foo") @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - bind = Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new) - result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, [bind]) + binds = [bind_attribute("id", "1-fuu", Type::Integer.new)] + result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, binds) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -324,13 +324,13 @@ module ActiveRecord reset_connection end - def test_only_reload_type_map_once_for_every_unknown_type + def test_only_reload_type_map_once_for_every_unrecognized_type silence_warnings do assert_queries 2, ignore_none: true do - @connection.select_all "SELECT NULL::anyelement" + @connection.select_all "select 'pg_catalog.pg_class'::regclass" end assert_queries 1, ignore_none: true do - @connection.select_all "SELECT NULL::anyelement" + @connection.select_all "select 'pg_catalog.pg_class'::regclass" end assert_queries 2, ignore_none: true do @connection.select_all "SELECT NULL::anyarray" @@ -340,13 +340,13 @@ module ActiveRecord reset_connection end - def test_only_warn_on_first_encounter_of_unknown_oid + def test_only_warn_on_first_encounter_of_unrecognized_oid warning = capture(:stderr) { - @connection.select_all "SELECT NULL::anyelement" - @connection.select_all "SELECT NULL::anyelement" - @connection.select_all "SELECT NULL::anyelement" + @connection.select_all "select 'pg_catalog.pg_class'::regclass" + @connection.select_all "select 'pg_catalog.pg_class'::regclass" + @connection.select_all "select 'pg_catalog.pg_class'::regclass" } - assert_match(/\Aunknown OID \d+: failed to recognize type of 'anyelement'. It will be treated as String.\n\z/, warning) + assert_match(/\Aunknown OID \d+: failed to recognize type of 'regclass'\. It will be treated as String\.\n\z/, warning) ensure reset_connection end diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index f6a07da85f..f86a76e08a 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -68,24 +68,13 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase USERS.each do |u| @connection.clear_cache! set_session_auth u - assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = $1", "SQL", [bind_param(1)]) + assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = $1", "SQL", [bind_attribute("id", 1)]) set_session_auth end end end end - def test_schema_uniqueness - assert_nothing_raised do - set_session_auth - USERS.each do |u| - set_session_auth u - assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = 1") - set_session_auth - end - end - end - def test_sequence_schema_caching assert_nothing_raised do USERS.each do |u| @@ -112,8 +101,4 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase def set_session_auth(auth = nil) @connection.session_auth = auth || "default" end - - def bind_param(value) - ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new) - end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 75e30e4fe9..f6b957476b 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -169,17 +169,17 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase def test_raise_wrapped_exception_on_bad_prepare assert_raises(ActiveRecord::StatementInvalid) do - @connection.exec_query "select * from developers where id = ?", "sql", [bind_param(1)] + @connection.exec_query "select * from developers where id = ?", "sql", [bind_attribute("id", 1)] end end if ActiveRecord::Base.connection.prepared_statements def test_schema_change_with_prepared_stmt altered = false - @connection.exec_query "select * from developers where id = $1", "sql", [bind_param(1)] + @connection.exec_query "select * from developers where id = $1", "sql", [bind_attribute("id", 1)] @connection.exec_query "alter table developers add column zomg int", "sql", [] altered = true - @connection.exec_query "select * from developers where id = $1", "sql", [bind_param(1)] + @connection.exec_query "select * from developers where id = $1", "sql", [bind_attribute("id", 1)] ensure # We are not using DROP COLUMN IF EXISTS because that syntax is only # supported by pg 9.X @@ -467,10 +467,6 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase assert_equal this_index_column, this_index.columns[0] assert_equal this_index_name, this_index.name end - - def bind_param(value) - ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new) - end end class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 52e4a38cae..8eddd81c38 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -40,7 +40,8 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase drop_table "uuid_data_type" end - if ActiveRecord::Base.connection.supports_pgcrypto_uuid? + if ActiveRecord::Base.connection.respond_to?(:supports_pgcrypto_uuid?) && + ActiveRecord::Base.connection.supports_pgcrypto_uuid? def test_uuid_column_default connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "gen_random_uuid()" UUIDType.reset_column_information @@ -63,6 +64,16 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase UUIDType.reset_column_information end + def test_add_column_with_null_true_and_default_nil + connection.add_column :uuid_data_type, :thingy, :uuid, null: true, default: nil + + UUIDType.reset_column_information + column = UUIDType.columns_hash["thingy"] + + assert column.null + assert_nil column.default + end + def test_data_type_of_uuid_types column = UUIDType.columns_hash["guid"] assert_equal :uuid, column.type diff --git a/activerecord/test/cases/adapters/sqlite3/collation_test.rb b/activerecord/test/cases/adapters/sqlite3/collation_test.rb index 28e8f12c18..dd88ed3656 100644 --- a/activerecord/test/cases/adapters/sqlite3/collation_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/collation_test.rb @@ -47,7 +47,7 @@ class SQLite3CollationTest < ActiveRecord::SQLite3TestCase test "schema dump includes collation" do output = dump_table_schema("collation_table_sqlite3") - assert_match %r{t.string\s+"string_nocase",\s+collation: "NOCASE"$}, output - assert_match %r{t.text\s+"text_rtrim",\s+collation: "RTRIM"$}, output + assert_match %r{t\.string\s+"string_nocase",\s+collation: "NOCASE"$}, output + assert_match %r{t\.text\s+"text_rtrim",\s+collation: "RTRIM"$}, output end end diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index 128acb79cf..29d97ae78c 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -7,13 +7,13 @@ class SQLite3ExplainTest < ActiveRecord::SQLite3TestCase def test_explain_for_one_query explain = Developer.where(id: 1).explain - assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\? \[\["id", 1\]\]|1)), explain + assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\? \[\["id", 1\]\]|1)), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) end def test_explain_with_eager_loading explain = Developer.where(id: 1).includes(:audit_logs).explain - assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\? \[\["id", 1\]\]|1)), explain + assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\? \[\["id", 1\]\]|1)), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain assert_match(/(SCAN )?TABLE audit_logs/, explain) diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 2179d1294c..9a812e325e 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -66,11 +66,11 @@ module ActiveRecord def test_exec_insert with_example_table do - vals = [Relation::QueryAttribute.new("number", 10, Type::Value.new)] - @conn.exec_insert("insert into ex (number) VALUES (?)", "SQL", vals) + binds = [bind_attribute("number", 10)] + @conn.exec_insert("insert into ex (number) VALUES (?)", "SQL", binds) result = @conn.exec_query( - "select number from ex where number = ?", "SQL", vals) + "select number from ex where number = ?", "SQL", binds) assert_equal 1, result.rows.length assert_equal 10, result.rows.first.first @@ -134,7 +134,7 @@ module ActiveRecord with_example_table "id int, data string" do @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') result = @conn.exec_query( - "SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new(nil, 1, Type::Value.new)]) + "SELECT id, data FROM ex WHERE id = ?", nil, [bind_attribute("id", 1)]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -148,7 +148,7 @@ module ActiveRecord @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') result = @conn.exec_query( - "SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)]) + "SELECT id, data FROM ex WHERE id = ?", nil, [bind_attribute("id", "1-fuu", Type::Integer.new)]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length diff --git a/activerecord/test/cases/associations/association_scope_test.rb b/activerecord/test/cases/associations/association_scope_test.rb index c322333f6d..c54542ff7b 100644 --- a/activerecord/test/cases/associations/association_scope_test.rb +++ b/activerecord/test/cases/associations/association_scope_test.rb @@ -6,8 +6,7 @@ module ActiveRecord module Associations class AssociationScopeTest < ActiveRecord::TestCase test "does not duplicate conditions" do - scope = AssociationScope.scope(Author.new.association(:welcome_posts), - Author.connection) + scope = AssociationScope.scope(Author.new.association(:welcome_posts)) binds = scope.where_clause.binds.map(&:value) assert_equal binds.uniq, binds end diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index 7721bd5cd9..f9d1e44595 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -128,7 +128,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase assert ar.developers_log.empty? alice = Developer.new(name: "alice") ar.developers_with_callbacks << alice - assert_equal"after_adding#{alice.id}", ar.developers_log.last + assert_equal "after_adding#{alice.id}", ar.developers_log.last bob = ar.developers_with_callbacks.create(name: "bob") assert_equal "after_adding#{bob.id}", ar.developers_log.last diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 3638c87968..7b0445025c 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -34,18 +34,12 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations - assert_nothing_raised do - Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).to_a - end authors = Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).to_a assert_equal 1, assert_no_queries { authors.size } assert_equal 10, assert_no_queries { authors[0].comments.size } end def test_eager_association_loading_grafts_stashed_associations_to_correct_parent - assert_nothing_raised do - Person.eager_load(primary_contact: :primary_contact).where("primary_contacts_people_2.first_name = ?", "Susan").order("people.id").to_a - end assert_equal people(:michael), Person.eager_load(primary_contact: :primary_contact).where("primary_contacts_people_2.first_name = ?", "Susan").order("people.id").first end diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb index 4f0fe3236e..61f39b4136 100644 --- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb +++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb @@ -11,25 +11,32 @@ end class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase def setup - generate_test_objects - end - - def generate_test_objects post = Namespaced::Post.create(title: "Great stuff", body: "This is not", author_id: 1) - Tagging.create(taggable: post) + @tagging = Tagging.create(taggable: post) + @old = ActiveRecord::Base.store_full_sti_class end - def test_class_names - old = ActiveRecord::Base.store_full_sti_class + def teardown + ActiveRecord::Base.store_full_sti_class = @old + end + def test_class_names_with_includes ActiveRecord::Base.store_full_sti_class = false post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff") assert_nil post.tagging ActiveRecord::Base.store_full_sti_class = true post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff") - assert_instance_of Tagging, post.tagging - ensure - ActiveRecord::Base.store_full_sti_class = old + assert_equal @tagging, post.tagging + end + + def test_class_names_with_eager_load + ActiveRecord::Base.store_full_sti_class = false + post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff") + assert_nil post.tagging + + ActiveRecord::Base.store_full_sti_class = true + post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff") + assert_equal @tagging, post.tagging end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 11f4aae5b3..55b294cfaa 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -271,9 +271,6 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_loading_from_an_association_that_has_a_hash_of_conditions - assert_nothing_raised do - Author.all.merge!(includes: :hello_posts_with_hash_conditions).to_a - end assert !Author.all.merge!(includes: :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts.empty? end @@ -1094,12 +1091,6 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal authors(:david), assert_no_queries { posts[0].author } posts = assert_queries(2) do - Post.all.merge!(select: "distinct posts.*", includes: :author, joins: [:comments], where: "comments.body like 'Thank you%'", order: "posts.id").to_a - end - assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author } - - posts = assert_queries(2) do Post.all.merge!(includes: :author, joins: { taggings: :tag }, where: "tags.name = 'General'", order: "posts.id").to_a end assert_equal posts(:welcome, :thinking), posts @@ -1363,6 +1354,7 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_nothing_raised do authors(:david).essays.includes(:writer).any? authors(:david).essays.includes(:writer).exists? + authors(:david).essays.includes(:owner).where("name IS NOT NULL").exists? end end diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 87d842f21d..f707a170f5 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -78,6 +78,12 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase assert_equal post.association(:comments), post.comments.where("1=1").the_association end + def test_association_with_default_scope + assert_raises OopsError do + posts(:welcome).comments.destroy_all + end + end + private def extend!(model) diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 8060790594..f73005b3cb 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -367,19 +367,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated end - def test_create_by_new_record - devel = Developer.new(name: "Marcel", salary: 75000) - devel.projects.build(name: "Make bed") - proj2 = devel.projects.build(name: "Lie in it") - assert_equal devel.projects.last, proj2 - assert !proj2.persisted? - devel.save - assert devel.persisted? - assert proj2.persisted? - assert_equal devel.projects.last, proj2 - assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated - end - def test_creation_respects_hash_condition # in Oracle '' is saved as null therefore need to save ' ' in not null column post = categories(:general).post_with_conditions.build(body: " ") @@ -954,7 +941,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_not_nil Developer._reflections["shared_computers"] # Checking the fixture for named association is important here, because it's the only way # we've been able to reproduce this bug - assert_not_nil File.read(File.expand_path("../../../fixtures/developers.yml", __FILE__)).index("shared_computers") + assert_not_nil File.read(File.expand_path("../../fixtures/developers.yml", __dir__)).index("shared_computers") assert_equal developers(:david).shared_computers.first, computers(:laptop) end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 6a479a344c..a936017ae3 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -741,6 +741,41 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal client2, firm.clients.merge!(where: ["#{QUOTED_TYPE} = :type", { type: "Client" }], order: "id").first end + def test_find_first_after_reset_scope + firm = Firm.all.merge!(order: "id").first + collection = firm.clients + + original_object = collection.first + assert_same original_object, collection.first, "Expected second call to #first to cache the same object" + + # It should return a different object, since the association has been reloaded + assert_not_same original_object, firm.clients.first, "Expected #first to return a new object" + end + + def test_find_first_after_reset + firm = Firm.all.merge!(order: "id").first + collection = firm.clients + + original_object = collection.first + assert_same original_object, collection.first, "Expected second call to #first to cache the same object" + collection.reset + + # It should return a different object, since the association has been reloaded + assert_not_same original_object, collection.first, "Expected #first after #reset to return a new object" + end + + def test_find_first_after_reload + firm = Firm.all.merge!(order: "id").first + collection = firm.clients + + original_object = collection.first + assert_same original_object, collection.first, "Expected second call to #first to cache the same object" + collection.reload + + # It should return a different object, since the association has been reloaded + assert_not_same original_object, collection.first, "Expected #first after #reload to return a new object" + end + def test_find_all_with_include_and_conditions assert_nothing_raised do Developer.all.merge!(joins: :audit_logs, where: { "audit_logs.message" => nil, :name => "Smith" }).to_a @@ -1355,7 +1390,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase Client.create(client_of: firm.id, name: "SmallTime Inc.") # only one of two clients is included in the association due to the :conditions key assert_equal 2, Client.where(client_of: firm.id).size - assert_equal 1, firm.dependent_sanitized_conditional_clients_of_firm.size + assert_equal 1, firm.dependent_hash_conditional_clients_of_firm.size firm.destroy # only the correctly associated client should have been deleted assert_equal 1, Client.where(client_of: firm.id).size @@ -2251,7 +2286,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "association with extend option with multiple extensions" do post = posts(:welcome) assert_equal "lifo", post.comments_with_extend_2.author - assert_equal "hello", post.comments_with_extend_2.greeting + assert_equal "hullo", post.comments_with_extend_2.greeting + end + + test "extend option affects per association" do + post = posts(:welcome) + assert_equal "lifo", post.comments_with_extend.author + assert_equal "lifo", post.comments_with_extend_2.author + assert_equal "hello", post.comments_with_extend.greeting + assert_equal "hullo", post.comments_with_extend_2.greeting end test "delete record with complex joins" do @@ -2311,8 +2354,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase car = Car.create! bulb = Bulb.create! name: "other", car: car - assert_equal bulb, Car.find(car.id).all_bulbs.first - assert_equal bulb, Car.includes(:all_bulbs).find(car.id).all_bulbs.first + assert_equal [bulb], Car.find(car.id).all_bulbs + assert_equal [bulb], Car.includes(:all_bulbs).find(car.id).all_bulbs + assert_equal [bulb], Car.eager_load(:all_bulbs).find(car.id).all_bulbs end test "raises RecordNotDestroyed when replaced child can't be destroyed" do diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index ea52fb5a67..1c2138a3d0 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -64,10 +64,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase club1.members.sort_by(&:id) end - def make_model(name) - Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } - end - def test_ordered_has_many_through person_prime = Class.new(ActiveRecord::Base) do def self.name; "Person"; end @@ -152,20 +148,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert after_destroy_called, "after destroy should be called" end - def make_no_pk_hm_t - lesson = make_model "Lesson" - student = make_model "Student" - - lesson_student = make_model "LessonStudent" - lesson_student.table_name = "lessons_students" - - lesson_student.belongs_to :lesson, anonymous_class: lesson - lesson_student.belongs_to :student, anonymous_class: student - lesson.has_many :lesson_students, anonymous_class: lesson_student - lesson.has_many :students, through: :lesson_students, anonymous_class: student - [lesson, lesson_student, student] - end - def test_pk_is_not_required_for_join post = Post.includes(:scategories).first post2 = Post.includes(:categories).first @@ -337,6 +319,17 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_includes post.single_people, person end + def test_build_then_remove_then_save + post = posts(:thinking) + post.people.build(first_name: "Bob") + ted = post.people.build(first_name: "Ted") + post.people.delete(ted) + post.save! + post.reload + + assert_equal ["Bob"], post.people.collect(&:first_name) + end + def test_both_parent_ids_set_when_saving_new post = Post.new(title: "Hello", body: "world") person = Person.new(first_name: "Sean") @@ -1252,4 +1245,23 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase ) end end + + private + def make_model(name) + Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } + end + + def make_no_pk_hm_t + lesson = make_model "Lesson" + student = make_model "Student" + + lesson_student = make_model "LessonStudent" + lesson_student.table_name = "lessons_students" + + lesson_student.belongs_to :lesson, anonymous_class: lesson + lesson_student.belongs_to :student, anonymous_class: student + lesson.has_many :lesson_students, anonymous_class: lesson_student + lesson.has_many :students, through: :lesson_students, anonymous_class: student + [lesson, lesson_student, student] + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 7c11d2e7fc..bf3b8dcd63 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -307,6 +307,15 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end + def test_create_when_parent_is_new_raises + firm = Firm.new + error = assert_raise(ActiveRecord::RecordNotSaved) do + firm.create_account + end + + assert_equal "You cannot call create unless the parent is saved", error.message + end + def test_reload_association odegy = companies(:odegy) diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 287b3e9ebc..467cc73ecd 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -651,20 +651,6 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" end - def test_child_instance_should_be_shared_with_replaced_via_method_parent - face = faces(:confused) - new_man = Man.new - - assert_not_nil face.polymorphic_man - face.polymorphic_man = new_man - - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance" - face.description = "Bongo" - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance" - new_man.polymorphic_face.description = "Mungo" - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" - end - def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed new_man = Man.new face = Face.new diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 4ab690bfc6..2eb31326a5 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -220,6 +220,18 @@ class AssociationProxyTest < ActiveRecord::TestCase assert_equal david.projects, david.projects.scope end + test "proxy object is cached" do + david = developers(:david) + assert_same david.projects, david.projects + end + + test "proxy object can be stubbed" do + david = developers(:david) + david.projects.define_singleton_method(:extra_method) { 42 } + + assert_equal 42, david.projects.extra_method + end + test "inverses get set of subsets of the association" do man = Man.create man.interests.create diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 15c253890b..dc32e995a4 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -907,80 +907,6 @@ class BasicsTest < ActiveRecord::TestCase end end - class NumericData < ActiveRecord::Base - self.table_name = "numeric_data" - - attribute :my_house_population, :integer - attribute :atoms_in_universe, :integer - end - - def test_big_decimal_conditions - m = NumericData.new( - bank_balance: 1586.43, - big_bank_balance: BigDecimal("1000234000567.95"), - world_population: 6000000000, - my_house_population: 3 - ) - assert m.save - assert_equal 0, NumericData.where("bank_balance > ?", 2000.0).count - end - - def test_numeric_fields - m = NumericData.new( - bank_balance: 1586.43, - big_bank_balance: BigDecimal("1000234000567.95"), - world_population: 6000000000, - my_house_population: 3 - ) - assert m.save - - m1 = NumericData.find(m.id) - assert_not_nil m1 - - # As with migration_test.rb, we should make world_population >= 2**62 - # to cover 64-bit platforms and test it is a Bignum, but the main thing - # is that it's an Integer. - assert_kind_of Integer, m1.world_population - assert_equal 6000000000, m1.world_population - - assert_kind_of Integer, m1.my_house_population - assert_equal 3, m1.my_house_population - - assert_kind_of BigDecimal, m1.bank_balance - assert_equal BigDecimal("1586.43"), m1.bank_balance - - assert_kind_of BigDecimal, m1.big_bank_balance - assert_equal BigDecimal("1000234000567.95"), m1.big_bank_balance - end - - def test_numeric_fields_with_scale - m = NumericData.new( - bank_balance: 1586.43122334, - big_bank_balance: BigDecimal("234000567.952344"), - world_population: 6000000000, - my_house_population: 3 - ) - assert m.save - - m1 = NumericData.find(m.id) - assert_not_nil m1 - - # As with migration_test.rb, we should make world_population >= 2**62 - # to cover 64-bit platforms and test it is a Bignum, but the main thing - # is that it's an Integer. - assert_kind_of Integer, m1.world_population - assert_equal 6000000000, m1.world_population - - assert_kind_of Integer, m1.my_house_population - assert_equal 3, m1.my_house_population - - assert_kind_of BigDecimal, m1.bank_balance - assert_equal BigDecimal("1586.43"), m1.bank_balance - - assert_kind_of BigDecimal, m1.big_bank_balance - assert_equal BigDecimal("234000567.95"), m1.big_bank_balance - end - def test_auto_id auto = AutoId.new auto.save diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 1a54c02fac..fbc3fbb44f 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -143,7 +143,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_quote_batch_order c = Post.connection - assert_sql(/ORDER BY #{c.quote_table_name('posts')}.#{c.quote_column_name('id')}/) do + assert_sql(/ORDER BY #{c.quote_table_name('posts')}\.#{c.quote_column_name('id')}/) do Post.find_in_batches(batch_size: 1) do |batch| assert_kind_of Array, batch assert_kind_of Post, batch.first @@ -408,7 +408,7 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_should_quote_batch_order c = Post.connection - assert_sql(/ORDER BY #{c.quote_table_name('posts')}.#{c.quote_column_name('id')}/) do + assert_sql(/ORDER BY #{c.quote_table_name('posts')}\.#{c.quote_column_name('id')}/) do Post.in_batches(of: 1) do |relation| assert_kind_of ActiveRecord::Relation, relation assert_kind_of Post, relation.first diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 6032aa9250..5af44c27eb 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -3,8 +3,7 @@ require "models/topic" require "models/author" require "models/post" -if ActiveRecord::Base.connection.supports_statement_cache? && - ActiveRecord::Base.connection.prepared_statements +if ActiveRecord::Base.connection.prepared_statements module ActiveRecord class BindParameterTest < ActiveRecord::TestCase fixtures :topics, :authors, :author_addresses, :posts @@ -40,9 +39,8 @@ if ActiveRecord::Base.connection.supports_statement_cache? && end def test_binds_are_logged - sub = Arel::Nodes::BindParam.new - binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)] - sql = "select * from topics where id = #{sub.to_sql}" + binds = [bind_attribute("id", 1)] + sql = "select * from topics where id = #{bind_param.to_sql}" @connection.exec_query(sql, "SQL", binds) @@ -57,7 +55,7 @@ if ActiveRecord::Base.connection.supports_statement_cache? && end def test_logs_binds_after_type_cast - binds = [Relation::QueryAttribute.new("id", "10", Type::Integer.new)] + binds = [bind_attribute("id", "10", Type::Integer.new)] assert_logs_binds(binds) end @@ -66,6 +64,10 @@ if ActiveRecord::Base.connection.supports_statement_cache? && assert_logs_binds(binds) end + def test_deprecate_supports_statement_cache + assert_deprecated { ActiveRecord::Base.connection.supports_statement_cache? } + end + private def assert_logs_binds(binds) payload = { diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb index 2c6a38ec35..7b8264e6e8 100644 --- a/activerecord/test/cases/cache_key_test.rb +++ b/activerecord/test/cases/cache_key_test.rb @@ -4,15 +4,23 @@ module ActiveRecord class CacheKeyTest < ActiveRecord::TestCase self.use_transactional_tests = false - class CacheMe < ActiveRecord::Base; end + class CacheMe < ActiveRecord::Base + self.cache_versioning = false + end + + class CacheMeWithVersion < ActiveRecord::Base + self.cache_versioning = true + end setup do @connection = ActiveRecord::Base.connection - @connection.create_table(:cache_mes) { |t| t.timestamps } + @connection.create_table(:cache_mes, force: true) { |t| t.timestamps } + @connection.create_table(:cache_me_with_versions, force: true) { |t| t.timestamps } end teardown do @connection.drop_table :cache_mes, if_exists: true + @connection.drop_table :cache_me_with_versions, if_exists: true end test "cache_key format is not too precise" do @@ -21,5 +29,23 @@ module ActiveRecord assert_equal key, record.reload.cache_key end + + test "cache_key has no version when versioning is on" do + record = CacheMeWithVersion.create + assert_equal "active_record/cache_key_test/cache_me_with_versions/#{record.id}", record.cache_key + end + + test "cache_version is only there when versioning is on" do + assert CacheMeWithVersion.create.cache_version.present? + assert_not CacheMe.create.cache_version.present? + end + + test "cache_key_with_version always has both key and version" do + r1 = CacheMeWithVersion.create + assert_equal "active_record/cache_key_test/cache_me_with_versions/#{r1.id}-#{r1.updated_at.to_s(:usec)}", r1.cache_key_with_version + + r2 = CacheMe.create + assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.to_s(:usec)}", r2.cache_key_with_version + end end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 3214d778d4..80baaac30a 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -8,6 +8,7 @@ require "models/organization" require "models/possession" require "models/topic" require "models/reply" +require "models/numeric_data" require "models/minivan" require "models/speedometer" require "models/ship_part" @@ -17,14 +18,6 @@ require "models/post" require "models/comment" require "models/rating" -class NumericData < ActiveRecord::Base - self.table_name = "numeric_data" - - attribute :world_population, :integer - attribute :my_house_population, :integer - attribute :atoms_in_universe, :integer -end - class CalculationsTest < ActiveRecord::TestCase fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books @@ -587,8 +580,11 @@ class CalculationsTest < ActiveRecord::TestCase end def test_pluck_without_column_names - assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, ""]], - Company.order(:id).limit(1).pluck + if current_adapter?(:OracleAdapter) + assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, nil]], Company.order(:id).limit(1).pluck + else + assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, ""]], Company.order(:id).limit(1).pluck + end end def test_pluck_type_cast @@ -809,4 +805,16 @@ class CalculationsTest < ActiveRecord::TestCase def test_group_by_attribute_with_custom_type assert_equal({ "proposed" => 2, "published" => 2 }, Book.group(:status).count) end + + def test_deprecate_count_with_block_and_column_name + assert_deprecated do + assert_equal 6, Account.count(:firm_id) { true } + end + end + + def test_deprecate_sum_with_block_and_column_name + assert_deprecated do + assert_equal 6, Account.sum(:firm_id) { 1 } + end + end end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 681399c8bb..2a71f08d90 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -9,6 +9,17 @@ module ActiveRecord @pool = @handler.establish_connection(ActiveRecord::Base.configurations["arunit"]) end + def test_default_env_fall_back_to_default_env_when_rails_env_or_rack_env_is_empty_string + original_rails_env = ENV["RAILS_ENV"] + original_rack_env = ENV["RACK_ENV"] + ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "" + + assert_equal "default_env", ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + ensure + ENV["RAILS_ENV"] = original_rails_env + ENV["RACK_ENV"] = original_rack_env + end + def test_establish_connection_uses_spec_name config = { "readonly" => { "adapter" => "sqlite3" } } resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(config) diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 7e88c9cf7a..00a0187b57 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -499,21 +499,8 @@ module ActiveRecord if failed second_thread_done.set - puts - puts ">>> test_disconnect_and_clear_reloadable_connections_are_able_to_preempt_other_waiting_threads / #{group_action_method}" - p [first_thread, second_thread] - p pool.stat - p pool.connections.map(&:owner) - first_thread.join(2) second_thread.join(2) - - puts "---" - p [first_thread, second_thread] - p pool.stat - p pool.connections.map(&:owner) - puts "<<<" - puts end first_thread.join(10) || raise("first_thread got stuck") diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 721861975a..f72e0d2ead 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -4,10 +4,7 @@ require "models/pirate" # For timestamps require "models/parrot" require "models/person" # For optimistic locking require "models/aircraft" - -class NumericData < ActiveRecord::Base - self.table_name = "numeric_data" -end +require "models/numeric_data" class DirtyTest < ActiveRecord::TestCase include InTimeZone diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index b7641fcf32..4ef9a125e6 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -1,8 +1,9 @@ require "cases/helper" +require "models/author" require "models/book" class EnumTest < ActiveRecord::TestCase - fixtures :books + fixtures :books, :authors setup do @book = books(:awdr) @@ -37,6 +38,8 @@ class EnumTest < ActiveRecord::TestCase assert_equal @book, Book.author_visibility_visible.first assert_equal @book, Book.illustrator_visibility_visible.first assert_equal @book, Book.medium_to_read.first + assert_equal books(:ddd), Book.forgotten.first + assert_equal books(:rfr), authors(:david).unpublished_books.first end test "find via where with values" do @@ -57,6 +60,7 @@ class EnumTest < ActiveRecord::TestCase assert_not_equal @book, Book.where(status: [:written]).first assert_not_equal @book, Book.where.not(status: :published).first assert_equal @book, Book.where.not(status: :written).first + assert_equal books(:ddd), Book.where(read_status: :forgotten).first end test "find via where with strings" do @@ -66,6 +70,7 @@ class EnumTest < ActiveRecord::TestCase assert_not_equal @book, Book.where(status: ["written"]).first assert_not_equal @book, Book.where.not(status: "published").first assert_equal @book, Book.where.not(status: "written").first + assert_equal books(:ddd), Book.where(read_status: "forgotten").first end test "build from scope" do diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb index 73feb831d0..e90669e0c7 100644 --- a/activerecord/test/cases/errors_test.rb +++ b/activerecord/test/cases/errors_test.rb @@ -1,4 +1,4 @@ -require_relative "../cases/helper" +require "cases/helper" class ErrorsTest < ActiveRecord::TestCase def test_can_be_instantiated_with_no_args diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index 86fe90ae51..4f6bd9327c 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -47,7 +47,7 @@ if ActiveRecord::Base.connection.supports_explain? def test_exec_explain_with_binds sqls = %w(foo bar) - binds = [[bind_param("wadus", 1)], [bind_param("chaflan", 2)]] + binds = [[bind_attribute("wadus", 1)], [bind_attribute("chaflan", 2)]] queries = sqls.zip(binds) stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do @@ -79,9 +79,5 @@ if ActiveRecord::Base.connection.supports_explain? yield end end - - def bind_param(name, value) - ActiveRecord::Relation::QueryAttribute.new(name, value, ActiveRecord::Type::Value.new) - end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index a7b6333010..420f552ef6 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -743,7 +743,6 @@ class FinderTest < ActiveRecord::TestCase assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1) assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "HHC", replies_count: 1, approved: false).find(1) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } end def test_condition_interpolation @@ -1024,16 +1023,6 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" } end - def test_find_all_with_join - developers_on_project_one = Developer. - joins("LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id"). - where("project_id=1").to_a - assert_equal 3, developers_on_project_one.length - developer_names = developers_on_project_one.map(&:name) - assert_includes developer_names, "David" - assert_includes developer_names, "Jamis" - end - def test_joins_dont_clobber_id first = Firm. joins("INNER JOIN companies clients ON clients.firm_id = companies.id"). diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index a0a6d3c7ef..b499e60922 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -54,6 +54,31 @@ class FixturesTest < ActiveRecord::TestCase end end + class InsertQuerySubscriber + attr_reader :events + + def initialize + @events = [] + end + + def call(_, _, _, _, values) + @events << values[:sql] if values[:sql] =~ /INSERT/ + end + end + + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + def test_bulk_insert + begin + subscriber = InsertQuerySubscriber.new + subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) + create_fixtures("bulbs") + assert_equal 1, subscriber.events.size, "It takes one INSERT query to insert two fixtures" + ensure + ActiveSupport::Notifications.unsubscribe(subscription) + end + end + end + def test_broken_yaml_exception badyaml = Tempfile.new ["foo", ".yml"] badyaml.write "a: : " @@ -248,7 +273,12 @@ class FixturesTest < ActiveRecord::TestCase e = assert_raise(ActiveRecord::Fixture::FixtureError) do ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "parrots") end - assert_equal(%(table "parrots" has no column named "arrr".), e.message) + + if current_adapter?(:SQLite3Adapter) + assert_equal(%(table "parrots" has no column named "arrr".), e.message) + else + assert_equal(%(table "parrots" has no columns named "arrr", "foobar".), e.message) + end end def test_yaml_file_with_symbol_columns diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index e570e9ac1d..fb5a7bcc31 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -1,6 +1,7 @@ require "cases/helper" require "models/author" require "models/company" +require "models/membership" require "models/person" require "models/post" require "models/project" @@ -29,7 +30,7 @@ end class InheritanceTest < ActiveRecord::TestCase include InheritanceTestHelper - fixtures :companies, :projects, :subscribers, :accounts, :vegetables + fixtures :companies, :projects, :subscribers, :accounts, :vegetables, :memberships def test_class_with_store_full_sti_class_returns_full_name with_store_full_sti_class do @@ -316,7 +317,7 @@ class InheritanceTest < ActiveRecord::TestCase end def test_new_with_autoload_paths - path = File.expand_path("../../models/autoloadable", __FILE__) + path = File.expand_path("../models/autoloadable", __dir__) ActiveSupport::Dependencies.autoload_paths << path firm = Company.new(type: "ExtraFirm") @@ -417,7 +418,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_eager_load_belongs_to_primary_key_quoting con = Account.connection - assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1/) do + assert_sql(/#{con.quote_table_name('companies')}\.#{con.quote_column_name('id')} = 1/) do Account.all.merge!(includes: :firm).find(1) end end @@ -435,6 +436,10 @@ class InheritanceTest < ActiveRecord::TestCase assert_nothing_raised { Company.of_first_firm } assert_nothing_raised { Client.of_first_firm } end + + def test_inheritance_with_default_scope + assert_equal 1, SelectedMembership.count(:all) + end end class InheritanceComputeTypeTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 0678bb714f..9104976126 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -168,13 +168,65 @@ class IntegrationTest < ActiveRecord::TestCase end def test_named_timestamps_for_cache_key - owner = owners(:blackbeard) - assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) + assert_deprecated do + owner = owners(:blackbeard) + assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) + end end def test_cache_key_when_named_timestamp_is_nil - owner = owners(:blackbeard) - owner.happy_at = nil - assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at) + assert_deprecated do + owner = owners(:blackbeard) + owner.happy_at = nil + assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at) + end + end + + def test_cache_key_is_stable_with_versioning_on + Developer.cache_versioning = true + + developer = Developer.first + first_key = developer.cache_key + + developer.touch + second_key = developer.cache_key + + assert_equal first_key, second_key + ensure + Developer.cache_versioning = false + end + + def test_cache_version_changes_with_versioning_on + Developer.cache_versioning = true + + developer = Developer.first + first_version = developer.cache_version + + travel 10.seconds do + developer.touch + end + + second_version = developer.cache_version + + assert_not_equal first_version, second_version + ensure + Developer.cache_versioning = false + end + + def test_cache_key_retains_version_when_custom_timestamp_is_used + Developer.cache_versioning = true + + developer = Developer.first + first_key = developer.cache_key_with_version + + travel 10.seconds do + developer.touch + end + + second_key = developer.cache_key_with_version + + assert_not_equal first_key, second_key + ensure + Developer.cache_versioning = false end end diff --git a/activerecord/test/cases/json_attribute_test.rb b/activerecord/test/cases/json_attribute_test.rb new file mode 100644 index 0000000000..e5848b45f8 --- /dev/null +++ b/activerecord/test/cases/json_attribute_test.rb @@ -0,0 +1,33 @@ +require "cases/helper" +require "cases/json_shared_test_cases" + +class JsonAttributeTest < ActiveRecord::TestCase + include JSONSharedTestCases + self.use_transactional_tests = false + + class JsonDataTypeOnText < ActiveRecord::Base + self.table_name = "json_data_type" + + attribute :payload, :json + attribute :settings, :json + + store_accessor :settings, :resolution + end + + def setup + super + @connection.create_table("json_data_type") do |t| + t.text "payload" + t.text "settings" + end + end + + private + def column_type + :text + end + + def klass + JsonDataTypeOnText + end +end diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb new file mode 100644 index 0000000000..9a1c1c3f3f --- /dev/null +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -0,0 +1,221 @@ +require "support/schema_dumping_helper" + +module JSONSharedTestCases + include SchemaDumpingHelper + + class JsonDataType < ActiveRecord::Base + self.table_name = "json_data_type" + + store_accessor :settings, :resolution + end + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table :json_data_type, if_exists: true + klass.reset_column_information + end + + def test_column + column = klass.columns_hash["payload"] + assert_equal column_type, column.type + assert_equal column_type.to_s, column.sql_type + + type = klass.type_for_attribute("payload") + assert_not type.binary? + end + + def test_change_table_supports_json + @connection.change_table("json_data_type") do |t| + t.public_send column_type, "users" + end + klass.reset_column_information + column = klass.columns_hash["users"] + assert_equal column_type, column.type + assert_equal column_type.to_s, column.sql_type + end + + def test_schema_dumping + output = dump_table_schema("json_data_type") + assert_match(/t\.#{column_type}\s+"settings"/, output) + end + + def test_cast_value_on_write + x = klass.new(payload: { "string" => "foo", :symbol => :bar }) + assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast) + assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload) + x.save! + assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) + end + + def test_type_cast_json + type = klass.type_for_attribute("payload") + + data = '{"a_key":"a_value"}' + hash = type.deserialize(data) + assert_equal({ "a_key" => "a_value" }, hash) + assert_equal({ "a_key" => "a_value" }, type.deserialize(data)) + + assert_equal({}, type.deserialize("{}")) + assert_equal({ "key" => nil }, type.deserialize('{"key": null}')) + assert_equal({ "c" => "}", '"a"' => 'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) + end + + def test_rewrite + @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + x = klass.first + x.payload = { '"a\'' => "b" } + assert x.save! + end + + def test_select + @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + x = klass.first + assert_equal({ "k" => "v" }, x.payload) + end + + def test_select_multikey + @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')|) + x = klass.first + assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload) + end + + def test_null_json + @connection.execute("insert into json_data_type (payload) VALUES(null)") + x = klass.first + assert_nil(x.payload) + end + + def test_select_nil_json_after_create + json = klass.create!(payload: nil) + x = klass.where(payload: nil).first + assert_equal(json, x) + end + + def test_select_nil_json_after_update + json = klass.create!(payload: "foo") + x = klass.where(payload: nil).first + assert_nil(x) + + json.update_attributes(payload: nil) + x = klass.where(payload: nil).first + assert_equal(json.reload, x) + end + + def test_select_array_json_value + @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|) + x = klass.first + assert_equal(["v0", { "k1" => "v1" }], x.payload) + end + + def test_rewrite_array_json_value + @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|) + x = klass.first + x.payload = ["v1", { "k2" => "v2" }, "v3"] + assert x.save! + end + + def test_with_store_accessors + x = klass.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + x.save! + x = klass.first + assert_equal "320×480", x.resolution + + x.resolution = "640×1136" + x.save! + + x = klass.first + assert_equal "640×1136", x.resolution + end + + def test_duplication_with_store_accessors + x = klass.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = x.dup + assert_equal "320×480", y.resolution + end + + def test_yaml_round_trip_with_store_accessors + x = klass.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = YAML.load(YAML.dump(x)) + assert_equal "320×480", y.resolution + end + + def test_changes_in_place + json = klass.new + assert_not json.changed? + + json.payload = { "one" => "two" } + assert json.changed? + assert json.payload_changed? + + json.save! + assert_not json.changed? + + json.payload["three"] = "four" + assert json.payload_changed? + + json.save! + json.reload + + assert_equal({ "one" => "two", "three" => "four" }, json.payload) + assert_not json.changed? + end + + def test_changes_in_place_ignores_key_order + json = klass.new + assert_not json.changed? + + json.payload = { "three" => "four", "one" => "two" } + json.save! + json.reload + + json.payload = { "three" => "four", "one" => "two" } + assert_not json.changed? + + json.payload = [{ "three" => "four", "one" => "two" }, { "seven" => "eight", "five" => "six" }] + json.save! + json.reload + + json.payload = [{ "three" => "four", "one" => "two" }, { "seven" => "eight", "five" => "six" }] + assert_not json.changed? + end + + def test_changes_in_place_with_ruby_object + time = Time.now.utc + json = klass.create!(payload: time) + + json.reload + assert_not json.changed? + + json.payload = time + assert_not json.changed? + end + + def test_assigning_string_literal + json = klass.create!(payload: "foo") + assert_equal "foo", json.payload + end + + def test_assigning_number + json = klass.create!(payload: 1.234) + assert_equal 1.234, json.payload + end + + def test_assigning_boolean + json = klass.create!(payload: true) + assert_equal true, json.payload + end + + private + def klass + JsonDataType + end +end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 3a3b8e51f9..2fc52393f2 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -167,6 +167,12 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 0, p1.lock_version end + def test_lock_new_when_explicitly_passing_value + p1 = Person.new(first_name: "Douglas Adams", lock_version: 42) + p1.save! + assert_equal 42, p1.lock_version + end + def test_touch_existing_lock p1 = Person.find(1) assert_equal 0, p1.lock_version @@ -186,6 +192,19 @@ class OptimisticLockingTest < ActiveRecord::TestCase end end + def test_explicit_update_lock_column_raise_error + person = Person.find(1) + + assert_raises(ActiveRecord::StaleObjectError) do + person.first_name = "Douglas Adams" + person.lock_version = 42 + + assert person.lock_version_changed? + + person.save + end + end + def test_lock_column_name_existing t1 = LegacyThing.find(1) t2 = LegacyThing.find(1) @@ -225,10 +244,33 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 0, t1.lock_version_before_type_cast end + def test_touch_existing_lock_without_default_should_work_with_null_in_the_database + ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')") + t1 = LockWithoutDefault.last + + assert_equal 0, t1.lock_version + assert_nil t1.lock_version_before_type_cast + + t1.touch + + assert_equal 1, t1.lock_version + end + + def test_touch_stale_object_with_lock_without_default + t1 = LockWithoutDefault.create!(title: "title1") + stale_object = LockWithoutDefault.find(t1.id) + + t1.update!(title: "title2") + + assert_raises(ActiveRecord::StaleObjectError) do + stale_object.touch + end + end + def test_lock_without_default_should_work_with_null_in_the_database ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')") t1 = LockWithoutDefault.last - t2 = LockWithoutDefault.last + t2 = LockWithoutDefault.find(t1.id) assert_equal 0, t1.lock_version assert_nil t1.lock_version_before_type_cast @@ -285,7 +327,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults_cust(title) VALUES('title1')") t1 = LockWithCustomColumnWithoutDefault.last - t2 = LockWithCustomColumnWithoutDefault.last + t2 = LockWithCustomColumnWithoutDefault.find(t1.id) assert_equal 0, t1.custom_lock_version assert_nil t1.custom_lock_version_before_type_cast @@ -434,6 +476,31 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase PersonalLegacyThing.reset_column_information end + def test_destroy_existing_object_with_locking_column_value_null_in_the_database + ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')") + t1 = LockWithoutDefault.last + + assert_equal 0, t1.lock_version + assert_nil t1.lock_version_before_type_cast + + t1.destroy + + assert t1.destroyed? + end + + def test_destroy_stale_object + t1 = LockWithoutDefault.create!(title: "title1") + stale_object = LockWithoutDefault.find(t1.id) + + t1.update!(title: "title2") + + assert_raises(ActiveRecord::StaleObjectError) do + stale_object.destroy! + end + + refute stale_object.destroyed? + end + private def add_counter_column_to(model, col = "test_count") diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 8c67aa8746..b80257962c 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -59,19 +59,19 @@ class LogSubscriberTest < ActiveRecord::TestCase logger = TestDebugLogSubscriber.new assert_equal 0, logger.debugs.length - logger.sql(Event.new(0, sql: "hi mom!")) + logger.sql(Event.new(0.9, sql: "hi mom!")) assert_equal 1, logger.debugs.length - logger.sql(Event.new(0, sql: "hi mom!", name: "foo")) + logger.sql(Event.new(0.9, sql: "hi mom!", name: "foo")) assert_equal 2, logger.debugs.length - logger.sql(Event.new(0, sql: "hi mom!", name: "SCHEMA")) + logger.sql(Event.new(0.9, sql: "hi mom!", name: "SCHEMA")) assert_equal 2, logger.debugs.length end def test_sql_statements_are_not_squeezed logger = TestDebugLogSubscriber.new - logger.sql(Event.new(0, sql: "ruby rails")) + logger.sql(Event.new(0.9, sql: "ruby rails")) assert_match(/ruby rails/, logger.debugs.first) end @@ -87,7 +87,7 @@ class LogSubscriberTest < ActiveRecord::TestCase logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, color_regex| - logger.sql(Event.new(0, sql: verb.to_s)) + logger.sql(Event.new(0.9, sql: verb.to_s)) assert_match(/#{REGEXP_BOLD}#{color_regex}#{verb}#{REGEXP_CLEAR}/i, logger.debugs.last) end end @@ -96,11 +96,11 @@ class LogSubscriberTest < ActiveRecord::TestCase logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, _| - logger.sql(Event.new(0, sql: verb.to_s)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(Event.new(0, sql: verb.to_s, name: "SQL")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "SQL")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) end end @@ -108,14 +108,14 @@ class LogSubscriberTest < ActiveRecord::TestCase logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, _| - logger.sql(Event.new(0, sql: verb.to_s, name: "Model Load")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "Model Load")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(Event.new(0, sql: verb.to_s, name: "Model Exists")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "Model Exists")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(Event.new(0, sql: verb.to_s, name: "ANY SPECIFIC NAME")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "ANY SPECIFIC NAME")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) end end @@ -123,8 +123,8 @@ class LogSubscriberTest < ActiveRecord::TestCase logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| - logger.sql(Event.new(0, sql: "#{verb} WHERE ID IN SELECT")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: "#{verb} WHERE ID IN SELECT")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last) end end @@ -138,8 +138,8 @@ class LogSubscriberTest < ActiveRecord::TestCase SELECT ID FROM THINGS ) EOS - logger.sql(Event.new(0, sql: sql)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + logger.sql(Event.new(0.9, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last) end end @@ -151,14 +151,14 @@ class LogSubscriberTest < ActiveRecord::TestCase (SELECT * FROM mytable FOR UPDATE) ss WHERE col1 = 5; EOS - logger.sql(Event.new(0, sql: sql)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + logger.sql(Event.new(0.9, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) sql = <<-EOS LOCK TABLE films IN SHARE MODE; EOS - logger.sql(Event.new(0, sql: sql)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + logger.sql(Event.new(0.9, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) end def test_exists_query_logging diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 802a969cb7..007926f1b9 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -211,11 +211,6 @@ module ActiveRecord assert_equal [:remove_index, [:table, { name: "new_index" }]], remove end - def test_invert_add_index_with_no_options - remove = @recorder.inverse_of :add_index, [:table, [:one, :two]] - assert_equal [:remove_index, [:table, { column: [:one, :two] }]], remove - end - def test_invert_remove_index add = @recorder.inverse_of :remove_index, [:table, :one] assert_equal [:add_index, [:table, :one]], add diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 7a80bfb899..596a21dcbc 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -90,6 +90,21 @@ module ActiveRecord connection.drop_table :more_testings rescue nil end + def test_timestamps_have_null_constraints_if_not_present_in_migration_of_change_table + migration = Class.new(ActiveRecord::Migration[4.2]) { + def migrate(x) + change_table :testings do |t| + t.timestamps + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.columns(:testings).find { |c| c.name == "created_at" }.null + assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null + end + def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table migration = Class.new(ActiveRecord::Migration[4.2]) { def migrate(x) diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index f1ddac1ee2..718b9a0613 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -139,6 +139,16 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end end + test "removing column removes foreign key" do + @connection.create_table :testings do |t| + t.references :testing_parent, index: true, foreign_key: true + end + + assert_difference "@connection.foreign_keys('testings').size", -1 do + @connection.remove_column :testings, :testing_parent_id + end + end + test "foreign key methods respect pluralize_table_names" do begin original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index 06c44c8c52..e9eb9968cb 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -50,6 +50,14 @@ module ActiveRecord assert column_exists?(table_name, :taggable_type, :string, default: "Photo") end + def test_creates_reference_type_column_with_not_null + connection.create_table table_name, force: true do |t| + t.references :taggable, null: false, polymorphic: true + end + assert column_exists?(table_name, :taggable_id, :integer, null: false) + assert column_exists?(table_name, :taggable_type, :string, null: false) + end + def test_does_not_share_options_with_reference_type_column add_reference table_name, :taggable, type: :integer, limit: 2, polymorphic: true assert column_exists?(table_name, :taggable_id, :integer, limit: 2) diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 7bcabd0cc6..5da3ad33a3 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -79,10 +79,33 @@ module ActiveRecord assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq end + def test_renaming_table_renames_primary_key + connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" + rename_table :cats, :felines + + assert connection.table_exists? :felines + refute connection.table_exists? :cats + + primary_key_name = connection.select_values(<<-SQL.strip_heredoc, "SCHEMA")[0] + SELECT c.relname + FROM pg_class c + JOIN pg_index i + ON c.oid = i.indexrelid + WHERE i.indisprimary + AND i.indrelid = 'felines'::regclass + SQL + + assert_equal "felines_pkey", primary_key_name + ensure + connection.drop_table :cats, if_exists: true + connection.drop_table :felines, if_exists: true + end + def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" assert_nothing_raised { rename_table :cats, :felines } assert connection.table_exists? :felines + refute connection.table_exists? :cats ensure connection.drop_table :cats, if_exists: true connection.drop_table :felines, if_exists: true diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index da7875187a..3a49a41580 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -402,33 +402,6 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.up(migrations_path) end - def test_migration_sets_internal_metadata_even_when_fully_migrated - current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - migrations_path = MIGRATIONS_ROOT + "/valid" - old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path - - ActiveRecord::Migrator.up(migrations_path) - assert_equal current_env, ActiveRecord::InternalMetadata[:environment] - - original_rails_env = ENV["RAILS_ENV"] - original_rack_env = ENV["RACK_ENV"] - ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" - new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - - refute_equal current_env, new_env - - sleep 1 # mysql by default does not store fractional seconds in the database - - ActiveRecord::Migrator.up(migrations_path) - assert_equal new_env, ActiveRecord::InternalMetadata[:environment] - ensure - ActiveRecord::Migrator.migrations_paths = old_path - ENV["RAILS_ENV"] = original_rails_env - ENV["RACK_ENV"] = original_rack_env - ActiveRecord::Migrator.up(migrations_path) - end - def test_internal_metadata_stores_environment_when_other_data_exists ActiveRecord::InternalMetadata.delete_all ActiveRecord::InternalMetadata[:foo] = "bar" @@ -529,11 +502,10 @@ class MigrationTest < ActiveRecord::TestCase unless mysql_enforcing_gtid_consistency? def test_create_table_with_query - Person.connection.create_table(:person, force: true) - - Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person" + Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM people WHERE id = 1" columns = Person.connection.columns(:table_from_query_testings) + assert_equal [1], Person.connection.select_values("SELECT * FROM table_from_query_testings") assert_equal 1, columns.length assert_equal "id", columns.first.name ensure @@ -541,11 +513,10 @@ class MigrationTest < ActiveRecord::TestCase end def test_create_table_with_query_from_relation - Person.connection.create_table(:person, force: true) - - Person.connection.create_table :table_from_query_testings, as: Person.select(:id) + Person.connection.create_table :table_from_query_testings, as: Person.select(:id).where(id: 1) columns = Person.connection.columns(:table_from_query_testings) + assert_equal [1], Person.connection.select_values("SELECT * FROM table_from_query_testings") assert_equal 1, columns.length assert_equal "id", columns.first.name ensure diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index b87419d203..154faa56aa 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -117,7 +117,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase def test_reject_if_with_a_proc_which_returns_true_always_for_has_one Pirate.accepts_nested_attributes_for :ship, reject_if: proc { |attributes| true } - pirate = Pirate.new(catchphrase: "Stop wastin' me time") + pirate = Pirate.create(catchphrase: "Stop wastin' me time") ship = pirate.create_ship(name: "s1") pirate.update(ship_attributes: { name: "s2", id: ship.id }) assert_equal "s1", ship.reload.name @@ -752,7 +752,7 @@ module NestedAttributesOnACollectionAssociationTests exception = assert_raise ArgumentError do @pirate.send(association_setter, "foo") end - assert_equal 'Hash or Array expected, got String ("foo")', exception.message + assert_equal %{Hash or Array expected for attribute `#{@association_name}`, got String ("foo")}, exception.message end def test_should_work_with_update_as_well diff --git a/activerecord/test/cases/numeric_data_test.rb b/activerecord/test/cases/numeric_data_test.rb new file mode 100644 index 0000000000..76b97033af --- /dev/null +++ b/activerecord/test/cases/numeric_data_test.rb @@ -0,0 +1,71 @@ +require "cases/helper" +require "models/numeric_data" + +class NumericDataTest < ActiveRecord::TestCase + def test_big_decimal_conditions + m = NumericData.new( + bank_balance: 1586.43, + big_bank_balance: BigDecimal("1000234000567.95"), + world_population: 6000000000, + my_house_population: 3 + ) + assert m.save + assert_equal 0, NumericData.where("bank_balance > ?", 2000.0).count + end + + def test_numeric_fields + m = NumericData.new( + bank_balance: 1586.43, + big_bank_balance: BigDecimal("1000234000567.95"), + world_population: 6000000000, + my_house_population: 3 + ) + assert m.save + + m1 = NumericData.find(m.id) + assert_not_nil m1 + + # As with migration_test.rb, we should make world_population >= 2**62 + # to cover 64-bit platforms and test it is a Bignum, but the main thing + # is that it's an Integer. + assert_kind_of Integer, m1.world_population + assert_equal 6000000000, m1.world_population + + assert_kind_of Integer, m1.my_house_population + assert_equal 3, m1.my_house_population + + assert_kind_of BigDecimal, m1.bank_balance + assert_equal BigDecimal("1586.43"), m1.bank_balance + + assert_kind_of BigDecimal, m1.big_bank_balance + assert_equal BigDecimal("1000234000567.95"), m1.big_bank_balance + end + + def test_numeric_fields_with_scale + m = NumericData.new( + bank_balance: 1586.43122334, + big_bank_balance: BigDecimal("234000567.952344"), + world_population: 6000000000, + my_house_population: 3 + ) + assert m.save + + m1 = NumericData.find(m.id) + assert_not_nil m1 + + # As with migration_test.rb, we should make world_population >= 2**62 + # to cover 64-bit platforms and test it is a Bignum, but the main thing + # is that it's an Integer. + assert_kind_of Integer, m1.world_population + assert_equal 6000000000, m1.world_population + + assert_kind_of Integer, m1.my_house_population + assert_equal 3, m1.my_house_population + + assert_kind_of BigDecimal, m1.bank_balance + assert_equal BigDecimal("1586.43"), m1.bank_balance + + assert_kind_of BigDecimal, m1.big_bank_balance + assert_equal BigDecimal("234000567.95"), m1.big_bank_balance + end +end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 5ded619716..56229b70bc 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -46,7 +46,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase topic = Topic.new topic.title = "New Topic" assert_nil topic.id - assert_nothing_raised { topic.save! } + topic.save! id = topic.id topicReloaded = Topic.find(id) @@ -56,23 +56,36 @@ class PrimaryKeysTest < ActiveRecord::TestCase def test_customized_primary_key_auto_assigns_on_save Keyboard.delete_all keyboard = Keyboard.new(name: "HHKB") - assert_nothing_raised { keyboard.save! } + keyboard.save! assert_equal keyboard.id, Keyboard.find_by_name("HHKB").id end def test_customized_primary_key_can_be_get_before_saving keyboard = Keyboard.new assert_nil keyboard.id - assert_nothing_raised { assert_nil keyboard.key_number } + assert_nil keyboard.key_number end def test_customized_string_primary_key_settable_before_save subscriber = Subscriber.new - assert_nothing_raised { subscriber.id = "webster123" } + subscriber.id = "webster123" assert_equal "webster123", subscriber.id assert_equal "webster123", subscriber.nick end + def test_update_with_non_primary_key_id_column + subscriber = Subscriber.first + subscriber.update(update_count: 1) + subscriber.reload + assert_equal 1, subscriber.update_count + end + + def test_update_columns_with_non_primary_key_id_column + subscriber = Subscriber.first + subscriber.update_columns(id: 1) + assert_not_equal 1, subscriber.nick + end + def test_string_key subscriber = Subscriber.find(subscribers(:first).nick) assert_equal(subscribers(:first).name, subscriber.name) @@ -83,7 +96,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase subscriber.id = "jdoe" assert_equal("jdoe", subscriber.id) subscriber.name = "John Doe" - assert_nothing_raised { subscriber.save! } + subscriber.save! assert_equal("jdoe", subscriber.id) subscriberReloaded = Subscriber.find("jdoe") diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 494663eb04..68d18e6471 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -42,7 +42,8 @@ class QueryCacheTest < ActiveRecord::TestCase mw = middleware { |env| Task.find 1 Task.find 1 - assert_equal 1, ActiveRecord::Base.connection.query_cache.length + query_cache = ActiveRecord::Base.connection.query_cache + assert_equal 1, query_cache.length, query_cache.keys raise "lol borked" } assert_raises(RuntimeError) { mw.call({}) } @@ -149,7 +150,8 @@ class QueryCacheTest < ActiveRecord::TestCase mw = middleware { |env| Task.find 1 Task.find 1 - assert_equal 1, ActiveRecord::Base.connection.query_cache.length + query_cache = ActiveRecord::Base.connection.query_cache + assert_equal 1, query_cache.length, query_cache.keys [200, {}, nil] } mw.call({}) @@ -204,6 +206,52 @@ class QueryCacheTest < ActiveRecord::TestCase end end + def test_exists_queries_with_cache + Post.cache do + assert_queries(1) { Post.exists?; Post.exists? } + end + end + + def test_select_all_with_cache + Post.cache do + assert_queries(1) do + 2.times { Post.connection.select_all(Post.all) } + end + end + end + + def test_select_one_with_cache + Post.cache do + assert_queries(1) do + 2.times { Post.connection.select_one(Post.all) } + end + end + end + + def test_select_value_with_cache + Post.cache do + assert_queries(1) do + 2.times { Post.connection.select_value(Post.all) } + end + end + end + + def test_select_values_with_cache + Post.cache do + assert_queries(1) do + 2.times { Post.connection.select_values(Post.all) } + end + end + end + + def test_select_rows_with_cache + Post.cache do + assert_queries(1) do + 2.times { Post.connection.select_rows(Post.all) } + end + end + end + def test_query_cache_dups_results_correctly Task.cache do now = Time.now.utc @@ -274,18 +322,7 @@ class QueryCacheTest < ActiveRecord::TestCase end end - def test_cache_is_available_when_connection_is_connected - conf = ActiveRecord::Base.configurations - - ActiveRecord::Base.configurations = {} - Task.cache do - assert_queries(1) { Task.find(1); Task.find(1) } - end - ensure - ActiveRecord::Base.configurations = conf - end - - def test_cache_is_not_available_when_using_a_not_connected_connection + def test_cache_is_available_when_using_a_not_connected_connection with_temporary_connection_pool do spec_name = Task.connection_specification_name conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2") @@ -302,8 +339,7 @@ class QueryCacheTest < ActiveRecord::TestCase end ActiveRecord::FixtureSet.create_fixtures(self.class.fixture_path, ["tasks"], {}, ActiveRecord::Base) end - Task.connection # warmup postgresql connection setup queries - assert_queries(2) { Task.find(1); Task.find(1) } + assert_queries(1) { Task.find(1); Task.find(1) } ensure ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name) Task.connection_specification_name = spec_name diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index f260d043e4..0819776fbf 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -81,8 +81,21 @@ module ActiveRecord end end + class QuotedOne + def quoted_id + 1 + end + end + class SubQuotedOne < QuotedOne + end def test_quote_with_quoted_id - assert_deprecated { assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1)) } + assert_deprecated(/defined on \S+::QuotedOne at .*quoting_test\.rb:[0-9]/) do + assert_equal 1, @quoter.quote(QuotedOne.new) + end + + assert_deprecated(/defined on \S+::SubQuotedOne\(\S+::QuotedOne\) at .*quoting_test\.rb:[0-9]/) do + assert_equal 1, @quoter.quote(SubQuotedOne.new) + end end def test_quote_nil diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 8cb7b82015..cb6e4d76d3 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -3,29 +3,7 @@ require "models/post" require "models/comment" module ActiveRecord - class DelegationTest < ActiveRecord::TestCase - fixtures :posts - - def call_method(target, method) - method_arity = target.to_a.method(method).arity - - if method_arity.zero? - target.public_send(method) - elsif method_arity < 0 - if method == :shuffle! - target.public_send(method) - else - target.public_send(method, 1) - end - elsif method_arity == 1 - target.public_send(method, 1) - else - raise NotImplementedError - end - end - end - - module DelegationWhitelistBlacklistTests + module DelegationWhitelistTests ARRAY_DELEGATES = [ :+, :-, :|, :&, :[], :shuffle, :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, @@ -43,16 +21,18 @@ module ActiveRecord end end - class DelegationAssociationTest < DelegationTest - include DelegationWhitelistBlacklistTests + class DelegationAssociationTest < ActiveRecord::TestCase + include DelegationWhitelistTests + + fixtures :posts def target Post.first.comments end end - class DelegationRelationTest < DelegationTest - include DelegationWhitelistBlacklistTests + class DelegationRelationTest < ActiveRecord::TestCase + include DelegationWhitelistTests fixtures :comments diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 64866eaf2d..3901824aac 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -56,7 +56,7 @@ class RelationMergingTest < ActiveRecord::TestCase def test_relation_merging_with_locks devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2)) - assert devs.locked.present? + assert devs.locked? end def test_relation_merging_with_preload @@ -143,7 +143,7 @@ class MergingDifferentRelationsTest < ActiveRecord::TestCase assert_equal ["Mary", "Mary", "Mary", "David"], posts_by_author_name end - test "relation merging (using a proc argument)" do + test "relation merging (using a proc argument)" do dev = Developer.where(name: "Jamis").first comment_1 = dev.comments.create!(body: "I'm Jamis", post: Post.first) diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 11ef0d8743..dea787c07f 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -36,7 +36,7 @@ module ActiveRecord @relation ||= Relation.new FakeKlass.new("posts"), Post.arel_table, Post.predicate_builder end - (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select, :left_joins]).each do |method| + (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal [:foo], relation.public_send("#{method}_values") diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb index abb7ca72dd..61b6601580 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -59,6 +59,31 @@ module ActiveRecord assert_equal "Relation passed to #or must be structurally compatible. Incompatible values: [:order]", error.message end + def test_or_with_unscope_where + expected = Post.where("id = 1 or id = 2") + partial = Post.where("id = 1 and id != 2") + assert_equal expected, partial.or(partial.unscope(:where).where("id = 2")).to_a + end + + def test_or_with_unscope_where_column + expected = Post.where("id = 1 or id = 2") + partial = Post.where(id: 1).where.not(id: 2) + assert_equal expected, partial.or(partial.unscope(where: :id).where("id = 2")).to_a + end + + def test_or_with_unscope_order + expected = Post.where("id = 1 or id = 2") + assert_equal expected, Post.order("body asc").where("id = 1").unscope(:order).or(Post.where("id = 2")).to_a + end + + def test_or_with_incompatible_unscope + error = assert_raises ArgumentError do + Post.order("body asc").where("id = 1").or(Post.order("body asc").where("id = 2").unscope(:order)).to_a + end + + assert_equal "Relation passed to #or must be structurally compatible. Incompatible values: [:order]", error.message + end + def test_or_when_grouping groups = Post.where("id < 10").group("body").select("body, COUNT(*) AS c") expected = groups.having("COUNT(*) > 1 OR body like 'Such%'").to_a.map { |o| [o.body, o.c] } diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index a96d1ae5b5..86e150ed79 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -25,7 +25,7 @@ module ActiveRecord end def test_association_not_eq - expected = Arel::Nodes::Grouping.new(Comment.arel_table[@name].not_eq(Arel::Nodes::BindParam.new)) + expected = Arel::Nodes::Grouping.new(Comment.arel_table[@name].not_eq(bind_param)) relation = Post.joins(:comments).where.not(comments: { title: "hello" }) assert_equal(expected.to_sql, relation.where_clause.ast.to_sql) end diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb index d8e4c304f0..f8eb0dee91 100644 --- a/activerecord/test/cases/relation/where_clause_test.rb +++ b/activerecord/test/cases/relation/where_clause_test.rb @@ -47,15 +47,15 @@ class ActiveRecord::Relation test "merge removes bind parameters matching overlapping equality clauses" do a = WhereClause.new( [table["id"].eq(bind_param), table["name"].eq(bind_param)], - [attribute("id", 1), attribute("name", "Sean")], + [bind_attribute("id", 1), bind_attribute("name", "Sean")], ) b = WhereClause.new( [table["name"].eq(bind_param)], - [attribute("name", "Jim")] + [bind_attribute("name", "Jim")] ) expected = WhereClause.new( [table["id"].eq(bind_param), table["name"].eq(bind_param)], - [attribute("id", 1), attribute("name", "Jim")], + [bind_attribute("id", 1), bind_attribute("name", "Jim")], ) assert_equal expected, a.merge(b) @@ -103,10 +103,10 @@ class ActiveRecord::Relation table["name"].eq(bind_param), table["age"].gteq(bind_param), ], [ - attribute("name", "Sean"), - attribute("age", 30), + bind_attribute("name", "Sean"), + bind_attribute("age", 30), ]) - expected = WhereClause.new([table["age"].gteq(bind_param)], [attribute("age", 30)]) + expected = WhereClause.new([table["age"].gteq(bind_param)], [bind_attribute("age", 30)]) assert_equal expected, where_clause.except("id", "name") end @@ -146,8 +146,8 @@ class ActiveRecord::Relation end test "or joins the two clauses using OR" do - where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)]) - other_clause = WhereClause.new([table["name"].eq(bind_param)], [attribute("name", "Sean")]) + where_clause = WhereClause.new([table["id"].eq(bind_param)], [bind_attribute("id", 1)]) + other_clause = WhereClause.new([table["name"].eq(bind_param)], [bind_attribute("name", "Sean")]) expected_ast = Arel::Nodes::Grouping.new( Arel::Nodes::Or.new(table["id"].eq(bind_param), table["name"].eq(bind_param)) @@ -159,7 +159,7 @@ class ActiveRecord::Relation end test "or returns an empty where clause when either side is empty" do - where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)]) + where_clause = WhereClause.new([table["id"].eq(bind_param)], [bind_attribute("id", 1)]) assert_equal WhereClause.empty, where_clause.or(WhereClause.empty) assert_equal WhereClause.empty, WhereClause.empty.or(where_clause) @@ -170,13 +170,5 @@ class ActiveRecord::Relation def table Arel::Table.new("table") end - - def bind_param - Arel::Nodes::BindParam.new - end - - def attribute(name, value) - ActiveRecord::Attribute.with_cast_value(name, value, ActiveRecord::Type::Value.new) - end end end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index cbc466d6b8..42dae4d569 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -15,7 +15,7 @@ require "models/vertex" module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors, :author_addresses, :binaries, :essays, :cars, :treasures, :price_estimates + fixtures :posts, :edges, :authors, :author_addresses, :binaries, :essays, :cars, :treasures, :price_estimates, :topics def test_where_copies_bind_params author = authors(:david) @@ -48,6 +48,10 @@ module ActiveRecord assert_equal [chef], chefs.to_a end + def test_where_with_casted_value_is_nil + assert_equal 4, Topic.where(last_read: "").count + end + def test_rewhere_on_root assert_equal posts(:welcome), Post.rewhere(title: "Welcome to the weblog").first end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 5fb32270b7..a403824f1a 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -6,7 +6,7 @@ require "models/rating" module ActiveRecord class RelationTest < ActiveRecord::TestCase - fixtures :posts, :comments, :authors, :author_addresses + fixtures :posts, :comments, :authors, :author_addresses, :ratings FakeKlass = Struct.new(:table_name, :name) do extend ActiveRecord::Delegation::DelegateCache @@ -224,7 +224,26 @@ module ActiveRecord def test_relation_merging_with_merged_joins_as_symbols special_comments_with_ratings = SpecialComment.joins(:ratings) posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) - assert_equal({ 2 => 1, 4 => 3, 5 => 1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count) + assert_equal({ 4 => 2 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count) + end + + def test_relation_merging_with_merged_symbol_joins_keeps_inner_joins + queries = capture_sql { Author.joins(:posts).merge(Post.joins(:comments)).to_a } + + nb_inner_join = queries.sum { |sql| sql.scan(/INNER\s+JOIN/i).size } + assert_equal 2, nb_inner_join, "Wrong amount of INNER JOIN in query" + assert queries.none? { |sql| /LEFT\s+(OUTER)?\s+JOIN/i.match?(sql) }, "Shouldn't have any LEFT JOIN in query" + end + + def test_relation_merging_with_merged_symbol_joins_has_correct_size_and_count + # Has one entry per comment + merged_authors_with_commented_posts_relation = Author.joins(:posts).merge(Post.joins(:comments)) + + post_ids_with_author = Post.joins(:author).pluck(:id) + manual_comments_on_post_that_have_author = Comment.where(post_id: post_ids_with_author).pluck(:id) + + assert_equal manual_comments_on_post_that_have_author.size, merged_authors_with_commented_posts_relation.count + assert_equal manual_comments_on_post_that_have_author.size, merged_authors_with_commented_posts_relation.to_a.size end def test_relation_merging_with_joins_as_join_dependency_pick_proper_parent diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 7a710f1004..5767dec315 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -695,16 +695,6 @@ class RelationTest < ActiveRecord::TestCase end end - def test_default_scope_with_conditions_string - assert_equal Developer.where(name: "David").map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort - assert_nil DeveloperCalledDavid.create!.name - end - - def test_default_scope_with_conditions_hash - assert_equal Developer.where(name: "Jamis").map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort - assert_equal "Jamis", DeveloperCalledJamis.create!.name - end - def test_default_scoping_finder_methods developers = DeveloperCalledDavid.order("id").map(&:id).sort assert_equal Developer.where(name: "David").map(&:id).sort, developers @@ -1727,6 +1717,9 @@ class RelationTest < ActiveRecord::TestCase scope = Post.order("comments.body") assert_equal ["comments"], scope.references_values + scope = Post.order("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}") + assert_equal ["comments"], scope.references_values + scope = Post.order("comments.body", "yaks.body") assert_equal ["comments", "yaks"], scope.references_values @@ -1745,6 +1738,9 @@ class RelationTest < ActiveRecord::TestCase scope = Post.reorder("comments.body") assert_equal %w(comments), scope.references_values + scope = Post.reorder("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}") + assert_equal ["comments"], scope.references_values + scope = Post.reorder("comments.body", "yaks.body") assert_equal %w(comments yaks), scope.references_values @@ -1981,24 +1977,28 @@ class RelationTest < ActiveRecord::TestCase end def test_unscope_removes_binds - left = Post.where(id: Arel::Nodes::BindParam.new) - column = Post.columns_hash["id"] - left.bind_values += [[column, 20]] + left = Post.where(id: 20) + + binds = [bind_attribute("id", 20, Post.type_for_attribute("id"))] + assert_equal binds, left.bound_attributes relation = left.unscope(where: :id) - assert_equal [], relation.bind_values + assert_equal [], relation.bound_attributes end - def test_merging_removes_rhs_bind_parameters + def test_merging_removes_rhs_binds left = Post.where(id: 20) right = Post.where(id: [1, 2, 3, 4]) + binds = [bind_attribute("id", 20, Post.type_for_attribute("id"))] + assert_equal binds, left.bound_attributes + merged = left.merge(right) - assert_equal [], merged.bind_values + assert_equal [], merged.bound_attributes end - def test_merging_keeps_lhs_bind_parameters - binds = [ActiveRecord::Relation::QueryAttribute.new("id", 20, Post.type_for_attribute("id"))] + def test_merging_keeps_lhs_binds + binds = [bind_attribute("id", 20, Post.type_for_attribute("id"))] right = Post.where(id: 20) left = Post.where(id: 10) @@ -2007,13 +2007,10 @@ class RelationTest < ActiveRecord::TestCase assert_equal binds, merged.bound_attributes end - def test_merging_reorders_bind_params - post = Post.first - right = Post.where(id: post.id) - left = Post.where(title: post.title) - - merged = left.merge(right) - assert_equal post, merged.first + def test_locked_should_not_build_arel + posts = Post.locked + assert posts.locked? + assert_nothing_raised { posts.lock!(false) } end def test_relation_join_method diff --git a/activerecord/test/cases/reload_models_test.rb b/activerecord/test/cases/reload_models_test.rb index 5dc9d6d8b7..3f4c0c03e3 100644 --- a/activerecord/test/cases/reload_models_test.rb +++ b/activerecord/test/cases/reload_models_test.rb @@ -13,7 +13,7 @@ class ReloadModelsTest < ActiveRecord::TestCase # development environment. Note that meanwhile the class Pet is not # reloaded, simulating a class that is present in a plugin. Object.class_eval { remove_const :Owner } - Kernel.load(File.expand_path(File.join(File.dirname(__FILE__), "../models/owner.rb"))) + Kernel.load(File.expand_path("../models/owner.rb", __dir__)) pet = Pet.find_by_name("parrot") pet.owner = Owner.find_by_name("ashley") diff --git a/activerecord/test/cases/reserved_word_test.rb b/activerecord/test/cases/reserved_word_test.rb new file mode 100644 index 0000000000..f3019a5326 --- /dev/null +++ b/activerecord/test/cases/reserved_word_test.rb @@ -0,0 +1,132 @@ +require "cases/helper" + +class ReservedWordTest < ActiveRecord::TestCase + self.use_instantiated_fixtures = true + self.use_transactional_tests = false + + class Group < ActiveRecord::Base + Group.table_name = "group" + belongs_to :select + has_one :values + end + + class Select < ActiveRecord::Base + Select.table_name = "select" + has_many :groups + end + + class Values < ActiveRecord::Base + Values.table_name = "values" + end + + class Distinct < ActiveRecord::Base + Distinct.table_name = "distinct" + has_and_belongs_to_many :selects + has_many :values, through: :groups + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :select, force: true + @connection.create_table :distinct, force: true + @connection.create_table :distinct_select, id: false, force: true do |t| + t.belongs_to :distinct + t.belongs_to :select + end + @connection.create_table :group, force: true do |t| + t.string :order + t.belongs_to :select + end + @connection.create_table :values, force: true do |t| + t.belongs_to :group + end + end + + def teardown + @connection.drop_table :select, if_exists: true + @connection.drop_table :distinct, if_exists: true + @connection.drop_table :distinct_select, if_exists: true + @connection.drop_table :group, if_exists: true + @connection.drop_table :values, if_exists: true + @connection.drop_table :order, if_exists: true + end + + def test_create_tables + assert_not @connection.table_exists?(:order) + + @connection.create_table :order do |t| + t.string :group + end + + assert @connection.table_exists?(:order) + end + + def test_rename_tables + assert_nothing_raised { @connection.rename_table(:group, :order) } + end + + def test_change_columns + assert_nothing_raised { @connection.change_column_default(:group, :order, "whatever") } + assert_nothing_raised { @connection.change_column("group", "order", :text, default: nil) } + assert_nothing_raised { @connection.rename_column(:group, :order, :values) } + end + + def test_introspect + assert_equal ["id", "order", "select_id"], @connection.columns(:group).map(&:name).sort + assert_equal ["index_group_on_select_id"], @connection.indexes(:group).map(&:name).sort + end + + def test_activerecord_model + x = Group.new + x.order = "x" + x.save! + x.order = "y" + x.save! + assert_equal x, Group.find_by_order("y") + assert_equal x, Group.find(x.id) + end + + def test_has_one_associations + create_test_fixtures :group, :values + v = Group.find(1).values + assert_equal 2, v.id + end + + def test_belongs_to_associations + create_test_fixtures :select, :group + gs = Select.find(2).groups + assert_equal 2, gs.length + assert_equal [2, 3], gs.collect(&:id).sort + end + + def test_has_and_belongs_to_many + create_test_fixtures :select, :distinct, :distinct_select + s = Distinct.find(1).selects + assert_equal 2, s.length + assert_equal [1, 2], s.collect(&:id).sort + end + + def test_activerecord_introspection + assert Group.table_exists? + assert_equal ["id", "order", "select_id"], Group.columns.map(&:name).sort + end + + def test_calculations_work_with_reserved_words + create_test_fixtures :group + assert_equal 3, Group.count + end + + def test_associations_work_with_reserved_words + create_test_fixtures :select, :group + selects = Select.all.merge!(includes: [:groups]).to_a + assert_no_queries do + selects.each { |select| select.groups } + end + end + + private + # custom fixture loader, uses FixtureSet#create_fixtures and appends base_path to the current file's path + def create_test_fixtures(*fixture_names) + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) + end +end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index cb8d449ba9..4c81e825fa 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -17,6 +17,12 @@ class SchemaDumperTest < ActiveRecord::TestCase dump_all_table_schema [] end + def test_dump_schema_information_with_empty_versions + ActiveRecord::SchemaMigration.delete_all + schema_info = ActiveRecord::Base.connection.dump_schema_information + assert_no_match(/INSERT INTO/, schema_info) + end + def test_dump_schema_information_outputs_lexically_ordered_versions versions = %w{ 20100101010101 20100201010101 20100301010101 } versions.reverse_each do |v| @@ -116,32 +122,22 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_includes_limit_constraint_for_integer_columns output = dump_all_table_schema([/^(?!integer_limits)/]) - assert_match %r{c_int_without_limit}, output + assert_match %r{"c_int_without_limit"(?!.*limit)}, output if current_adapter?(:PostgreSQLAdapter) - assert_no_match %r{c_int_without_limit.*limit:}, output - assert_match %r{c_int_1.*limit: 2}, output assert_match %r{c_int_2.*limit: 2}, output # int 3 is 4 bytes in postgresql - assert_match %r{c_int_3.*}, output - assert_no_match %r{c_int_3.*limit:}, output - - assert_match %r{c_int_4.*}, output - assert_no_match %r{c_int_4.*limit:}, output + assert_match %r{"c_int_3"(?!.*limit)}, output + assert_match %r{"c_int_4"(?!.*limit)}, output elsif current_adapter?(:Mysql2Adapter) - assert_match %r{c_int_without_limit"$}, output - assert_match %r{c_int_1.*limit: 1}, output assert_match %r{c_int_2.*limit: 2}, output assert_match %r{c_int_3.*limit: 3}, output - assert_match %r{c_int_4.*}, output - assert_no_match %r{c_int_4.*:limit}, output + assert_match %r{"c_int_4"(?!.*limit)}, output elsif current_adapter?(:SQLite3Adapter) - assert_no_match %r{c_int_without_limit.*limit:}, output - assert_match %r{c_int_1.*limit: 1}, output assert_match %r{c_int_2.*limit: 2}, output assert_match %r{c_int_3.*limit: 3}, output @@ -330,7 +326,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_keeps_id_false_when_id_is_false_and_unique_not_null_column_added output = standard_dump - assert_match %r{create_table "subscribers", id: false}, output + assert_match %r{create_table "string_key_objects", id: false}, output end if ActiveRecord::Base.connection.supports_foreign_keys? diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 0c2cffe0d3..483ea7128d 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -551,6 +551,12 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal 1, SpecialComment.where(body: "go crazy").created.count end + def test_model_class_should_respond_to_extending + assert_raises OopsError do + Comment.unscoped.oops_comments.destroy_all + end + end + def test_model_class_should_respond_to_none assert !Topic.none? Topic.delete_all diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 3fbff7664b..8535be8402 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -229,12 +229,19 @@ class RelationScopingTest < ActiveRecord::TestCase end end - def test_circular_joins_with_current_scope_does_not_crash + def test_circular_joins_with_scoping_does_not_crash posts = Post.joins(comments: :post).scoping do - Post.current_scope.first(10) + Post.first(10) end assert_equal posts, Post.joins(comments: :post).first(10) end + + def test_circular_left_joins_with_scoping_does_not_crash + posts = Post.left_joins(comments: :post).scoping do + Post.first(10) + end + assert_equal posts, Post.left_joins(comments: :post).first(10) + end end class NestedRelationScopingTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 673392b4c4..e1bdaab5cf 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -349,4 +349,32 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic.foo refute topic.changed? end + + def test_serialized_attribute_works_under_concurrent_initial_access + model = Topic.dup + + topic = model.last + topic.update group: "1" + + model.serialize :group, JSON + model.reset_column_information + + # This isn't strictly necessary for the test, but a little bit of + # knowledge of internals allows us to make failures far more likely. + model.define_singleton_method(:define_attribute) do |*args| + Thread.pass + super(*args) + end + + threads = 4.times.map do + Thread.new do + topic.reload.group + end + end + + # All the threads should retrieve the value knowing it is JSON, and + # thus decode it. If this fails, some threads will instead see the + # raw string ("1"), or raise an exception. + assert_equal [1] * threads.size, threads.map(&:value) + end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index b85d303a91..9c6fb14376 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -296,7 +296,7 @@ if current_adapter?(:Mysql2Adapter) def test_structure_dump_with_extra_flags filename = "awesome-file.sql" - expected_command = ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--noop", "test-db"] + expected_command = ["mysqldump", "--noop", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"] assert_called_with(Kernel, :system, expected_command, returns: true) do with_structure_dump_flags(["--noop"]) do @@ -305,6 +305,15 @@ if current_adapter?(:Mysql2Adapter) end end + def test_structure_dump_with_ignore_tables + filename = "awesome-file.sql" + ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"]) + + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--ignore-table=test-db.foo", "--ignore-table=test-db.bar", "test-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end + def test_warn_when_external_structure_dump_command_execution_fails filename = "awesome-file.sql" Kernel.expects(:system) @@ -355,7 +364,7 @@ if current_adapter?(:Mysql2Adapter) def test_structure_load filename = "awesome-file.sql" - expected_command = ["mysql", "--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db", "--noop"] + expected_command = ["mysql", "--noop", "--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db"] assert_called_with(Kernel, :system, expected_command, returns: true) do with_structure_load_flags(["--noop"]) do diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 512388af6b..a2e968aedf 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -259,6 +259,14 @@ if current_adapter?(:PostgreSQLAdapter) end end + def test_structure_dump_with_ignore_tables + ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"]) + + Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "-T", "foo", "-T", "bar", "my-app-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end + def test_structure_dump_with_schema_search_path @configuration["schema_search_path"] = "foo,bar" diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 0d917f3f6c..ccb3834fee 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -180,6 +180,9 @@ if current_adapter?(:SQLite3Adapter) "adapter" => "sqlite3", "database" => @database } + + `sqlite3 #{@database} 'CREATE TABLE bar(id INTEGER)'` + `sqlite3 #{@database} 'CREATE TABLE foo(id INTEGER)'` end def test_structure_dump @@ -189,6 +192,23 @@ if current_adapter?(:SQLite3Adapter) ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, "/rails/root" assert File.exist?(dbfile) assert File.exist?(filename) + assert_match(/CREATE TABLE foo/, File.read(filename)) + assert_match(/CREATE TABLE bar/, File.read(filename)) + ensure + FileUtils.rm_f(filename) + FileUtils.rm_f(dbfile) + end + + def test_structure_dump_with_ignore_tables + dbfile = @database + filename = "awesome-file.sql" + ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo"]) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root") + assert File.exist?(dbfile) + assert File.exist?(filename) + assert_match(/bar/, File.read(filename)) + assert_no_match(/foo/, File.read(filename)) ensure FileUtils.rm_f(filename) FileUtils.rm_f(dbfile) diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 31b11c19f7..9f594fef85 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -75,6 +75,14 @@ module ActiveRecord model.reset_column_information model.column_names.include?(column_name.to_s) end + + def bind_param + Arel::Nodes::BindParam.new + end + + def bind_attribute(name, value, type = ActiveRecord::Type.default_value) + ActiveRecord::Relation::QueryAttribute.new(name, value, type) + end end class PostgreSQLTestCase < TestCase diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 5c6d78b574..79ba306ef5 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -595,6 +595,52 @@ class TransactionTest < ActiveRecord::TestCase assert_not topic.frozen? end + def test_restore_id_after_rollback + topic = Topic.new + + Topic.transaction do + topic.save! + raise ActiveRecord::Rollback + end + + assert_nil topic.id + end + + def test_restore_custom_primary_key_after_rollback + movie = Movie.new(name: "foo") + + Movie.transaction do + movie.save! + raise ActiveRecord::Rollback + end + + assert_nil movie.id + end + + def test_assign_id_after_rollback + topic = Topic.create! + + Topic.transaction do + topic.save! + raise ActiveRecord::Rollback + end + + topic.id = nil + assert_nil topic.id + end + + def test_assign_custom_primary_key_after_rollback + movie = Movie.create!(name: "foo") + + Movie.transaction do + movie.save! + raise ActiveRecord::Rollback + end + + movie.id = nil + assert_nil movie.id + end + def test_rollback_of_frozen_records topic = Topic.create.freeze Topic.transaction do diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 5d9aa99497..a305aa295a 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -167,6 +167,20 @@ class ValidationsTest < ActiveRecord::TestCase assert topic.valid? end + def test_numericality_validation_checks_against_raw_value + klass = Class.new(Topic) do + def self.model_name + ActiveModel::Name.new(self, nil, "Topic") + end + attribute :wibble, :decimal, scale: 2, precision: 9 + validates_numericality_of :wibble, greater_than_or_equal_to: BigDecimal.new("97.18") + end + + assert_not klass.new(wibble: "97.179").valid? + assert_not klass.new(wibble: 97.179).valid? + assert_not klass.new(wibble: BigDecimal.new("97.179")).valid? + end + def test_acceptance_validator_doesnt_require_db_connection klass = Class.new(ActiveRecord::Base) do self.table_name = "posts" diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index ab0e67cd9d..bfc13d683d 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -123,8 +123,8 @@ class YamlSerializationTest < ActiveRecord::TestCase def yaml_fixture(file_name) path = File.expand_path( - "../../support/yaml_compatibility_fixtures/#{file_name}.yml", - __FILE__ + "../support/yaml_compatibility_fixtures/#{file_name}.yml", + __dir__ ) File.read(path) end diff --git a/activerecord/test/config.rb b/activerecord/test/config.rb index 6e2e8b2145..a65e6ff776 100644 --- a/activerecord/test/config.rb +++ b/activerecord/test/config.rb @@ -1,4 +1,4 @@ -TEST_ROOT = File.expand_path(File.dirname(__FILE__)) +TEST_ROOT = __dir__ ASSETS_ROOT = TEST_ROOT + "/assets" FIXTURES_ROOT = TEST_ROOT + "/fixtures" MIGRATIONS_ROOT = TEST_ROOT + "/migrations" diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml index b3625ee72e..699623a6f9 100644 --- a/activerecord/test/fixtures/books.yml +++ b/activerecord/test/fixtures/books.yml @@ -25,6 +25,7 @@ ddd: name: "Domain-Driven Design" format: "hardcover" status: 2 + read_status: "forgotten" tlg: author_id: 1 diff --git a/activerecord/test/fixtures/naked/yml/parrots.yml b/activerecord/test/fixtures/naked/yml/parrots.yml index 3e10331105..76f66e01ae 100644 --- a/activerecord/test/fixtures/naked/yml/parrots.yml +++ b/activerecord/test/fixtures/naked/yml/parrots.yml @@ -1,2 +1,3 @@ george: arrr: "Curious George" + foobar: Foobar diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index fab613afd1..2d9cba77e0 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -106,6 +106,7 @@ class Author < ActiveRecord::Base has_many :tags_with_primary_key, through: :posts has_many :books + has_many :unpublished_books, -> { where(status: [:proposed, :written]) }, class_name: "Book" has_many :subscriptions, through: :books has_many :subscribers, -> { order("subscribers.nick") }, through: :subscriptions has_many :distinct_subscribers, -> { select("DISTINCT subscribers.*").order("subscribers.nick") }, through: :subscriptions, source: :subscriber diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 17bf3fbcb4..6466e1b341 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -1,5 +1,5 @@ class Book < ActiveRecord::Base - has_many :authors + belongs_to :author has_many :citations, foreign_key: "book1_id" has_many :references, -> { distinct }, through: :citations, source: :reference_of @@ -8,7 +8,7 @@ class Book < ActiveRecord::Base has_many :subscribers, through: :subscriptions enum status: [:proposed, :written, :published] - enum read_status: { unread: 0, reading: 2, read: 3 } + enum read_status: { unread: 0, reading: 2, read: 3, forgotten: nil } enum nullable_status: [:single, :married] enum language: [:english, :spanish, :french], _prefix: :in enum author_visibility: [:visible, :invisible], _prefix: true diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index e8654dca01..4b2840c653 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -29,6 +29,15 @@ class Category < ActiveRecord::Base has_many :authors_with_select, -> { select "authors.*, categorizations.post_id" }, through: :categorizations, source: :author scope :general, -> { where(name: "General") } + + # Should be delegated `ast` and `locked` to `arel`. + def self.ast + raise + end + + def self.locked + raise + end end class SpecialCategory < Category diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 76b484e616..eecf923046 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -9,7 +9,6 @@ class Comment < ActiveRecord::Base belongs_to :post, counter_cache: true belongs_to :author, polymorphic: true belongs_to :resource, polymorphic: true - belongs_to :developer has_many :ratings @@ -19,6 +18,18 @@ class Comment < ActiveRecord::Base has_many :children, class_name: "Comment", foreign_key: :parent_id belongs_to :parent, class_name: "Comment", counter_cache: :children_count + class ::OopsError < RuntimeError; end + + module OopsExtension + def destroy_all(*) + raise OopsError + end + end + + default_scope { extending OopsExtension } + + scope :oops_comments, -> { extending OopsExtension } + # Should not be called if extending modules that having the method exists on an association. def self.greeting raise diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index d269a95e8c..c6a5bf1c92 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -175,6 +175,7 @@ end class ExclusivelyDependentFirm < Company has_one :account, foreign_key: "firm_id", dependent: :delete has_many :dependent_sanitized_conditional_clients_of_firm, -> { order("id").where("name = 'BigShot Inc.'") }, foreign_key: "client_of", class_name: "Client", dependent: :delete_all + has_many :dependent_hash_conditional_clients_of_firm, -> { order("id").where(name: "BigShot Inc.") }, foreign_key: "client_of", class_name: "Client", dependent: :delete_all has_many :dependent_conditional_clients_of_firm, -> { order("id").where("name = ?", "BigShot Inc.") }, foreign_key: "client_of", class_name: "Client", dependent: :delete_all end diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb index 2c3ad230a7..0f8be0ad85 100644 --- a/activerecord/test/models/membership.rb +++ b/activerecord/test/models/membership.rb @@ -1,4 +1,5 @@ class Membership < ActiveRecord::Base + enum type: %i(Membership CurrentMembership SuperMembership SelectedMembership TenantMembership) belongs_to :member belongs_to :club end diff --git a/activerecord/test/models/numeric_data.rb b/activerecord/test/models/numeric_data.rb new file mode 100644 index 0000000000..c6e025a9ce --- /dev/null +++ b/activerecord/test/models/numeric_data.rb @@ -0,0 +1,8 @@ +class NumericData < ActiveRecord::Base + self.table_name = "numeric_data" + # Decimal columns with 0 scale being automatically treated as integers + # is deprecated, and will be removed in a future version of Rails. + attribute :world_population, :big_integer + attribute :my_house_population, :big_integer + attribute :atoms_in_universe, :big_integer +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index a2028b3eb9..ed64e0ee52 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -13,7 +13,7 @@ class Post < ActiveRecord::Base module NamedExtension2 def greeting - "hello" + "hullo" end end @@ -22,6 +22,7 @@ class Post < ActiveRecord::Base scope :ranked_by_comments, -> { order("comments_count DESC") } scope :limit_by, lambda { |l| limit(l) } + scope :locked, -> { lock } belongs_to :author belongs_to :readonly_author, -> { readonly }, class_name: "Author", foreign_key: :author_id diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 0420e2d15c..d9381ac9cf 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -14,7 +14,7 @@ class Topic < ActiveRecord::Base scope :replied, -> { where "replies_count > 0" } scope "approved_as_string", -> { where(approved: true) } - scope :anonymous_extension, -> { all } do + scope :anonymous_extension, -> {} do def one 1 end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 50f1d9bfe7..f534e9c00e 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -107,7 +107,7 @@ ActiveRecord::Schema.define do t.boolean :has_fun, null: false, default: false end - create_table :bulbs, force: true do |t| + create_table :bulbs, primary_key: "ID", force: true do |t| t.integer :car_id t.string :name t.boolean :frickinawesome, default: false @@ -453,11 +453,13 @@ ActiveRecord::Schema.define do create_table :lock_without_defaults, force: true do |t| t.column :title, :string t.column :lock_version, :integer + t.timestamps null: true end create_table :lock_without_defaults_cust, force: true do |t| t.column :title, :string t.column :custom_lock_version, :integer + t.timestamps null: true end create_table :magazines, force: true do |t| @@ -489,7 +491,7 @@ ActiveRecord::Schema.define do t.datetime :joined_on t.integer :club_id, :member_id t.boolean :favourite, default: false - t.string :type + t.integer :type end create_table :member_types, force: true do |t| @@ -807,16 +809,19 @@ ActiveRecord::Schema.define do t.string :sponsorable_type end - create_table :string_key_objects, id: false, primary_key: :id, force: true do |t| - t.string :id - t.string :name - t.integer :lock_version, null: false, default: 0 + create_table :string_key_objects, id: false, force: true do |t| + t.string :id, null: false + t.string :name + t.integer :lock_version, null: false, default: 0 + t.index :id, unique: true end - create_table :subscribers, force: true, id: false do |t| + create_table :subscribers, id: false, force: true do |t| t.string :nick, null: false t.string :name - t.column :books_count, :integer, null: false, default: 0 + t.integer :id + t.integer :books_count, null: false, default: 0 + t.integer :update_count, null: false, default: 0 t.index :nick, unique: true end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index a4fc1e34eb..ab5237488a 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,87 @@ +* Default `ActiveSupport::MessageEncryptor` to use AES 256 GCM encryption. + + On for new Rails 5.2 apps. Upgrading apps can find the config as a new + framework default. + + *Assain Jaleel* + +* Cache: `write_multi` + + Rails.cache.write_multi foo: 'bar', baz: 'qux' + + Plus faster fetch_multi with stores that implement `write_multi_entries`. + Keys that aren't found may be written to the cache store in one shot + instead of separate writes. + + The default implementation simply calls `write_entry` for each entry. + Stores may override if they're capable of one-shot bulk writes, like + Redis `MSET`. + + *Jeremy Daer* + +* Add default option to module and class attribute accessors. + + mattr_accessor :settings, default: {} + + Works for `mattr_reader`, `mattr_writer`, `cattr_accessor`, `cattr_reader`, + and `cattr_writer` as well. + + *Genadi Samokovarov* + +* Add `Date#prev_occurring` and `Date#next_occurring` to return specified next/previous occurring day of week. + + *Shota Iguchi* + +* Add default option to `class_attribute`. + + Before: + + class_attribute :settings + self.settings = {} + + Now: + + class_attribute :settings, default: {} + + *DHH* + +* `#singularize` and `#pluralize` now respect uncountables for the specified locale. + + *Eilis Hamilton* + +* Add `ActiveSupport::CurrentAttributes` to provide a thread-isolated attributes singleton. + Primary use case is keeping all the per-request attributes easily available to the whole system. + + *DHH* + +* Fix implicit coercion calculations with scalars and durations + + Previously calculations where the scalar is first would be converted to a duration + of seconds but this causes issues with dates being converted to times, e.g: + + Time.zone = "Beijing" # => Asia/Shanghai + date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017 + 2 * 1.day # => 172800 seconds + date + 2 * 1.day # => Mon, 22 May 2017 00:00:00 CST +08:00 + + Now the `ActiveSupport::Duration::Scalar` calculation methods will try to maintain + the part structure of the duration where possible, e.g: + + Time.zone = "Beijing" # => Asia/Shanghai + date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017 + 2 * 1.day # => 2 days + date + 2 * 1.day # => Mon, 22 May 2017 + + Fixes #29160, #28970. + + *Andrew White* + +* Add support for versioned cache entries. This enables the cache stores to recycle cache keys, greatly saving + on storage in cases with frequent churn. Works together with the separation of `#cache_key` and `#cache_version` + in Active Record and its use in Action Pack's fragment caching. + + *DHH* + * Pass gem name and deprecation horizon to deprecation notifications. *Willem van Bergen* @@ -18,4 +102,5 @@ *Josh Pencheon* + Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activesupport/CHANGELOG.md) for previous changes. diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index 08370cba85..16e912694c 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -20,6 +20,11 @@ Gem::Specification.new do |s| s.rdoc_options.concat ["--encoding", "UTF-8"] + s.metadata = { + "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/activesupport", + "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activesupport/CHANGELOG.md" + } + s.add_dependency "i18n", "~> 0.7" s.add_dependency "tzinfo", "~> 1.1" s.add_dependency "minitest", "~> 5.1" diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables index aa36a01b5b..6f62593f14 100755 --- a/activesupport/bin/generate_tables +++ b/activesupport/bin/generate_tables @@ -1,7 +1,7 @@ #!/usr/bin/env ruby begin - $:.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib")) + $:.unshift(File.expand_path("../lib", __dir__)) require "active_support" rescue IOError end diff --git a/activesupport/bin/test b/activesupport/bin/test index a7beb14b27..470ce93f10 100755 --- a/activesupport/bin/test +++ b/activesupport/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 03e3ce821a..a667fbb54a 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -32,6 +32,7 @@ module ActiveSupport extend ActiveSupport::Autoload autoload :Concern + autoload :CurrentAttributes autoload :Dependencies autoload :DescendantsTracker autoload :ExecutionWrapper diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 4d8c2046e8..3847d8b7ae 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -88,10 +88,11 @@ module ActiveSupport private def retrieve_cache_key(key) case - when key.respond_to?(:cache_key) then key.cache_key - when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param - when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a) - else key.to_param + when key.respond_to?(:cache_key_with_version) then key.cache_key_with_version + when key.respond_to?(:cache_key) then key.cache_key + when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param + when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a) + else key.to_param end.to_s end @@ -219,6 +220,10 @@ module ActiveSupport # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes) # cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry # + # Setting <tt>:version</tt> verifies the cache stored under <tt>name</tt> + # is of the same version. nil is returned on mismatches despite contents. + # This feature is used to support recyclable cache keys. + # # Setting <tt>:race_condition_ttl</tt> is very useful in situations where # a cache entry is used very frequently and is under heavy load. If a # cache expires and due to heavy load several different processes will try @@ -287,6 +292,7 @@ module ActiveSupport instrument(:read, name, options) do |payload| cached_entry = read_entry(key, options) unless options[:force] entry = handle_expired_entry(cached_entry, key, options) + entry = nil if entry && entry.mismatched?(normalize_version(name, options)) payload[:super_operation] = :fetch if payload payload[:hit] = !!entry if payload end @@ -303,21 +309,30 @@ module ActiveSupport end end - # Fetches data from the cache, using the given key. If there is data in + # Reads data from the cache, using the given key. If there is data in # the cache with the given key, then that data is returned. Otherwise, # +nil+ is returned. # + # Note, if data was written with the <tt>:expires_in<tt> or <tt>:version</tt> options, + # both of these conditions are applied before the data is returned. + # # Options are passed to the underlying cache implementation. def read(name, options = nil) options = merged_options(options) - key = normalize_key(name, options) + key = normalize_key(name, options) + version = normalize_version(name, options) + instrument(:read, name, options) do |payload| entry = read_entry(key, options) + if entry if entry.expired? delete_entry(key, options) payload[:hit] = false if payload nil + elsif entry.mismatched?(version) + payload[:hit] = false if payload + nil else payload[:hit] = true if payload entry.value @@ -341,11 +356,15 @@ module ActiveSupport results = {} names.each do |name| - key = normalize_key(name, options) - entry = read_entry(key, options) + key = normalize_key(name, options) + version = normalize_version(name, options) + entry = read_entry(key, options) + if entry if entry.expired? delete_entry(key, options) + elsif entry.mismatched?(version) + # Skip mismatched versions else results[name] = entry.value end @@ -354,6 +373,19 @@ module ActiveSupport results end + # Cache Storage API to write multiple values at once. + def write_multi(hash, options = nil) + options = merged_options(options) + + instrument :write_multi, hash, options do |payload| + entries = hash.each_with_object({}) do |(name, value), memo| + memo[normalize_key(name, options)] = Entry.new(value, options.merge(version: normalize_version(name, options))) + end + + write_multi_entries entries, options + end + end + # Fetches data from the cache, using the given keys. If there is data in # the cache with the given keys, then that data is returned. Otherwise, # the supplied block is called for each key for which there was no data, @@ -378,14 +410,15 @@ module ActiveSupport options = names.extract_options! options = merged_options(options) - results = read_multi(*names, options) - names.each_with_object({}) do |name, memo| - memo[name] = results.fetch(name) do - value = yield name - write(name, value, options) - value + read_multi(*names, options).tap do |results| + writes = {} + + (names - results.keys).each do |name| + results[name] = writes[name] = yield(name) end + + write_multi writes, options end end @@ -396,7 +429,7 @@ module ActiveSupport options = merged_options(options) instrument(:write, name, options) do - entry = Entry.new(value, options) + entry = Entry.new(value, options.merge(version: normalize_version(name, options))) write_entry(normalize_key(name, options), entry, options) end end @@ -420,7 +453,7 @@ module ActiveSupport instrument(:exist?, name) do entry = read_entry(normalize_key(name, options), options) - (entry && !entry.expired?) || false + (entry && !entry.expired? && !entry.mismatched?(normalize_version(name, options))) || false end end @@ -466,7 +499,7 @@ module ActiveSupport # The options hash is passed to the underlying cache implementation. # # All implementations may not support this method. - def clear + def clear(options = nil) raise NotImplementedError.new("#{self.class.name} does not support clear") end @@ -502,6 +535,14 @@ module ActiveSupport raise NotImplementedError.new end + # Writes multiple entries to the cache implementation. Subclasses MAY + # implement this method. + def write_multi_entries(hash, options) + hash.each do |key, entry| + write_entry key, entry, options + end + end + # Deletes an entry from the cache implementation. Subclasses must # implement this method. def delete_entry(key, options) @@ -517,6 +558,16 @@ module ActiveSupport end end + # Prefixes a key with the namespace. Namespace and key will be delimited + # with a colon. + def normalize_key(key, options) + key = expanded_key(key) + namespace = options[:namespace] if options + prefix = namespace.is_a?(Proc) ? namespace.call : namespace + key = "#{prefix}:#{key}" if prefix + key + end + # Expands key to be a consistent string value. Invokes +cache_key+ if # object responds to +cache_key+. Otherwise, +to_param+ method will be # called. If the key is a Hash, then keys will be sorted alphabetically. @@ -537,14 +588,16 @@ module ActiveSupport key.to_param end - # Prefixes a key with the namespace. Namespace and key will be delimited - # with a colon. - def normalize_key(key, options) - key = expanded_key(key) - namespace = options[:namespace] if options - prefix = namespace.is_a?(Proc) ? namespace.call : namespace - key = "#{prefix}:#{key}" if prefix - key + def normalize_version(key, options = nil) + (options && options[:version].try(:to_param)) || expanded_version(key) + end + + def expanded_version(key) + case + when key.respond_to?(:cache_version) then key.cache_version.to_param + when key.is_a?(Array) then key.map { |element| expanded_version(element) }.compact.to_param + when key.respond_to?(:to_a) then expanded_version(key.to_a) + end end def instrument(operation, key, options = nil) @@ -591,13 +644,16 @@ module ActiveSupport end end - # This class is used to represent cache entries. Cache entries have a value and an optional - # expiration time. The expiration time is used to support the :race_condition_ttl option - # on the cache. + # This class is used to represent cache entries. Cache entries have a value, an optional + # expiration time, and an optional version. The expiration time is used to support the :race_condition_ttl option + # on the cache. The version is used to support the :version option on the cache for rejecting + # mismatches. # # Since cache entries in most instances will be serialized, the internals of this class are highly optimized # using short instance variable names that are lazily defined. class Entry # :nodoc: + attr_reader :version + DEFAULT_COMPRESS_LIMIT = 16.kilobytes # Creates a new cache entry for the specified value. Options supported are @@ -610,6 +666,7 @@ module ActiveSupport @value = value end + @version = options[:version] @created_at = Time.now.to_f @expires_in = options[:expires_in] @expires_in = @expires_in.to_f if @expires_in @@ -619,6 +676,10 @@ module ActiveSupport compressed? ? uncompress(@value) : @value end + def mismatched?(version) + @version && version && @version != version + end + # Checks if the entry is expired. The +expires_in+ parameter can override # the value set when the entry was created. def expired? diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index d5c8585816..945f50a56e 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -27,7 +27,7 @@ module ActiveSupport # Deletes all items from the cache. In this case it deletes all the entries in the specified # file store directory except for .keep or .gitkeep. Be careful which directory is specified in your # config file when using +FileStore+ because everything in that directory will be deleted. - def clear + def clear(options = nil) root_dirs = exclude_from(cache_path, EXCLUDED_DIRS + GITKEEP_FILES) FileUtils.rm_r(root_dirs.collect { |f| File.join(cache_path, f) }) rescue Errno::ENOENT diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index e09cee3335..06fa9f67ad 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -97,12 +97,18 @@ module ActiveSupport options = merged_options(options) keys_to_names = Hash[names.map { |name| [normalize_key(name, options), name] }] + raw_values = @data.get_multi(keys_to_names.keys) values = {} + raw_values.each do |key, value| entry = deserialize_entry(value) - values[keys_to_names[key]] = entry.value unless entry.expired? + + unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options)) + values[keys_to_names[key]] = entry.value + end end + values end diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index 672eb2bb80..69b3a93a05 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -44,7 +44,7 @@ module ActiveSupport yield end - def clear + def clear(options = nil) @data.clear end @@ -79,15 +79,15 @@ module ActiveSupport local_cache_key) end - def clear # :nodoc: + def clear(options = nil) # :nodoc: return super unless cache = local_cache - cache.clear + cache.clear(options) super end def cleanup(options = nil) # :nodoc: return super unless cache = local_cache - cache.clear(options) + cache.clear super end @@ -115,7 +115,12 @@ module ActiveSupport end def write_entry(key, entry, options) - local_cache.write_entry(key, entry, options) if local_cache + if options[:unless_exist] + local_cache.delete_entry(key, options) if local_cache + else + local_cache.write_entry(key, entry, options) if local_cache + end + super end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index d771cab68b..df18c35199 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -62,8 +62,7 @@ module ActiveSupport included do extend ActiveSupport::DescendantsTracker - class_attribute :__callbacks, instance_writer: false - self.__callbacks ||= {} + class_attribute :__callbacks, instance_writer: false, default: {} end CALLBACK_FILTER_TYPES = [:before, :after, :around] @@ -597,7 +596,7 @@ module ActiveSupport Proc.new do |target, result_lambda| terminate = true catch(:abort) do - result_lambda.call if result_lambda.is_a?(Proc) + result_lambda.call terminate = false end terminate @@ -663,8 +662,10 @@ module ActiveSupport if options[:if].is_a?(String) || options[:unless].is_a?(String) ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing string to :if and :unless conditional options is deprecated - and will be removed in Rails 5.2 without replacement. + Passing string to be evaluated in :if and :unless conditional + options is deprecated and will be removed in Rails 5.2 without + replacement. Pass a symbol for an instance method, or a lambda, + proc or block, instead. MSG end diff --git a/activesupport/lib/active_support/core_ext.rb b/activesupport/lib/active_support/core_ext.rb index f397f658f3..42e0acf66a 100644 --- a/activesupport/lib/active_support/core_ext.rb +++ b/activesupport/lib/active_support/core_ext.rb @@ -1,3 +1,3 @@ -(Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"]).each do |path| +Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).each do |path| require path end diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index ba422f9071..8caddcd5c3 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -68,11 +68,16 @@ class Class # object.setting = false # => NoMethodError # # To opt out of both instance methods, pass <tt>instance_accessor: false</tt>. + # + # To set a default value for the attribute, pass <tt>default:</tt>, like so: + # + # class_attribute :settings, default: {} def class_attribute(*attrs) options = attrs.extract_options! - instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true) - instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true) + instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true) + instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true) instance_predicate = options.fetch(:instance_predicate, true) + default_value = options.fetch(:default, nil) attrs.each do |name| remove_possible_singleton_method(name) @@ -123,6 +128,10 @@ class Class remove_possible_method "#{name}=" attr_writer name end + + unless default_value.nil? + self.send("#{name}=", default_value) + end end end end diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb index f2ba7fdda5..e2e1d3e359 100644 --- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb @@ -320,6 +320,22 @@ module DateAndTime beginning_of_year..end_of_year end + # Returns specific next occurring day of week + def next_occurring(day_of_week) + current_day_number = wday != 0 ? wday - 1 : 6 + from_now = DAYS_INTO_WEEK.fetch(day_of_week) - current_day_number + from_now += 7 unless from_now > 0 + since(from_now.days) + end + + # Returns specific previous occurring day of week + def prev_occurring(day_of_week) + current_day_number = wday != 0 ? wday - 1 : 6 + ago = current_day_number - DAYS_INTO_WEEK.fetch(day_of_week) + ago += 7 unless ago > 0 + ago(ago.days) + end + private def first_hour(date_or_time) date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time diff --git a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb index ab80392460..2d45e16546 100644 --- a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb +++ b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb @@ -9,6 +9,6 @@ module DateAndTime # of the receiver. For backwards compatibility we're overriding # this behavior, but new apps will have an initializer that sets # this to true, because the new behavior is preferred. - mattr_accessor(:preserve_timezone, instance_writer: false) { false } + mattr_accessor :preserve_timezone, instance_writer: false, default: false end end diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index 2c24081eb9..9244cfa157 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -38,13 +38,10 @@ class Module # # Person.new.hair_colors # => NoMethodError # - # - # Also, you can pass a block to set up the attribute with a default value. + # You can set a default value for the attribute. # # module HairColors - # mattr_reader :hair_colors do - # [:brown, :black, :blonde, :red] - # end + # mattr_reader :hair_colors, default: [:brown, :black, :blonde, :red] # end # # class Person @@ -52,8 +49,7 @@ class Module # end # # Person.new.hair_colors # => [:brown, :black, :blonde, :red] - def mattr_reader(*syms) - options = syms.extract_options! + def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil) syms.each do |sym| raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym) class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@ -64,14 +60,16 @@ class Module end EOS - unless options[:instance_reader] == false || options[:instance_accessor] == false + if instance_reader && instance_accessor class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym} @@#{sym} end EOS end - class_variable_set("@@#{sym}", yield) if block_given? + + sym_default_value = (block_given? && default.nil?) ? yield : default + class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil? end end alias :cattr_reader :mattr_reader @@ -107,12 +105,10 @@ class Module # # Person.new.hair_colors = [:blonde, :red] # => NoMethodError # - # Also, you can pass a block to set up the attribute with a default value. + # You can set a default value for the attribute. # # module HairColors - # mattr_writer :hair_colors do - # [:brown, :black, :blonde, :red] - # end + # mattr_writer :hair_colors, default: [:brown, :black, :blonde, :red] # end # # class Person @@ -120,8 +116,7 @@ class Module # end # # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] - def mattr_writer(*syms) - options = syms.extract_options! + def mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil) syms.each do |sym| raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym) class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@ -132,14 +127,16 @@ class Module end EOS - unless options[:instance_writer] == false || options[:instance_accessor] == false + if instance_writer && instance_accessor class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym}=(obj) @@#{sym} = obj end EOS end - send("#{sym}=", yield) if block_given? + + sym_default_value = (block_given? && default.nil?) ? yield : default + send("#{sym}=", sym_default_value) unless sym_default_value.nil? end end alias :cattr_writer :mattr_writer @@ -197,12 +194,10 @@ class Module # Person.new.hair_colors = [:brown] # => NoMethodError # Person.new.hair_colors # => NoMethodError # - # Also you can pass a block to set up the attribute with a default value. + # You can set a default value for the attribute. # # module HairColors - # mattr_accessor :hair_colors do - # [:brown, :black, :blonde, :red] - # end + # mattr_accessor :hair_colors, default: [:brown, :black, :blonde, :red] # end # # class Person @@ -210,9 +205,9 @@ class Module # end # # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] - def mattr_accessor(*syms, &blk) - mattr_reader(*syms, &blk) - mattr_writer(*syms) + def mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil, &blk) + mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default, &blk) + mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor, default: default) end alias :cattr_accessor :mattr_accessor end diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 85ab739095..13f3894e6c 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -219,48 +219,43 @@ class Module # When building decorators, a common pattern may emerge: # # class Partition - # def initialize(first_event) - # @events = [ first_event ] + # def initialize(event) + # @event = event # end # - # def people - # if @events.first.detail.people.any? - # @events.collect { |e| Array(e.detail.people) }.flatten.uniq - # else - # @events.collect(&:creator).uniq - # end + # def person + # @event.detail.person || @event.creator # end # # private # def respond_to_missing?(name, include_private = false) - # @events.respond_to?(name, include_private) + # @event.respond_to?(name, include_private) # end # # def method_missing(method, *args, &block) - # @events.send(method, *args, &block) + # @event.send(method, *args, &block) # end # end # - # With `Module#delegate_missing_to`, the above is condensed to: + # With <tt>Module#delegate_missing_to</tt>, the above is condensed to: # # class Partition - # delegate_missing_to :@events + # delegate_missing_to :@event # - # def initialize(first_event) - # @events = [ first_event ] + # def initialize(event) + # @event = event # end # - # def people - # if @events.first.detail.people.any? - # @events.collect { |e| Array(e.detail.people) }.flatten.uniq - # else - # @events.collect(&:creator).uniq - # end + # def person + # @event.detail.person || @event.creator # end # end # - # The target can be anything callable within the object. E.g. instance - # variables, methods, constants and the likes. + # The target can be anything callable within the object, e.g. instance + # variables, methods, constants, etc. + # + # The delegated method must be public on the target, otherwise it will + # raise +NoMethodError+. def delegate_missing_to(target) target = target.to_s target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target) diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb new file mode 100644 index 0000000000..872b0663c7 --- /dev/null +++ b/activesupport/lib/active_support/current_attributes.rb @@ -0,0 +1,193 @@ +module ActiveSupport + # Abstract super class that provides a thread-isolated attributes singleton, which resets automatically + # before and after each request. This allows you to keep all the per-request attributes easily + # available to the whole system. + # + # The following full app-like example demonstrates how to use a Current class to + # facilitate easy access to the global, per-request attributes without passing them deeply + # around everywhere: + # + # # app/models/current.rb + # class Current < ActiveSupport::CurrentAttributes + # attribute :account, :user + # attribute :request_id, :user_agent, :ip_address + # + # resets { Time.zone = nil } + # + # def user=(user) + # super + # self.account = user.account + # Time.zone = user.time_zone + # end + # end + # + # # app/controllers/concerns/authentication.rb + # module Authentication + # extend ActiveSupport::Concern + # + # included do + # before_action :authenticate + # end + # + # private + # def authenticate + # if authenticated_user = User.find_by(id: cookies.signed[:user_id]) + # Current.user = authenticated_user + # else + # redirect_to new_session_url + # end + # end + # end + # + # # app/controllers/concerns/set_current_request_details.rb + # module SetCurrentRequestDetails + # extend ActiveSupport::Concern + # + # included do + # before_action do + # Current.request_id = request.uuid + # Current.user_agent = request.user_agent + # Current.ip_address = request.ip + # end + # end + # end + # + # class ApplicationController < ActionController::Base + # include Authentication + # include SetCurrentRequestDetails + # end + # + # class MessagesController < ApplicationController + # def create + # Current.account.messages.create(message_params) + # end + # end + # + # class Message < ApplicationRecord + # belongs_to :creator, default: -> { Current.user } + # after_create { |message| Event.create(record: message) } + # end + # + # class Event < ApplicationRecord + # before_create do + # self.request_id = Current.request_id + # self.user_agent = Current.user_agent + # self.ip_address = Current.ip_address + # end + # end + # + # A word of caution: It's easy to overdo a global singleton like Current and tangle your model as a result. + # Current should only be used for a few, top-level globals, like account, user, and request details. + # The attributes stuck in Current should be used by more or less all actions on all requests. If you start + # sticking controller-specific attributes in there, you're going to create a mess. + class CurrentAttributes + include ActiveSupport::Callbacks + define_callbacks :reset + + class << self + # Returns singleton instance for this class in this thread. If none exists, one is created. + def instance + current_instances[name] ||= new + end + + # Declares one or more attributes that will be given both class and instance accessor methods. + def attribute(*names) + generated_attribute_methods.module_eval do + names.each do |name| + define_method(name) do + attributes[name.to_sym] + end + + define_method("#{name}=") do |attribute| + attributes[name.to_sym] = attribute + end + end + end + + names.each do |name| + define_singleton_method(name) do + instance.public_send(name) + end + + define_singleton_method("#{name}=") do |attribute| + instance.public_send("#{name}=", attribute) + end + end + end + + # Calls this block after #reset is called on the instance. Used for resetting external collaborators, like Time.zone. + def resets(&block) + set_callback :reset, :after, &block + end + + delegate :set, :reset, to: :instance + + def reset_all # :nodoc: + current_instances.each_value(&:reset) + end + + def clear_all # :nodoc: + reset_all + current_instances.clear + end + + private + def generated_attribute_methods + @generated_attribute_methods ||= Module.new.tap { |mod| include mod } + end + + def current_instances + Thread.current[:current_attributes_instances] ||= {} + end + + def method_missing(name, *args, &block) + # Caches the method definition as a singleton method of the receiver. + # + # By letting #delegate handle it, we avoid an enclosure that'll capture args. + singleton_class.delegate name, to: :instance + + send(name, *args, &block) + end + end + + attr_accessor :attributes + + def initialize + @attributes = {} + end + + # Expose one or more attributes within a block. Old values are returned after the block concludes. + # Example demonstrating the common use of needing to set Current attributes outside the request-cycle: + # + # class Chat::PublicationJob < ApplicationJob + # def perform(attributes, room_number, creator) + # Current.set(person: creator) do + # Chat::Publisher.publish(attributes: attributes, room_number: room_number) + # end + # end + # end + def set(set_attributes) + old_attributes = compute_attributes(set_attributes.keys) + assign_attributes(set_attributes) + yield + ensure + assign_attributes(old_attributes) + end + + # Reset all attributes. Should be called before and after actions, when used as a per-request singleton. + def reset + run_callbacks :reset do + self.attributes = {} + end + end + + private + def assign_attributes(new_attributes) + new_attributes.each { |key, value| public_send("#{key}=", value) } + end + + def compute_attributes(keys) + keys.collect { |key| [ key, public_send(key) ] }.to_h + end + end +end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index e125b657f2..3cd8f3d0ac 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -18,8 +18,7 @@ module ActiveSupport #:nodoc: module Dependencies #:nodoc: extend self - mattr_accessor :interlock - self.interlock = Interlock.new + mattr_accessor :interlock, default: Interlock.new # :doc: @@ -46,46 +45,37 @@ module ActiveSupport #:nodoc: # :nodoc: # Should we turn on Ruby warnings on the first load of dependent files? - mattr_accessor :warnings_on_first_load - self.warnings_on_first_load = false + mattr_accessor :warnings_on_first_load, default: false # All files ever loaded. - mattr_accessor :history - self.history = Set.new + mattr_accessor :history, default: Set.new # All files currently loaded. - mattr_accessor :loaded - self.loaded = Set.new + mattr_accessor :loaded, default: Set.new # Stack of files being loaded. - mattr_accessor :loading - self.loading = [] + mattr_accessor :loading, default: [] # Should we load files or require them? - mattr_accessor :mechanism - self.mechanism = ENV["NO_RELOAD"] ? :require : :load + mattr_accessor :mechanism, default: ENV["NO_RELOAD"] ? :require : :load # The set of directories from which we may automatically load files. Files # under these directories will be reloaded on each request in development mode, # unless the directory also appears in autoload_once_paths. - mattr_accessor :autoload_paths - self.autoload_paths = [] + mattr_accessor :autoload_paths, default: [] # The set of directories from which automatically loaded constants are loaded # only once. All directories in this set must also be present in +autoload_paths+. - mattr_accessor :autoload_once_paths - self.autoload_once_paths = [] + mattr_accessor :autoload_once_paths, default: [] # An array of qualified constant names that have been loaded. Adding a name # to this array will cause it to be unloaded the next time Dependencies are # cleared. - mattr_accessor :autoloaded_constants - self.autoloaded_constants = [] + mattr_accessor :autoloaded_constants, default: [] # An array of constant names that need to be unloaded on every request. Used # to allow arbitrary constants to be marked for unloading. - mattr_accessor :explicitly_unloadable_constants - self.explicitly_unloadable_constants = [] + mattr_accessor :explicitly_unloadable_constants, default: [] # The WatchStack keeps a stack of the modules being watched as files are # loaded. If a file in the process of being loaded (parent.rb) triggers the @@ -175,8 +165,7 @@ module ActiveSupport #:nodoc: end # An internal stack used to record which constants are loaded by any block. - mattr_accessor :constant_watch_stack - self.constant_watch_stack = WatchStack.new + mattr_accessor :constant_watch_stack, default: WatchStack.new # Module includes this module. module ModuleConstMissing #:nodoc: diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index 851d8eeda1..140bdccbb3 100644 --- a/activesupport/lib/active_support/deprecation/reporting.rb +++ b/activesupport/lib/active_support/deprecation/reporting.rb @@ -102,7 +102,7 @@ module ActiveSupport end end - RAILS_GEM_ROOT = File.expand_path("../../../../..", __FILE__) + "/" + RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) def ignored_callstack(path) path.start_with?(RAILS_GEM_ROOT) || path.start_with?(RbConfig::CONFIG["rubylibdir"]) diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index d4424ed792..39deb2313f 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -37,27 +37,56 @@ module ActiveSupport end def +(other) - calculate(:+, other) + if Duration === other + seconds = value + other.parts[:seconds] + new_parts = other.parts.merge(seconds: seconds) + new_value = value + other.value + + Duration.new(new_value, new_parts) + else + calculate(:+, other) + end end def -(other) - calculate(:-, other) + if Duration === other + seconds = value - other.parts[:seconds] + new_parts = other.parts.map { |part, other_value| [part, -other_value] }.to_h + new_parts = new_parts.merge(seconds: seconds) + new_value = value - other.value + + Duration.new(new_value, new_parts) + else + calculate(:-, other) + end end def *(other) - calculate(:*, other) + if Duration === other + new_parts = other.parts.map { |part, other_value| [part, value * other_value] }.to_h + new_value = value * other.value + + Duration.new(new_value, new_parts) + else + calculate(:*, other) + end end def /(other) - calculate(:/, other) + if Duration === other + new_parts = other.parts.map { |part, other_value| [part, value / other_value] }.to_h + new_value = new_parts.inject(0) { |total, (part, value)| total + value * Duration::PARTS_IN_SECONDS[part] } + + Duration.new(new_value, new_parts) + else + calculate(:/, other) + end end private def calculate(op, other) if Scalar === other Scalar.new(value.public_send(op, other.value)) - elsif Duration === other - Duration.seconds(value).public_send(op, other) elsif Numeric === other Scalar.new(value.public_send(op, other)) else diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb index f0408f429c..1a1f1a1257 100644 --- a/activesupport/lib/active_support/i18n.rb +++ b/activesupport/lib/active_support/i18n.rb @@ -10,4 +10,4 @@ end require "active_support/lazy_load_hooks" ActiveSupport.run_load_hooks(:i18n) -I18n.load_path << "#{File.dirname(__FILE__)}/locale/en.yml" +I18n.load_path << File.expand_path("locale/en.yml", __dir__) diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index b749913ee9..51fe6f3418 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -42,7 +42,7 @@ module I18n case setting when :railties_load_path reloadable_paths = value - app.config.i18n.load_path.unshift(*value.map(&:existent).flatten) + app.config.i18n.load_path.unshift(*value.flat_map(&:existent)) when :load_path I18n.load_path += value else @@ -58,7 +58,7 @@ module I18n directories = watched_dirs_with_extensions(reloadable_paths) reloader = app.config.file_watcher.new(I18n.load_path.dup, directories) do I18n.load_path.keep_if { |p| File.exist?(p) } - I18n.load_path |= reloadable_paths.map(&:existent).flatten + I18n.load_path |= reloadable_paths.flat_map(&:existent) I18n.reload! end @@ -66,10 +66,6 @@ module I18n app.reloaders << reloader app.reloader.to_run do reloader.execute_if_updated { require_unload_lock! } - # TODO: remove the following line as soon as the return value of - # callbacks is ignored, that is, returning `false` does not - # display a deprecation warning or halts the callback chain. - true end reloader.execute diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 1b089a7538..ff1a0cb8c7 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -28,7 +28,7 @@ module ActiveSupport # pluralize('CamelOctopus') # => "CamelOctopi" # pluralize('ley', :es) # => "leyes" def pluralize(word, locale = :en) - apply_inflections(word, inflections(locale).plurals) + apply_inflections(word, inflections(locale).plurals, locale) end # The reverse of #pluralize, returns the singular form of a word in a @@ -45,7 +45,7 @@ module ActiveSupport # singularize('CamelOctopi') # => "CamelOctopus" # singularize('leyes', :es) # => "ley" def singularize(word, locale = :en) - apply_inflections(word, inflections(locale).singulars) + apply_inflections(word, inflections(locale).singulars, locale) end # Converts strings to UpperCamelCase. @@ -387,12 +387,15 @@ module ActiveSupport # Applies inflection rules for +singularize+ and +pluralize+. # - # apply_inflections('post', inflections.plurals) # => "posts" - # apply_inflections('posts', inflections.singulars) # => "post" - def apply_inflections(word, rules) + # If passed an optional +locale+ parameter, the uncountables will be + # found for that locale. + # + # apply_inflections('post', inflections.plurals, :en) # => "posts" + # apply_inflections('posts', inflections.singulars, :en) # => "post" + def apply_inflections(word, rules, locale = :en) result = word.to_s.dup - if word.empty? || inflections.uncountables.uncountable?(result) + if word.empty? || inflections(locale).uncountables.uncountable?(result) result else rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) } diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index e2c4f33565..a05758d6aa 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -49,8 +49,7 @@ module ActiveSupport CYAN = "\e[36m" WHITE = "\e[37m" - mattr_accessor :colorize_logging - self.colorize_logging = true + mattr_accessor :colorize_logging, default: true class << self def logger @@ -81,8 +80,10 @@ module ActiveSupport def finish(name, id, payload) super if logger - rescue Exception => e - logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}" + rescue => e + if logger + logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}" + end end private diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb index 632994cf50..9c64afaaca 100644 --- a/activesupport/lib/active_support/logger_silence.rb +++ b/activesupport/lib/active_support/logger_silence.rb @@ -6,8 +6,7 @@ module LoggerSilence extend ActiveSupport::Concern included do - cattr_accessor :silencer - self.silencer = true + cattr_accessor :silencer, default: true end # Silences the logger for the duration of the block. diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 24053b4fe5..e576766c64 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -19,7 +19,17 @@ module ActiveSupport # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..." # crypt.decrypt_and_verify(encrypted_data) # => "my secret data" class MessageEncryptor - DEFAULT_CIPHER = "aes-256-cbc" + class << self + attr_accessor :use_authenticated_message_encryption #:nodoc: + + def default_cipher #:nodoc: + if use_authenticated_message_encryption + "aes-256-gcm" + else + "aes-256-cbc" + end + end + end module NullSerializer #:nodoc: def self.load(value) @@ -45,7 +55,7 @@ module ActiveSupport OpenSSLCipherError = OpenSSL::Cipher::CipherError # Initialize a new MessageEncryptor. +secret+ must be at least as long as - # the cipher key size. For the default 'aes-256-cbc' cipher, this is 256 + # the cipher key size. For the default 'aes-256-gcm' cipher, this is 256 # bits. If you are using a user-entered secret, you can generate a suitable # key by using <tt>ActiveSupport::KeyGenerator</tt> or a similar key # derivation function. @@ -57,7 +67,7 @@ module ActiveSupport # # Options: # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by - # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'. + # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-gcm'. # * <tt>:digest</tt> - String of digest to use for signing. Default is # +SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'. # * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+. @@ -66,7 +76,7 @@ module ActiveSupport sign_secret = signature_key_or_options.first @secret = secret @sign_secret = sign_secret - @cipher = options[:cipher] || DEFAULT_CIPHER + @cipher = options[:cipher] || self.class.default_cipher @digest = options[:digest] || "SHA1" unless aead_mode? @verifier = resolve_verifier @serializer = options[:serializer] || Marshal @@ -85,7 +95,7 @@ module ActiveSupport end # Given a cipher, returns the key length of the cipher to help generate the key of desired size - def self.key_len(cipher = DEFAULT_CIPHER) + def self.key_len(cipher = default_cipher) OpenSSL::Cipher.new(cipher).key_len end @@ -115,7 +125,7 @@ module ActiveSupport # Currently the OpenSSL bindings do not raise an error if auth_tag is # truncated, which would allow an attacker to easily forge it. See # https://github.com/ruby/openssl/issues/63 - raise InvalidMessage if aead_mode? && auth_tag.bytes.length != 16 + raise InvalidMessage if aead_mode? && (auth_tag.nil? || auth_tag.bytes.length != 16) cipher.decrypt cipher.key = @secret diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 0912912aba..8223e45e5a 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -357,7 +357,7 @@ module ActiveSupport # Returns the directory in which the data files are stored. def self.dirname - File.dirname(__FILE__) + "/../values/" + File.expand_path("../values", __dir__) end # Returns the filename for the data file for this version. diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 880340ca86..9cb2821cb6 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -4,6 +4,7 @@ module ActiveSupport eager_autoload do autoload :NumberConverter + autoload :RoundingHelper autoload :NumberToRoundedConverter autoload :NumberToDelimitedConverter autoload :NumberToHumanConverter diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb index 56185ddf4b..040343b5dd 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb @@ -9,6 +9,7 @@ module ActiveSupport self.validate_float = true def convert # :nodoc: + @number = RoundingHelper.new(options).round(number) @number = Float(number) # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files @@ -20,10 +21,7 @@ module ActiveSupport exponent = calculate_exponent(units) @number = number / (10**exponent) - until (rounded_number = NumberToRoundedConverter.convert(number, options)) != NumberToRoundedConverter.convert(1000, options) - @number = number / 1000.0 - exponent += 3 - end + rounded_number = NumberToRoundedConverter.convert(number, options) unit = determine_unit(units, exponent) format.gsub("%n".freeze, rounded_number).gsub("%u".freeze, unit).strip end diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb index 1f013990ea..c32d85a45f 100644 --- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb @@ -5,26 +5,14 @@ module ActiveSupport self.validate_float = true def convert - precision = options.delete :precision + helper = RoundingHelper.new(options) + rounded_number = helper.round(number) - if precision - case number - when Float, String - @number = BigDecimal(number.to_s) - when Rational - @number = BigDecimal(number, digit_count(number.to_i) + precision) - else - @number = number.to_d - end - - if options.delete(:significant) && precision > 0 - digits, rounded_number = digits_and_rounded_number(precision) + if precision = options[:precision] + if options[:significant] && precision > 0 + digits = helper.digit_count(rounded_number) precision -= digits precision = 0 if precision < 0 # don't let it be negative - else - rounded_number = number.round(precision) - rounded_number = rounded_number.to_i if precision == 0 && rounded_number.finite? - rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros end formatted_string = @@ -38,7 +26,7 @@ module ActiveSupport "%00.#{precision}f" % rounded_number end else - formatted_string = number + formatted_string = rounded_number end delimited_number = NumberToDelimitedConverter.convert(formatted_string, options) @@ -79,14 +67,6 @@ module ActiveSupport number end end - - def absolute_number(number) - number.respond_to?(:abs) ? number.abs : number.to_d.abs - end - - def zero? - number.respond_to?(:zero?) ? number.zero? : number.to_d.zero? - end end end end diff --git a/activesupport/lib/active_support/number_helper/rounding_helper.rb b/activesupport/lib/active_support/number_helper/rounding_helper.rb new file mode 100644 index 0000000000..63b48444a6 --- /dev/null +++ b/activesupport/lib/active_support/number_helper/rounding_helper.rb @@ -0,0 +1,64 @@ +module ActiveSupport + module NumberHelper + class RoundingHelper # :nodoc: + attr_reader :options + + def initialize(options) + @options = options + end + + def round(number) + return number unless precision + number = convert_to_decimal(number) + if significant && precision > 0 + round_significant(number) + else + round_without_significant(number) + end + end + + def digit_count(number) + return 1 if number.zero? + (Math.log10(absolute_number(number)) + 1).floor + end + + private + def round_without_significant(number) + number = number.round(precision) + number = number.to_i if precision == 0 && number.finite? + number = number.abs if number.zero? # prevent showing negative zeros + number + end + + def round_significant(number) + return 0 if number.zero? + digits = digit_count(number) + multiplier = 10**(digits - precision) + (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier + end + + def convert_to_decimal(number) + case number + when Float, String + BigDecimal(number.to_s) + when Rational + BigDecimal(number, digit_count(number.to_i) + precision) + else + number.to_d + end + end + + def precision + options[:precision] + end + + def significant + options[:significant] + end + + def absolute_number(number) + number.respond_to?(:abs) ? number.abs : number.to_d.abs + end + end + end +end diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index b875875afe..45bc51311b 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -7,6 +7,19 @@ module ActiveSupport config.eager_load_namespaces << ActiveSupport + initializer "active_support.set_authenticated_message_encryption" do |app| + if app.config.active_support.respond_to?(:use_authenticated_message_encryption) + ActiveSupport::MessageEncryptor.use_authenticated_message_encryption = + app.config.active_support.use_authenticated_message_encryption + end + end + + initializer "active_support.reset_all_current_attributes_instances" do |app| + app.reloader.before_class_unload { ActiveSupport::CurrentAttributes.clear_all } + app.executor.to_run { ActiveSupport::CurrentAttributes.reset_all } + app.executor.to_complete { ActiveSupport::CurrentAttributes.reset_all } + end + initializer "active_support.deprecation_behavior" do |app| if deprecation = app.config.active_support.deprecation ActiveSupport::Deprecation.behavior = deprecation @@ -22,14 +35,7 @@ module ActiveSupport raise e.exception "tzinfo-data is not present. Please add gem 'tzinfo-data' to your Gemfile and run bundle install" end require "active_support/core_ext/time/zones" - zone_default = Time.find_zone!(app.config.time_zone) - - unless zone_default - raise "Value assigned to config.time_zone not recognized. " \ - 'Run "rake time:zones:all" for a time zone names list.' - end - - Time.zone_default = zone_default + Time.zone_default = Time.find_zone!(app.config.time_zone) end # Sets the default week start diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb index 121c621751..9558146201 100644 --- a/activesupport/lib/active_support/reloader.rb +++ b/activesupport/lib/active_support/reloader.rb @@ -69,11 +69,8 @@ module ActiveSupport end end - class_attribute :executor - class_attribute :check - - self.executor = Executor - self.check = lambda { false } + class_attribute :executor, default: Executor + class_attribute :check, default: lambda { false } def self.check! # :nodoc: @should_reload ||= check.call diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb index ee6592fb5a..826832ba7d 100644 --- a/activesupport/lib/active_support/rescuable.rb +++ b/activesupport/lib/active_support/rescuable.rb @@ -8,8 +8,7 @@ module ActiveSupport extend Concern included do - class_attribute :rescue_handlers - self.rescue_handlers = [] + class_attribute :rescue_handlers, default: [] end module ClassMethods @@ -84,12 +83,18 @@ module ActiveSupport # end # # Returns the exception if it was handled and +nil+ if it was not. - def rescue_with_handler(exception, object: self) + def rescue_with_handler(exception, object: self, visited_exceptions: []) + visited_exceptions << exception + if handler = handler_for_rescue(exception, object: object) handler.call exception exception elsif exception - rescue_with_handler(exception.cause, object: object) + if visited_exceptions.include?(exception.cause) + nil + else + rescue_with_handler(exception.cause, object: object, visited_exceptions: visited_exceptions) + end end end diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 28cf2953bf..28e1df8870 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -167,7 +167,7 @@ module ActiveSupport retval end - # Assertion that the result of evaluating an expression is changed before + # Assertion that the result of evaluating an expression is not changed before # and after invoking the passed in block. # # assert_no_changes 'Status.all_good?' do diff --git a/activesupport/test/cache/behaviors.rb b/activesupport/test/cache/behaviors.rb new file mode 100644 index 0000000000..efd045ac5e --- /dev/null +++ b/activesupport/test/cache/behaviors.rb @@ -0,0 +1,7 @@ +require_relative "behaviors/autoloading_cache_behavior" +require_relative "behaviors/cache_delete_matched_behavior" +require_relative "behaviors/cache_increment_decrement_behavior" +require_relative "behaviors/cache_store_behavior" +require_relative "behaviors/cache_store_version_behavior" +require_relative "behaviors/encoded_key_cache_behavior" +require_relative "behaviors/local_cache_behavior" diff --git a/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb b/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb new file mode 100644 index 0000000000..5f8af331f6 --- /dev/null +++ b/activesupport/test/cache/behaviors/autoloading_cache_behavior.rb @@ -0,0 +1,41 @@ +require "dependencies_test_helpers" + +module AutoloadingCacheBehavior + include DependenciesTestHelpers + + def test_simple_autoloading + with_autoloading_fixtures do + @cache.write("foo", EM.new) + end + + remove_constants(:EM) + ActiveSupport::Dependencies.clear + + with_autoloading_fixtures do + assert_kind_of EM, @cache.read("foo") + end + + remove_constants(:EM) + ActiveSupport::Dependencies.clear + end + + def test_two_classes_autoloading + with_autoloading_fixtures do + @cache.write("foo", [EM.new, ClassFolder.new]) + end + + remove_constants(:EM, :ClassFolder) + ActiveSupport::Dependencies.clear + + with_autoloading_fixtures do + loaded = @cache.read("foo") + assert_kind_of Array, loaded + assert_equal 2, loaded.size + assert_kind_of EM, loaded[0] + assert_kind_of ClassFolder, loaded[1] + end + + remove_constants(:EM, :ClassFolder) + ActiveSupport::Dependencies.clear + end +end diff --git a/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb new file mode 100644 index 0000000000..b872eb0279 --- /dev/null +++ b/activesupport/test/cache/behaviors/cache_delete_matched_behavior.rb @@ -0,0 +1,13 @@ +module CacheDeleteMatchedBehavior + def test_delete_matched + @cache.write("foo", "bar") + @cache.write("fu", "baz") + @cache.write("foo/bar", "baz") + @cache.write("fu/baz", "bar") + @cache.delete_matched(/oo/) + assert !@cache.exist?("foo") + assert @cache.exist?("fu") + assert !@cache.exist?("foo/bar") + assert @cache.exist?("fu/baz") + end +end diff --git a/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb b/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb new file mode 100644 index 0000000000..0d32339565 --- /dev/null +++ b/activesupport/test/cache/behaviors/cache_increment_decrement_behavior.rb @@ -0,0 +1,21 @@ +module CacheIncrementDecrementBehavior + def test_increment + @cache.write("foo", 1, raw: true) + assert_equal 1, @cache.read("foo").to_i + assert_equal 2, @cache.increment("foo") + assert_equal 2, @cache.read("foo").to_i + assert_equal 3, @cache.increment("foo") + assert_equal 3, @cache.read("foo").to_i + assert_nil @cache.increment("bar") + end + + def test_decrement + @cache.write("foo", 3, raw: true) + assert_equal 3, @cache.read("foo").to_i + assert_equal 2, @cache.decrement("foo") + assert_equal 2, @cache.read("foo").to_i + assert_equal 1, @cache.decrement("foo") + assert_equal 1, @cache.read("foo").to_i + assert_nil @cache.decrement("bar") + end +end diff --git a/activesupport/test/cache/behaviors/cache_store_behavior.rb b/activesupport/test/cache/behaviors/cache_store_behavior.rb new file mode 100644 index 0000000000..03c366e164 --- /dev/null +++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb @@ -0,0 +1,329 @@ +# Tests the base functionality that should be identical across all cache stores. +module CacheStoreBehavior + def test_should_read_and_write_strings + assert @cache.write("foo", "bar") + assert_equal "bar", @cache.read("foo") + end + + def test_should_overwrite + @cache.write("foo", "bar") + @cache.write("foo", "baz") + assert_equal "baz", @cache.read("foo") + end + + def test_fetch_without_cache_miss + @cache.write("foo", "bar") + assert_not_called(@cache, :write) do + assert_equal "bar", @cache.fetch("foo") { "baz" } + end + end + + def test_fetch_with_cache_miss + assert_called_with(@cache, :write, ["foo", "baz", @cache.options]) do + assert_equal "baz", @cache.fetch("foo") { "baz" } + end + end + + def test_fetch_with_cache_miss_passes_key_to_block + cache_miss = false + assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } + assert cache_miss + + cache_miss = false + assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } + assert !cache_miss + end + + def test_fetch_with_forced_cache_miss + @cache.write("foo", "bar") + assert_not_called(@cache, :read) do + assert_called_with(@cache, :write, ["foo", "bar", @cache.options.merge(force: true)]) do + @cache.fetch("foo", force: true) { "bar" } + end + end + end + + def test_fetch_with_cached_nil + @cache.write("foo", nil) + assert_not_called(@cache, :write) do + assert_nil @cache.fetch("foo") { "baz" } + end + end + + def test_fetch_with_forced_cache_miss_with_block + @cache.write("foo", "bar") + assert_equal "foo_bar", @cache.fetch("foo", force: true) { "foo_bar" } + end + + def test_fetch_with_forced_cache_miss_without_block + @cache.write("foo", "bar") + assert_raises(ArgumentError) do + @cache.fetch("foo", force: true) + end + + assert_equal "bar", @cache.read("foo") + end + + def test_should_read_and_write_hash + assert @cache.write("foo", a: "b") + assert_equal({ a: "b" }, @cache.read("foo")) + end + + def test_should_read_and_write_integer + assert @cache.write("foo", 1) + assert_equal 1, @cache.read("foo") + end + + def test_should_read_and_write_nil + assert @cache.write("foo", nil) + assert_nil @cache.read("foo") + end + + def test_should_read_and_write_false + assert @cache.write("foo", false) + assert_equal false, @cache.read("foo") + end + + def test_read_multi + @cache.write("foo", "bar") + @cache.write("fu", "baz") + @cache.write("fud", "biz") + assert_equal({ "foo" => "bar", "fu" => "baz" }, @cache.read_multi("foo", "fu")) + end + + def test_read_multi_with_expires + time = Time.now + @cache.write("foo", "bar", expires_in: 10) + @cache.write("fu", "baz") + @cache.write("fud", "biz") + Time.stub(:now, time + 11) do + assert_equal({ "fu" => "baz" }, @cache.read_multi("foo", "fu")) + end + end + + def test_fetch_multi + @cache.write("foo", "bar") + @cache.write("fud", "biz") + + values = @cache.fetch_multi("foo", "fu", "fud") { |value| value * 2 } + + assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values) + assert_equal("fufu", @cache.read("fu")) + end + + def test_multi_with_objects + cache_struct = Struct.new(:cache_key, :title) + foo = cache_struct.new("foo", "FOO!") + bar = cache_struct.new("bar") + + @cache.write("bar", "BAM!") + + values = @cache.fetch_multi(foo, bar) { |object| object.title } + + assert_equal({ foo => "FOO!", bar => "BAM!" }, values) + end + + def test_fetch_multi_without_block + assert_raises(ArgumentError) do + @cache.fetch_multi("foo") + end + end + + def test_read_and_write_compressed_small_data + @cache.write("foo", "bar", compress: true) + assert_equal "bar", @cache.read("foo") + end + + def test_read_and_write_compressed_large_data + @cache.write("foo", "bar", compress: true, compress_threshold: 2) + assert_equal "bar", @cache.read("foo") + end + + def test_read_and_write_compressed_nil + @cache.write("foo", nil, compress: true) + assert_nil @cache.read("foo") + end + + def test_cache_key + obj = Object.new + def obj.cache_key + :foo + end + @cache.write(obj, "bar") + assert_equal "bar", @cache.read("foo") + end + + def test_param_as_cache_key + obj = Object.new + def obj.to_param + "foo" + end + @cache.write(obj, "bar") + assert_equal "bar", @cache.read("foo") + end + + def test_array_as_cache_key + @cache.write([:fu, "foo"], "bar") + assert_equal "bar", @cache.read("fu/foo") + end + + def test_hash_as_cache_key + @cache.write({ foo: 1, fu: 2 }, "bar") + assert_equal "bar", @cache.read("foo=1/fu=2") + end + + def test_keys_are_case_sensitive + @cache.write("foo", "bar") + assert_nil @cache.read("FOO") + end + + def test_exist + @cache.write("foo", "bar") + assert_equal true, @cache.exist?("foo") + assert_equal false, @cache.exist?("bar") + end + + def test_nil_exist + @cache.write("foo", nil) + assert @cache.exist?("foo") + end + + def test_delete + @cache.write("foo", "bar") + assert @cache.exist?("foo") + assert @cache.delete("foo") + assert !@cache.exist?("foo") + end + + def test_original_store_objects_should_not_be_immutable + bar = "bar" + @cache.write("foo", bar) + assert_nothing_raised { bar.gsub!(/.*/, "baz") } + end + + def test_expires_in + time = Time.local(2008, 4, 24) + + Time.stub(:now, time) do + @cache.write("foo", "bar") + assert_equal "bar", @cache.read("foo") + end + + Time.stub(:now, time + 30) do + assert_equal "bar", @cache.read("foo") + end + + Time.stub(:now, time + 61) do + assert_nil @cache.read("foo") + end + end + + def test_race_condition_protection_skipped_if_not_defined + @cache.write("foo", "bar") + time = @cache.send(:read_entry, @cache.send(:normalize_key, "foo", {}), {}).expires_at + + Time.stub(:now, Time.at(time)) do + result = @cache.fetch("foo") do + assert_nil @cache.read("foo") + "baz" + end + assert_equal "baz", result + end + end + + def test_race_condition_protection_is_limited + time = Time.now + @cache.write("foo", "bar", expires_in: 60) + Time.stub(:now, time + 71) do + result = @cache.fetch("foo", race_condition_ttl: 10) do + assert_nil @cache.read("foo") + "baz" + end + assert_equal "baz", result + end + end + + def test_race_condition_protection_is_safe + time = Time.now + @cache.write("foo", "bar", expires_in: 60) + Time.stub(:now, time + 61) do + begin + @cache.fetch("foo", race_condition_ttl: 10) do + assert_equal "bar", @cache.read("foo") + raise ArgumentError.new + end + rescue ArgumentError + end + assert_equal "bar", @cache.read("foo") + end + Time.stub(:now, time + 91) do + assert_nil @cache.read("foo") + end + end + + def test_race_condition_protection + time = Time.now + @cache.write("foo", "bar", expires_in: 60) + Time.stub(:now, time + 61) do + result = @cache.fetch("foo", race_condition_ttl: 10) do + assert_equal "bar", @cache.read("foo") + "baz" + end + assert_equal "baz", result + end + end + + def test_crazy_key_characters + crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-" + assert @cache.write(crazy_key, "1", raw: true) + assert_equal "1", @cache.read(crazy_key) + assert_equal "1", @cache.fetch(crazy_key) + assert @cache.delete(crazy_key) + assert_equal "2", @cache.fetch(crazy_key, raw: true) { "2" } + assert_equal 3, @cache.increment(crazy_key) + assert_equal 2, @cache.decrement(crazy_key) + end + + def test_really_long_keys + key = "" + 900.times { key << "x" } + assert @cache.write(key, "bar") + assert_equal "bar", @cache.read(key) + assert_equal "bar", @cache.fetch(key) + assert_nil @cache.read("#{key}x") + assert_equal({ key => "bar" }, @cache.read_multi(key)) + assert @cache.delete(key) + end + + def test_cache_hit_instrumentation + key = "test_key" + @events = [] + ActiveSupport::Notifications.subscribe "cache_read.active_support" do |*args| + @events << ActiveSupport::Notifications::Event.new(*args) + end + assert @cache.write(key, "1", raw: true) + assert @cache.fetch(key) {} + assert_equal 1, @events.length + assert_equal "cache_read.active_support", @events[0].name + assert_equal :fetch, @events[0].payload[:super_operation] + assert @events[0].payload[:hit] + ensure + ActiveSupport::Notifications.unsubscribe "cache_read.active_support" + end + + def test_cache_miss_instrumentation + @events = [] + ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args| + @events << ActiveSupport::Notifications::Event.new(*args) + end + assert_not @cache.fetch("bad_key") {} + assert_equal 3, @events.length + assert_equal "cache_read.active_support", @events[0].name + assert_equal "cache_generate.active_support", @events[1].name + assert_equal "cache_write.active_support", @events[2].name + assert_equal :fetch, @events[0].payload[:super_operation] + assert_not @events[0].payload[:hit] + ensure + ActiveSupport::Notifications.unsubscribe "cache_read.active_support" + end +end diff --git a/activesupport/test/cache/behaviors/cache_store_version_behavior.rb b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb new file mode 100644 index 0000000000..a0170c896f --- /dev/null +++ b/activesupport/test/cache/behaviors/cache_store_version_behavior.rb @@ -0,0 +1,86 @@ +module CacheStoreVersionBehavior + ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version) + + def test_fetch_with_right_version_should_hit + @cache.fetch("foo", version: 1) { "bar" } + assert_equal "bar", @cache.read("foo", version: 1) + end + + def test_fetch_with_wrong_version_should_miss + @cache.fetch("foo", version: 1) { "bar" } + assert_nil @cache.read("foo", version: 2) + end + + def test_read_with_right_version_should_hit + @cache.write("foo", "bar", version: 1) + assert_equal "bar", @cache.read("foo", version: 1) + end + + def test_read_with_wrong_version_should_miss + @cache.write("foo", "bar", version: 1) + assert_nil @cache.read("foo", version: 2) + end + + def test_exist_with_right_version_should_be_true + @cache.write("foo", "bar", version: 1) + assert @cache.exist?("foo", version: 1) + end + + def test_exist_with_wrong_version_should_be_false + @cache.write("foo", "bar", version: 1) + assert !@cache.exist?("foo", version: 2) + end + + def test_reading_and_writing_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write(m1v1, "bar") + assert_equal "bar", @cache.read(m1v1) + assert_nil @cache.read(m1v2) + end + + def test_reading_and_writing_with_model_supporting_cache_version_using_nested_key + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write([ "something", m1v1 ], "bar") + assert_equal "bar", @cache.read([ "something", m1v1 ]) + assert_nil @cache.read([ "something", m1v2 ]) + end + + def test_fetching_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.fetch(m1v1) { "bar" } + assert_equal "bar", @cache.fetch(m1v1) { "bu" } + assert_equal "bu", @cache.fetch(m1v2) { "bu" } + end + + def test_exist_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write(m1v1, "bar") + assert @cache.exist?(m1v1) + assert_not @cache.fetch(m1v2) + end + + def test_fetch_multi_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m2v1 = ModelWithKeyAndVersion.new("model/2", 1) + m2v2 = ModelWithKeyAndVersion.new("model/2", 2) + + first_fetch_values = @cache.fetch_multi(m1v1, m2v1) { |m| m.cache_key } + second_fetch_values = @cache.fetch_multi(m1v1, m2v2) { |m| m.cache_key + " 2nd" } + + assert_equal({ m1v1 => "model/1", m2v1 => "model/2" }, first_fetch_values) + assert_equal({ m1v1 => "model/1", m2v2 => "model/2 2nd" }, second_fetch_values) + end + + def test_version_is_normalized + @cache.write("foo", "bar", version: 1) + assert_equal "bar", @cache.read("foo", version: "1") + end +end diff --git a/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb b/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb new file mode 100644 index 0000000000..4d8e2946b2 --- /dev/null +++ b/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb @@ -0,0 +1,34 @@ +# https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters +# The error is caused by character encodings that can't be compared with ASCII-8BIT regular expressions and by special +# characters like the umlaut in UTF-8. +module EncodedKeyCacheBehavior + Encoding.list.each do |encoding| + define_method "test_#{encoding.name.underscore}_encoded_values" do + key = "foo".force_encoding(encoding) + assert @cache.write(key, "1", raw: true) + assert_equal "1", @cache.read(key) + assert_equal "1", @cache.fetch(key) + assert @cache.delete(key) + assert_equal "2", @cache.fetch(key, raw: true) { "2" } + assert_equal 3, @cache.increment(key) + assert_equal 2, @cache.decrement(key) + end + end + + def test_common_utf8_values + key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8) + assert @cache.write(key, "1", raw: true) + assert_equal "1", @cache.read(key) + assert_equal "1", @cache.fetch(key) + assert @cache.delete(key) + assert_equal "2", @cache.fetch(key, raw: true) { "2" } + assert_equal 3, @cache.increment(key) + assert_equal 2, @cache.decrement(key) + end + + def test_retains_encoding + key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8) + assert @cache.write(key, "1", raw: true) + assert_equal Encoding::UTF_8, key.encoding + end +end diff --git a/activesupport/test/cache/behaviors/local_cache_behavior.rb b/activesupport/test/cache/behaviors/local_cache_behavior.rb new file mode 100644 index 0000000000..8530296374 --- /dev/null +++ b/activesupport/test/cache/behaviors/local_cache_behavior.rb @@ -0,0 +1,126 @@ +module LocalCacheBehavior + def test_local_writes_are_persistent_on_the_remote_cache + retval = @cache.with_local_cache do + @cache.write("foo", "bar") + end + assert retval + assert_equal "bar", @cache.read("foo") + end + + def test_clear_also_clears_local_cache + @cache.with_local_cache do + @cache.write("foo", "bar") + @cache.clear + assert_nil @cache.read("foo") + end + + assert_nil @cache.read("foo") + end + + def test_cleanup_clears_local_cache_but_not_remote_cache + skip unless @cache.class.instance_methods(false).include?(:cleanup) + + @cache.with_local_cache do + @cache.write("foo", "bar") + assert_equal "bar", @cache.read("foo") + + @cache.send(:bypass_local_cache) { @cache.write("foo", "baz") } + assert_equal "bar", @cache.read("foo") + + @cache.cleanup + assert_equal "baz", @cache.read("foo") + end + end + + def test_local_cache_of_write + @cache.with_local_cache do + @cache.write("foo", "bar") + @peek.delete("foo") + assert_equal "bar", @cache.read("foo") + end + end + + def test_local_cache_of_read + @cache.write("foo", "bar") + @cache.with_local_cache do + assert_equal "bar", @cache.read("foo") + end + end + + def test_local_cache_of_read_nil + @cache.with_local_cache do + assert_nil @cache.read("foo") + @cache.send(:bypass_local_cache) { @cache.write "foo", "bar" } + assert_nil @cache.read("foo") + end + end + + def test_local_cache_fetch + @cache.with_local_cache do + @cache.send(:local_cache).write "foo", "bar" + assert_equal "bar", @cache.send(:local_cache).fetch("foo") + end + end + + def test_local_cache_of_write_nil + @cache.with_local_cache do + assert @cache.write("foo", nil) + assert_nil @cache.read("foo") + @peek.write("foo", "bar") + assert_nil @cache.read("foo") + end + end + + def test_local_cache_of_write_with_unless_exist + @cache.with_local_cache do + @cache.write("foo", "bar") + @cache.write("foo", "baz", unless_exist: true) + assert_equal @peek.read("foo"), @cache.read("foo") + end + end + + def test_local_cache_of_delete + @cache.with_local_cache do + @cache.write("foo", "bar") + @cache.delete("foo") + assert_nil @cache.read("foo") + end + end + + def test_local_cache_of_exist + @cache.with_local_cache do + @cache.write("foo", "bar") + @peek.delete("foo") + assert @cache.exist?("foo") + end + end + + def test_local_cache_of_increment + @cache.with_local_cache do + @cache.write("foo", 1, raw: true) + @peek.write("foo", 2, raw: true) + @cache.increment("foo") + assert_equal 3, @cache.read("foo") + end + end + + def test_local_cache_of_decrement + @cache.with_local_cache do + @cache.write("foo", 1, raw: true) + @peek.write("foo", 3, raw: true) + @cache.decrement("foo") + assert_equal 2, @cache.read("foo") + end + end + + def test_middleware + app = lambda { |env| + result = @cache.write("foo", "bar") + assert_equal "bar", @cache.read("foo") # make sure 'foo' was written + assert result + [200, {}, []] + } + app = @cache.middleware.new(app) + app.call({}) + end +end diff --git a/activesupport/test/cache/cache_entry_test.rb b/activesupport/test/cache/cache_entry_test.rb new file mode 100644 index 0000000000..e446e39b10 --- /dev/null +++ b/activesupport/test/cache/cache_entry_test.rb @@ -0,0 +1,28 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheEntryTest < ActiveSupport::TestCase + def test_expired + entry = ActiveSupport::Cache::Entry.new("value") + assert !entry.expired?, "entry not expired" + entry = ActiveSupport::Cache::Entry.new("value", expires_in: 60) + assert !entry.expired?, "entry not expired" + Time.stub(:now, Time.now + 61) do + assert entry.expired?, "entry is expired" + end + end + + def test_compress_values + value = "value" * 100 + entry = ActiveSupport::Cache::Entry.new(value, compress: true, compress_threshold: 1) + assert_equal value, entry.value + assert(value.bytesize > entry.size, "value is compressed") + end + + def test_non_compress_values + value = "value" * 100 + entry = ActiveSupport::Cache::Entry.new(value) + assert_equal value, entry.value + assert_equal value.bytesize, entry.size + end +end diff --git a/activesupport/test/cache/cache_key_test.rb b/activesupport/test/cache/cache_key_test.rb new file mode 100644 index 0000000000..149d0f66ee --- /dev/null +++ b/activesupport/test/cache/cache_key_test.rb @@ -0,0 +1,88 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheKeyTest < ActiveSupport::TestCase + def test_entry_legacy_optional_ivars + legacy = Class.new(ActiveSupport::Cache::Entry) do + def initialize(value, options = {}) + @value = value + @expires_in = nil + @created_at = nil + super + end + end + + entry = legacy.new "foo" + assert_equal "foo", entry.value + end + + def test_expand_cache_key + assert_equal "1/2/true", ActiveSupport::Cache.expand_cache_key([1, "2", true]) + assert_equal "name/1/2/true", ActiveSupport::Cache.expand_cache_key([1, "2", true], :name) + end + + def test_expand_cache_key_with_rails_cache_id + with_env("RAILS_CACHE_ID" => "c99") do + assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo) + assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key([:foo]) + assert_equal "c99/foo/bar", ActiveSupport::Cache.expand_cache_key([:foo, :bar]) + assert_equal "nm/c99/foo", ActiveSupport::Cache.expand_cache_key(:foo, :nm) + assert_equal "nm/c99/foo", ActiveSupport::Cache.expand_cache_key([:foo], :nm) + assert_equal "nm/c99/foo/bar", ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm) + end + end + + def test_expand_cache_key_with_rails_app_version + with_env("RAILS_APP_VERSION" => "rails3") do + assert_equal "rails3/foo", ActiveSupport::Cache.expand_cache_key(:foo) + end + end + + def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version + with_env("RAILS_CACHE_ID" => "c99", "RAILS_APP_VERSION" => "rails3") do + assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo) + end + end + + def test_expand_cache_key_respond_to_cache_key + key = "foo" + def key.cache_key + :foo_key + end + assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key(key) + end + + def test_expand_cache_key_array_with_something_that_responds_to_cache_key + key = "foo" + def key.cache_key + :foo_key + end + assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key([key]) + end + + def test_expand_cache_key_of_nil + assert_equal "", ActiveSupport::Cache.expand_cache_key(nil) + end + + def test_expand_cache_key_of_false + assert_equal "false", ActiveSupport::Cache.expand_cache_key(false) + end + + def test_expand_cache_key_of_true + assert_equal "true", ActiveSupport::Cache.expand_cache_key(true) + end + + def test_expand_cache_key_of_array_like_object + assert_equal "foo/bar/baz", ActiveSupport::Cache.expand_cache_key(%w{foo bar baz}.to_enum) + end + + private + + def with_env(kv) + old_values = {} + kv.each { |key, value| old_values[key], ENV[key] = ENV[key], value } + yield + ensure + old_values.each { |key, value| ENV[key] = value } + end +end diff --git a/activesupport/test/cache/cache_store_logger_test.rb b/activesupport/test/cache/cache_store_logger_test.rb new file mode 100644 index 0000000000..621cfebb10 --- /dev/null +++ b/activesupport/test/cache/cache_store_logger_test.rb @@ -0,0 +1,34 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheStoreLoggerTest < ActiveSupport::TestCase + def setup + @cache = ActiveSupport::Cache.lookup_store(:memory_store) + + @buffer = StringIO.new + @cache.logger = ActiveSupport::Logger.new(@buffer) + end + + def test_logging + @cache.fetch("foo") { "bar" } + assert @buffer.string.present? + end + + def test_log_with_string_namespace + @cache.fetch("foo", namespace: "string_namespace") { "bar" } + assert_match %r{string_namespace:foo}, @buffer.string + end + + def test_log_with_proc_namespace + proc = Proc.new do + "proc_namespace" + end + @cache.fetch("foo", namespace: proc) { "bar" } + assert_match %r{proc_namespace:foo}, @buffer.string + end + + def test_mute_logging + @cache.mute { @cache.fetch("foo") { "bar" } } + assert @buffer.string.blank? + end +end diff --git a/activesupport/test/cache/cache_store_namespace_test.rb b/activesupport/test/cache/cache_store_namespace_test.rb new file mode 100644 index 0000000000..e395c88271 --- /dev/null +++ b/activesupport/test/cache/cache_store_namespace_test.rb @@ -0,0 +1,38 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheStoreNamespaceTest < ActiveSupport::TestCase + def test_static_namespace + cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "tester") + cache.write("foo", "bar") + assert_equal "bar", cache.read("foo") + assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value + end + + def test_proc_namespace + test_val = "tester" + proc = lambda { test_val } + cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: proc) + cache.write("foo", "bar") + assert_equal "bar", cache.read("foo") + assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value + end + + def test_delete_matched_key_start + cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "tester") + cache.write("foo", "bar") + cache.write("fu", "baz") + cache.delete_matched(/^fo/) + assert !cache.exist?("foo") + assert cache.exist?("fu") + end + + def test_delete_matched_key + cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "foo") + cache.write("foo", "bar") + cache.write("fu", "baz") + cache.delete_matched(/OO/i) + assert !cache.exist?("foo") + assert cache.exist?("fu") + end +end diff --git a/activesupport/test/cache/cache_store_setting_test.rb b/activesupport/test/cache/cache_store_setting_test.rb new file mode 100644 index 0000000000..cb9b006abe --- /dev/null +++ b/activesupport/test/cache/cache_store_setting_test.rb @@ -0,0 +1,66 @@ +require "abstract_unit" +require "active_support/cache" +require "dalli" + +class CacheStoreSettingTest < ActiveSupport::TestCase + def test_memory_store_gets_created_if_no_arguments_passed_to_lookup_store_method + store = ActiveSupport::Cache.lookup_store + assert_kind_of(ActiveSupport::Cache::MemoryStore, store) + end + + def test_memory_store + store = ActiveSupport::Cache.lookup_store :memory_store + assert_kind_of(ActiveSupport::Cache::MemoryStore, store) + end + + def test_file_fragment_cache_store + store = ActiveSupport::Cache.lookup_store :file_store, "/path/to/cache/directory" + assert_kind_of(ActiveSupport::Cache::FileStore, store) + assert_equal "/path/to/cache/directory", store.cache_path + end + + def test_mem_cache_fragment_cache_store + assert_called_with(Dalli::Client, :new, [%w[localhost], {}]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost" + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end + end + + def test_mem_cache_fragment_cache_store_with_given_mem_cache + mem_cache = Dalli::Client.new + assert_not_called(Dalli::Client, :new) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end + end + + def test_mem_cache_fragment_cache_store_with_not_dalli_client + assert_not_called(Dalli::Client, :new) do + memcache = Object.new + assert_raises(ArgumentError) do + ActiveSupport::Cache.lookup_store :mem_cache_store, memcache + end + end + end + + def test_mem_cache_fragment_cache_store_with_multiple_servers + assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], {}]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1" + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + end + end + + def test_mem_cache_fragment_cache_store_with_options + assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], { timeout: 10 }]) do + store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1", namespace: "foo", timeout: 10 + assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) + assert_equal "foo", store.options[:namespace] + end + end + + def test_object_assigned_fragment_cache_store + store = ActiveSupport::Cache.lookup_store ActiveSupport::Cache::FileStore.new("/path/to/cache/directory") + assert_kind_of(ActiveSupport::Cache::FileStore, store) + assert_equal "/path/to/cache/directory", store.cache_path + end +end diff --git a/activesupport/test/cache/cache_store_write_multi_test.rb b/activesupport/test/cache/cache_store_write_multi_test.rb new file mode 100644 index 0000000000..16e3f3b842 --- /dev/null +++ b/activesupport/test/cache/cache_store_write_multi_test.rb @@ -0,0 +1,60 @@ +require "abstract_unit" +require "active_support/cache" + +class CacheStoreWriteMultiEntriesStoreProviderInterfaceTest < ActiveSupport::TestCase + setup do + @cache = ActiveSupport::Cache.lookup_store(:null_store) + end + + test "fetch_multi uses write_multi_entries store provider interface" do + assert_called_with(@cache, :write_multi_entries) do + @cache.fetch_multi "a", "b", "c" do |key| + key * 2 + end + end + end +end + +class CacheStoreWriteMultiInstrumentationTest < ActiveSupport::TestCase + setup do + @cache = ActiveSupport::Cache.lookup_store(:null_store) + end + + test "instrumentation" do + writes = { "a" => "aa", "b" => "bb" } + + events = with_instrumentation "write_multi" do + @cache.write_multi(writes) + end + + assert_equal %w[ cache_write_multi.active_support ], events.map(&:name) + assert_nil events[0].payload[:super_operation] + assert_equal({ "a" => "aa", "b" => "bb" }, events[0].payload[:key]) + end + + test "instrumentation with fetch_multi as super operation" do + skip "fetch_multi isn't instrumented yet" + + events = with_instrumentation "write_multi" do + @cache.fetch_multi("a", "b") { |key| key * 2 } + end + + assert_equal %w[ cache_write_multi.active_support ], events.map(&:name) + assert_nil events[0].payload[:super_operation] + assert !events[0].payload[:hit] + end + + private + def with_instrumentation(method) + event_name = "cache_#{method}.active_support" + + [].tap do |events| + ActiveSupport::Notifications.subscribe event_name do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + yield + end + ensure + ActiveSupport::Notifications.unsubscribe event_name + end +end diff --git a/activesupport/test/cache/local_cache_middleware_test.rb b/activesupport/test/cache/local_cache_middleware_test.rb new file mode 100644 index 0000000000..352502fb43 --- /dev/null +++ b/activesupport/test/cache/local_cache_middleware_test.rb @@ -0,0 +1,61 @@ +require "abstract_unit" +require "active_support/cache" + +module ActiveSupport + module Cache + module Strategy + module LocalCache + class MiddlewareTest < ActiveSupport::TestCase + def test_local_cache_cleared_on_close + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new("<3", key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), "should have a cache" + [200, {}, []] + }) + _, _, body = middleware.call({}) + assert LocalCacheRegistry.cache_for(key), "should still have a cache" + body.each {} + assert LocalCacheRegistry.cache_for(key), "should still have a cache" + body.close + assert_nil LocalCacheRegistry.cache_for(key) + end + + def test_local_cache_cleared_and_response_should_be_present_on_invalid_parameters_error + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new("<3", key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), "should have a cache" + raise Rack::Utils::InvalidParameterError + }) + response = middleware.call({}) + assert response, "response should exist" + assert_nil LocalCacheRegistry.cache_for(key) + end + + def test_local_cache_cleared_on_exception + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new("<3", key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), "should have a cache" + raise + }) + assert_raises(RuntimeError) { middleware.call({}) } + assert_nil LocalCacheRegistry.cache_for(key) + end + + def test_local_cache_cleared_on_throw + key = "super awesome key" + assert_nil LocalCacheRegistry.cache_for key + middleware = Middleware.new("<3", key).new(->(env) { + assert LocalCacheRegistry.cache_for(key), "should have a cache" + throw :warden + }) + assert_throws(:warden) { middleware.call({}) } + assert_nil LocalCacheRegistry.cache_for(key) + end + end + end + end + end +end diff --git a/activesupport/test/cache/stores/file_store_test.rb b/activesupport/test/cache/stores/file_store_test.rb new file mode 100644 index 0000000000..48b304fe6e --- /dev/null +++ b/activesupport/test/cache/stores/file_store_test.rb @@ -0,0 +1,128 @@ +require "abstract_unit" +require "active_support/cache" +require_relative "../behaviors" +require "pathname" + +class FileStoreTest < ActiveSupport::TestCase + def setup + Dir.mkdir(cache_dir) unless File.exist?(cache_dir) + @cache = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, expires_in: 60) + @peek = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, expires_in: 60) + @cache_with_pathname = ActiveSupport::Cache.lookup_store(:file_store, Pathname.new(cache_dir), expires_in: 60) + + @buffer = StringIO.new + @cache.logger = ActiveSupport::Logger.new(@buffer) + end + + def teardown + FileUtils.rm_r(cache_dir) + rescue Errno::ENOENT + end + + def cache_dir + File.join(Dir.pwd, "tmp_cache") + end + + include CacheStoreBehavior + include CacheStoreVersionBehavior + include LocalCacheBehavior + include CacheDeleteMatchedBehavior + include CacheIncrementDecrementBehavior + include AutoloadingCacheBehavior + + def test_clear + gitkeep = File.join(cache_dir, ".gitkeep") + keep = File.join(cache_dir, ".keep") + FileUtils.touch([gitkeep, keep]) + @cache.clear + assert File.exist?(gitkeep) + assert File.exist?(keep) + end + + def test_clear_without_cache_dir + FileUtils.rm_r(cache_dir) + @cache.clear + end + + def test_long_uri_encoded_keys + @cache.write("%" * 870, 1) + assert_equal 1, @cache.read("%" * 870) + end + + def test_key_transformation + key = @cache.send(:normalize_key, "views/index?id=1", {}) + assert_equal "views/index?id=1", @cache.send(:file_path_key, key) + end + + def test_key_transformation_with_pathname + FileUtils.touch(File.join(cache_dir, "foo")) + key = @cache_with_pathname.send(:normalize_key, "views/index?id=1", {}) + assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key) + end + + # Test that generated cache keys are short enough to have Tempfile stuff added to them and + # remain valid + def test_filename_max_size + key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}" + path = @cache.send(:normalize_key, key, {}) + Dir::Tmpname.create(path) do |tmpname, n, opts| + assert File.basename(tmpname + ".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname + '.lock').length}" + end + end + + # Because file systems have a maximum filename size, filenames > max size should be split in to directories + # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B + def test_key_transformation_max_filename_size + key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B" + path = @cache.send(:normalize_key, key, {}) + assert path.split("/").all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE } + assert_equal "B", File.basename(path) + end + + # If nothing has been stored in the cache, there is a chance the cache directory does not yet exist + # Ensure delete_matched gracefully handles this case + def test_delete_matched_when_cache_directory_does_not_exist + assert_nothing_raised do + ActiveSupport::Cache::FileStore.new("/test/cache/directory").delete_matched(/does_not_exist/) + end + end + + def test_delete_does_not_delete_empty_parent_dir + sub_cache_dir = File.join(cache_dir, "subdir/") + sub_cache_store = ActiveSupport::Cache::FileStore.new(sub_cache_dir) + assert_nothing_raised do + assert sub_cache_store.write("foo", "bar") + assert sub_cache_store.delete("foo") + end + assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!" + assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!" + assert Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) }.empty? + end + + def test_log_exception_when_cache_read_fails + File.stub(:exist?, -> { raise StandardError.new("failed") }) do + @cache.send(:read_entry, "winston", {}) + assert @buffer.string.present? + end + end + + def test_cleanup_removes_all_expired_entries + time = Time.now + @cache.write("foo", "bar", expires_in: 10) + @cache.write("baz", "qux") + @cache.write("quux", "corge", expires_in: 20) + Time.stub(:now, time + 15) do + @cache.cleanup + assert_not @cache.exist?("foo") + assert @cache.exist?("baz") + assert @cache.exist?("quux") + end + end + + def test_write_with_unless_exist + assert_equal true, @cache.write(1, "aaaaaaaaaa") + assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) + @cache.write(1, nil) + assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) + end +end diff --git a/activesupport/test/cache/stores/mem_cache_store_test.rb b/activesupport/test/cache/stores/mem_cache_store_test.rb new file mode 100644 index 0000000000..2dd5264818 --- /dev/null +++ b/activesupport/test/cache/stores/mem_cache_store_test.rb @@ -0,0 +1,74 @@ +require "abstract_unit" +require "active_support/cache" +require_relative "../behaviors" +require "dalli" + +class MemCacheStoreTest < ActiveSupport::TestCase + begin + ss = Dalli::Client.new("localhost:11211").stats + raise Dalli::DalliError unless ss["localhost:11211"] + + MEMCACHE_UP = true + rescue Dalli::DalliError + $stderr.puts "Skipping memcached tests. Start memcached and try again." + MEMCACHE_UP = false + end + + def setup + skip "memcache server is not up" unless MEMCACHE_UP + + @cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, expires_in: 60) + @peek = ActiveSupport::Cache.lookup_store(:mem_cache_store) + @data = @cache.instance_variable_get(:@data) + @cache.clear + @cache.silence! + @cache.logger = ActiveSupport::Logger.new("/dev/null") + end + + include CacheStoreBehavior + include CacheStoreVersionBehavior + include LocalCacheBehavior + include CacheIncrementDecrementBehavior + include EncodedKeyCacheBehavior + include AutoloadingCacheBehavior + + def test_raw_values + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache.clear + cache.write("foo", 2) + assert_equal "2", cache.read("foo") + end + + def test_raw_values_with_marshal + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache.clear + cache.write("foo", Marshal.dump([])) + assert_equal [], cache.read("foo") + end + + def test_local_cache_raw_values + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache.clear + cache.with_local_cache do + cache.write("foo", 2) + assert_equal "2", cache.read("foo") + end + end + + def test_local_cache_raw_values_with_marshal + cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) + cache.clear + cache.with_local_cache do + cache.write("foo", Marshal.dump([])) + assert_equal [], cache.read("foo") + end + end + + def test_read_should_return_a_different_object_id_each_time_it_is_called + @cache.write("foo", "bar") + value = @cache.read("foo") + assert_not_equal value.object_id, @cache.read("foo").object_id + value << "bingo" + assert_not_equal value, @cache.read("foo") + end +end diff --git a/activesupport/test/cache/stores/memory_store_test.rb b/activesupport/test/cache/stores/memory_store_test.rb new file mode 100644 index 0000000000..3dd1646d56 --- /dev/null +++ b/activesupport/test/cache/stores/memory_store_test.rb @@ -0,0 +1,107 @@ +require "abstract_unit" +require "active_support/cache" +require_relative "../behaviors" + +class MemoryStoreTest < ActiveSupport::TestCase + def setup + @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa")) + @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1) + end + + include CacheStoreBehavior + include CacheStoreVersionBehavior + include CacheDeleteMatchedBehavior + include CacheIncrementDecrementBehavior + + def test_prune_size + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.read(2) && sleep(0.001) + @cache.read(4) + @cache.prune(@record_size * 3) + assert @cache.exist?(5) + assert @cache.exist?(4) + assert !@cache.exist?(3), "no entry" + assert @cache.exist?(2) + assert !@cache.exist?(1), "no entry" + end + + def test_prune_size_on_write + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.write(6, "ffffffffff") && sleep(0.001) + @cache.write(7, "gggggggggg") && sleep(0.001) + @cache.write(8, "hhhhhhhhhh") && sleep(0.001) + @cache.write(9, "iiiiiiiiii") && sleep(0.001) + @cache.write(10, "kkkkkkkkkk") && sleep(0.001) + @cache.read(2) && sleep(0.001) + @cache.read(4) && sleep(0.001) + @cache.write(11, "llllllllll") + assert @cache.exist?(11) + assert @cache.exist?(10) + assert @cache.exist?(9) + assert @cache.exist?(8) + assert @cache.exist?(7) + assert !@cache.exist?(6), "no entry" + assert !@cache.exist?(5), "no entry" + assert @cache.exist?(4) + assert !@cache.exist?(3), "no entry" + assert @cache.exist?(2) + assert !@cache.exist?(1), "no entry" + end + + def test_prune_size_on_write_based_on_key_length + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.write(6, "ffffffffff") && sleep(0.001) + @cache.write(7, "gggggggggg") && sleep(0.001) + @cache.write(8, "hhhhhhhhhh") && sleep(0.001) + @cache.write(9, "iiiiiiiiii") && sleep(0.001) + long_key = "*" * 2 * @record_size + @cache.write(long_key, "llllllllll") + assert @cache.exist?(long_key) + assert @cache.exist?(9) + assert @cache.exist?(8) + assert @cache.exist?(7) + assert @cache.exist?(6) + assert !@cache.exist?(5), "no entry" + assert !@cache.exist?(4), "no entry" + assert !@cache.exist?(3), "no entry" + assert !@cache.exist?(2), "no entry" + assert !@cache.exist?(1), "no entry" + end + + def test_pruning_is_capped_at_a_max_time + def @cache.delete_entry(*args) + sleep(0.01) + super + end + @cache.write(1, "aaaaaaaaaa") && sleep(0.001) + @cache.write(2, "bbbbbbbbbb") && sleep(0.001) + @cache.write(3, "cccccccccc") && sleep(0.001) + @cache.write(4, "dddddddddd") && sleep(0.001) + @cache.write(5, "eeeeeeeeee") && sleep(0.001) + @cache.prune(30, 0.001) + assert @cache.exist?(5) + assert @cache.exist?(4) + assert @cache.exist?(3) + assert @cache.exist?(2) + assert !@cache.exist?(1) + end + + def test_write_with_unless_exist + assert_equal true, @cache.write(1, "aaaaaaaaaa") + assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) + @cache.write(1, nil) + assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) + end +end diff --git a/activesupport/test/cache/stores/null_store_test.rb b/activesupport/test/cache/stores/null_store_test.rb new file mode 100644 index 0000000000..23c4e64ee4 --- /dev/null +++ b/activesupport/test/cache/stores/null_store_test.rb @@ -0,0 +1,57 @@ +require "abstract_unit" +require "active_support/cache" +require_relative "../behaviors" + +class NullStoreTest < ActiveSupport::TestCase + def setup + @cache = ActiveSupport::Cache.lookup_store(:null_store) + end + + def test_clear + @cache.clear + end + + def test_cleanup + @cache.cleanup + end + + def test_write + assert_equal true, @cache.write("name", "value") + end + + def test_read + @cache.write("name", "value") + assert_nil @cache.read("name") + end + + def test_delete + @cache.write("name", "value") + assert_equal false, @cache.delete("name") + end + + def test_increment + @cache.write("name", 1, raw: true) + assert_nil @cache.increment("name") + end + + def test_decrement + @cache.write("name", 1, raw: true) + assert_nil @cache.increment("name") + end + + def test_delete_matched + @cache.write("name", "value") + @cache.delete_matched(/name/) + end + + def test_local_store_strategy + @cache.with_local_cache do + @cache.write("name", "value") + assert_equal "value", @cache.read("name") + @cache.delete("name") + assert_nil @cache.read("name") + @cache.write("name", "value") + end + assert_nil @cache.read("name") + end +end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb deleted file mode 100644 index c67ffe69b8..0000000000 --- a/activesupport/test/caching_test.rb +++ /dev/null @@ -1,1203 +0,0 @@ -require "logger" -require "abstract_unit" -require "active_support/cache" -require "dependencies_test_helpers" - -require "pathname" - -module ActiveSupport - module Cache - module Strategy - module LocalCache - class MiddlewareTest < ActiveSupport::TestCase - def test_local_cache_cleared_on_close - key = "super awesome key" - assert_nil LocalCacheRegistry.cache_for key - middleware = Middleware.new("<3", key).new(->(env) { - assert LocalCacheRegistry.cache_for(key), "should have a cache" - [200, {}, []] - }) - _, _, body = middleware.call({}) - assert LocalCacheRegistry.cache_for(key), "should still have a cache" - body.each {} - assert LocalCacheRegistry.cache_for(key), "should still have a cache" - body.close - assert_nil LocalCacheRegistry.cache_for(key) - end - - def test_local_cache_cleared_and_response_should_be_present_on_invalid_parameters_error - key = "super awesome key" - assert_nil LocalCacheRegistry.cache_for key - middleware = Middleware.new("<3", key).new(->(env) { - assert LocalCacheRegistry.cache_for(key), "should have a cache" - raise Rack::Utils::InvalidParameterError - }) - response = middleware.call({}) - assert response, "response should exist" - assert_nil LocalCacheRegistry.cache_for(key) - end - - def test_local_cache_cleared_on_exception - key = "super awesome key" - assert_nil LocalCacheRegistry.cache_for key - middleware = Middleware.new("<3", key).new(->(env) { - assert LocalCacheRegistry.cache_for(key), "should have a cache" - raise - }) - assert_raises(RuntimeError) { middleware.call({}) } - assert_nil LocalCacheRegistry.cache_for(key) - end - - def test_local_cache_cleared_on_throw - key = "super awesome key" - assert_nil LocalCacheRegistry.cache_for key - middleware = Middleware.new("<3", key).new(->(env) { - assert LocalCacheRegistry.cache_for(key), "should have a cache" - throw :warden - }) - assert_throws(:warden) { middleware.call({}) } - assert_nil LocalCacheRegistry.cache_for(key) - end - end - end - end - end -end - -class CacheKeyTest < ActiveSupport::TestCase - def test_entry_legacy_optional_ivars - legacy = Class.new(ActiveSupport::Cache::Entry) do - def initialize(value, options = {}) - @value = value - @expires_in = nil - @created_at = nil - super - end - end - - entry = legacy.new "foo" - assert_equal "foo", entry.value - end - - def test_expand_cache_key - assert_equal "1/2/true", ActiveSupport::Cache.expand_cache_key([1, "2", true]) - assert_equal "name/1/2/true", ActiveSupport::Cache.expand_cache_key([1, "2", true], :name) - end - - def test_expand_cache_key_with_rails_cache_id - with_env("RAILS_CACHE_ID" => "c99") do - assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo) - assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key([:foo]) - assert_equal "c99/foo/bar", ActiveSupport::Cache.expand_cache_key([:foo, :bar]) - assert_equal "nm/c99/foo", ActiveSupport::Cache.expand_cache_key(:foo, :nm) - assert_equal "nm/c99/foo", ActiveSupport::Cache.expand_cache_key([:foo], :nm) - assert_equal "nm/c99/foo/bar", ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm) - end - end - - def test_expand_cache_key_with_rails_app_version - with_env("RAILS_APP_VERSION" => "rails3") do - assert_equal "rails3/foo", ActiveSupport::Cache.expand_cache_key(:foo) - end - end - - def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version - with_env("RAILS_CACHE_ID" => "c99", "RAILS_APP_VERSION" => "rails3") do - assert_equal "c99/foo", ActiveSupport::Cache.expand_cache_key(:foo) - end - end - - def test_expand_cache_key_respond_to_cache_key - key = "foo" - def key.cache_key - :foo_key - end - assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key(key) - end - - def test_expand_cache_key_array_with_something_that_responds_to_cache_key - key = "foo" - def key.cache_key - :foo_key - end - assert_equal "foo_key", ActiveSupport::Cache.expand_cache_key([key]) - end - - def test_expand_cache_key_of_nil - assert_equal "", ActiveSupport::Cache.expand_cache_key(nil) - end - - def test_expand_cache_key_of_false - assert_equal "false", ActiveSupport::Cache.expand_cache_key(false) - end - - def test_expand_cache_key_of_true - assert_equal "true", ActiveSupport::Cache.expand_cache_key(true) - end - - def test_expand_cache_key_of_array_like_object - assert_equal "foo/bar/baz", ActiveSupport::Cache.expand_cache_key(%w{foo bar baz}.to_enum) - end - - private - - def with_env(kv) - old_values = {} - kv.each { |key, value| old_values[key], ENV[key] = ENV[key], value } - yield - ensure - old_values.each { |key, value| ENV[key] = value } - end -end - -class CacheStoreSettingTest < ActiveSupport::TestCase - def test_memory_store_gets_created_if_no_arguments_passed_to_lookup_store_method - store = ActiveSupport::Cache.lookup_store - assert_kind_of(ActiveSupport::Cache::MemoryStore, store) - end - - def test_memory_store - store = ActiveSupport::Cache.lookup_store :memory_store - assert_kind_of(ActiveSupport::Cache::MemoryStore, store) - end - - def test_file_fragment_cache_store - store = ActiveSupport::Cache.lookup_store :file_store, "/path/to/cache/directory" - assert_kind_of(ActiveSupport::Cache::FileStore, store) - assert_equal "/path/to/cache/directory", store.cache_path - end - - def test_mem_cache_fragment_cache_store - assert_called_with(Dalli::Client, :new, [%w[localhost], {}]) do - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost" - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - end - end - - def test_mem_cache_fragment_cache_store_with_given_mem_cache - mem_cache = Dalli::Client.new - assert_not_called(Dalli::Client, :new) do - store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - end - end - - def test_mem_cache_fragment_cache_store_with_not_dalli_client - assert_not_called(Dalli::Client, :new) do - memcache = Object.new - assert_raises(ArgumentError) do - ActiveSupport::Cache.lookup_store :mem_cache_store, memcache - end - end - end - - def test_mem_cache_fragment_cache_store_with_multiple_servers - assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], {}]) do - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1" - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - end - end - - def test_mem_cache_fragment_cache_store_with_options - assert_called_with(Dalli::Client, :new, [%w[localhost 192.168.1.1], { timeout: 10 }]) do - store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", "192.168.1.1", namespace: "foo", timeout: 10 - assert_kind_of(ActiveSupport::Cache::MemCacheStore, store) - assert_equal "foo", store.options[:namespace] - end - end - - def test_object_assigned_fragment_cache_store - store = ActiveSupport::Cache.lookup_store ActiveSupport::Cache::FileStore.new("/path/to/cache/directory") - assert_kind_of(ActiveSupport::Cache::FileStore, store) - assert_equal "/path/to/cache/directory", store.cache_path - end -end - -class CacheStoreNamespaceTest < ActiveSupport::TestCase - def test_static_namespace - cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "tester") - cache.write("foo", "bar") - assert_equal "bar", cache.read("foo") - assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value - end - - def test_proc_namespace - test_val = "tester" - proc = lambda { test_val } - cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: proc) - cache.write("foo", "bar") - assert_equal "bar", cache.read("foo") - assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value - end - - def test_delete_matched_key_start - cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "tester") - cache.write("foo", "bar") - cache.write("fu", "baz") - cache.delete_matched(/^fo/) - assert !cache.exist?("foo") - assert cache.exist?("fu") - end - - def test_delete_matched_key - cache = ActiveSupport::Cache.lookup_store(:memory_store, namespace: "foo") - cache.write("foo", "bar") - cache.write("fu", "baz") - cache.delete_matched(/OO/i) - assert !cache.exist?("foo") - assert cache.exist?("fu") - end -end - -# Tests the base functionality that should be identical across all cache stores. -module CacheStoreBehavior - def test_should_read_and_write_strings - assert @cache.write("foo", "bar") - assert_equal "bar", @cache.read("foo") - end - - def test_should_overwrite - @cache.write("foo", "bar") - @cache.write("foo", "baz") - assert_equal "baz", @cache.read("foo") - end - - def test_fetch_without_cache_miss - @cache.write("foo", "bar") - assert_not_called(@cache, :write) do - assert_equal "bar", @cache.fetch("foo") { "baz" } - end - end - - def test_fetch_with_cache_miss - assert_called_with(@cache, :write, ["foo", "baz", @cache.options]) do - assert_equal "baz", @cache.fetch("foo") { "baz" } - end - end - - def test_fetch_with_cache_miss_passes_key_to_block - cache_miss = false - assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } - assert cache_miss - - cache_miss = false - assert_equal 3, @cache.fetch("foo") { |key| cache_miss = true; key.length } - assert !cache_miss - end - - def test_fetch_with_forced_cache_miss - @cache.write("foo", "bar") - assert_not_called(@cache, :read) do - assert_called_with(@cache, :write, ["foo", "bar", @cache.options.merge(force: true)]) do - @cache.fetch("foo", force: true) { "bar" } - end - end - end - - def test_fetch_with_cached_nil - @cache.write("foo", nil) - assert_not_called(@cache, :write) do - assert_nil @cache.fetch("foo") { "baz" } - end - end - - def test_fetch_with_forced_cache_miss_with_block - @cache.write("foo", "bar") - assert_equal "foo_bar", @cache.fetch("foo", force: true) { "foo_bar" } - end - - def test_fetch_with_forced_cache_miss_without_block - @cache.write("foo", "bar") - assert_raises(ArgumentError) do - @cache.fetch("foo", force: true) - end - - assert_equal "bar", @cache.read("foo") - end - - def test_should_read_and_write_hash - assert @cache.write("foo", a: "b") - assert_equal({ a: "b" }, @cache.read("foo")) - end - - def test_should_read_and_write_integer - assert @cache.write("foo", 1) - assert_equal 1, @cache.read("foo") - end - - def test_should_read_and_write_nil - assert @cache.write("foo", nil) - assert_nil @cache.read("foo") - end - - def test_should_read_and_write_false - assert @cache.write("foo", false) - assert_equal false, @cache.read("foo") - end - - def test_read_multi - @cache.write("foo", "bar") - @cache.write("fu", "baz") - @cache.write("fud", "biz") - assert_equal({ "foo" => "bar", "fu" => "baz" }, @cache.read_multi("foo", "fu")) - end - - def test_read_multi_with_expires - time = Time.now - @cache.write("foo", "bar", expires_in: 10) - @cache.write("fu", "baz") - @cache.write("fud", "biz") - Time.stub(:now, time + 11) do - assert_equal({ "fu" => "baz" }, @cache.read_multi("foo", "fu")) - end - end - - def test_fetch_multi - @cache.write("foo", "bar") - @cache.write("fud", "biz") - - values = @cache.fetch_multi("foo", "fu", "fud") { |value| value * 2 } - - assert_equal({ "foo" => "bar", "fu" => "fufu", "fud" => "biz" }, values) - assert_equal("fufu", @cache.read("fu")) - end - - def test_multi_with_objects - cache_struct = Struct.new(:cache_key, :title) - foo = cache_struct.new("foo", "FOO!") - bar = cache_struct.new("bar") - - @cache.write("bar", "BAM!") - - values = @cache.fetch_multi(foo, bar) { |object| object.title } - - assert_equal({ foo => "FOO!", bar => "BAM!" }, values) - end - - def test_fetch_multi_without_block - assert_raises(ArgumentError) do - @cache.fetch_multi("foo") - end - end - - def test_read_and_write_compressed_small_data - @cache.write("foo", "bar", compress: true) - assert_equal "bar", @cache.read("foo") - end - - def test_read_and_write_compressed_large_data - @cache.write("foo", "bar", compress: true, compress_threshold: 2) - assert_equal "bar", @cache.read("foo") - end - - def test_read_and_write_compressed_nil - @cache.write("foo", nil, compress: true) - assert_nil @cache.read("foo") - end - - def test_cache_key - obj = Object.new - def obj.cache_key - :foo - end - @cache.write(obj, "bar") - assert_equal "bar", @cache.read("foo") - end - - def test_param_as_cache_key - obj = Object.new - def obj.to_param - "foo" - end - @cache.write(obj, "bar") - assert_equal "bar", @cache.read("foo") - end - - def test_array_as_cache_key - @cache.write([:fu, "foo"], "bar") - assert_equal "bar", @cache.read("fu/foo") - end - - def test_hash_as_cache_key - @cache.write({ foo: 1, fu: 2 }, "bar") - assert_equal "bar", @cache.read("foo=1/fu=2") - end - - def test_keys_are_case_sensitive - @cache.write("foo", "bar") - assert_nil @cache.read("FOO") - end - - def test_exist - @cache.write("foo", "bar") - assert_equal true, @cache.exist?("foo") - assert_equal false, @cache.exist?("bar") - end - - def test_nil_exist - @cache.write("foo", nil) - assert @cache.exist?("foo") - end - - def test_delete - @cache.write("foo", "bar") - assert @cache.exist?("foo") - assert @cache.delete("foo") - assert !@cache.exist?("foo") - end - - def test_original_store_objects_should_not_be_immutable - bar = "bar" - @cache.write("foo", bar) - assert_nothing_raised { bar.gsub!(/.*/, "baz") } - end - - def test_expires_in - time = Time.local(2008, 4, 24) - - Time.stub(:now, time) do - @cache.write("foo", "bar") - assert_equal "bar", @cache.read("foo") - end - - Time.stub(:now, time + 30) do - assert_equal "bar", @cache.read("foo") - end - - Time.stub(:now, time + 61) do - assert_nil @cache.read("foo") - end - end - - def test_race_condition_protection_skipped_if_not_defined - @cache.write("foo", "bar") - time = @cache.send(:read_entry, @cache.send(:normalize_key, "foo", {}), {}).expires_at - - Time.stub(:now, Time.at(time)) do - result = @cache.fetch("foo") do - assert_nil @cache.read("foo") - "baz" - end - assert_equal "baz", result - end - end - - def test_race_condition_protection_is_limited - time = Time.now - @cache.write("foo", "bar", expires_in: 60) - Time.stub(:now, time + 71) do - result = @cache.fetch("foo", race_condition_ttl: 10) do - assert_nil @cache.read("foo") - "baz" - end - assert_equal "baz", result - end - end - - def test_race_condition_protection_is_safe - time = Time.now - @cache.write("foo", "bar", expires_in: 60) - Time.stub(:now, time + 61) do - begin - @cache.fetch("foo", race_condition_ttl: 10) do - assert_equal "bar", @cache.read("foo") - raise ArgumentError.new - end - rescue ArgumentError - end - assert_equal "bar", @cache.read("foo") - end - Time.stub(:now, time + 91) do - assert_nil @cache.read("foo") - end - end - - def test_race_condition_protection - time = Time.now - @cache.write("foo", "bar", expires_in: 60) - Time.stub(:now, time + 61) do - result = @cache.fetch("foo", race_condition_ttl: 10) do - assert_equal "bar", @cache.read("foo") - "baz" - end - assert_equal "baz", result - end - end - - def test_crazy_key_characters - crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-" - assert @cache.write(crazy_key, "1", raw: true) - assert_equal "1", @cache.read(crazy_key) - assert_equal "1", @cache.fetch(crazy_key) - assert @cache.delete(crazy_key) - assert_equal "2", @cache.fetch(crazy_key, raw: true) { "2" } - assert_equal 3, @cache.increment(crazy_key) - assert_equal 2, @cache.decrement(crazy_key) - end - - def test_really_long_keys - key = "" - 900.times { key << "x" } - assert @cache.write(key, "bar") - assert_equal "bar", @cache.read(key) - assert_equal "bar", @cache.fetch(key) - assert_nil @cache.read("#{key}x") - assert_equal({ key => "bar" }, @cache.read_multi(key)) - assert @cache.delete(key) - end - - def test_cache_hit_instrumentation - key = "test_key" - @events = [] - ActiveSupport::Notifications.subscribe "cache_read.active_support" do |*args| - @events << ActiveSupport::Notifications::Event.new(*args) - end - assert @cache.write(key, "1", raw: true) - assert @cache.fetch(key) {} - assert_equal 1, @events.length - assert_equal "cache_read.active_support", @events[0].name - assert_equal :fetch, @events[0].payload[:super_operation] - assert @events[0].payload[:hit] - ensure - ActiveSupport::Notifications.unsubscribe "cache_read.active_support" - end - - def test_cache_miss_instrumentation - @events = [] - ActiveSupport::Notifications.subscribe(/^cache_(.*)\.active_support$/) do |*args| - @events << ActiveSupport::Notifications::Event.new(*args) - end - assert_not @cache.fetch("bad_key") {} - assert_equal 3, @events.length - assert_equal "cache_read.active_support", @events[0].name - assert_equal "cache_generate.active_support", @events[1].name - assert_equal "cache_write.active_support", @events[2].name - assert_equal :fetch, @events[0].payload[:super_operation] - assert_not @events[0].payload[:hit] - ensure - ActiveSupport::Notifications.unsubscribe "cache_read.active_support" - end -end - -# https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters -# The error is caused by character encodings that can't be compared with ASCII-8BIT regular expressions and by special -# characters like the umlaut in UTF-8. -module EncodedKeyCacheBehavior - Encoding.list.each do |encoding| - define_method "test_#{encoding.name.underscore}_encoded_values" do - key = "foo".force_encoding(encoding) - assert @cache.write(key, "1", raw: true) - assert_equal "1", @cache.read(key) - assert_equal "1", @cache.fetch(key) - assert @cache.delete(key) - assert_equal "2", @cache.fetch(key, raw: true) { "2" } - assert_equal 3, @cache.increment(key) - assert_equal 2, @cache.decrement(key) - end - end - - def test_common_utf8_values - key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8) - assert @cache.write(key, "1", raw: true) - assert_equal "1", @cache.read(key) - assert_equal "1", @cache.fetch(key) - assert @cache.delete(key) - assert_equal "2", @cache.fetch(key, raw: true) { "2" } - assert_equal 3, @cache.increment(key) - assert_equal 2, @cache.decrement(key) - end - - def test_retains_encoding - key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8) - assert @cache.write(key, "1", raw: true) - assert_equal Encoding::UTF_8, key.encoding - end -end - -module CacheDeleteMatchedBehavior - def test_delete_matched - @cache.write("foo", "bar") - @cache.write("fu", "baz") - @cache.write("foo/bar", "baz") - @cache.write("fu/baz", "bar") - @cache.delete_matched(/oo/) - assert !@cache.exist?("foo") - assert @cache.exist?("fu") - assert !@cache.exist?("foo/bar") - assert @cache.exist?("fu/baz") - end -end - -module CacheIncrementDecrementBehavior - def test_increment - @cache.write("foo", 1, raw: true) - assert_equal 1, @cache.read("foo").to_i - assert_equal 2, @cache.increment("foo") - assert_equal 2, @cache.read("foo").to_i - assert_equal 3, @cache.increment("foo") - assert_equal 3, @cache.read("foo").to_i - assert_nil @cache.increment("bar") - end - - def test_decrement - @cache.write("foo", 3, raw: true) - assert_equal 3, @cache.read("foo").to_i - assert_equal 2, @cache.decrement("foo") - assert_equal 2, @cache.read("foo").to_i - assert_equal 1, @cache.decrement("foo") - assert_equal 1, @cache.read("foo").to_i - assert_nil @cache.decrement("bar") - end -end - -module LocalCacheBehavior - def test_local_writes_are_persistent_on_the_remote_cache - retval = @cache.with_local_cache do - @cache.write("foo", "bar") - end - assert retval - assert_equal "bar", @cache.read("foo") - end - - def test_clear_also_clears_local_cache - @cache.with_local_cache do - @cache.write("foo", "bar") - @cache.clear - assert_nil @cache.read("foo") - end - - assert_nil @cache.read("foo") - end - - def test_local_cache_of_write - @cache.with_local_cache do - @cache.write("foo", "bar") - @peek.delete("foo") - assert_equal "bar", @cache.read("foo") - end - end - - def test_local_cache_of_read - @cache.write("foo", "bar") - @cache.with_local_cache do - assert_equal "bar", @cache.read("foo") - end - end - - def test_local_cache_of_read_nil - @cache.with_local_cache do - assert_nil @cache.read("foo") - @cache.send(:bypass_local_cache) { @cache.write "foo", "bar" } - assert_nil @cache.read("foo") - end - end - - def test_local_cache_fetch - @cache.with_local_cache do - @cache.send(:local_cache).write "foo", "bar" - assert_equal "bar", @cache.send(:local_cache).fetch("foo") - end - end - - def test_local_cache_of_write_nil - @cache.with_local_cache do - assert @cache.write("foo", nil) - assert_nil @cache.read("foo") - @peek.write("foo", "bar") - assert_nil @cache.read("foo") - end - end - - def test_local_cache_of_delete - @cache.with_local_cache do - @cache.write("foo", "bar") - @cache.delete("foo") - assert_nil @cache.read("foo") - end - end - - def test_local_cache_of_exist - @cache.with_local_cache do - @cache.write("foo", "bar") - @peek.delete("foo") - assert @cache.exist?("foo") - end - end - - def test_local_cache_of_increment - @cache.with_local_cache do - @cache.write("foo", 1, raw: true) - @peek.write("foo", 2, raw: true) - @cache.increment("foo") - assert_equal 3, @cache.read("foo") - end - end - - def test_local_cache_of_decrement - @cache.with_local_cache do - @cache.write("foo", 1, raw: true) - @peek.write("foo", 3, raw: true) - @cache.decrement("foo") - assert_equal 2, @cache.read("foo") - end - end - - def test_middleware - app = lambda { |env| - result = @cache.write("foo", "bar") - assert_equal "bar", @cache.read("foo") # make sure 'foo' was written - assert result - [200, {}, []] - } - app = @cache.middleware.new(app) - app.call({}) - end -end - -module AutoloadingCacheBehavior - include DependenciesTestHelpers - def test_simple_autoloading - with_autoloading_fixtures do - @cache.write("foo", EM.new) - end - - remove_constants(:EM) - ActiveSupport::Dependencies.clear - - with_autoloading_fixtures do - assert_kind_of EM, @cache.read("foo") - end - - remove_constants(:EM) - ActiveSupport::Dependencies.clear - end - - def test_two_classes_autoloading - with_autoloading_fixtures do - @cache.write("foo", [EM.new, ClassFolder.new]) - end - - remove_constants(:EM, :ClassFolder) - ActiveSupport::Dependencies.clear - - with_autoloading_fixtures do - loaded = @cache.read("foo") - assert_kind_of Array, loaded - assert_equal 2, loaded.size - assert_kind_of EM, loaded[0] - assert_kind_of ClassFolder, loaded[1] - end - - remove_constants(:EM, :ClassFolder) - ActiveSupport::Dependencies.clear - end -end - -class FileStoreTest < ActiveSupport::TestCase - def setup - Dir.mkdir(cache_dir) unless File.exist?(cache_dir) - @cache = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, expires_in: 60) - @peek = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, expires_in: 60) - @cache_with_pathname = ActiveSupport::Cache.lookup_store(:file_store, Pathname.new(cache_dir), expires_in: 60) - - @buffer = StringIO.new - @cache.logger = ActiveSupport::Logger.new(@buffer) - end - - def teardown - FileUtils.rm_r(cache_dir) - rescue Errno::ENOENT - end - - def cache_dir - File.join(Dir.pwd, "tmp_cache") - end - - include CacheStoreBehavior - include LocalCacheBehavior - include CacheDeleteMatchedBehavior - include CacheIncrementDecrementBehavior - include AutoloadingCacheBehavior - - def test_clear - gitkeep = File.join(cache_dir, ".gitkeep") - keep = File.join(cache_dir, ".keep") - FileUtils.touch([gitkeep, keep]) - @cache.clear - assert File.exist?(gitkeep) - assert File.exist?(keep) - end - - def test_clear_without_cache_dir - FileUtils.rm_r(cache_dir) - @cache.clear - end - - def test_long_uri_encoded_keys - @cache.write("%" * 870, 1) - assert_equal 1, @cache.read("%" * 870) - end - - def test_key_transformation - key = @cache.send(:normalize_key, "views/index?id=1", {}) - assert_equal "views/index?id=1", @cache.send(:file_path_key, key) - end - - def test_key_transformation_with_pathname - FileUtils.touch(File.join(cache_dir, "foo")) - key = @cache_with_pathname.send(:normalize_key, "views/index?id=1", {}) - assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key) - end - - # Test that generated cache keys are short enough to have Tempfile stuff added to them and - # remain valid - def test_filename_max_size - key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}" - path = @cache.send(:normalize_key, key, {}) - Dir::Tmpname.create(path) do |tmpname, n, opts| - assert File.basename(tmpname + ".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname + '.lock').length}" - end - end - - # Because file systems have a maximum filename size, filenames > max size should be split in to directories - # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B - def test_key_transformation_max_filename_size - key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B" - path = @cache.send(:normalize_key, key, {}) - assert path.split("/").all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE } - assert_equal "B", File.basename(path) - end - - # If nothing has been stored in the cache, there is a chance the cache directory does not yet exist - # Ensure delete_matched gracefully handles this case - def test_delete_matched_when_cache_directory_does_not_exist - assert_nothing_raised do - ActiveSupport::Cache::FileStore.new("/test/cache/directory").delete_matched(/does_not_exist/) - end - end - - def test_delete_does_not_delete_empty_parent_dir - sub_cache_dir = File.join(cache_dir, "subdir/") - sub_cache_store = ActiveSupport::Cache::FileStore.new(sub_cache_dir) - assert_nothing_raised do - assert sub_cache_store.write("foo", "bar") - assert sub_cache_store.delete("foo") - end - assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!" - assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!" - assert Dir.entries(sub_cache_dir).reject { |f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f) }.empty? - end - - def test_log_exception_when_cache_read_fails - File.stub(:exist?, -> { raise StandardError.new("failed") }) do - @cache.send(:read_entry, "winston", {}) - assert @buffer.string.present? - end - end - - def test_cleanup_removes_all_expired_entries - time = Time.now - @cache.write("foo", "bar", expires_in: 10) - @cache.write("baz", "qux") - @cache.write("quux", "corge", expires_in: 20) - Time.stub(:now, time + 15) do - @cache.cleanup - assert_not @cache.exist?("foo") - assert @cache.exist?("baz") - assert @cache.exist?("quux") - end - end - - def test_write_with_unless_exist - assert_equal true, @cache.write(1, "aaaaaaaaaa") - assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) - @cache.write(1, nil) - assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) - end -end - -class MemoryStoreTest < ActiveSupport::TestCase - def setup - @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa")) - @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1) - end - - include CacheStoreBehavior - include CacheDeleteMatchedBehavior - include CacheIncrementDecrementBehavior - - def test_prune_size - @cache.write(1, "aaaaaaaaaa") && sleep(0.001) - @cache.write(2, "bbbbbbbbbb") && sleep(0.001) - @cache.write(3, "cccccccccc") && sleep(0.001) - @cache.write(4, "dddddddddd") && sleep(0.001) - @cache.write(5, "eeeeeeeeee") && sleep(0.001) - @cache.read(2) && sleep(0.001) - @cache.read(4) - @cache.prune(@record_size * 3) - assert @cache.exist?(5) - assert @cache.exist?(4) - assert !@cache.exist?(3), "no entry" - assert @cache.exist?(2) - assert !@cache.exist?(1), "no entry" - end - - def test_prune_size_on_write - @cache.write(1, "aaaaaaaaaa") && sleep(0.001) - @cache.write(2, "bbbbbbbbbb") && sleep(0.001) - @cache.write(3, "cccccccccc") && sleep(0.001) - @cache.write(4, "dddddddddd") && sleep(0.001) - @cache.write(5, "eeeeeeeeee") && sleep(0.001) - @cache.write(6, "ffffffffff") && sleep(0.001) - @cache.write(7, "gggggggggg") && sleep(0.001) - @cache.write(8, "hhhhhhhhhh") && sleep(0.001) - @cache.write(9, "iiiiiiiiii") && sleep(0.001) - @cache.write(10, "kkkkkkkkkk") && sleep(0.001) - @cache.read(2) && sleep(0.001) - @cache.read(4) && sleep(0.001) - @cache.write(11, "llllllllll") - assert @cache.exist?(11) - assert @cache.exist?(10) - assert @cache.exist?(9) - assert @cache.exist?(8) - assert @cache.exist?(7) - assert !@cache.exist?(6), "no entry" - assert !@cache.exist?(5), "no entry" - assert @cache.exist?(4) - assert !@cache.exist?(3), "no entry" - assert @cache.exist?(2) - assert !@cache.exist?(1), "no entry" - end - - def test_prune_size_on_write_based_on_key_length - @cache.write(1, "aaaaaaaaaa") && sleep(0.001) - @cache.write(2, "bbbbbbbbbb") && sleep(0.001) - @cache.write(3, "cccccccccc") && sleep(0.001) - @cache.write(4, "dddddddddd") && sleep(0.001) - @cache.write(5, "eeeeeeeeee") && sleep(0.001) - @cache.write(6, "ffffffffff") && sleep(0.001) - @cache.write(7, "gggggggggg") && sleep(0.001) - @cache.write(8, "hhhhhhhhhh") && sleep(0.001) - @cache.write(9, "iiiiiiiiii") && sleep(0.001) - long_key = "*" * 2 * @record_size - @cache.write(long_key, "llllllllll") - assert @cache.exist?(long_key) - assert @cache.exist?(9) - assert @cache.exist?(8) - assert @cache.exist?(7) - assert @cache.exist?(6) - assert !@cache.exist?(5), "no entry" - assert !@cache.exist?(4), "no entry" - assert !@cache.exist?(3), "no entry" - assert !@cache.exist?(2), "no entry" - assert !@cache.exist?(1), "no entry" - end - - def test_pruning_is_capped_at_a_max_time - def @cache.delete_entry(*args) - sleep(0.01) - super - end - @cache.write(1, "aaaaaaaaaa") && sleep(0.001) - @cache.write(2, "bbbbbbbbbb") && sleep(0.001) - @cache.write(3, "cccccccccc") && sleep(0.001) - @cache.write(4, "dddddddddd") && sleep(0.001) - @cache.write(5, "eeeeeeeeee") && sleep(0.001) - @cache.prune(30, 0.001) - assert @cache.exist?(5) - assert @cache.exist?(4) - assert @cache.exist?(3) - assert @cache.exist?(2) - assert !@cache.exist?(1) - end - - def test_write_with_unless_exist - assert_equal true, @cache.write(1, "aaaaaaaaaa") - assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) - @cache.write(1, nil) - assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true) - end -end - -class MemCacheStoreTest < ActiveSupport::TestCase - require "dalli" - - begin - ss = Dalli::Client.new("localhost:11211").stats - raise Dalli::DalliError unless ss["localhost:11211"] - - MEMCACHE_UP = true - rescue Dalli::DalliError - $stderr.puts "Skipping memcached tests. Start memcached and try again." - MEMCACHE_UP = false - end - - def setup - skip "memcache server is not up" unless MEMCACHE_UP - - @cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, expires_in: 60) - @peek = ActiveSupport::Cache.lookup_store(:mem_cache_store) - @data = @cache.instance_variable_get(:@data) - @cache.clear - @cache.silence! - @cache.logger = ActiveSupport::Logger.new("/dev/null") - end - - include CacheStoreBehavior - include LocalCacheBehavior - include CacheIncrementDecrementBehavior - include EncodedKeyCacheBehavior - include AutoloadingCacheBehavior - - def test_raw_values - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) - cache.clear - cache.write("foo", 2) - assert_equal "2", cache.read("foo") - end - - def test_raw_values_with_marshal - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) - cache.clear - cache.write("foo", Marshal.dump([])) - assert_equal [], cache.read("foo") - end - - def test_local_cache_raw_values - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) - cache.clear - cache.with_local_cache do - cache.write("foo", 2) - assert_equal "2", cache.read("foo") - end - end - - def test_local_cache_raw_values_with_marshal - cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, raw: true) - cache.clear - cache.with_local_cache do - cache.write("foo", Marshal.dump([])) - assert_equal [], cache.read("foo") - end - end - - def test_read_should_return_a_different_object_id_each_time_it_is_called - @cache.write("foo", "bar") - value = @cache.read("foo") - assert_not_equal value.object_id, @cache.read("foo").object_id - value << "bingo" - assert_not_equal value, @cache.read("foo") - end -end - -class NullStoreTest < ActiveSupport::TestCase - def setup - @cache = ActiveSupport::Cache.lookup_store(:null_store) - end - - def test_clear - @cache.clear - end - - def test_cleanup - @cache.cleanup - end - - def test_write - assert_equal true, @cache.write("name", "value") - end - - def test_read - @cache.write("name", "value") - assert_nil @cache.read("name") - end - - def test_delete - @cache.write("name", "value") - assert_equal false, @cache.delete("name") - end - - def test_increment - @cache.write("name", 1, raw: true) - assert_nil @cache.increment("name") - end - - def test_decrement - @cache.write("name", 1, raw: true) - assert_nil @cache.increment("name") - end - - def test_delete_matched - @cache.write("name", "value") - @cache.delete_matched(/name/) - end - - def test_local_store_strategy - @cache.with_local_cache do - @cache.write("name", "value") - assert_equal "value", @cache.read("name") - @cache.delete("name") - assert_nil @cache.read("name") - @cache.write("name", "value") - end - assert_nil @cache.read("name") - end -end - -class CacheStoreLoggerTest < ActiveSupport::TestCase - def setup - @cache = ActiveSupport::Cache.lookup_store(:memory_store) - - @buffer = StringIO.new - @cache.logger = ActiveSupport::Logger.new(@buffer) - end - - def test_logging - @cache.fetch("foo") { "bar" } - assert @buffer.string.present? - end - - def test_log_with_string_namespace - @cache.fetch("foo", namespace: "string_namespace") { "bar" } - assert_match %r{string_namespace:foo}, @buffer.string - end - - def test_log_with_proc_namespace - proc = Proc.new do - "proc_namespace" - end - @cache.fetch("foo", namespace: proc) { "bar" } - assert_match %r{proc_namespace:foo}, @buffer.string - end - - def test_mute_logging - @cache.mute { @cache.fetch("foo") { "bar" } } - assert @buffer.string.blank? - end -end - -class CacheEntryTest < ActiveSupport::TestCase - def test_expired - entry = ActiveSupport::Cache::Entry.new("value") - assert !entry.expired?, "entry not expired" - entry = ActiveSupport::Cache::Entry.new("value", expires_in: 60) - assert !entry.expired?, "entry not expired" - Time.stub(:now, Time.now + 61) do - assert entry.expired?, "entry is expired" - end - end - - def test_compress_values - value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value, compress: true, compress_threshold: 1) - assert_equal value, entry.value - assert(value.bytesize > entry.size, "value is compressed") - end - - def test_non_compress_values - value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value) - assert_equal value, entry.value - assert_equal value.bytesize, entry.size - end -end diff --git a/activesupport/test/core_ext/class/attribute_test.rb b/activesupport/test/core_ext/class/attribute_test.rb index 5a9ec78cc1..f16043c612 100644 --- a/activesupport/test/core_ext/class/attribute_test.rb +++ b/activesupport/test/core_ext/class/attribute_test.rb @@ -3,7 +3,11 @@ require "active_support/core_ext/class/attribute" class ClassAttributeTest < ActiveSupport::TestCase def setup - @klass = Class.new { class_attribute :setting } + @klass = Class.new do + class_attribute :setting + class_attribute :timeout, default: 5 + end + @sub = Class.new(@klass) end @@ -12,6 +16,10 @@ class ClassAttributeTest < ActiveSupport::TestCase assert_nil @sub.setting end + test "custom default" do + assert_equal 5, @klass.timeout + end + test "inheritable" do @klass.setting = 1 assert_equal 1, @sub.setting diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index be7c14e9b4..276fa2bfd3 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -28,6 +28,28 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase end end + def test_next_occur + datetime = DateTime.new(2016, 9, 24, 0, 0) # saturday + assert_equal datetime.next_occurring(:monday), datetime.since(2.days) + assert_equal datetime.next_occurring(:tuesday), datetime.since(3.days) + assert_equal datetime.next_occurring(:wednesday), datetime.since(4.days) + assert_equal datetime.next_occurring(:thursday), datetime.since(5.days) + assert_equal datetime.next_occurring(:friday), datetime.since(6.days) + assert_equal datetime.next_occurring(:saturday), datetime.since(1.week) + assert_equal datetime.next_occurring(:sunday), datetime.since(1.day) + end + + def test_prev_occur + datetime = DateTime.new(2016, 9, 24, 0, 0) # saturday + assert_equal datetime.prev_occurring(:monday), datetime.ago(5.days) + assert_equal datetime.prev_occurring(:tuesday), datetime.ago(4.days) + assert_equal datetime.prev_occurring(:wednesday), datetime.ago(3.days) + assert_equal datetime.prev_occurring(:thursday), datetime.ago(2.days) + assert_equal datetime.prev_occurring(:friday), datetime.ago(1.day) + assert_equal datetime.prev_occurring(:saturday), datetime.ago(1.week) + assert_equal datetime.prev_occurring(:sunday), datetime.ago(6.days) + end + def test_readable_inspect datetime = DateTime.new(2005, 2, 21, 14, 30, 0) assert_equal "Mon, 21 Feb 2005 14:30:00 +0000", datetime.readable_inspect diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 1648a9b270..cd1b505c34 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -315,7 +315,7 @@ class DurationTest < ActiveSupport::TestCase assert_equal(1, scalar <=> 5) assert_equal(0, scalar <=> 10) assert_equal(-1, scalar <=> 15) - assert_equal(nil, scalar <=> "foo") + assert_nil(scalar <=> "foo") end def test_scalar_plus @@ -337,6 +337,13 @@ class DurationTest < ActiveSupport::TestCase assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message end + def test_scalar_plus_parts + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal({ days: 1, seconds: 10 }, (scalar + 1.day).parts) + assert_equal({ days: -1, seconds: 10 }, (scalar + -1.day).parts) + end + def test_scalar_minus scalar = ActiveSupport::Duration::Scalar.new(10) @@ -349,6 +356,9 @@ class DurationTest < ActiveSupport::TestCase assert_equal 5, scalar - 5.seconds assert_instance_of ActiveSupport::Duration, scalar - 5.seconds + assert_equal({ days: -1, seconds: 10 }, (scalar - 1.day).parts) + assert_equal({ days: 1, seconds: 10 }, (scalar - -1.day).parts) + exception = assert_raises(TypeError) do scalar - "foo" end @@ -356,6 +366,13 @@ class DurationTest < ActiveSupport::TestCase assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message end + def test_scalar_minus_parts + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal({ days: -1, seconds: 10 }, (scalar - 1.day).parts) + assert_equal({ days: 1, seconds: 10 }, (scalar - -1.day).parts) + end + def test_scalar_multiply scalar = ActiveSupport::Duration::Scalar.new(5) @@ -375,6 +392,14 @@ class DurationTest < ActiveSupport::TestCase assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message end + def test_scalar_multiply_parts + scalar = ActiveSupport::Duration::Scalar.new(1) + assert_equal({ days: 2 }, (scalar * 2.days).parts) + assert_equal(172800, (scalar * 2.days).value) + assert_equal({ days: -2 }, (scalar * -2.days).parts) + assert_equal(-172800, (scalar * -2.days).value) + end + def test_scalar_divide scalar = ActiveSupport::Duration::Scalar.new(10) @@ -394,6 +419,15 @@ class DurationTest < ActiveSupport::TestCase assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message end + def test_scalar_divide_parts + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal({ days: 2 }, (scalar / 5.days).parts) + assert_equal(172800, (scalar / 5.days).value) + assert_equal({ days: -2 }, (scalar / -5.days).parts) + assert_equal(-172800, (scalar / -5.days).value) + end + def test_twelve_months_equals_one_year assert_equal 12.months, 1.year end diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb index 464a000d59..9b185e9381 100644 --- a/activesupport/test/core_ext/module/attribute_accessor_test.rb +++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb @@ -12,7 +12,14 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase cattr_accessor(:defa) { "default_accessor_value" } cattr_reader(:defr) { "default_reader_value" } cattr_writer(:defw) { "default_writer_value" } + cattr_accessor(:deff) { false } cattr_accessor(:quux) { :quux } + + cattr_accessor :def_accessor, default: "default_accessor_value" + cattr_reader :def_reader, default: "default_reader_value" + cattr_writer :def_writer, default: "default_writer_value" + cattr_accessor :def_false, default: false + cattr_accessor(:def_priority, default: false) { :no_priority } end @class = Class.new @class.instance_eval { include m } @@ -24,6 +31,21 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase assert_nil @object.foo end + def test_mattr_default_keyword_arguments + assert_equal "default_accessor_value", @module.def_accessor + assert_equal "default_reader_value", @module.def_reader + assert_equal "default_writer_value", @module.class_variable_get(:@@def_writer) + end + + def test_mattr_can_default_to_false + assert_equal false, @module.def_false + assert_equal false, @module.deff + end + + def test_mattr_default_priority + assert_equal false, @module.def_priority + end + def test_should_set_mattr_value @module.foo = :test assert_equal :test, @object.foo @@ -91,9 +113,23 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase assert_equal "default_writer_value", @module.class_variable_get("@@defw") end - def test_should_not_invoke_default_value_block_multiple_times + def test_method_invocation_should_not_invoke_the_default_block count = 0 + @module.cattr_accessor(:defcount) { count += 1 } + assert_equal 1, count + assert_no_difference "count" do + @module.defcount + end + end + + def test_declaring_multiple_attributes_at_once_invokes_the_block_multiple_times + count = 0 + + @module.cattr_accessor(:defn1, :defn2) { count += 1 } + + assert_equal 1, @module.defn1 + assert_equal 2, @module.defn2 end end diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index 085fd6592d..a4d4444d69 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -348,15 +348,15 @@ class ModuleTest < ActiveSupport::TestCase assert has_block.hello? end - def test_delegate_to_missing_with_method + def test_delegate_missing_to_with_method assert_equal "David", DecoratedTester.new(@david).name end - def test_delegate_to_missing_with_reserved_methods + def test_delegate_missing_to_with_reserved_methods assert_equal "David", DecoratedReserved.new(@david).name end - def test_delegate_to_missing_does_not_delegate_to_private_methods + def test_delegate_missing_to_does_not_delegate_to_private_methods e = assert_raises(NoMethodError) do DecoratedReserved.new(@david).private_name end @@ -364,7 +364,7 @@ class ModuleTest < ActiveSupport::TestCase assert_match(/undefined method `private_name' for/, e.message) end - def test_delegate_to_missing_does_not_delegate_to_fake_methods + def test_delegate_missing_to_does_not_delegate_to_fake_methods e = assert_raises(NoMethodError) do DecoratedReserved.new(@david).my_fake_method end @@ -372,7 +372,7 @@ class ModuleTest < ActiveSupport::TestCase assert_match(/undefined method `my_fake_method' for/, e.message) end - def test_delegate_to_missing_affects_respond_to + def test_delegate_missing_to_affects_respond_to assert DecoratedTester.new(@david).respond_to?(:name) assert_not DecoratedTester.new(@david).respond_to?(:private_name) assert_not DecoratedTester.new(@david).respond_to?(:my_fake_method) @@ -382,7 +382,7 @@ class ModuleTest < ActiveSupport::TestCase assert_not DecoratedTester.new(@david).respond_to?(:my_fake_method, true) end - def test_delegate_to_missing_respects_superclass_missing + def test_delegate_missing_to_respects_superclass_missing assert_equal 42, DecoratedTester.new(@david).extra_missing assert_respond_to DecoratedTester.new(@david), :extra_missing diff --git a/activesupport/test/current_attributes_test.rb b/activesupport/test/current_attributes_test.rb new file mode 100644 index 0000000000..67ef6ef619 --- /dev/null +++ b/activesupport/test/current_attributes_test.rb @@ -0,0 +1,96 @@ +require "abstract_unit" + +class CurrentAttributesTest < ActiveSupport::TestCase + Person = Struct.new(:name, :time_zone) + + class Current < ActiveSupport::CurrentAttributes + attribute :world, :account, :person, :request + delegate :time_zone, to: :person + + resets { Time.zone = "UTC" } + + def account=(account) + super + self.person = "#{account}'s person" + end + + def person=(person) + super + Time.zone = person.try(:time_zone) + end + + def request + "#{super} something" + end + + def intro + "#{person.name}, in #{time_zone}" + end + end + + setup { Current.reset } + + test "read and write attribute" do + Current.world = "world/1" + assert_equal "world/1", Current.world + end + + test "read overwritten attribute method" do + Current.request = "request/1" + assert_equal "request/1 something", Current.request + end + + test "set attribute via overwritten method" do + Current.account = "account/1" + assert_equal "account/1", Current.account + assert_equal "account/1's person", Current.person + end + + test "set auxiliary class via overwritten method" do + Current.person = Person.new("David", "Central Time (US & Canada)") + assert_equal "Central Time (US & Canada)", Time.zone.name + end + + test "resets auxiliary class via callback" do + Current.person = Person.new("David", "Central Time (US & Canada)") + assert_equal "Central Time (US & Canada)", Time.zone.name + + Current.reset + assert_equal "UTC", Time.zone.name + end + + test "set attribute only via scope" do + Current.world = "world/1" + + Current.set(world: "world/2") do + assert_equal "world/2", Current.world + end + + assert_equal "world/1", Current.world + end + + test "set multiple attributes" do + Current.world = "world/1" + Current.account = "account/1" + + Current.set(world: "world/2", account: "account/2") do + assert_equal "world/2", Current.world + assert_equal "account/2", Current.account + end + + assert_equal "world/1", Current.world + assert_equal "account/1", Current.account + end + + test "delegation" do + Current.person = Person.new("David", "Central Time (US & Canada)") + assert_equal "Central Time (US & Canada)", Current.time_zone + assert_equal "Central Time (US & Canada)", Current.instance.time_zone + end + + test "all methods forward to the instance" do + Current.person = Person.new("David", "Central Time (US & Canada)") + assert_equal "David, in Central Time (US & Canada)", Current.intro + assert_equal "David, in Central Time (US & Canada)", Current.instance.intro + end +end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index e38d4e83e5..1ea36418ff 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -104,7 +104,7 @@ class DependenciesTest < ActiveSupport::TestCase with_loading "dependencies" do old_warnings, ActiveSupport::Dependencies.warnings_on_first_load = ActiveSupport::Dependencies.warnings_on_first_load, true filename = "check_warnings" - expanded = File.expand_path("#{File.dirname(__FILE__)}/dependencies/#{filename}") + expanded = File.expand_path("dependencies/#{filename}", __dir__) $check_warnings_load_count = 0 assert_not ActiveSupport::Dependencies.loaded.include?(expanded) @@ -293,7 +293,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_doesnt_break_normal_require - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) with_autoloading_fixtures do @@ -312,7 +312,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_doesnt_break_normal_require_nested - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -332,7 +332,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_require_returns_true_when_file_not_yet_required - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -345,7 +345,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_require_returns_true_when_file_not_yet_required_even_when_no_new_constants_added - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -359,7 +359,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_require_returns_false_when_file_already_required - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -379,7 +379,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_load_returns_true_when_file_found - path = File.expand_path("../autoloading_fixtures/load_path", __FILE__) + path = File.expand_path("autoloading_fixtures/load_path", __dir__) original_path = $:.dup $:.push(path) @@ -438,7 +438,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_loadable_constants_for_path_should_handle_relative_paths fake_root = "dependencies" - relative_root = File.dirname(__FILE__) + "/dependencies" + relative_root = File.expand_path("dependencies", __dir__) ["", "/"].each do |suffix| with_loading fake_root + suffix do assert_equal ["A::B"], ActiveSupport::Dependencies.loadable_constants_for_path(relative_root + "/a/b") @@ -463,7 +463,7 @@ class DependenciesTest < ActiveSupport::TestCase end def test_loadable_constants_with_load_path_without_trailing_slash - path = File.dirname(__FILE__) + "/autoloading_fixtures/class_folder/inline_class.rb" + path = File.expand_path("autoloading_fixtures/class_folder/inline_class.rb", __dir__) with_loading "autoloading_fixtures/class/" do assert_equal [], ActiveSupport::Dependencies.loadable_constants_for_path(path) end @@ -991,7 +991,7 @@ class DependenciesTest < ActiveSupport::TestCase def test_remove_constant_does_not_trigger_loading_autoloads constant = "ShouldNotBeAutoloaded" Object.class_eval do - autoload constant, File.expand_path("../autoloading_fixtures/should_not_be_required", __FILE__) + autoload constant, File.expand_path("autoloading_fixtures/should_not_be_required", __dir__) end assert_nil ActiveSupport::Dependencies.remove_constant(constant), "Kernel#autoload has been triggered by remove_constant" diff --git a/activesupport/test/dependencies_test_helpers.rb b/activesupport/test/dependencies_test_helpers.rb index 9bc63ed89e..451195a143 100644 --- a/activesupport/test/dependencies_test_helpers.rb +++ b/activesupport/test/dependencies_test_helpers.rb @@ -1,7 +1,7 @@ module DependenciesTestHelpers def with_loading(*from) old_mechanism, ActiveSupport::Dependencies.mechanism = ActiveSupport::Dependencies.mechanism, :load - this_dir = File.dirname(__FILE__) + this_dir = __dir__ parent_dir = File.dirname(this_dir) path_copy = $LOAD_PATH.dup $LOAD_PATH.unshift(parent_dir) unless $LOAD_PATH.include?(parent_dir) diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 14bc10513b..ef956eda90 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -420,6 +420,8 @@ class InflectorTest < ActiveSupport::TestCase inflect.singular(/es$/, "") inflect.irregular("el", "los") + + inflect.uncountable("agua") end assert_equal("hijos", "hijo".pluralize(:es)) @@ -432,12 +434,17 @@ class InflectorTest < ActiveSupport::TestCase assert_equal("los", "el".pluralize(:es)) assert_equal("els", "el".pluralize) + assert_equal("agua", "agua".pluralize(:es)) + assert_equal("aguas", "agua".pluralize) + ActiveSupport::Inflector.inflections(:es) { |inflect| inflect.clear } assert ActiveSupport::Inflector.inflections(:es).plurals.empty? assert ActiveSupport::Inflector.inflections(:es).singulars.empty? + assert ActiveSupport::Inflector.inflections(:es).uncountables.empty? assert !ActiveSupport::Inflector.inflections.plurals.empty? assert !ActiveSupport::Inflector.inflections.singulars.empty? + assert !ActiveSupport::Inflector.inflections.uncountables.empty? end def test_clear_all diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb index 56a436f751..4c3515b5e1 100644 --- a/activesupport/test/message_encryptor_test.rb +++ b/activesupport/test/message_encryptor_test.rb @@ -86,20 +86,32 @@ class MessageEncryptorTest < ActiveSupport::TestCase assert_equal @data, encryptor.decrypt_and_verify(message) end + def test_aead_mode_with_hmac_cbc_cipher_text + encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") + + assert_aead_not_decrypted(encryptor, "eHdGeExnZEwvMSt3U3dKaFl1WFo0TjVvYzA0eGpjbm5WSkt5MXlsNzhpZ0ZnbWhBWFlQZTRwaXE1bVJCS2oxMDZhYVp2dVN3V0lNZUlWQ3c2eVhQbnhnVjFmeVVubmhRKzF3WnZyWHVNMDg9LS1HSisyakJVSFlPb05ISzRMaXRzcFdBPT0=--831a1d54a3cda8a0658dc668a03dedcbce13b5ca") + end + def test_messing_with_aead_values_causes_failures encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") text, iv, auth_tag = encryptor.encrypt_and_sign(@data).split("--") - assert_not_decrypted([iv, text, auth_tag] * "--") - assert_not_decrypted([munge(text), iv, auth_tag] * "--") - assert_not_decrypted([text, munge(iv), auth_tag] * "--") - assert_not_decrypted([text, iv, munge(auth_tag)] * "--") - assert_not_decrypted([munge(text), munge(iv), munge(auth_tag)] * "--") - assert_not_decrypted([text, iv] * "--") - assert_not_decrypted([text, iv, auth_tag[0..-2]] * "--") + assert_aead_not_decrypted(encryptor, [iv, text, auth_tag] * "--") + assert_aead_not_decrypted(encryptor, [munge(text), iv, auth_tag] * "--") + assert_aead_not_decrypted(encryptor, [text, munge(iv), auth_tag] * "--") + assert_aead_not_decrypted(encryptor, [text, iv, munge(auth_tag)] * "--") + assert_aead_not_decrypted(encryptor, [munge(text), munge(iv), munge(auth_tag)] * "--") + assert_aead_not_decrypted(encryptor, [text, iv] * "--") + assert_aead_not_decrypted(encryptor, [text, iv, auth_tag[0..-2]] * "--") end private + def assert_aead_not_decrypted(encryptor, value) + assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do + encryptor.decrypt_and_verify(value) + end + end + def assert_not_decrypted(value) assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do @encryptor.decrypt_and_verify(@verifier.generate(value)) diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index dc0c34d4e2..4caf1428ea 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -321,12 +321,18 @@ module ActiveSupport gangster = { hundred: "hundred bucks", million: "thousand quids" } assert_equal "1 hundred bucks", number_helper.number_to_human(100, units: gangster) assert_equal "25 hundred bucks", number_helper.number_to_human(2500, units: gangster) + assert_equal "1000 hundred bucks", number_helper.number_to_human(100_000, units: gangster) + assert_equal "1 thousand quids", number_helper.number_to_human(999_999, units: gangster) + assert_equal "1 thousand quids", number_helper.number_to_human(1_000_000, units: gangster) assert_equal "25 thousand quids", number_helper.number_to_human(25000000, units: gangster) assert_equal "12300 thousand quids", number_helper.number_to_human(12345000000, units: gangster) #Spaces are stripped from the resulting string assert_equal "4", number_helper.number_to_human(4, units: { unit: "", ten: "tens " }) assert_equal "4.5 tens", number_helper.number_to_human(45, units: { unit: "", ten: " tens " }) + + #Uses only the provided units and does not try to use larger ones + assert_equal "1000 kilometers", number_helper.number_to_human(1_000_000, units: { unit: "meter", thousand: "kilometers" }) end end diff --git a/activesupport/test/rescuable_test.rb b/activesupport/test/rescuable_test.rb index f7eb047d44..3bdd1651e7 100644 --- a/activesupport/test/rescuable_test.rb +++ b/activesupport/test/rescuable_test.rb @@ -43,7 +43,9 @@ class Stargate def dispatch(method) send(method) rescue Exception => e - rescue_with_handler(e) + unless rescue_with_handler(e) + @result = "unhandled" + end end def attack @@ -58,6 +60,26 @@ class Stargate raise MadRonon.new("dex") end + def crash + raise "unhandled RuntimeError" + end + + def looped_crash + ex1 = StandardError.new("error 1") + ex2 = StandardError.new("error 2") + begin + begin + raise ex1 + rescue + # sets the cause on ex2 to be ex1 + raise ex2 + end + rescue + # sets the cause on ex1 to be ex2 + raise ex1 + end + end + def fall_back_to_cause # This exception is the cause and has a handler. ronanize @@ -139,4 +161,14 @@ class RescuableTest < ActiveSupport::TestCase @stargate.dispatch :fall_back_to_cause assert_equal "dex", @stargate.result end + + def test_unhandled_exceptions + @stargate.dispatch(:crash) + assert_equal "unhandled", @stargate.result + end + + def test_rescue_handles_loops_in_exception_cause_chain + @stargate.dispatch :looped_crash + assert_equal "unhandled", @stargate.result + end end diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index af7fc44d66..40dfbe2542 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -237,9 +237,6 @@ class AssertDifferenceTest < ActiveSupport::TestCase end end -class AlsoDoingNothingTest < ActiveSupport::TestCase -end - # Setup and teardown callbacks. class SetupAndTeardownTest < ActiveSupport::TestCase setup :reset_callback_record, :foo diff --git a/activesupport/test/testing/file_fixtures_test.rb b/activesupport/test/testing/file_fixtures_test.rb index e44fe58ce9..9f28252c31 100644 --- a/activesupport/test/testing/file_fixtures_test.rb +++ b/activesupport/test/testing/file_fixtures_test.rb @@ -3,12 +3,12 @@ require "abstract_unit" require "pathname" class FileFixturesTest < ActiveSupport::TestCase - self.file_fixture_path = File.expand_path("../../file_fixtures", __FILE__) + self.file_fixture_path = File.expand_path("../file_fixtures", __dir__) test "#file_fixture returns Pathname to file fixture" do path = file_fixture("sample.txt") assert_kind_of Pathname, path - assert_match %r{.*/test/file_fixtures/sample.txt$}, path.to_s + assert_match %r{.*/test/file_fixtures/sample\.txt$}, path.to_s end test "raises an exception when the fixture file does not exist" do @@ -20,11 +20,11 @@ class FileFixturesTest < ActiveSupport::TestCase end class FileFixturesPathnameDirectoryTest < ActiveSupport::TestCase - self.file_fixture_path = Pathname.new(File.expand_path("../../file_fixtures", __FILE__)) + self.file_fixture_path = Pathname.new(File.expand_path("../file_fixtures", __dir__)) test "#file_fixture_path returns Pathname to file fixture" do path = file_fixture("sample.txt") assert_kind_of Pathname, path - assert_match %r{.*/test/file_fixtures/sample.txt$}, path.to_s + assert_match %r{.*/test/file_fixtures/sample\.txt$}, path.to_s end end diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb index e0d3fb0cf5..9d354f14f4 100644 --- a/activesupport/test/time_travel_test.rb +++ b/activesupport/test/time_travel_test.rb @@ -99,7 +99,7 @@ class TimeTravelTest < ActiveSupport::TestCase #noop end end - assert_match(/Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing./, e.message) + assert_match(/Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing\./, e.message) end end end diff --git a/activesupport/test/xml_mini/jdom_engine_test.rb b/activesupport/test/xml_mini/jdom_engine_test.rb index e783cea67c..fc35ac113b 100644 --- a/activesupport/test/xml_mini/jdom_engine_test.rb +++ b/activesupport/test/xml_mini/jdom_engine_test.rb @@ -2,7 +2,7 @@ require_relative "xml_mini_engine_test" XMLMiniEngineTest.run_with_platform("java") do class JDOMEngineTest < XMLMiniEngineTest - FILES_DIR = File.dirname(__FILE__) + "/../fixtures/xml" + FILES_DIR = File.expand_path("../fixtures/xml", __dir__) def test_not_allowed_to_expand_entities_to_files attack_xml = <<-EOT diff --git a/guides/assets/images/belongs_to.png b/guides/assets/images/belongs_to.png Binary files differindex 077d237e4e..1a9926e578 100644 --- a/guides/assets/images/belongs_to.png +++ b/guides/assets/images/belongs_to.png diff --git a/guides/assets/images/getting_started/article_with_comments.png b/guides/assets/images/getting_started/article_with_comments.png Binary files differindex c489e4c00e..3f16f3b280 100644 --- a/guides/assets/images/getting_started/article_with_comments.png +++ b/guides/assets/images/getting_started/article_with_comments.png diff --git a/guides/assets/images/getting_started/challenge.png b/guides/assets/images/getting_started/challenge.png Binary files differindex 5b88a842b2..d05ef31bbe 100644 --- a/guides/assets/images/getting_started/challenge.png +++ b/guides/assets/images/getting_started/challenge.png diff --git a/guides/assets/images/getting_started/confirm_dialog.png b/guides/assets/images/getting_started/confirm_dialog.png Binary files differindex 9755f581a6..ce65734e6c 100644 --- a/guides/assets/images/getting_started/confirm_dialog.png +++ b/guides/assets/images/getting_started/confirm_dialog.png diff --git a/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png b/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png Binary files differindex 9f32c68472..50b178808e 100644 --- a/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png +++ b/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png diff --git a/guides/assets/images/getting_started/form_with_errors.png b/guides/assets/images/getting_started/form_with_errors.png Binary files differindex 98bff37d4a..6eefd2885a 100644 --- a/guides/assets/images/getting_started/form_with_errors.png +++ b/guides/assets/images/getting_started/form_with_errors.png diff --git a/guides/assets/images/getting_started/index_action_with_edit_link.png b/guides/assets/images/getting_started/index_action_with_edit_link.png Binary files differindex 0566a3ffde..a2a087a598 100644 --- a/guides/assets/images/getting_started/index_action_with_edit_link.png +++ b/guides/assets/images/getting_started/index_action_with_edit_link.png diff --git a/guides/assets/images/getting_started/new_article.png b/guides/assets/images/getting_started/new_article.png Binary files differindex bd3ae4fa67..6edcc161b6 100644 --- a/guides/assets/images/getting_started/new_article.png +++ b/guides/assets/images/getting_started/new_article.png diff --git a/guides/assets/images/getting_started/rails_welcome.png b/guides/assets/images/getting_started/rails_welcome.png Binary files differindex baccb11322..44f89ec8de 100644 --- a/guides/assets/images/getting_started/rails_welcome.png +++ b/guides/assets/images/getting_started/rails_welcome.png diff --git a/guides/assets/images/getting_started/routing_error_no_controller.png b/guides/assets/images/getting_started/routing_error_no_controller.png Binary files differindex ed62862291..52150f0426 100644 --- a/guides/assets/images/getting_started/routing_error_no_controller.png +++ b/guides/assets/images/getting_started/routing_error_no_controller.png diff --git a/guides/assets/images/getting_started/show_action_for_articles.png b/guides/assets/images/getting_started/show_action_for_articles.png Binary files differindex 4dad704f89..68837131f7 100644 --- a/guides/assets/images/getting_started/show_action_for_articles.png +++ b/guides/assets/images/getting_started/show_action_for_articles.png diff --git a/guides/assets/images/getting_started/template_is_missing_articles_new.png b/guides/assets/images/getting_started/template_is_missing_articles_new.png Binary files differindex f4f054f3c6..a1603f5d28 100644 --- a/guides/assets/images/getting_started/template_is_missing_articles_new.png +++ b/guides/assets/images/getting_started/template_is_missing_articles_new.png diff --git a/guides/assets/images/getting_started/unknown_action_create_for_articles.png b/guides/assets/images/getting_started/unknown_action_create_for_articles.png Binary files differindex fd20cd53dc..ec4758e085 100644 --- a/guides/assets/images/getting_started/unknown_action_create_for_articles.png +++ b/guides/assets/images/getting_started/unknown_action_create_for_articles.png diff --git a/guides/assets/images/getting_started/unknown_action_new_for_articles.png b/guides/assets/images/getting_started/unknown_action_new_for_articles.png Binary files differindex e948a51e4a..f7e7464d61 100644 --- a/guides/assets/images/getting_started/unknown_action_new_for_articles.png +++ b/guides/assets/images/getting_started/unknown_action_new_for_articles.png diff --git a/guides/assets/images/habtm.png b/guides/assets/images/habtm.png Binary files differindex b062bc73fe..41013b743d 100644 --- a/guides/assets/images/habtm.png +++ b/guides/assets/images/habtm.png diff --git a/guides/assets/images/has_many.png b/guides/assets/images/has_many.png Binary files differindex 79da2613d7..0d67bea38b 100644 --- a/guides/assets/images/has_many.png +++ b/guides/assets/images/has_many.png diff --git a/guides/assets/images/has_many_through.png b/guides/assets/images/has_many_through.png Binary files differindex 858c898dc1..b4da60e1fb 100644 --- a/guides/assets/images/has_many_through.png +++ b/guides/assets/images/has_many_through.png diff --git a/guides/assets/images/has_one.png b/guides/assets/images/has_one.png Binary files differindex 93faa05b07..c70763856a 100644 --- a/guides/assets/images/has_one.png +++ b/guides/assets/images/has_one.png diff --git a/guides/assets/images/has_one_through.png b/guides/assets/images/has_one_through.png Binary files differindex 07dac1a27d..888a02b775 100644 --- a/guides/assets/images/has_one_through.png +++ b/guides/assets/images/has_one_through.png diff --git a/guides/assets/images/header_backdrop.png b/guides/assets/images/header_backdrop.png Binary files differindex 72b030478f..81f4d91774 100644 --- a/guides/assets/images/header_backdrop.png +++ b/guides/assets/images/header_backdrop.png diff --git a/guides/assets/images/i18n/demo_html_safe.png b/guides/assets/images/i18n/demo_html_safe.png Binary files differindex 9afa8ebec1..be75d4830e 100644 --- a/guides/assets/images/i18n/demo_html_safe.png +++ b/guides/assets/images/i18n/demo_html_safe.png diff --git a/guides/assets/images/i18n/demo_localized_pirate.png b/guides/assets/images/i18n/demo_localized_pirate.png Binary files differindex bf8d0b558c..528cc27900 100644 --- a/guides/assets/images/i18n/demo_localized_pirate.png +++ b/guides/assets/images/i18n/demo_localized_pirate.png diff --git a/guides/assets/images/i18n/demo_translated_en.png b/guides/assets/images/i18n/demo_translated_en.png Binary files differindex e887bfa306..bbb5e93c3a 100644 --- a/guides/assets/images/i18n/demo_translated_en.png +++ b/guides/assets/images/i18n/demo_translated_en.png diff --git a/guides/assets/images/i18n/demo_translated_pirate.png b/guides/assets/images/i18n/demo_translated_pirate.png Binary files differindex aa5618a865..305fa93a14 100644 --- a/guides/assets/images/i18n/demo_translated_pirate.png +++ b/guides/assets/images/i18n/demo_translated_pirate.png diff --git a/guides/assets/images/i18n/demo_translation_missing.png b/guides/assets/images/i18n/demo_translation_missing.png Binary files differindex 867aa7c42d..e9833ba307 100644 --- a/guides/assets/images/i18n/demo_translation_missing.png +++ b/guides/assets/images/i18n/demo_translation_missing.png diff --git a/guides/assets/images/i18n/demo_untranslated.png b/guides/assets/images/i18n/demo_untranslated.png Binary files differindex 2ea6404822..2653abc491 100644 --- a/guides/assets/images/i18n/demo_untranslated.png +++ b/guides/assets/images/i18n/demo_untranslated.png diff --git a/guides/assets/images/icons/callouts/14.png b/guides/assets/images/icons/callouts/14.png Binary files differindex 4274e6580a..dbde9ca749 100644 --- a/guides/assets/images/icons/callouts/14.png +++ b/guides/assets/images/icons/callouts/14.png diff --git a/guides/assets/images/icons/example.png b/guides/assets/images/icons/example.png Binary files differindex de23c0aa87..a0e855befa 100644 --- a/guides/assets/images/icons/example.png +++ b/guides/assets/images/icons/example.png diff --git a/guides/assets/images/icons/home.png b/guides/assets/images/icons/home.png Binary files differindex 24149d6e78..e70e164522 100644 --- a/guides/assets/images/icons/home.png +++ b/guides/assets/images/icons/home.png diff --git a/guides/assets/images/icons/important.png b/guides/assets/images/icons/important.png Binary files differindex dafcf0f59e..bab53bf3aa 100644 --- a/guides/assets/images/icons/important.png +++ b/guides/assets/images/icons/important.png diff --git a/guides/assets/images/icons/next.png b/guides/assets/images/icons/next.png Binary files differindex 355b329f5a..a158832725 100644 --- a/guides/assets/images/icons/next.png +++ b/guides/assets/images/icons/next.png diff --git a/guides/assets/images/icons/note.png b/guides/assets/images/icons/note.png Binary files differindex 08d35a6f5c..62eec7845f 100644 --- a/guides/assets/images/icons/note.png +++ b/guides/assets/images/icons/note.png diff --git a/guides/assets/images/icons/prev.png b/guides/assets/images/icons/prev.png Binary files differindex ea564c865e..8a96960422 100644 --- a/guides/assets/images/icons/prev.png +++ b/guides/assets/images/icons/prev.png diff --git a/guides/assets/images/icons/tip.png b/guides/assets/images/icons/tip.png Binary files differindex d834e6d1bb..a5316d318f 100644 --- a/guides/assets/images/icons/tip.png +++ b/guides/assets/images/icons/tip.png diff --git a/guides/assets/images/icons/up.png b/guides/assets/images/icons/up.png Binary files differindex 379f0045af..6cac818170 100644 --- a/guides/assets/images/icons/up.png +++ b/guides/assets/images/icons/up.png diff --git a/guides/assets/images/polymorphic.png b/guides/assets/images/polymorphic.png Binary files differindex a3cbc4502a..e0a7f6d64a 100644 --- a/guides/assets/images/polymorphic.png +++ b/guides/assets/images/polymorphic.png diff --git a/guides/assets/images/rails4_features.png b/guides/assets/images/rails4_features.png Binary files differindex b3bd5ef69e..ac73f05cf7 100644 --- a/guides/assets/images/rails4_features.png +++ b/guides/assets/images/rails4_features.png diff --git a/guides/assets/images/session_fixation.png b/guides/assets/images/session_fixation.png Binary files differindex ac3ab01614..e009484f09 100644 --- a/guides/assets/images/session_fixation.png +++ b/guides/assets/images/session_fixation.png diff --git a/guides/assets/images/tab_yellow.png b/guides/assets/images/tab_yellow.png Binary files differindex 3ab1c56c4d..053c807d28 100644 --- a/guides/assets/images/tab_yellow.png +++ b/guides/assets/images/tab_yellow.png diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index 46fabca3e8..8b7aa893fd 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -8,14 +8,14 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "rails", "5.1.0.rc1" + gem "rails", "5.1.0" end require "rack/test" require "action_controller/railtie" class TestApp < Rails::Application - config.root = File.dirname(__FILE__) + config.root = __dir__ config.session_store :cookie_store, key: "cookie_store_key" secrets.secret_token = "secret_token" secrets.secret_key_base = "secret_key_base" diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index 7644f6fe4a..3dd66c95ec 100644 --- a/guides/bug_report_templates/action_controller_master.rb +++ b/guides/bug_report_templates/action_controller_master.rb @@ -14,7 +14,7 @@ end require "action_controller/railtie" class TestApp < Rails::Application - config.root = File.dirname(__FILE__) + config.root = __dir__ secrets.secret_token = "secret_token" secrets.secret_key_base = "secret_key_base" diff --git a/guides/bug_report_templates/active_job_gem.rb b/guides/bug_report_templates/active_job_gem.rb index 71fe356ea0..252b270a0c 100644 --- a/guides/bug_report_templates/active_job_gem.rb +++ b/guides/bug_report_templates/active_job_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activejob", "5.1.0.rc1" + gem "activejob", "5.1.0" end require "minitest/autorun" diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index a685c257ea..61d4e8d395 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activerecord", "5.1.0.rc1" + gem "activerecord", "5.1.0" gem "sqlite3" end diff --git a/guides/bug_report_templates/active_record_migrations_gem.rb b/guides/bug_report_templates/active_record_migrations_gem.rb index b4e822dfe0..00ba3c1cd6 100644 --- a/guides/bug_report_templates/active_record_migrations_gem.rb +++ b/guides/bug_report_templates/active_record_migrations_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activerecord", "5.1.0.rc1" + gem "activerecord", "5.1.0" gem "sqlite3" end @@ -48,16 +48,14 @@ end class BugTest < Minitest::Test def test_migration_up - migrator = ActiveRecord::Migrator.new(:up, [ChangeAmountToAddScale]) - migrator.run + ChangeAmountToAddScale.migrate(:up) Payment.reset_column_information assert_equal "decimal(10,2)", Payment.columns.last.sql_type end def test_migration_down - migrator = ActiveRecord::Migrator.new(:down, [ChangeAmountToAddScale]) - migrator.run + ChangeAmountToAddScale.migrate(:down) Payment.reset_column_information assert_equal "decimal(10,0)", Payment.columns.last.sql_type diff --git a/guides/bug_report_templates/active_record_migrations_master.rb b/guides/bug_report_templates/active_record_migrations_master.rb index 84a4b71909..52c9028b0f 100644 --- a/guides/bug_report_templates/active_record_migrations_master.rb +++ b/guides/bug_report_templates/active_record_migrations_master.rb @@ -48,16 +48,14 @@ end class BugTest < Minitest::Test def test_migration_up - migrator = ActiveRecord::Migrator.new(:up, [ChangeAmountToAddScale]) - migrator.run + ChangeAmountToAddScale.migrate(:up) Payment.reset_column_information assert_equal "decimal(10,2)", Payment.columns.last.sql_type end def test_migration_down - migrator = ActiveRecord::Migrator.new(:down, [ChangeAmountToAddScale]) - migrator.run + ChangeAmountToAddScale.migrate(:down) Payment.reset_column_information assert_equal "decimal(10,0)", Payment.columns.last.sql_type diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb index e1b705bea4..4dcd04ea27 100644 --- a/guides/bug_report_templates/generic_gem.rb +++ b/guides/bug_report_templates/generic_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activesupport", "5.1.0.rc1" + gem "activesupport", "5.1.0" end require "active_support/core_ext/object/blank" diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb index 28164a3cb4..35f014747c 100644 --- a/guides/rails_guides/generator.rb +++ b/guides/rails_guides/generator.rb @@ -73,7 +73,7 @@ module RailsGuides @output_dir = "#{@guides_dir}/output" @output_dir += "/kindle" if @kindle - @source_dir += "/#{@language}" if @language + @output_dir += "/#{@language}" if @language end def create_output_dir_if_needed diff --git a/guides/rails_guides/helpers.rb b/guides/rails_guides/helpers.rb index 6f4b0b492c..520aa7f7cc 100644 --- a/guides/rails_guides/helpers.rb +++ b/guides/rails_guides/helpers.rb @@ -15,7 +15,7 @@ module RailsGuides end def documents_by_section - @documents_by_section ||= YAML.load_file(File.expand_path("../../source/#{@lang ? @lang + '/' : ''}documents.yaml", __FILE__)) + @documents_by_section ||= YAML.load_file(File.expand_path("../source/#{@language ? @language + '/' : ''}documents.yaml", __dir__)) end def documents_flat diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb index 9d43c10be6..7ac3d417a4 100644 --- a/guides/rails_guides/markdown/renderer.rb +++ b/guides/rails_guides/markdown/renderer.rb @@ -93,7 +93,7 @@ HTML def github_file_url(file_path) tree = version || edge - root = file_path[%r{(.+)/}, 1] + root = file_path[%r{(\w+)/}, 1] path = \ case root when "abstract_controller", "action_controller", "action_dispatch" diff --git a/guides/source/5_1_release_notes.md b/guides/source/5_1_release_notes.md index fc79287d4c..fa92b9e5f8 100644 --- a/guides/source/5_1_release_notes.md +++ b/guides/source/5_1_release_notes.md @@ -87,7 +87,7 @@ screenshots. [Pull Request](https://github.com/rails/rails/pull/28038) Rails now allows management of application secrets in a secure way, -building on top of the [sekrets](https://github.com/ahoward/sekrets) gem. +inspired by the [sekrets](https://github.com/ahoward/sekrets) gem. Run `bin/rails secrets:setup` to setup a new encrypted secrets file. This will also generate a master key, which must be stored outside of the repository. The @@ -328,9 +328,6 @@ Please refer to the [Changelog][action-cable] for detailed changes. with multiple applications. ([Pull Request](https://github.com/rails/rails/pull/27425)) -* Permit same-origin connections by default. - ([commit](https://github.com/rails/rails/commit/dae404473409fcab0e07976aec626df670e52282)) - * Add `ActiveSupport::Notifications` hook for broadcasting data. ([Pull Request](https://github.com/rails/rails/pull/24988)) @@ -356,9 +353,6 @@ Please refer to the [Changelog][action-pack] for detailed changes. ### Deprecations -* Deprecated `:controller` and `:action` path parameters. - ([Pull Request](https://github.com/rails/rails/pull/23980)) - * Deprecated `config.action_controller.raise_on_unfiltered_parameters`. It doesn't have any effect in Rails 5.1. ([Commit](https://github.com/rails/rails/commit/c6640fb62b10db26004a998d2ece98baede509e5)) @@ -416,10 +410,6 @@ Please refer to the [Changelog][action-mailer] for detailed changes. ### Notable changes -* Exception handling: use `rescue_from` to handle exceptions raised by - mailer actions, by message delivery, and by deferred delivery jobs. - ([commit](https://github.com/rails/rails/commit/e35b98e6f5c54330245645f2ed40d56c74538902)) - * Allowed setting custom content type when attachments are included and body is set inline. ([Pull Request](https://github.com/rails/rails/pull/27227)) diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 5d987264f5..22537f960c 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -715,6 +715,9 @@ end Now, the `LoginsController`'s `new` and `create` actions will work as before without requiring the user to be logged in. The `:only` option is used to skip this filter only for these actions, and there is also an `:except` option which works the other way. These options can be used when adding filters too, so you can add a filter which only runs for selected actions in the first place. +NOTE: Calling the same filter multiple times with different options will not work, +since the last filter definition will overwrite the previous ones. + ### After Filters and Around Filters In addition to "before" filters, you can also run filters after an action has been executed, or both before and after. diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 65146ee7da..7751ac00df 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -781,7 +781,8 @@ config.action_mailer.smtp_settings = { enable_starttls_auto: true } ``` Note: As of July 15, 2014, Google increased [its security measures](https://support.google.com/accounts/answer/6010255) and now blocks attempts from apps it deems less secure. -You can change your gmail settings [here](https://www.google.com/settings/security/lesssecureapps) to allow the attempts or +You can change your Gmail settings [here](https://www.google.com/settings/security/lesssecureapps) to allow the attempts. If your Gmail account has 2-factor authentication enabled, +then you will need to set an [app password](https://myaccount.google.com/apppasswords) and use that instead of your regular password. Alternatively, you can use another ESP to send email by replacing 'smtp.gmail.com' above with the address of your provider. Mailer Testing diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index c835adeab6..10412128cc 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -419,7 +419,7 @@ image_tag("rails.png") # => <img src="http://assets.example.com/images/rails.png #### auto_discovery_link_tag -Returns a link tag that browsers and feed readers can use to auto-detect an RSS or Atom feed. +Returns a link tag that browsers and feed readers can use to auto-detect an RSS, Atom, or JSON feed. ```ruby auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", { title: "RSS Feed" }) # => diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index b58ca61848..443be77934 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -310,6 +310,12 @@ UserMailer.welcome(@user).deliver_now UserMailer.welcome(@user).deliver_later ``` +NOTE: Using the asynchronous queue from a Rake task (for example, to +send an email using `.deliver_later`) will generally not work because Rake will +likely end, causing the in-process thread pool to be deleted, before any/all +of the `.deliver_later` emails are processed. To avoid this problem, use +`.deliver_now` or run a persistent queue in development. + Internationalization -------------------- diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md index 6d07291b07..8543fcd20f 100644 --- a/guides/source/active_record_postgresql.md +++ b/guides/source/active_record_postgresql.md @@ -84,7 +84,7 @@ Book.where("array_length(ratings, 1) >= 3") ### Hstore * [type definition](http://www.postgresql.org/docs/current/static/hstore.html) -* [functions and operators](http://www.postgresql.org/docs/current/static/hstore.html#AEN167712) +* [functions and operators](http://www.postgresql.org/docs/current/static/hstore.html#AEN179902) NOTE: You need to enable the `hstore` extension to use hstore. @@ -114,16 +114,21 @@ Profile.where("settings->'color' = ?", "yellow") # => #<ActiveRecord::Relation [#<Profile id: 1, settings: {"color"=>"yellow", "resolution"=>"1280x1024"}>]> ``` -### JSON +### JSON and JSONB * [type definition](http://www.postgresql.org/docs/current/static/datatype-json.html) * [functions and operators](http://www.postgresql.org/docs/current/static/functions-json.html) ```ruby # db/migrate/20131220144913_create_events.rb +# ... for json datatype: create_table :events do |t| t.json 'payload' end +# ... or for jsonb datatype: +create_table :events do |t| + t.jsonb 'payload' +end # app/models/event.rb class Event < ApplicationRecord @@ -285,7 +290,7 @@ SELECT n.nspname AS enum_schema, ### UUID * [type definition](http://www.postgresql.org/docs/current/static/datatype-uuid.html) -* [pgcrypto generator function](http://www.postgresql.org/docs/current/static/pgcrypto.html#AEN159361) +* [pgcrypto generator function](http://www.postgresql.org/docs/current/static/pgcrypto.html#AEN182570) * [uuid-ossp generator functions](http://www.postgresql.org/docs/current/static/uuid-ossp.html) NOTE: You need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp` diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 26d01d4ede..215142223d 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -118,7 +118,7 @@ You can also use this method to query for multiple objects. Call the `find` meth ```ruby # Find the clients with primary keys 1 and 10. -client = Client.find([1, 10]) # Or even Client.find(1, 10) +clients = Client.find([1, 10]) # Or even Client.find(1, 10) # => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">] ``` @@ -150,7 +150,7 @@ The `take` method returns `nil` if no record is found and no exception will be r You can pass in a numerical argument to the `take` method to return up to that number of results. For example ```ruby -client = Client.take(2) +clients = Client.take(2) # => [ # #<Client id: 1, first_name: "Lifo">, # #<Client id: 220, first_name: "Sara"> @@ -189,7 +189,7 @@ If your [default scope](active_record_querying.html#applying-a-default-scope) co You can pass in a numerical argument to the `first` method to return up to that number of results. For example ```ruby -client = Client.first(3) +clients = Client.first(3) # => [ # #<Client id: 1, first_name: "Lifo">, # #<Client id: 2, first_name: "Fifo">, @@ -240,7 +240,7 @@ If your [default scope](active_record_querying.html#applying-a-default-scope) co You can pass in a numerical argument to the `last` method to return up to that number of results. For example ```ruby -client = Client.last(3) +clients = Client.last(3) # => [ # #<Client id: 219, first_name: "James">, # #<Client id: 220, first_name: "Sara">, @@ -513,8 +513,6 @@ Article.where(author: author) Author.joins(:articles).where(articles: { author: author }) ``` -NOTE: The values cannot be symbols. For example, you cannot do `Client.where(status: :active)`. - #### Range Conditions ```ruby @@ -557,6 +555,19 @@ In other words, this query can be generated by calling `where` with no argument, SELECT * FROM clients WHERE (clients.locked != 1) ``` +### OR Conditions + +`OR` conditions between two relations can be built by calling `or` on the first +relation, and passing the second one as an argument. + +```ruby +Client.where(locked: true).or(Client.where(orders_count: [1,3,5])) +``` + +```sql +SELECT * FROM clients WHERE (clients.locked = 1 OR clients.orders_count IN (1,3,5)) +``` + Ordering -------- diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 5313361dfd..6eb5de78be 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -953,7 +953,7 @@ should happen, an `Array` can be used. Moreover, you can apply both `:if` and ```ruby class Computer < ApplicationRecord validates :mouse, presence: true, - if: ["market.retail?", :desktop?], + if: [Proc.new { |c| c.market.retail? }, :desktop?], unless: Proc.new { |c| c.trackpad.present? } end ``` diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 67bed4c8da..23f53ac084 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -755,6 +755,8 @@ NOTE: Defined in `active_support/core_ext/module/anonymous.rb`. ### Method Delegation +#### `delegate` + The macro `delegate` offers an easy way to forward methods. Let's imagine that users in some application have login information in the `User` model but name and other data in a separate `Profile` model: @@ -837,13 +839,32 @@ In the previous example the macro generates `avatar_size` rather than `size`. NOTE: Defined in `active_support/core_ext/module/delegation.rb` +#### `delegate_missing_to` + +Imagine you would like to delegate everything missing from the `User` object, +to the `Profile` one. The `delegate_missing_to` macro lets you implement this +in a breeze: + +```ruby +class User < ApplicationRecord + has_one :profile + + delegate_missing_to :profile +end +``` + +The target can be anything callable within the object, e.g. instance variables, +methods, constants, etc. Only the public methods of the target are delegated. + +NOTE: Defined in `active_support/core_ext/module/delegation.rb`. + ### Redefining Methods There are cases where you need to define a method with `define_method`, but don't know whether a method with that name already exists. If it does, a warning is issued if they are enabled. No big deal, but not clean either. The method `redefine_method` prevents such a potential warning, removing the existing method before if needed. -NOTE: Defined in `active_support/core_ext/module/remove_method.rb` +NOTE: Defined in `active_support/core_ext/module/remove_method.rb`. Extensions to `Class` --------------------- @@ -931,7 +952,7 @@ When `:instance_reader` is `false`, the instance predicate returns a `NoMethodEr If you do not want the instance predicate, pass `instance_predicate: false` and it will not be defined. -NOTE: Defined in `active_support/core_ext/class/attribute.rb` +NOTE: Defined in `active_support/core_ext/class/attribute.rb`. #### `cattr_reader`, `cattr_writer`, and `cattr_accessor` @@ -940,8 +961,7 @@ The macros `cattr_reader`, `cattr_writer`, and `cattr_accessor` are analogous to ```ruby class MysqlAdapter < AbstractAdapter # Generates class methods to access @@emulate_booleans. - cattr_accessor :emulate_booleans - self.emulate_booleans = true + cattr_accessor :emulate_booleans, default: true end ``` @@ -950,8 +970,7 @@ Instance methods are created as well for convenience, they are just proxies to t ```ruby module ActionView class Base - cattr_accessor :field_error_proc - @@field_error_proc = Proc.new{ ... } + cattr_accessor :field_error_proc, default: Proc.new { ... } end end ``` @@ -963,7 +982,7 @@ Also, you can pass a block to `cattr_*` to set up the attribute with a default v ```ruby class MysqlAdapter < AbstractAdapter # Generates class methods to access @@emulate_booleans with default value of true. - cattr_accessor(:emulate_booleans) { true } + cattr_accessor :emulate_booleans, default: true end ``` @@ -1829,7 +1848,7 @@ as well as adding or subtracting their results from a Time object. For example: (4.months + 5.years).from_now ``` -NOTE: Defined in `active_support/core_ext/numeric/time.rb` +NOTE: Defined in `active_support/core_ext/numeric/time.rb`. ### Formatting diff --git a/guides/source/api_app.md b/guides/source/api_app.md index f373d313cc..64200ec242 100644 --- a/guides/source/api_app.md +++ b/guides/source/api_app.md @@ -206,16 +206,17 @@ An API application comes with the following middleware by default: - `ActiveSupport::Cache::Strategy::LocalCache::Middleware` - `Rack::Runtime` - `ActionDispatch::RequestId` +- `ActionDispatch::RemoteIp` - `Rails::Rack::Logger` - `ActionDispatch::ShowExceptions` - `ActionDispatch::DebugExceptions` -- `ActionDispatch::RemoteIp` - `ActionDispatch::Reloader` - `ActionDispatch::Callbacks` - `ActiveRecord::Migration::CheckPending` - `Rack::Head` - `Rack::ConditionalGet` - `Rack::ETag` +- `MyApi::Application::Routes` See the [internal middleware](rails_on_rack.html#internal-middleware-stack) section of the Rack guide for further information on them. @@ -360,7 +361,7 @@ middleware set, you can remove it with: config.middleware.delete ::Rack::Sendfile ``` -Keep in mind that removing these middleware will remove support for certain +Keep in mind that removing these middlewares will remove support for certain features in Action Controller. Choosing Controller Modules @@ -385,8 +386,9 @@ controller modules by default: hooks defined by Action Controller (see [the instrumentation guide](active_support_instrumentation.html#action-controller) for more information regarding this). -- `ActionController::ParamsWrapper`: Wraps the parameters hash into a nested hash, +- `ActionController::ParamsWrapper`: Wraps the parameters hash into a nested hash, so that you don't have to specify root elements sending POST requests for instance. +- `ActionController::Head`: Support for returning a response with no content, only headers Other plugins may add additional modules. You can get a list of all modules included into `ActionController::API` in the rails console: @@ -394,12 +396,12 @@ included into `ActionController::API` in the rails console: ```bash $ bin/rails c >> ActionController::API.ancestors - ActionController::Metal.ancestors -=> [ActionController::API, - ActiveRecord::Railties::ControllerRuntime, - ActionDispatch::Routing::RouteSet::MountedHelpers, - ActionController::ParamsWrapper, - ... , - AbstractController::Rendering, +=> [ActionController::API, + ActiveRecord::Railties::ControllerRuntime, + ActionDispatch::Routing::RouteSet::MountedHelpers, + ActionController::ParamsWrapper, + ... , + AbstractController::Rendering, ActionView::ViewPaths] ``` diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index 3c61754982..c3c7367304 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -281,7 +281,7 @@ Methods created with `(module|class)_eval(STRING)` have a comment by their side ```ruby for severity in Severity.constants - class_eval <<-EOT, __FILE__, __LINE__ + class_eval <<-EOT, __FILE__, __LINE__ + 1 def #{severity.downcase}(message = nil, progname = nil, &block) # def debug(message = nil, progname = nil, &block) add(#{severity}, message, progname, &block) # add(DEBUG, message, progname, &block) end # end diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 61b7112247..a02eebf263 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -447,15 +447,15 @@ For example, a new Rails application includes a default ```js // ... -//= require jquery -//= require jquery_ujs +//= require rails-ujs +//= require turbolinks //= require_tree . ``` In JavaScript files, Sprockets directives begin with `//=`. In the above case, the file is using the `require` and the `require_tree` directives. The `require` directive is used to tell Sprockets the files you wish to require. Here, you are -requiring the files `jquery.js` and `jquery_ujs.js` that are available somewhere +requiring the files `rails-ujs.js` and `turbolinks.js` that are available somewhere in the search path for Sprockets. You need not supply the extensions explicitly. Sprockets assumes you are requiring a `.js` file when done from within a `.js` file. @@ -572,20 +572,6 @@ would generate this HTML: The `body` param is required by Sprockets. -### Runtime Error Checking - -By default the asset pipeline will check for potential errors in development mode during -runtime. To disable this behavior you can set: - -```ruby -config.assets.raise_runtime_errors = false -``` - -When this option is true, the asset pipeline will check if all the assets loaded -in your application are included in the `config.assets.precompile` list. -If `config.assets.digest` is also true, the asset pipeline will require that -all requests for assets include digests. - ### Raise an Error When an Asset is Not Found If you are using sprockets-rails >= 3.2.0 you can configure what happens @@ -868,7 +854,7 @@ pre-existing JavaScript runtimes, you may want to add one to your Gemfile: ```ruby group :production do - gem 'therubyracer' + gem 'mini_racer' end ``` diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 5794bfa666..bead931529 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -599,7 +599,7 @@ class CreateBooks < ActiveRecord::Migration[5.0] t.string :book_number t.integer :author_id end - + add_index :books, :author_id add_foreign_key :books, :authors end @@ -960,7 +960,7 @@ class Author < ApplicationRecord end ``` -NOTE: You only need to specify the :counter_cache option on the `belongs_to` +NOTE: You only need to specify the `:counter_cache` option on the `belongs_to` side of the association. Counter cache columns are added to the containing model's list of read-only attributes through `attr_readonly`. @@ -1417,7 +1417,7 @@ If either of these saves fails due to validation errors, then the assignment sta If the parent object (the one declaring the `has_one` association) is unsaved (that is, `new_record?` returns `true`) then the child objects are not saved. They will automatically when the parent object is saved. -If you want to assign an object to a `has_one` association without saving the object, use the `association.build` method. +If you want to assign an object to a `has_one` association without saving the object, use the `build_association` method. ### `has_many` Association Reference @@ -1559,7 +1559,7 @@ The `collection.size` method returns the number of objects in the collection. The `collection.find` method finds objects within the collection. It uses the same syntax and options as `ActiveRecord::Base.find`. ```ruby -@available_books = @author.books.find(1) +@available_book = @author.books.find(1) ``` ##### `collection.where(...)` @@ -1831,7 +1831,7 @@ The `limit` method lets you restrict the total number of objects that will be fe class Author < ApplicationRecord has_many :recent_books, -> { order('published_at desc').limit(100) }, - class_name: "Book", + class_name: "Book" end ``` diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md index 61657023e7..05743ee4ce 100644 --- a/guides/source/autoloading_and_reloading_constants.md +++ b/guides/source/autoloading_and_reloading_constants.md @@ -983,20 +983,19 @@ WHERE "polygons"."type" IN ("Rectangle") That is not a bug, the query includes all *known* descendants of `Rectangle`. A way to ensure this works correctly regardless of the order of execution is to -load the leaves of the tree by hand at the bottom of the file that defines the -root class: +manually load the direct subclasses at the bottom of the file that defines each +intermediate class: ```ruby -# app/models/polygon.rb -class Polygon < ApplicationRecord +# app/models/rectangle.rb +class Rectangle < Polygon end -require_dependency ‘square’ +require_dependency 'square' ``` -Only the leaves that are **at least grandchildren** need to be loaded this -way. Direct subclasses do not need to be preloaded. If the hierarchy is -deeper, intermediate classes will be autoloaded recursively from the bottom -because their constant will appear in the class definitions as superclass. +This needs to happen for every intermediate (non-root and non-leaf) class. The +root class does not scope the query by type, and therefore does not necessarily +have to know all its descendants. ### Autoloading and `require` diff --git a/guides/source/configuring.md b/guides/source/configuring.md index bf9456a482..21b3ca0efa 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -157,8 +157,6 @@ defaults to `:debug` for all environments. The available log levels are: `:debug * `config.assets.enabled` a flag that controls whether the asset pipeline is enabled. It is set to `true` by default. -* `config.assets.raise_runtime_errors` Set this flag to `true` to enable additional runtime error checking. Recommended in `config/environments/development.rb` to minimize unexpected behavior when deploying to `production`. - * `config.assets.css_compressor` defines the CSS compressor to use. It is set by default by `sass-rails`. The unique alternative value at the moment is `:yui`, which uses the `yui-compressor` gem. * `config.assets.js_compressor` defines the JavaScript compressor to use. Possible values are `:closure`, `:uglifier` and `:yui` which require the use of the `closure-compiler`, `uglifier` or `yui-compressor` gems respectively. @@ -456,10 +454,14 @@ to `'http authentication'`. Defaults to `'signed cookie'`. * `config.action_dispatch.encrypted_cookie_salt` sets the encrypted cookies salt -value. Defaults to `'encrypted cookie'`. + value. Defaults to `'encrypted cookie'`. * `config.action_dispatch.encrypted_signed_cookie_salt` sets the signed -encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. + encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. + +* `config.action_dispatch.authenticated_encrypted_cookie_salt` sets the + authenticated encrypted cookie salt. Defaults to `'authenticated encrypted + cookie'`. * `config.action_dispatch.perform_deep_munge` configures whether `deep_munge` method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation) @@ -493,8 +495,6 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. * `ActionDispatch::Callbacks.before` takes a block of code to run before the request. -* `ActionDispatch::Callbacks.to_prepare` takes a block to run after `ActionDispatch::Callbacks.before`, but before the request. Runs for every request in `development` mode, but only once for `production` or environments with `cache_classes` set to `true`. - * `ActionDispatch::Callbacks.after` takes a block of code to run after the request. ### Configuring Action View @@ -1188,7 +1188,7 @@ Below is a comprehensive list of all the initializers found in Rails in the orde * `finisher_hook`: Provides a hook for after the initialization of process of the application is complete, as well as running all the `config.after_initialize` blocks for the application, railties and engines. -* `set_routes_reloader_hook`: Configures Action Dispatch to reload the routes file using `ActionDispatch::Callbacks.to_prepare`. +* `set_routes_reloader_hook`: Configures Action Dispatch to reload the routes file using `ActiveSupport::Callbacks.to_run`. * `disable_dependency_loading`: Disables the automatic dependency loading if the `config.eager_load` is set to `true`. diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 39f4272b3c..2f2962a3e6 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -141,14 +141,15 @@ NOTE: To help our CI servers you should add [ci skip] to your documentation comm Translating Rails Guides ------------------------ -We are happy to have people volunteer to translate the Rails guides into their own language. -If you want to translate the Rails guides in your own language, follows these steps: +We are happy to have people volunteer to translate the Rails guides. Just follow these steps: -* Fork the project (rails/rails). +* Fork https://github.com/rails/rails. * Add a source folder for your own language, for example: *guides/source/it-IT* for Italian. * Copy the contents of *guides/source* into your own language directory and translate them. * Do NOT translate the HTML files, as they are automatically generated. +Note that translations are not submitted to the Rails repository. As detailed above, your work happens in a fork. This is so because in practice documentation maintenance via patches is only sustainable in English. + To generate the guides in HTML format cd into the *guides* directory then run (eg. for it-IT): ```bash diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md index 7ec038eb4d..c57efd6362 100644 --- a/guides/source/development_dependencies_install.md +++ b/guides/source/development_dependencies_install.md @@ -62,7 +62,7 @@ $ sudo apt-get install sqlite3 libsqlite3-dev If you are on Fedora or CentOS, you're done with ```bash -$ sudo yum install sqlite3 sqlite3-devel +$ sudo yum install libsqlite3x libsqlite3x-devel ``` If you are on Arch Linux, you will need to run: diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 2afef57fc2..59205ee465 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -130,11 +130,6 @@ url: active_support_instrumentation.html description: This guide explains how to use the instrumentation API inside of Active Support to measure events inside of Rails and other Ruby code. - - name: Profiling Rails Applications - work_in_progress: true - url: profiling.html - description: This guide explains how to profile your Rails applications to improve performance. - - name: Using Rails for API-only Applications url: api_app.html description: This guide explains how to effectively use Rails to develop a JSON API application. diff --git a/guides/source/generators.md b/guides/source/generators.md index a554e08204..be1be75e7a 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -96,7 +96,7 @@ This is the generator just created: ```ruby class InitializerGenerator < Rails::Generators::NamedBase - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) end ``` @@ -122,7 +122,7 @@ And now let's change the generator to copy this template when invoked: ```ruby class InitializerGenerator < Rails::Generators::NamedBase - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) def copy_initializer_file copy_file "initializer.rb", "config/initializers/#{file_name}.rb" @@ -689,14 +689,6 @@ Available options are: * `:env` - Specifies the environment in which to run this rake task. * `:sudo` - Whether or not to run this task using `sudo`. Defaults to `false`. -### `capify!` - -Runs the `capify` command from Capistrano at the root of the application which generates Capistrano configuration. - -```ruby -capify! -``` - ### `route` Adds text to the `config/routes.rb` file: diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index f3ae5a5b28..49c691c841 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -20,16 +20,7 @@ Guide Assumptions This guide is designed for beginners who want to get started with a Rails application from scratch. It does not assume that you have any prior experience -with Rails. However, to get the most out of it, you need to have some -prerequisites installed: - -* The [Ruby](https://www.ruby-lang.org/en/downloads) language version 2.2.2 or newer. -* Right version of [Development Kit](http://rubyinstaller.org/downloads/), if you - are using Windows. -* The [RubyGems](https://rubygems.org) packaging system, which is installed with - Ruby by default. To learn more about RubyGems, please read the - [RubyGems Guides](http://guides.rubygems.org). -* A working installation of the [SQLite3 Database](https://www.sqlite.org). +with Rails. Rails is a web application framework running on the Ruby programming language. If you have no prior experience with Ruby, you will find a very steep learning @@ -86,6 +77,9 @@ your prompt will look something like `c:\source_code>` ### Installing Rails +Before you install Rails, you should check to make sure that your system has the +proper prerequisites installed. These include Ruby and SQLite3. + Open up a command line prompt. On macOS open Terminal.app, on Windows choose "Run" from your Start menu and type 'cmd.exe'. Any commands prefaced with a dollar sign `$` should be run in the command line. Verify that you have a @@ -96,12 +90,19 @@ $ ruby -v ruby 2.3.1p112 ``` +Rails requires Ruby version 2.2.2 or later. If the version number returned is +less than that number, you'll need to install a fresh copy of Ruby. + TIP: A number of tools exist to help you quickly install Ruby and Ruby on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org), while macOS users can use [Tokaido](https://github.com/tokaido/tokaidoapp). For more installation methods for most Operating Systems take a look at [ruby-lang.org](https://www.ruby-lang.org/en/documentation/installation/). +If you are working on Windows, you should also install the +[Ruby Installer Development Kit](http://rubyinstaller.org/downloads/). + +You will also need an installation of the SQLite3 database. Many popular UNIX-like OSes ship with an acceptable version of SQLite3. On Windows, if you installed Rails through Rails Installer, you already have SQLite installed. Others can find installation instructions @@ -127,7 +128,7 @@ run the following: $ rails --version ``` -If it says something like "Rails 5.1.0", you are ready to continue. +If it says something like "Rails 5.1.1", you are ready to continue. ### Creating the Blog Application @@ -207,7 +208,7 @@ TIP: Compiling CoffeeScript and JavaScript asset compression requires you have a JavaScript runtime available on your system, in the absence of a runtime you will see an `execjs` error during asset compilation. Usually macOS and Windows come with a JavaScript runtime installed. -Rails adds the `therubyracer` gem to the generated `Gemfile` in a +Rails adds the `mini_racer` gem to the generated `Gemfile` in a commented line for new apps and you can uncomment if you need it. `therubyrhino` is the recommended runtime for JRuby users and is added by default to the `Gemfile` in apps generated under JRuby. You can investigate @@ -909,6 +910,7 @@ And then finally, add the view for this action, located at <tr> <th>Title</th> <th>Text</th> + <th></th> </tr> <% @articles.each do |article| %> @@ -1195,7 +1197,7 @@ it look as follows: This time we point the form to the `update` action, which is not defined yet but will be very soon. -Passing the article object to the method, will automagically create url for submitting the edited article form. +Passing the article object to the method, will automagically create url for submitting the edited article form. This option tells Rails that we want this form to be submitted via the `PATCH` HTTP method which is the HTTP method you're expected to use to **update** resources according to the REST protocol. @@ -1488,14 +1490,14 @@ second argument, and then the options as another argument. The `method: :delete` and `data: { confirm: 'Are you sure?' }` options are used as HTML5 attributes so that when the link is clicked, Rails will first show a confirm dialog to the user, and then submit the link with method `delete`. This is done via the -JavaScript file `jquery_ujs` which is automatically included in your +JavaScript file `rails-ujs` which is automatically included in your application's layout (`app/views/layouts/application.html.erb`) when you generated the application. Without this file, the confirmation dialog box won't appear.  -TIP: Learn more about jQuery Unobtrusive Adapter (jQuery UJS) on +TIP: Learn more about Unobtrusive JavaScript on [Working With JavaScript in Rails](working_with_javascript_in_rails.html) guide. Congratulations, you can now create, show, list, update and destroy diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 3ea156c6fe..86aea2c24d 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -155,7 +155,7 @@ defined here to find the matching command. ### `rails/command.rb` When one types a Rails command, `invoke` tries to lookup a command for the given -namespace and executing the command if found. +namespace and executes the command if found. If Rails doesn't recognize the command, it hands the reins over to Rake to run a task of the same name. diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index 48bb3147f3..c96cf61761 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -768,7 +768,7 @@ WARNING: The asset tag helpers do _not_ verify the existence of the assets at th #### Linking to Feeds with the `auto_discovery_link_tag` -The `auto_discovery_link_tag` helper builds HTML that most browsers and feed readers can use to detect the presence of RSS or Atom feeds. It takes the type of the link (`:rss` or `:atom`), a hash of options that are passed through to url_for, and a hash of options for the tag: +The `auto_discovery_link_tag` helper builds HTML that most browsers and feed readers can use to detect the presence of RSS, Atom, or JSON feeds. It takes the type of the link (`:rss`, `:atom`, or `:json`), a hash of options that are passed through to url_for, and a hash of options for the tag: ```erb <%= auto_discovery_link_tag(:rss, {action: "feed"}, @@ -1171,7 +1171,7 @@ To pass a local variable to a partial in only specific cases use the `local_assi This way it is possible to use the partial without the need to declare all local variables. -Every partial also has a local variable with the same name as the partial (minus the underscore). You can pass an object in to this local variable via the `:object` option: +Every partial also has a local variable with the same name as the partial (minus the leading underscore). You can pass an object in to this local variable via the `:object` option: ```erb <%= render partial: "customer", object: @new_customer %> diff --git a/guides/source/nested_model_forms.md b/guides/source/nested_model_forms.md deleted file mode 100644 index 71efa4b0d0..0000000000 --- a/guides/source/nested_model_forms.md +++ /dev/null @@ -1,230 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Rails Nested Model Forms -======================== - -Creating a form for a model _and_ its associations can become quite tedious. Therefore Rails provides helpers to assist in dealing with the complexities of generating these forms _and_ the required CRUD operations to create, update, and destroy associations. - -After reading this guide, you will know: - -* do stuff. - --------------------------------------------------------------------------------- - -NOTE: This guide assumes the user knows how to use the [Rails form helpers](form_helpers.html) in general. Also, it's **not** an API reference. For a complete reference please visit [the Rails API documentation](http://api.rubyonrails.org/). - - -Model setup ------------ - -To be able to use the nested model functionality in your forms, the model will need to support some basic operations. - -First of all, it needs to define a writer method for the attribute that corresponds to the association you are building a nested model form for. The `fields_for` form helper will look for this method to decide whether or not a nested model form should be built. - -If the associated object is an array, a form builder will be yielded for each object, else only a single form builder will be yielded. - -Consider a Person model with an associated Address. When asked to yield a nested FormBuilder for the `:address` attribute, the `fields_for` form helper will look for a method on the Person instance named `address_attributes=`. - -### ActiveRecord::Base model - -For an ActiveRecord::Base model and association this writer method is commonly defined with the `accepts_nested_attributes_for` class method: - -#### has_one - -```ruby -class Person < ApplicationRecord - has_one :address - accepts_nested_attributes_for :address -end -``` - -#### belongs_to - -```ruby -class Person < ApplicationRecord - belongs_to :firm - accepts_nested_attributes_for :firm -end -``` - -#### has_many / has_and_belongs_to_many - -```ruby -class Person < ApplicationRecord - has_many :projects - accepts_nested_attributes_for :projects -end -``` - -NOTE: For greater detail on associations see [Active Record Associations](association_basics.html). -For a complete reference on associations please visit the API documentation for [ActiveRecord::Associations::ClassMethods](http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html). - -### Custom model - -As you might have inflected from this explanation, you _don't_ necessarily need an ActiveRecord::Base model to use this functionality. The following examples are sufficient to enable the nested model form behavior: - -#### Single associated object - -```ruby -class Person - def address - Address.new - end - - def address_attributes=(attributes) - # ... - end -end -``` - -#### Association collection - -```ruby -class Person - def projects - [Project.new, Project.new] - end - - def projects_attributes=(attributes) - # ... - end -end -``` - -NOTE: See (TODO) in the advanced section for more information on how to deal with the CRUD operations in your custom model. - -Views ------ - -### Controller code - -A nested model form will _only_ be built if the associated object(s) exist. This means that for a new model instance you would probably want to build the associated object(s) first. - -Consider the following typical RESTful controller which will prepare a new Person instance and its `address` and `projects` associations before rendering the `new` template: - -```ruby -class PeopleController < ApplicationController - def new - @person = Person.new - @person.build_address - 2.times { @person.projects.build } - end - - def create - @person = Person.new(params[:person]) - if @person.save - # ... - end - end -end -``` - -NOTE: Obviously the instantiation of the associated object(s) can become tedious and not DRY, so you might want to move that into the model itself. ActiveRecord::Base provides an `after_initialize` callback which is a good way to refactor this. - -### Form code - -Now that you have a model instance, with the appropriate methods and associated object(s), you can start building the nested model form. - -#### Standard form - -Start out with a regular RESTful form: - -```erb -<%= form_for @person do |f| %> - <%= f.text_field :name %> -<% end %> -``` - -This will generate the following html: - -```html -<form action="/people" class="new_person" id="new_person" method="post"> - <input id="person_name" name="person[name]" type="text" /> -</form> -``` - -#### Nested form for a single associated object - -Now add a nested form for the `address` association: - -```erb -<%= form_for @person do |f| %> - <%= f.text_field :name %> - - <%= f.fields_for :address do |af| %> - <%= af.text_field :street %> - <% end %> -<% end %> -``` - -This generates: - -```html -<form action="/people" class="new_person" id="new_person" method="post"> - <input id="person_name" name="person[name]" type="text" /> - - <input id="person_address_attributes_street" name="person[address_attributes][street]" type="text" /> -</form> -``` - -Notice that `fields_for` recognized the `address` as an association for which a nested model form should be built by the way it has namespaced the `name` attribute. - -When this form is posted the Rails parameter parser will construct a hash like the following: - -```ruby -{ - "person" => { - "name" => "Eloy Duran", - "address_attributes" => { - "street" => "Nieuwe Prinsengracht" - } - } -} -``` - -That's it. The controller will simply pass this hash on to the model from the `create` action. The model will then handle building the `address` association for you and automatically save it when the parent (`person`) is saved. - -#### Nested form for a collection of associated objects - -The form code for an association collection is pretty similar to that of a single associated object: - -```erb -<%= form_for @person do |f| %> - <%= f.text_field :name %> - - <%= f.fields_for :projects do |pf| %> - <%= pf.text_field :name %> - <% end %> -<% end %> -``` - -Which generates: - -```html -<form action="/people" class="new_person" id="new_person" method="post"> - <input id="person_name" name="person[name]" type="text" /> - - <input id="person_projects_attributes_0_name" name="person[projects_attributes][0][name]" type="text" /> - <input id="person_projects_attributes_1_name" name="person[projects_attributes][1][name]" type="text" /> -</form> -``` - -As you can see it has generated 2 `project name` inputs, one for each new `project` that was built in the controller's `new` action. Only this time the `name` attribute of the input contains a digit as an extra namespace. This will be parsed by the Rails parameter parser as: - -```ruby -{ - "person" => { - "name" => "Eloy Duran", - "projects_attributes" => { - "0" => { "name" => "Project 1" }, - "1" => { "name" => "Project 2" } - } - } -} -``` - -You can basically see the `projects_attributes` hash as an array of attribute hashes, one for each model instance. - -NOTE: The reason that `fields_for` constructed a hash instead of an array is that it won't work for any form nested deeper than one level deep. - -TIP: You _can_ however pass an array to the writer method generated by `accepts_nested_attributes_for` if you're using plain Ruby or some other API access. See (TODO) for more info and example. diff --git a/guides/source/plugins.md b/guides/source/plugins.md index 760ff431c0..8c2d56ceb8 100644 --- a/guides/source/plugins.md +++ b/guides/source/plugins.md @@ -340,8 +340,7 @@ module Yaffle module ClassMethods def acts_as_yaffle(options = {}) - cattr_accessor :yaffle_text_field - self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s + cattr_accessor :yaffle_text_field, default: (options[:yaffle_text_field] || :last_squawk).to_s end end end @@ -411,8 +410,7 @@ module Yaffle module ClassMethods def acts_as_yaffle(options = {}) - cattr_accessor :yaffle_text_field - self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s + cattr_accessor :yaffle_text_field, default: (options[:yaffle_text_field] || :last_squawk).to_s include Yaffle::ActsAsYaffle::LocalInstanceMethods end diff --git a/guides/source/profiling.md b/guides/source/profiling.md deleted file mode 100644 index ce093f78ba..0000000000 --- a/guides/source/profiling.md +++ /dev/null @@ -1,16 +0,0 @@ -*DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -A Guide to Profiling Rails Applications -======================================= - -This guide covers built-in mechanisms in Rails for profiling your application. - -After reading this guide, you will know: - -* Rails profiling terminology. -* How to write benchmark tests for your application. -* Other benchmarking approaches and plugins. - --------------------------------------------------------------------------------- - - diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md index 3e99ee7021..e087834a2f 100644 --- a/guides/source/rails_application_templates.md +++ b/guides/source/rails_application_templates.md @@ -277,6 +277,6 @@ relative paths to your template's location. ```ruby def source_paths - [File.expand_path(File.dirname(__FILE__))] + [__dir__] end ``` diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index f25b185fb5..cef8450ee4 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -110,11 +110,12 @@ use ActiveSupport::Cache::Strategy::LocalCache::Middleware use Rack::Runtime use Rack::MethodOverride use ActionDispatch::RequestId +use ActionDispatch::RemoteIp +use Sprockets::Rails::QuietAssets use Rails::Rack::Logger use ActionDispatch::ShowExceptions use WebConsole::Middleware use ActionDispatch::DebugExceptions -use ActionDispatch::RemoteIp use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::Migration::CheckPending @@ -124,7 +125,7 @@ use ActionDispatch::Flash use Rack::Head use Rack::ConditionalGet use Rack::ETag -run Rails.application.routes +run MyApp.application.routes ``` The default middlewares shown here (and some others) are each summarized in the [Internal Middlewares](#internal-middleware-stack) section, below. @@ -238,6 +239,14 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol * Makes a unique `X-Request-Id` header available to the response and enables the `ActionDispatch::Request#request_id` method. +**`ActionDispatch::RemoteIp`** + +* Checks for IP spoofing attacks. + +**`Sprockets::Rails::QuietAssets`** + +* Suppresses logger output for asset requests. + **`Rails::Rack::Logger`** * Notifies the logs that the request has began. After request is complete, flushes all the logs. @@ -250,10 +259,6 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol * Responsible for logging exceptions and showing a debugging page in case the request is local. -**`ActionDispatch::RemoteIp`** - -* Checks for IP spoofing attacks. - **`ActionDispatch::Reloader`** * Provides prepare and cleanup callbacks, intended to assist with code reloading during development. diff --git a/guides/source/security.md b/guides/source/security.md index c305350243..297680b176 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -95,16 +95,23 @@ Rails 2 introduced a new default session storage, CookieStore. CookieStore saves * The client can see everything you store in a session, because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, _you don't want to store any secrets here_. To prevent session hash tampering, a digest is calculated from the session with a server-side secret (`secrets.secret_token`) and inserted into the end of the cookie. -However, since Rails 4, the default store is EncryptedCookieStore. With -EncryptedCookieStore the session is encrypted before being stored in a cookie. -This prevents the user from accessing and tampering the content of the cookie. -Thus the session becomes a more secure place to store data. The encryption is -done using a server-side secret key `secrets.secret_key_base` stored in -`config/secrets.yml`. +In Rails 4, encrypted cookies through AES in CBC mode with HMAC using SHA1 for +verification was introduced. This prevents the user from accessing and tampering +the content of the cookie. Thus the session becomes a more secure place to store +data. The encryption is performed using a server-side `secrets.secret_key_base`. +Two salts are used when deriving keys for encryption and verification. These +salts are set via the `config.action_dispatch.encrypted_cookie_salt` and +`config.action_dispatch.encrypted_signed_cookie_salt` configuration values. -That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA1, for compatibility). So _don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters, use `rails secret` instead_. +Rails 5.2 uses AES-GCM for the encryption which couples authentication +and encryption in one faster step and produces shorter ciphertexts. -`secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`, e.g.: +Encrypted cookies are automatically upgraded if the +`config.action_dispatch.use_authenticated_cookie_encryption` is enabled. + +_Do not use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters! Instead use `rails secret` to generate secret keys!_ + +Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`, e.g.: development: secret_key_base: a75d... @@ -356,7 +363,7 @@ send_file('/var/www/uploads/' + params[:filename]) Simply pass a file name like "../../../etc/passwd" to download the server's login information. A simple solution against this, is to _check that the requested file is in the expected directory_: ```ruby -basename = File.expand_path(File.join(File.dirname(__FILE__), '../../files')) +basename = File.expand_path('../../files', __dir__) filename = File.expand_path(File.join(basename, @file.public_filename)) raise if basename != File.expand_path(File.join(File.dirname(filename), '../../../')) @@ -796,7 +803,7 @@ In December 2006, 34,000 actual user names and passwords were stolen in a [MySpa INFO: _CSS Injection is actually JavaScript injection, because some browsers (IE, some versions of Safari and others) allow JavaScript in CSS. Think twice about allowing custom CSS in your web application._ -CSS Injection is explained best by the well-known [MySpace Samy worm](http://namb.la/popular/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm. +CSS Injection is explained best by the well-known [MySpace Samy worm](https://samy.pl/popular/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm. MySpace blocked many tags, but allowed CSS. So the worm's author put JavaScript into CSS like this: @@ -1053,6 +1060,7 @@ Additional Resources The security landscape shifts and it is important to keep up to date, because missing a new vulnerability can be catastrophic. You can find additional resources about (Rails) security here: -* Subscribe to the Rails security [mailing list](http://groups.google.com/group/rubyonrails-security) -* [Keep up to date on the other application layers](http://secunia.com/) (they have a weekly newsletter, too) -* A [good security blog](https://www.owasp.org) including the [Cross-Site scripting Cheat Sheet](https://www.owasp.org/index.php/DOM_based_XSS_Prevention_Cheat_Sheet) +* Subscribe to the Rails security [mailing list.](http://groups.google.com/group/rubyonrails-security) +* [Brakeman - Rails Security Scanner](http://brakemanscanner.org/) - To perform static security analysis for Rails applications. +* [Keep up to date on the other application layers.](http://secunia.com/) (they have a weekly newsletter, too) +* A [good security blog](https://www.owasp.org) including the [Cross-Site scripting Cheat Sheet.](https://www.owasp.org/index.php/DOM_based_XSS_Prevention_Cheat_Sheet) diff --git a/guides/source/testing.md b/guides/source/testing.md index 7741834153..7abf3af187 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -350,7 +350,9 @@ Rails adds some custom assertions of its own to the `minitest` framework: | --------------------------------------------------------------------------------- | ------- | | [`assert_difference(expressions, difference = 1, message = nil) {...}`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_difference) | Test numeric difference between the return value of an expression as a result of what is evaluated in the yielded block.| | [`assert_no_difference(expressions, message = nil, &block)`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_no_difference) | Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.| -| [`assert_nothing_raised { block }`](http://api.rubyonrails.org/classes/ActiveSupport/TestCase.html#method-i-assert_nothing_raised) | Ensures that the given block doesn't raise any exceptions.| +| [`assert_changes(expressions, message = nil, from:, to:, &block)`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_changes) | Test that the result of evaluating an expression is changed after invoking the passed in block.| +| [`assert_no_changes(expressions, message = nil, &block)`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_no_changes) | Test the result of evaluating an expression is not changed after invoking the passed in block.| +| [`assert_nothing_raised { block }`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_nothing_raised) | Ensures that the given block doesn't raise any exceptions.| | [`assert_recognizes(expected_options, path, extras={}, message=nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_recognizes) | Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.| | [`assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_generates) | Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes. The extras parameter is used to tell the request the names and values of additional request parameters that would be in a query string. The message parameter allows you to specify a custom error message for assertion failures.| | [`assert_response(type, message = nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/ResponseAssertions.html#method-i-assert_response) | Asserts that the response comes with a specific status code. You can specify `:success` to indicate 200-299, `:redirect` to indicate 300-399, `:missing` to indicate 404, or `:error` to match the 500-599 range. You can also pass an explicit status number or its symbolic equivalent. For more information, see [full list of status codes](http://rubydoc.info/github/rack/rack/master/Rack/Utils#HTTP_STATUS_CODES-constant) and how their [mapping](http://rubydoc.info/github/rack/rack/master/Rack/Utils#SYMBOL_TO_STATUS_CODE-constant) works.| @@ -600,19 +602,16 @@ Model tests don't have their own superclass like `ActionMailer::TestCase` instea System Testing -------------- -System tests are full-browser tests that can be used to test your application's -JavaScript and user experience. System tests use Capybara as a base. - -System tests allow for running tests in either a real browser or a headless -driver for testing full user interactions with your application. +System tests allow you to test user interactions with your application, running tests +in either a real or a headless browser. System tests uses Capybara under the hood. For creating Rails system tests, you use the `test/system` directory in your application. Rails provides a generator to create a system test skeleton for you. ```bash -$ bin/rails generate system_test users_create +$ bin/rails generate system_test users invoke test_unit - create test/system/users_creates_test.rb + create test/system/users_test.rb ``` Here's what a freshly-generated system test looks like: @@ -620,11 +619,11 @@ Here's what a freshly-generated system test looks like: ```ruby require "application_system_test_case" -class UsersCreatesTest < ApplicationSystemTestCase +class UsersTest < ApplicationSystemTestCase # test "visiting the index" do - # visit users_creates_url + # visit users_url # - # assert_selector "h1", text: "UsersCreate" + # assert_selector "h1", text: "Users" # end end ``` @@ -658,8 +657,9 @@ end The driver name is a required argument for `driven_by`. The optional arguments that can be passed to `driven_by` are `:using` for the browser (this will only -be used for non-headless drivers like Selenium), and `:screen_size` to change -the size of the screen for screenshots. +be used by Selenium), `:screen_size` to change the size of the screen for +screenshots, and `:options` which can be used to set options supported by the +driver. ```ruby require "test_helper" @@ -669,8 +669,9 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase end ``` -If your Capybara configuration requires more setup than provided by Rails, all -of that configuration can be put into the `application_system_test_case.rb` file. +If your Capybara configuration requires more setup than provided by Rails, this +additional configuration could be added into the `application_system_test_case.rb` +file. Please see [Capybara's documentation](https://github.com/teamcapybara/capybara#setup) for additional settings. @@ -693,9 +694,9 @@ take a screenshot of the browser. Now we're going to add a system test to our blog application. We'll demonstrate writing a system test by visiting the index page and creating a new blog article. -If you used the scaffold generator, a system test skeleton is automatically -created for you. If you did not use the generator start by creating a system -test skeleton. +If you used the scaffold generator, a system test skeleton was automatically +created for you. If you didn't use the scaffold generator, start by creating a +system test skeleton. ```bash $ bin/rails generate system_test articles @@ -1406,7 +1407,7 @@ In order to test that your mailer is working as expected, you can use unit tests For the purposes of unit testing a mailer, fixtures are used to provide an example of how the output _should_ look. Because these are example emails, and not Active Record data like the other fixtures, they are kept in their own subdirectory apart from the other fixtures. The name of the directory within `test/fixtures` directly corresponds to the name of the mailer. So, for a mailer named `UserMailer`, the fixtures should reside in `test/fixtures/user_mailer` directory. -When you generated your mailer, the generator creates stub fixtures for each of the mailers actions. If you didn't use the generator, you'll have to create those files yourself. +If you generated your mailer, the generator does not create stub fixtures for the mailers actions. You'll have to create those files yourself as described above. #### The Basic Test Case @@ -1482,7 +1483,7 @@ class UserControllerTest < ActionDispatch::IntegrationTest assert_equal "You have been invited by me@example.com", invite_email.subject assert_equal 'friend@example.com', invite_email.to[0] - assert_match(/Hi friend@example.com/, invite_email.body.to_s) + assert_match(/Hi friend@example\.com/, invite_email.body.to_s) end end ``` diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index ff747a95a0..88a7d0a464 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -83,7 +83,7 @@ Also, if you have pretty old YAML documents containing dumps of such objects, you may need to load and dump them again to make sure that they reference the right constant, and that loading them won't break in the future. -### `config.secrets` now loaded with all keys as symbols +### `application.secrets` now loaded with all keys as symbols If your application stores nested configuration in `config/secrets.yml`, all keys are now loaded as symbols, so access using strings should be changed. @@ -91,13 +91,13 @@ are now loaded as symbols, so access using strings should be changed. From: ```ruby -Rails.appplication.config.secrets[:smtp_settings]["address"] +Rails.application.secrets[:smtp_settings]["address"] ``` To: ```ruby -Rails.application.config.secrets[:smtp_settings][:address] +Rails.application.secrets[:smtp_settings][:address] ``` Upgrading from Rails 4.2 to Rails 5.0 @@ -238,7 +238,7 @@ Run `bin/rails` to see the list of commands available. ### `ActionController::Parameters` No Longer Inherits from `HashWithIndifferentAccess` Calling `params` in your application will now return an object instead of a hash. If your -parameters are already permitted, then you will not need to make any changes. If you are using `slice` +parameters are already permitted, then you will not need to make any changes. If you are using `map` and other methods that depend on being able to read the hash regardless of `permitted?` you will need to upgrade your application to first permit and then convert to a hash. diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index 047aeae37d..ed27752a06 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -250,7 +250,7 @@ Since it's just a `<form>`, all of the information on `form_with` also applies. ### Customize remote elements It is possible to customize the behavior of elements with a `data-remote` -attribute without writing a line of JavaScript. Your can specify extra `data-` +attribute without writing a line of JavaScript. You can specify extra `data-` attributes to accomplish this. #### `data-method` @@ -351,7 +351,7 @@ that have a `data-remote` attribute: NOTE: All handlers bound to these events are always passed the event object as the first argument. The table below describes the extra parameters passed after the -event argument. For exemple, if the extra parameters are listed as `xhr, settings`, +event argument. For example, if the extra parameters are listed as `xhr, settings`, then to access them, you would define your handler with `function(event, xhr, settings)`. | Event name | Extra parameters | Fired | @@ -376,6 +376,35 @@ browser to submit the form via normal means (i.e. non-AJAX submission) will be canceled and the form will not be submitted at all. This is useful for implementing your own AJAX file upload workaround. +### Rails-ujs event handlers + +Rails 5.1 introduced rails-ujs and dropped jQuery as a dependency. +As a result the Unobtrusive JavaScript (UJS) driver has been rewritten to operate without jQuery. +These introductions cause small changes to `custom events` fired during the request: + +NOTE: Signature of calls to UJS’s event handlers has changed. +Unlike the version with jQuery, all custom events return only one parameter: `event`. +In this parameter, there is an additional attribute `detail` which contains an array of extra parameters. + +| Event name | Extra parameters (event.detail) | Fired | +|---------------------|---------------------------------|-------------------------------------------------------------| +| `ajax:before` | | Before the whole ajax business. | +| `ajax:beforeSend` | [xhr, options] | Before the request is sent. | +| `ajax:send` | [xhr] | When the request is sent. | +| `ajax:stopped` | | When the request is stopped. | +| `ajax:success` | [response, status, xhr] | After completion, if the response was a success. | +| `ajax:error` | [response, status, xhr] | After completion, if the response was an error. | +| `ajax:complete` | [xhr, status] | After the request has been completed, no matter the outcome.| + +Example usage: + +```html +document.body.addEventListener('ajax:success', function(event) { + var detail = event.detail; + var data = detail[0], status = detail[1], xhr = detail[2]; +}) +``` + Server-Side Concerns -------------------- diff --git a/rails.gemspec b/rails.gemspec index 2d5be58c17..91316f089f 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index e6ef177451..b9a530258d 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,17 @@ +* Add `railtie.rb` to the plugin generator + + *Tsukuru Tanimichi* + +* Deprecate `capify!` method in generators and templates. + + *Yuji Yaginuma* + +* Allow irb options to be passed from `rails console` command. + + Fixes #28988. + + *Yuji Yaginuma* + * Added a shared section to config/database.yml that will be loaded for all environments. *Pierre Schambacher* @@ -8,5 +22,4 @@ *Jan Krutisch* - Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/railties/CHANGELOG.md) for previous changes. diff --git a/railties/Rakefile b/railties/Rakefile index 680ed03f75..d6284b7dc5 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -16,10 +16,10 @@ namespace :test do dash_i = [ "test", "lib", - "#{File.dirname(__FILE__)}/../activesupport/lib", - "#{File.dirname(__FILE__)}/../actionpack/lib", - "#{File.dirname(__FILE__)}/../actionview/lib", - "#{File.dirname(__FILE__)}/../activemodel/lib" + "#{__dir__}/../activesupport/lib", + "#{__dir__}/../actionpack/lib", + "#{__dir__}/../actionview/lib", + "#{__dir__}/../activemodel/lib" ] ruby "-w", "-I#{dash_i.join ':'}", file end @@ -27,7 +27,7 @@ namespace :test do end Rake::TestTask.new("test:regular") do |t| - t.libs << "test" << "#{File.dirname(__FILE__)}/../activesupport/lib" + t.libs << "test" << "#{__dir__}/../activesupport/lib" t.pattern = "test/**/*_test.rb" t.warning = false t.verbose = true diff --git a/railties/bin/test b/railties/bin/test index a7beb14b27..470ce93f10 100755 --- a/railties/bin/test +++ b/railties/bin/test @@ -1,4 +1,4 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" diff --git a/railties/exe/rails b/railties/exe/rails index 7e791c1f99..a5635c2297 100755 --- a/railties/exe/rails +++ b/railties/exe/rails @@ -1,9 +1,9 @@ #!/usr/bin/env ruby -git_path = File.expand_path("../../../.git", __FILE__) +git_path = File.expand_path("../../.git", __dir__) if File.exist?(git_path) - railties_path = File.expand_path("../../lib", __FILE__) + railties_path = File.expand_path("../lib", __dir__) $:.unshift(railties_path) end require "rails/cli" diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index f8a923141d..39ca2db8e1 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -260,6 +260,7 @@ module Rails "action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt, "action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt, "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt, + "action_dispatch.authenticated_encrypted_cookie_salt" => config.action_dispatch.authenticated_encrypted_cookie_salt, "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer, "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest ) diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 27c1572357..fb635c6ae8 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -77,9 +77,25 @@ module Rails assets.unknown_asset_fallback = false end + if respond_to?(:action_view) + action_view.form_with_generates_remote_forms = true + end + when "5.2" load_defaults "5.1" + if respond_to?(:active_record) + active_record.cache_versioning = true + end + + if respond_to?(:action_dispatch) + action_dispatch.use_authenticated_cookie_encryption = true + end + + if respond_to?(:active_support) + active_support.use_authenticated_message_encryption = true + end + else raise "Unknown version #{target_version.to_s.inspect}" end diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index 8fe48feefb..63300ffef3 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -10,7 +10,7 @@ module Rails end def build_stack - ActionDispatch::MiddlewareStack.new.tap do |middleware| + ActionDispatch::MiddlewareStack.new do |middleware| if config.force_ssl middleware.use ::ActionDispatch::SSL, config.ssl_options end diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb index a98e51fd28..f7d112900a 100644 --- a/railties/lib/rails/application_controller.rb +++ b/railties/lib/rails/application_controller.rb @@ -1,5 +1,5 @@ class Rails::ApplicationController < ActionController::Base # :nodoc: - self.view_paths = File.expand_path("../templates", __FILE__) + self.view_paths = File.expand_path("templates", __dir__) layout "application" private diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb index 0d4e6dc5a1..ee020b58f9 100644 --- a/railties/lib/rails/command.rb +++ b/railties/lib/rails/command.rb @@ -23,7 +23,7 @@ module Rails end def environment # :nodoc: - ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" + ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development" end # Receives a namespace, arguments and the behavior to invoke the command. diff --git a/railties/lib/rails/command/actions.rb b/railties/lib/rails/command/actions.rb index 8fda1c87c6..a00e58997c 100644 --- a/railties/lib/rails/command/actions.rb +++ b/railties/lib/rails/command/actions.rb @@ -5,7 +5,7 @@ module Rails # This allows us to run `rails server` from other directories, but still get # the main config.ru and properly set the tmp directory. def set_application_directory! - Dir.chdir(File.expand_path("../../", APP_PATH)) unless File.exist?(File.expand_path("config.ru")) + Dir.chdir(File.expand_path("../..", APP_PATH)) unless File.exist?(File.expand_path("config.ru")) end def require_application_and_environment! diff --git a/railties/lib/rails/commands/console/console_command.rb b/railties/lib/rails/commands/console/console_command.rb index 62e3aa19df..ec58540923 100644 --- a/railties/lib/rails/commands/console/console_command.rb +++ b/railties/lib/rails/commands/console/console_command.rb @@ -73,14 +73,26 @@ module Rails class_option :environment, aliases: "-e", type: :string, desc: "Specifies the environment to run this console under (test/development/production)." + def initialize(args = [], local_options = {}, config = {}) + console_options = [] + + # For the same behavior as OptionParser, leave only options after "--" in ARGV. + termination = local_options.find_index("--") + if termination + console_options = local_options[termination + 1..-1] + local_options = local_options[0...termination] + end + + ARGV.replace(console_options) + super(args, local_options, config) + end + def perform extract_environment_option_from_argument # RAILS_ENV needs to be set before config/application is required. ENV["RAILS_ENV"] = options[:environment] - ARGV.clear # Clear ARGV so IRB doesn't freak. - require_application_and_environment! Rails::Console.start(Rails.application, options) end diff --git a/railties/lib/rails/commands/help/USAGE b/railties/lib/rails/commands/help/USAGE index c5f8ab72bb..8eb98319d2 100644 --- a/railties/lib/rails/commands/help/USAGE +++ b/railties/lib/rails/commands/help/USAGE @@ -1,13 +1,14 @@ 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") - test Run tests (short-cut alias: "t") - dbconsole Start a console for the database specified in config/database.yml - (short-cut alias: "db") + 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") + test Run tests except system tests (short-cut alias: "t") + test:system Run system tests + dbconsole Start a console for the database specified in config/database.yml + (short-cut alias: "db") <% unless engine? %> - new Create a new Rails application. "rails new my_app" creates a - new application called MyApp in "./my_app" + new Create a new Rails application. "rails new my_app" creates a + new application called MyApp in "./my_app" <% end %> All commands can be run with -h (or --help) for more information. diff --git a/railties/lib/rails/commands/secrets/secrets_command.rb b/railties/lib/rails/commands/secrets/secrets_command.rb index 03a640bd65..5f077a5bcb 100644 --- a/railties/lib/rails/commands/secrets/secrets_command.rb +++ b/railties/lib/rails/commands/secrets/secrets_command.rb @@ -13,10 +13,7 @@ module Rails end def setup - require "rails/generators" - require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" - - Rails::Generators::EncryptedSecretsGenerator.start + generator.start end def edit @@ -34,8 +31,7 @@ module Rails require_application_and_environment! Rails::Secrets.read_for_editing do |tmp_path| - say "Waiting for secrets file to be saved. Abort with Ctrl-C." - system("\$EDITOR #{tmp_path}") + system("#{ENV["EDITOR"]} #{tmp_path}") end say "New secrets encrypted and saved." @@ -43,7 +39,22 @@ module Rails say "Aborted changing encrypted secrets: nothing saved." rescue Rails::Secrets::MissingKeyError => error say error.message + rescue Errno::ENOENT => error + raise unless error.message =~ /secrets\.yml\.enc/ + + Rails::Secrets.read_template_for_editing do |tmp_path| + system("#{ENV["EDITOR"]} #{tmp_path}") + generator.skip_secrets_file { setup } + end end + + private + def generator + require "rails/generators" + require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" + + Rails::Generators::EncryptedSecretsGenerator + end end end end diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index 278fe63c51..ebb4ae795a 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -95,10 +95,11 @@ module Rails module Command class ServerCommand < Base # :nodoc: + DEFAULT_PORT = 3000 DEFAULT_PID_PATH = "tmp/pids/server.pid".freeze class_option :port, aliases: "-p", type: :numeric, - desc: "Runs Rails on the specified port.", banner: :port, default: 3000 + desc: "Runs Rails on the specified port - defaults to 3000.", banner: :port class_option :binding, aliases: "-b", type: :string, desc: "Binds Rails to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments'.", banner: :IP @@ -154,9 +155,16 @@ module Rails def user_supplied_options @user_supplied_options ||= begin # Convert incoming options array to a hash of flags - # ["-p", "3001", "-c", "foo"] # => {"-p" => true, "-c" => true} + # ["-p3001", "-C", "--binding", "127.0.0.1"] # => {"-p"=>true, "-C"=>true, "--binding"=>true} user_flag = {} - @original_options.each_with_index { |command, i| user_flag[command] = true if i.even? } + @original_options.each do |command| + if command.to_s.start_with?("--") + option = command.split("=")[0] + user_flag[option] = true + elsif command =~ /\A(-.)/ + user_flag[Regexp.last_match[0]] = true + end + end # Collect all options that the user has explicitly defined so we can # differentiate them from defaults @@ -184,7 +192,7 @@ module Rails end def port - ENV.fetch("PORT", options[:port]).to_i + options[:port] || ENV.fetch("PORT", DEFAULT_PORT).to_i end def host diff --git a/railties/lib/rails/commands/test/test_command.rb b/railties/lib/rails/commands/test/test_command.rb index 65e16900ba..dce85cf12d 100644 --- a/railties/lib/rails/commands/test/test_command.rb +++ b/railties/lib/rails/commands/test/test_command.rb @@ -11,7 +11,7 @@ module Rails end def perform(*) - $LOAD_PATH << Rails::Command.root.join("test") + $LOAD_PATH << Rails::Command.root.join("test").to_s Minitest.run_via = :rails diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index dc0b158bd4..2732485c5a 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -40,7 +40,7 @@ module Rails # # class MyEngine < Rails::Engine # # Add a load path for this specific Engine - # config.autoload_paths << File.expand_path("../lib/some/path", __FILE__) + # config.autoload_paths << File.expand_path("lib/some/path", __dir__) # # initializer "my_engine.add_middleware" do |app| # app.middleware.use MyEngine::Middleware diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 8ec805370b..8f15f3a594 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -1,4 +1,4 @@ -activesupport_path = File.expand_path("../../../../activesupport/lib", __FILE__) +activesupport_path = File.expand_path("../../../activesupport/lib", __dir__) $:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) require "thor/group" diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index 0bd0615b7e..5cf0985050 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -227,6 +227,7 @@ module Rails # # capify! def capify! + ActiveSupport::Deprecation.warn("`capify!` is deprecated and will be removed in the next version of Rails.") log :capify, "" in_root { run("#{extify(:capify)} .", verbose: false) } end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index c715e5ac9f..8429b6c7b8 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -13,7 +13,6 @@ module Rails DATABASES = %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver ) JDBC_DATABASES = %w( jdbcmysql jdbcsqlite3 jdbcpostgresql jdbc ) DATABASES.concat(JDBC_DATABASES) - WEBPACKS = %w( react vue angular ) attr_accessor :rails_template add_shebang_option! @@ -31,9 +30,6 @@ module Rails class_option :database, type: :string, aliases: "-d", default: "sqlite3", desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})" - class_option :webpack, type: :string, default: nil, - desc: "Preconfigure for app-like JavaScript with Webpack (options: #{WEBPACKS.join('/')})" - class_option :skip_yarn, type: :boolean, default: false, desc: "Don't use Yarn for managing JavaScript dependencies" @@ -353,7 +349,7 @@ module Rails if defined?(JRUBY_VERSION) GemfileEntry.version "therubyrhino", nil, comment else - GemfileEntry.new "therubyracer", nil, comment, { platforms: :ruby }, true + GemfileEntry.new "mini_racer", nil, comment, { platforms: :ruby }, true end end diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index a650c52626..e7f51dba99 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -215,7 +215,7 @@ module Rails # Returns the base root for a common set of generators. This is used to dynamically # guess the default source root. def self.base_root - File.dirname(__FILE__) + __dir__ end # Cache source root and add lib/generators/base/generator/templates to diff --git a/railties/lib/rails/generators/css/assets/assets_generator.rb b/railties/lib/rails/generators/css/assets/assets_generator.rb index 20baf31a34..af7b5cf609 100644 --- a/railties/lib/rails/generators/css/assets/assets_generator.rb +++ b/railties/lib/rails/generators/css/assets/assets_generator.rb @@ -3,7 +3,7 @@ require "rails/generators/named_base" module Css # :nodoc: module Generators # :nodoc: class AssetsGenerator < Rails::Generators::NamedBase # :nodoc: - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) def copy_stylesheet copy_file "stylesheet.css", File.join("app/assets/stylesheets", class_path, "#{file_name}.css") diff --git a/railties/lib/rails/generators/js/assets/assets_generator.rb b/railties/lib/rails/generators/js/assets/assets_generator.rb index 64d706ec91..52a71b58cd 100644 --- a/railties/lib/rails/generators/js/assets/assets_generator.rb +++ b/railties/lib/rails/generators/js/assets/assets_generator.rb @@ -3,7 +3,7 @@ require "rails/generators/named_base" module Js # :nodoc: module Generators # :nodoc: class AssetsGenerator < Rails::Generators::NamedBase # :nodoc: - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) def copy_javascript copy_file "javascript.js", File.join("app/assets/javascripts", class_path, "#{file_name}.js") diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index f4717bb35b..45b9e7bdff 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -67,6 +67,10 @@ module Rails end end + def package_json + template "package.json" + end + def app directory "app" @@ -117,7 +121,6 @@ module Rails action_cable_config_exist = File.exist?("config/cable.yml") rack_cors_config_exist = File.exist?("config/initializers/cors.rb") assets_config_exist = File.exist?("config/initializers/assets.rb") - new_framework_defaults_5_1_exist = File.exist?("config/initializers/new_framework_defaults_5_1.rb") config @@ -141,12 +144,6 @@ module Rails unless assets_config_exist remove_file "config/initializers/assets.rb" end - - # Sprockets owns the only new default for 5.1: - # In API-only Applications, we don't want the file. - unless new_framework_defaults_5_1_exist - remove_file "config/initializers/new_framework_defaults_5_1.rb" - end end end @@ -198,20 +195,18 @@ module Rails def vendor empty_directory_with_keep_file "vendor" - - unless options[:skip_yarn] - template "package.json" - end end end module Generators # We need to store the RAILS_DEV_PATH in a constant, otherwise the path # can change in Ruby 1.8.7 when we FileUtils.cd. - RAILS_DEV_PATH = File.expand_path("../../../../../..", File.dirname(__FILE__)) + RAILS_DEV_PATH = File.expand_path("../../../../../..", __dir__) RESERVED_NAMES = %w[application destroy plugin runner test] class AppGenerator < AppBase # :nodoc: + WEBPACKS = %w( react vue angular elm ) + add_shared_options_for "application" # Add bin/rails options @@ -224,6 +219,9 @@ 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 for app-like JavaScript with Webpack (options: #{WEBPACKS.join('/')})" + def initialize(*args) super @@ -248,6 +246,7 @@ module Rails build(:gitignore) unless options[:skip_git] build(:gemfile) unless options[:skip_gemfile] build(:version_control) + build(:package_json) unless options[:skip_yarn] end def create_app_files @@ -317,10 +316,6 @@ module Rails def create_vendor_files build(:vendor) - - if options[:skip_yarn] - remove_file "package.json" - end end def delete_app_assets_if_api_option @@ -404,7 +399,7 @@ module Rails def delete_new_framework_defaults unless options[:update] - remove_file "config/initializers/new_framework_defaults_5_1.rb" + remove_file "config/initializers/new_framework_defaults_5_2.rb" end end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 06f0dd6d6d..64e2062aea 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -1,9 +1,5 @@ source 'https://rubygems.org' - -git_source(:github) do |repo_name| - repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") - "https://github.com/#{repo_name}.git" -end +git_source(:github) { |repo| "https://github.com/#{repo}.git" } <% gemfile_entries.each do |gem| -%> <% if gem.comment -%> @@ -34,14 +30,14 @@ group :development, :test do gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] <%- if depends_on_system_test? -%> # Adds support for Capybara system testing and selenium driver - gem 'capybara', '~> 2.13.0' + gem 'capybara', '~> 2.13' gem 'selenium-webdriver' <%- end -%> end group :development do <%- unless options.api? -%> - # Access an IRB console on exception pages or by using <%%= console %> anywhere in the code. + # Access an interactive console on exception pages or by calling 'console' anywhere in the code. <%- if options.dev? || options.edge? -%> gem 'web-console', github: 'rails/web-console' <%- else -%> diff --git a/railties/lib/rails/generators/rails/app/templates/bin/bundle b/railties/lib/rails/generators/rails/app/templates/bin/bundle index 1123dcf501..a84f0afe47 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/bundle +++ b/railties/lib/rails/generators/rails/app/templates/bin/bundle @@ -1,2 +1,2 @@ -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) load Gem.bin_path('bundler', 'bundle') 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 52b3de5ee5..ee9d077c30 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt @@ -1,9 +1,8 @@ -require 'pathname' require 'fileutils' include FileUtils # path to your application root. -APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) +APP_ROOT = File.expand_path('..', __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update.tt b/railties/lib/rails/generators/rails/app/templates/bin/update.tt index d385b363c6..5b6e50883e 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/update.tt +++ b/railties/lib/rails/generators/rails/app/templates/bin/update.tt @@ -1,9 +1,8 @@ -require 'pathname' require 'fileutils' include FileUtils # path to your application root. -APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) +APP_ROOT = File.expand_path('..', __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml index a21555e573..049de65f22 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml @@ -1,4 +1,4 @@ -# SQL Server (2012 or higher recommended) +# SQL Server (2012 or higher required) # # Install the adapters and driver # gem install tiny_tds @@ -12,7 +12,7 @@ default: &default adapter: sqlserver encoding: utf8 username: sa - password: <%= ENV['SA_PASSWORD'] %> + password: <%%= ENV['SA_PASSWORD'] %> host: localhost development: diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 9c4a77fd1d..d44331a888 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -36,8 +36,8 @@ Rails.application.configure do config.assets.compile = false # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb - <%- end -%> + <%- end -%> # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' @@ -50,8 +50,8 @@ Rails.application.configure do # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] - <%- end -%> + <%- end -%> # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true @@ -68,14 +68,15 @@ Rails.application.configure do # Use a real queuing backend for Active Job (and separate queues per environment) # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "<%= app_name %>_#{Rails.env}" + <%- unless options.skip_action_mailer? -%> config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false - <%- end -%> + <%- end -%> # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_1.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_1.rb.tt deleted file mode 100644 index a0c7f44b60..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_1.rb.tt +++ /dev/null @@ -1,16 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.1 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Make `form_with` generate non-remote forms. -Rails.application.config.action_view.form_with_generates_remote_forms = false -<%- unless options[:skip_sprockets] -%> - -# Unknown asset fallback will return the path passed in when the given -# asset is not present in the asset pipeline. -# Rails.application.config.assets.unknown_asset_fallback = false -<%- end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt new file mode 100644 index 0000000000..3809936f9f --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt @@ -0,0 +1,19 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 5.2 upgrade. +# +# Once upgraded flip defaults one by one to migrate to the new default. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. + +# Make Active Record use stable #cache_key alongside new #cache_version method. +# This is needed for recyclable cache keys. +# Rails.application.config.active_record.cache_versioning = true + +# Use AES 256 GCM authenticated encryption for encrypted cookies. +# Existing cookies will be converted on read then written with the new scheme. +# Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true + +# Use AES-256-GCM authenticated encryption as default cipher for encrypting messages +# instead of AES-256-CBC, when use_authenticated_message_encryption is set to true. +# Rails.application.config.active_support.use_authenticated_message_encryption = true diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore index 7221c26729..1e6b9afcd2 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore +++ b/railties/lib/rails/generators/rails/app/templates/gitignore @@ -26,4 +26,8 @@ /yarn-error.log <% end -%> + +<% unless options[:api] -%> +/public/assets +<% end -%> .byebug_history diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb index 2f92168eef..6ad1f11781 100644 --- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb +++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb @@ -1,4 +1,4 @@ -require File.expand_path('../../config/environment', __FILE__) +require_relative '../config/environment' require 'rails/test_help' class ActiveSupport::TestCase diff --git a/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb b/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb index 8b29213610..1da2fbc1a5 100644 --- a/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb +++ b/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb @@ -36,25 +36,29 @@ module Rails end def add_encrypted_secrets_file - unless File.exist?("config/secrets.yml.enc") + unless (defined?(@@skip_secrets_file) && @@skip_secrets_file) || File.exist?("config/secrets.yml.enc") say "Adding config/secrets.yml.enc to store secrets that needs to be encrypted." say "" + say "For now the file contains this but it's been encrypted with the generated key:" + say "" + say Secrets.template, :on_green + say "" - template "config/secrets.yml.enc" do |prefill| - say "" - say "For now the file contains this but it's been encrypted with the generated key:" - say "" - say prefill, :on_green - say "" - - Secrets.encrypt(prefill) - end + Secrets.write(Secrets.template) say "You can edit encrypted secrets with `bin/rails secrets:edit`." - - say "Add this to your config/environments/production.rb:" - say "config.read_encrypted_secrets = true" + say "" end + + say "Add this to your config/environments/production.rb:" + say "config.read_encrypted_secrets = true" + end + + def self.skip_secrets_file + @@skip_secrets_file = true + yield + ensure + @@skip_secrets_file = false end private diff --git a/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc b/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc deleted file mode 100644 index 70426a66a5..0000000000 --- a/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc +++ /dev/null @@ -1,3 +0,0 @@ -# See `secrets.yml` for tips on generating suitable keys. -# production: -# external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289… diff --git a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt index d0575772bc..178d5c3f9f 100644 --- a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt +++ b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt @@ -1,3 +1,3 @@ class <%= class_name %>Generator < Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) end diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 118e44d9d0..445235852d 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -60,7 +60,12 @@ module Rails template "lib/%namespaced_name%.rb" template "lib/tasks/%namespaced_name%_tasks.rake" template "lib/%namespaced_name%/version.rb" - template "lib/%namespaced_name%/engine.rb" if engine? + + if engine? + template "lib/%namespaced_name%/engine.rb" + else + template "lib/%namespaced_name%/railtie.rb" + end end def config diff --git a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec index d84d1aabdb..9a8c4bf098 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec +++ b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec @@ -1,4 +1,4 @@ -$:.push File.expand_path("../lib", __FILE__) +$:.push File.expand_path("lib", __dir__) # Maintain your gem's version: require "<%= namespaced_name %>/version" diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile index 383d2fb2d1..3581dd401a 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile +++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile @@ -15,7 +15,7 @@ RDoc::Task.new(:rdoc) do |rdoc| end <% if engine? && !options[:skip_active_record] && with_dummy_app? -%> -APP_RAKEFILE = File.expand_path("../<%= dummy_path -%>/Rakefile", __FILE__) +APP_RAKEFILE = File.expand_path("<%= dummy_path -%>/Rakefile", __dir__) load 'rails/tasks/engine.rake' <% end %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt index c03d9953d4..ffa277e334 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt @@ -1,12 +1,12 @@ # This command will automatically be run when you run "rails" with Rails gems # installed from the root of your application. -ENGINE_ROOT = File.expand_path('../..', __FILE__) -ENGINE_PATH = File.expand_path('../../lib/<%= namespaced_name -%>/engine', __FILE__) -APP_PATH = File.expand_path('../../<%= dummy_path -%>/config/application', __FILE__) +ENGINE_ROOT = File.expand_path('..', __dir__) +ENGINE_PATH = File.expand_path('../lib/<%= namespaced_name -%>/engine', __dir__) +APP_PATH = File.expand_path('../<%= dummy_path -%>/config/application', __dir__) # Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) require 'rails/all' diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt index 8385e6a8a2..8e7d321626 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt @@ -1,4 +1,4 @@ -$: << File.expand_path(File.expand_path("../../test", __FILE__)) +$: << File.expand_path("../test", __dir__) require "bundler/setup" require "rails/plugin/test" diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb index 40b1c4cee7..3285055eb7 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb @@ -1,5 +1,7 @@ <% if engine? -%> require "<%= namespaced_name %>/engine" - +<% else -%> +require "<%= namespaced_name %>/railtie" <% end -%> + <%= wrap_in_modules "# Your code goes here..." %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb new file mode 100644 index 0000000000..7bdf4ee5fb --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb @@ -0,0 +1,5 @@ +<%= wrap_in_modules <<-rb.strip_heredoc + class Railtie < ::Rails::Railtie + end +rb +%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb index e84e403018..c281fc42ca 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb @@ -1,8 +1,8 @@ -require File.expand_path("../../<%= options[:dummy_path] -%>/config/environment.rb", __FILE__) +require_relative "<%= File.join('..', options[:dummy_path], 'config/environment') -%>" <% unless options[:skip_active_record] -%> -ActiveRecord::Migrator.migrations_paths = [File.expand_path("../../<%= options[:dummy_path] -%>/db/migrate", __FILE__)] +ActiveRecord::Migrator.migrations_paths = [File.expand_path("../<%= options[:dummy_path] -%>/db/migrate", __dir__)] <% if options[:mountable] -%> -ActiveRecord::Migrator.migrations_paths << File.expand_path('../../db/migrate', __FILE__) +ActiveRecord::Migrator.migrations_paths << File.expand_path('../db/migrate', __dir__) <% end -%> <% end -%> require "rails/test_help" @@ -17,7 +17,7 @@ Rails::TestUnitReporter.executable = 'bin/test' # Load fixtures from the engine if ActiveSupport::TestCase.respond_to?(:fixture_path=) - ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) + ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" ActiveSupport::TestCase.fixtures :all diff --git a/railties/lib/rails/generators/rails/scaffold_controller/USAGE b/railties/lib/rails/generators/rails/scaffold_controller/USAGE index 8ba4c5ccbc..28f229510b 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/USAGE +++ b/railties/lib/rails/generators/rails/scaffold_controller/USAGE @@ -12,7 +12,7 @@ Description: Example: `rails generate scaffold_controller CreditCard` - Credit card controller with URLs like /credit_card/debit. + Credit card controller with URLs like /credit_cards. Controller: app/controllers/credit_cards_controller.rb Test: test/controllers/credit_cards_controller_test.rb Views: app/views/credit_cards/index.html.erb [...] diff --git a/railties/lib/rails/generators/test_case.rb b/railties/lib/rails/generators/test_case.rb index 3eec929aeb..575af80303 100644 --- a/railties/lib/rails/generators/test_case.rb +++ b/railties/lib/rails/generators/test_case.rb @@ -14,7 +14,7 @@ module Rails # # class AppGeneratorTest < Rails::Generators::TestCase # tests AppGenerator - # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # destination File.expand_path("../tmp", __dir__) # end # # If you want to ensure your destination root is clean before running each test, @@ -22,7 +22,7 @@ module Rails # # class AppGeneratorTest < Rails::Generators::TestCase # tests AppGenerator - # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # destination File.expand_path("../tmp", __dir__) # setup :prepare_destination # end class TestCase < ActiveSupport::TestCase diff --git a/railties/lib/rails/generators/test_unit/system/system_generator.rb b/railties/lib/rails/generators/test_unit/system/system_generator.rb index aec415a4e5..0514957d9c 100644 --- a/railties/lib/rails/generators/test_unit/system/system_generator.rb +++ b/railties/lib/rails/generators/test_unit/system/system_generator.rb @@ -10,7 +10,7 @@ module TestUnit # :nodoc: template "application_system_test_case.rb", File.join("test", "application_system_test_case.rb") end - template "system_test.rb", File.join("test/system", "#{file_name.pluralize}_test.rb") + template "system_test.rb", File.join("test/system", class_path, "#{file_name.pluralize}_test.rb") end end end diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb index 64d641d096..ce0e42e60d 100644 --- a/railties/lib/rails/generators/testing/behaviour.rb +++ b/railties/lib/rails/generators/testing/behaviour.rb @@ -14,12 +14,12 @@ module Rails include ActiveSupport::Testing::Stream included do - class_attribute :destination_root, :current_path, :generator_class, :default_arguments - # Generators frequently change the current path using +FileUtils.cd+. # So we need to store the path at file load and revert back to it after each test. - self.current_path = File.expand_path(Dir.pwd) - self.default_arguments = [] + class_attribute :current_path, default: File.expand_path(Dir.pwd) + class_attribute :default_arguments, default: [] + class_attribute :destination_root + class_attribute :generator_class end module ClassMethods @@ -40,7 +40,7 @@ module Rails # Sets the destination of generator files: # - # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # destination File.expand_path("../tmp", __dir__) def destination(path) self.destination_root = path end @@ -51,7 +51,7 @@ module Rails # # class AppGeneratorTest < Rails::Generators::TestCase # tests AppGenerator - # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # destination File.expand_path("../tmp", __dir__) # setup :prepare_destination # # test "database.yml is not created when skipping Active Record" do diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb index fc064dac32..db08d578c0 100644 --- a/railties/lib/rails/info.rb +++ b/railties/lib/rails/info.rb @@ -5,8 +5,9 @@ module Rails # Rails::InfoController responses. These include the active Rails version, # Ruby version, Rack version, and so on. module Info - mattr_accessor :properties - class << (@@properties = []) + mattr_accessor :properties, default: [] + + class << @@properties def names map(&:first) end diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index 3476bb0eb5..eca8335559 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -103,6 +103,9 @@ module Rails # end # end # + # Since filenames on the load path are shared across gems, be sure that files you load + # through a railtie have unique names. + # # == Application and Engine # # An engine is nothing more than a railtie with some initializers already set. And since diff --git a/railties/lib/rails/secrets.rb b/railties/lib/rails/secrets.rb index 8b644f212c..c7a8676d7b 100644 --- a/railties/lib/rails/secrets.rb +++ b/railties/lib/rails/secrets.rb @@ -1,5 +1,6 @@ require "yaml" require "active_support/message_encryptor" +require "active_support/core_ext/string/strip" module Rails # Greatly inspired by Ara T. Howard's magnificent sekrets gem. 😘 @@ -37,6 +38,15 @@ module Rails ENV["RAILS_MASTER_KEY"] || read_key_file || handle_missing_key end + def template + <<-end_of_template.strip_heredoc + # See `secrets.yml` for tips on generating suitable keys. + # production: + # external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289 + + end_of_template + end + def encrypt(data) encryptor.encrypt_and_sign(data) end @@ -54,15 +64,12 @@ module Rails FileUtils.mv("#{path}.tmp", path) end - def read_for_editing - tmp_path = File.join(Dir.tmpdir, File.basename(path)) - IO.binwrite(tmp_path, read) - - yield tmp_path + def read_for_editing(&block) + writing(read, &block) + end - write(IO.binread(tmp_path)) - ensure - FileUtils.rm(tmp_path) if File.exist?(tmp_path) + def read_template_for_editing(&block) + writing(template, &block) end private @@ -92,6 +99,17 @@ module Rails end end + def writing(contents) + tmp_path = File.join(Dir.tmpdir, File.basename(path)) + File.write(tmp_path, contents) + + yield tmp_path + + write(File.read(tmp_path)) + ensure + FileUtils.rm(tmp_path) if File.exist?(tmp_path) + end + def encryptor @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: @cipher) end diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake index 32a6b109bc..80720a42ff 100644 --- a/railties/lib/rails/tasks/framework.rake +++ b/railties/lib/rails/tasks/framework.rake @@ -16,7 +16,7 @@ namespace :app do namespace :templates do # desc "Copy all the templates from rails to the application directory for customization. Already existing local copies will be overwritten" task :copy do - generators_lib = File.expand_path("../../generators", __FILE__) + generators_lib = File.expand_path("../generators", __dir__) project_templates = "#{Rails.root}/lib/templates" default_templates = { "erb" => %w{controller mailer scaffold}, diff --git a/railties/lib/rails/templates/rails/welcome/index.html.erb b/railties/lib/rails/templates/rails/welcome/index.html.erb index 5cdb7e6a20..5a82bf913c 100644 --- a/railties/lib/rails/templates/rails/welcome/index.html.erb +++ b/railties/lib/rails/templates/rails/welcome/index.html.erb @@ -26,18 +26,28 @@ p { font-family: monospace; } .container { - width: 960px; + max-width: 960px; margin: 0 auto 40px; overflow: hidden; } - section { margin: 0 auto 2rem; padding: 1rem 0 0; - width: 700px; text-align: center; } + + @media only screen and (max-width: 500px) { + h1 { font-size: 2rem; } + + .version { font-size: 1.1rem; } + } + + .welcome { + width: 600px; + max-width: 100%; + height: auto; + } </style> </head> @@ -52,7 +62,7 @@ <h1>Yay! You’re on Rails!</h1> - <img alt="Welcome" width="600" height="350" src="data:image/jpg;base64,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" /> + <img alt="Welcome" class="welcome" src="data:image/jpg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEABALCwsMCxAMDBAXDw0PFxsUEBAUGx8XFxcXFx8eFxoaGhoXHh4jJSclIx4vLzMzLy9AQEBAQEBAQEBAQEBAQEABEQ8PERMRFRISFRQRFBEUGhQWFhQaJhoaHBoaJjAjHh4eHiMwKy4nJycuKzU1MDA1NUBAP0BAQEBAQEBAQEBAQP/CABEIArwEsAMBIgACEQEDEQH/xAAbAAEAAgMBAQAAAAAAAAAAAAAAAwQBAgUGB//aAAgBAQAAAAD6AAAAAAAAAAAMZAAAGMgMZABjIAAAAAAAAAAAAAAAAAAYyDGQAAADBkAxljIAADGQAAAAAAAAAAAANMSAAAAAAABjLGQAAAAAAAAAAAAADEUwDSGyGGQAAxkAAAAAAAAAxkGMjGQAAAAAAFS2Yy1xtVthSt7AAAABjIAAAAAAAAAAAAAAAAMZBDtJVtFS3TuYxtjSQAAAAAADGQAAAADGQAAAAAAACrY2BVtEM1S3BmbSvbAAMZAxkAAAAAAMMgAAMZAGM4yAAAAK02+DKragnKlqNLHHYi23DGQYyBjIAAAAAAAAAAAAAAA1znDEE0FnMcdiOJLJVtU7hWn2Yj1nq2NgwyGGQAAGMgAAYyAAABjIAAAAAhkVLdW1mtPvipPvBttLVtQywz1pt9a0qapayFWaSpbKlrIAAAAAAxljIAAMZAAAABptkxkxmncp3EWs6pmfeLWeraK9ivLvjEO28FipdVrEE+adwhmFS2a6SmMgAAAAAAAYyAAAACHWxhTuhUtVbO2KttTs521gs1bQV5JCrYzBJBbYya17QK89W2VbOYdpAAAAAYyAADGTGQAAADFS5rVW8jSCZJirbRyK1itaq2kciDeSvYqWs184sMY2Q5l1inKe82zRjercYyMZAYZxkAYyAAMZAAAAAabqtjaOGxtrtjNO4hkr2gp3KlupbQzVd95Kdynb1gTa7R7TKu+2c7otN5YpMw676zgGuwMZAAAAAYyAAACPbY1jk21r2mta1SvVLdO2hzHaMZq2qkmlmtaRxyb1bdOxAzY13qW8qk0mVaxUt5Yyp3KV2HMpXm2ijsgAAAAAGMgACPfIMV7JWlkilqZn3p3KlrKvvtmstwT0rla1U3zvBbK8mYLVfZCt5U7jFW2rT87pVLhXsQZmxmlZ22xTu4p3MgAAAAAAAAI4bQDGWKtuGWLXM9WxiFZ0gmhngs1Leu9WzVtw5itQT15MwWNoNoLgp28wpYYrdWTaU1zUuEUebCpZ2rSyAxkDGQAAAAAGMitYyBDNjNeSRirJNivaqW2KdyndryxSSK09a3HpOp3INsxR3a8le5VtYq63KtqHfePWLeaJvFY212o3dYpIbUUdkAAAAAAwyABhkBjIQS5qXIJ6k+d6V2vLtSzdo3oW0djEWYbete0qW4NWNblWetco3ocRXKdypbVLVSxvVsYiTV5tLFK5FLrWuAAAAAGMgAAMZAVbVWWXEFgQbRWNNZ49ZsVbeteard1hsVLlO1BpbQTbVLdffMOLVWzUu0rtaatcqW6dxUk2lqWdqdqCTend0jsGCtaAAAAADGQBjIMZIZiDaSpcU7bMMtS1XtU7lK5lTt51q2N1W3TuUrlWbbWWraqW6+6PW1Ut1LdO7UtVLdW3TuaQx3dK9uOPM2dNJqdwFSxuAAAAAADGcZCGZSt7K0shUtxxz6wZkxiOzXs1bUe2yHMdmrbKdyGancp3MVLlexUt1pcR626dynZisU7jTWSrbqWYbFO3mlcyFfeWtYgnqz53rWQAAAAAAAV584q2ypaj2QsSa62Kl2nagnrzRWMxwWqdjbdUtVbeNa1zRtruVLUeuUVuCWvaKdwR6zU7jFO7HFYrWjWtbr7RWa82mLDGQAADGQYzjIAADTdriGxrvWkkqXMVJNtorKpbpXMqlujdZVpYLUcelrancYyqWcokNnbJFLTuBpiCztX2mp26tjSXNOSSC2hxPrs0p3sgAAAAYywyACpayVJ9NpY0FuLSWLeC3VtVt8xzRzVbbOuc6Q62al2DO+spjNSxiQjxJko3qduKarag0l0sQzYhkgs07sDMNvMcNoR17cUwDGQAAAMMgVbTBVtRTYq2qtnNSTeWCWqtRS151a3ghzPjwfuN9kGYbmtVbKnL9BrmpMmr7ya74q2qlmKendabsZFS0xmpcrTb61reKtuOC1ppOAAGMgAGGQGM1LNW5FpYqzop6dyLOlgr42mzUtKtjcxjaPyElX3I03UZJ9o/M6+tId9s+W1irei6ke0dlpFYw12rWNJcVbG2aljEaWvazHnEFsY13AGGQMZAAAYyhxtvtVn2jlhbb5qW68kmIoLlO3X1s1bjME7m+Z6G3po5AxXlr+F7kPsAYpeUit2vUqk8irY2qz1reDarPWuaQWalurmaSOSKKzStbqVvaKYAAwyBgyMZBSuZVp9mKlxjFefeDbWapc1zpCmr2cbax2KVytP5T1fn/RVrQIWPJel53oI+VXudTLlxcH0/QQy17NO5Tt51zkU7mM07debeDM0GskVmnaq3KdvYGMgAAAADFW3Xnq2tYJorOKN6pb1g1ts15JKlpipdKtqtJDb8/wBjjehxFwqtnt2TTiUvU7eWvVNofU55knG9RhlptUtVrbys/a3m00lxHjeKzHFZigsQWqdvKnb1SaaTAAAAxkAxkj03k0hsRTVZpNa1mrca1rdS1kRS4yYzileR+fn6u3n7/H7XN6vScbm+kn18zNnGvWvcXpcT0TI1i0mSeZcn1fQyqWK9qncU7ulW1XtU7W+tO5HHZ1rzw2gAAAwyDDKCdXkkQpmKdzOkEmu8tSxiNPlDM08/0rkmWK1rzfb5db0m3Bp9nznq7PmsemU+Smq3te157r8zuM4ZIZalzzfd4fo4pWMo874Yp3alupYkxSu19pytrazHJpHOGMgGGQGMtdo5NNq1mCavPvGgtotJ68u9O7UsaTMM8fg35IOh2lO3ByaPQlq5nn5sXa836i287f6NLkZ5/puZYn6dHh707vpcsa78O1D2KdwjkQTsUrke9efbGyrNrmSpYkhnxHXuZAAAArSyVZZcRxWatqpbp3KlnSbXNabfKGbWtbcbqyV/OenkxV872OrptngVPUUbele55npcS96XGvmsda7wNc3+Tz/X2PLX99uXa9KHGi608UxBO03Vpt9NcS0rmallHjS1rWtbYrWgAAADFO5mpZaa52imgsVLlO41r2qN2rbyVrFbzno7Xm/Qbx8rl759LK0853IuR6WnQscyxS9HZQ+XsXJehxa+lX1NvyEG/Smg36lscWv6DavriSXbGTSDzXpLZjFW3kjkrLEGZ2mdgAAAGGcVLcFitLoZzDYrbTSMNW4cLbrcN6DyTs+W9Rb5PoXO4Hd6PjvVcbbXp2fN+kly85yt+ra48MnZ63Bodjl197fofP8Aoxxa/ek8tRvTKvfu5eS53Y9LmPXevazBNnWtPIwIJ8gAAABjWC0xVswWadrOa+8mcRTYy05Haqcx2/MWu/xK/Ugkl8/65S4/at8vgdOpU9TJ53me2n14F7zUt+POvqeRzYut5W736/f816XGXMpdDoeT7EcMfJ9jO087X9HaQb17eYU0GlmKZrUt7AAADGTDLTbWvvvKxUt51qXYJs6xTkMuvA3v1Ox570fO4u/o4+Dnqcij7CxyqUPd81Yt1NqnoOL6nOfP9WLm1p6lf2UulLyFn08cM3T836PjV7PQ5vHvcjq75npem3eektdMxBYYzitLJSt7Z1zW2sGMgAABVsYgtQZguawTwbya6TsqNzmzXsee7Xk7druee6OPPZ6XT0pw1JJ/SPM7VbGKtiWDp8rtdPHhree3yJoej2UXl6vVwg9Pv5rq17VK3y9+1xqOIqtmX2HJpW+xQ2u4zpvChs74R17mlezvjIAABjIxXswpqs1S+xFpLIVbSOPyvYr9HXp+e9BS5fRqwc6Wt0sNeP170vWxnzmApevzlpwoaN67V1k9C8zZ7Pm9ZIexp5z0XLvT9Tz/AEOT3KvX4vY4Xc53Ps9vTxdf1HXV7Gla3litmxV3nzrFOxkAAGMtNzCDeSpbawz1pt9mMipzrfn/AE9rzPf5/Q34EG/V04Vd1afruFF6U8xtXxNNi32ChDV5F7fq8uL1PM5FP21TSz41Y9Bf5Fvhepo1uF6rmT25OQin7mODxNez6MihtIJa1nSG3lhnXOQGMgDGNq6fMUFytNmtapXsNY56Xm9/Q7xX/PegzjPLxF1nkLHoOX32vE7vF6HH37MzyNmTMEWJfT5ef7XB5PX5fep1fXeUkk9JxuxTqcC/6fPl7fSt48v3uNiSKWG1nt8CHz8dr03aVllpDizVklq24Y7VbFrIDGQMZYpXc1pJa0kdmnb0is607w8bep1LHsduRdn5lzgpO9L4633uJ6Q4fcrcmGWXt48NdsZFD2G7zXb4GLuLnQ4fS8V7Pm+g4/YaePt+oo8Gb02MeX9TyOZtmzH0uj53q3NOZ5T3kyOpYnrt4bVdJDNHpa2AAAxlq2VZsSRxzZ2rbTwbyHk4ou7JT9Dy6XUn248Pf4fV810Y5O1I4utDm9KDo+hg8peBBf7mnmqXY07dHTpczbzvobVvj9lU8ta9V57Tq33C60/FqekeY9NtX5V68z5v0hp4n03TxW22is4gkhs7Y1xKYyMZADGRVmkjxHZpW9ZAcXgdvu8ypY6nA9Dlwt+15retvy/XcaejWsWqPsuff24nB6exXo5u+ml816Lbz/oePZ6FHWfz8fTqd+Wh5uf1nm5PQZ14fecPoXOPbuvN+g811LGvnvR3Xmoef7WGzBJMNc5rYs5GMgADGNcSa1569qPErYR8HqvH+txyr3C6nM9hjHl/UcL0fA1oWfT8XkXa2noed38nM8zWso6/fvdgK3O7XnupdrU+rxuJR3n2xcr+y83L25aPK723L62fO+iPMbdCTocCD0m3nOhxPUZzFZzpFY01la7AAAYyMa7qk0M+UWLGXLo1+vxL1LtNed1NvKXcT9u7xep52z24bWPNd2rzvScu1aEPkJYOvvv19zDi37nlet09OV2edSiqaYr9J1KPV8z6DpeZdahX9Dyu5H56r6Wtc4tzq485I7yrLpNIDFWxrLjIAADXNexFtDNBNHZxnkVre3Q85yvc7eZ03u9XzWIrtrsczo8Tu8PrTHF7Tz/oK9HrDHA73E7vF17eRjzvo8edn7mvC79WGeGfTh3fP9q/c4a3HJtWs628+Z335HQzD62zxKnQ6yPfOm+PI+hvVt5Y4J5QAxkAQboLUM9O1DOcSj2ub2LXmu7Ycbfrcipf5nc5NyzzO7xO5Th6RzbFrgd7HC74ee73I7XD17zGXO8n0bNqPu48/wCh05Nri+ozxO3pVjnzP4/q3azXbr8Dm3s70bGa3rvH9TuhUn4/Hk9bvpmtJOAAAARwyyKlvG1Cp0uJ3NYJuNLYj4fqYafX8707/Pi6EK1Jy+yY4nc5kt7g94OD3PNdGjF6Sral87zrMNP1vnrG8XqccbtcXtuF3XmvQcL0PP4tb09Pz/d7lLnU58FePHp/H+jrYtdbOM+bsbdrFbNjMO0gAABiGevneGzWnrx3vMbzc27c7EPkN56PSz6CvzOxy+7wadmH0Pj+9au5OL1ZOB2/M0rmNdKfpOPrPzZ9MYxSs9GD1fmMSQ49dQ7HH7OOF3see3p9nWWbh0fWUulw6VrQc3eXow8/afs+b9hLW5Nf02YJdmsctaaQAABFHZqzY0lrTaWKsFOv2daPQ81n2HIq9LzvW6nKtWKcHSp2PJWPYyI6VXjWqN3nz7xz61ptodJ5ttIZNsY3owyIut3OV3+V2s+b9JTpc2Kv3LXSh5Pc4VWG1toY5ncs1eNj0snI5fQ9T5uP0u2u+K+01bFnYAACKTMMmatiNLtW2nOJz5L/ABLkV/pec9RJwrVP0HEuc+D03k47umscMma221mKKSZrBJsxrrGuIM6a5zvHnaBfc6WTrUOrztavqvLWZu/pxYOt5XrZKl/fuefmmg2oXKHX4fpLlOxIjizPsAABjMek+Mwa62IJI7OUVSx5Ordq9dz/AFvjfSdXmz83ucDn9Pi3oLUtKLONpNwAAwyabVdsrEcWMZu9arzqdi3y7E9zrcHf0fPns86lQWdLR1eNz+/zdqtzfndfrqqxXzYyjglmAAAK208e9O3Xsmeb5vtwc67z7VjTe/0t3B6lJyOh1vJTSgMZAAAABWm71rhY2l59XSSK/wCq8vD6bm9rPC7EnN4dH1XM7nH5npPMXZ96M1vt7kGk7fGcMgDGSOQBBixptRvqdOzL5PtR6x05LPL9VfHP810upVzbi87aAAABHpFvZACh7XTn9anzezx7OeRTu9ejX9HzuvHyrXn/AFUfH4/e6XL6VfyXraXR81a9LrPFDNFtLXzajSAAAAgZ23y4nHvw0O1yPT8C1zJrPR66DlpIpenQ6fF1pU20cyKPoDSlJjbXaTMCzds1ubve5+qaRDr1e6MQWOdyZZJMw7aS9aTkdOhJf4Xcgzxe9nycfruFNFW9TnSFievtYzivYyYyAACCXXTE9bndvgXotfL+t5vofB29Pbcql2+ZLrB08XqVrcAOBBTiz0+xayxXqRdOTYOdx796Plx7dTo5AIORHvnpz8qbh9Cj6nid+jp0VWh2ccC/f4O3d1hklr7RZs5AAAAaQ2EcE0UHletJyrUGvrLWIJ/LdvfzvTxtfoOjz+nKyYyAxFtIYyAANcbZAAY51Pe9xZpelyYYu1T7p5n0ufN8T32YKnR10mgjllgxZikV7GQAACDEkM0NnSvzIYedavcn2kxwKfSitXeVPd5+nXk51/YAYyAAAADGQAItqfM6kVWv0dafUvPJegu8LHdzwu5nWKKeVjTEElgxkAABFtrHZRwWK9rEevkLF6btcyC30OdZ5st7mTdOtZyAAAAAABjIAAxUuVtqWvWh48fal8Xc79w876JDieHSzHXksGKsk4AADGu2ta1SuVrelZai4O+ZOnSnm58213n9XIYyYZBjIAAAAAMaSABVtYzjiaZxRm9HyOTa2t9oNNYd58xwT767gAAEE9ZYp2JdalyPeGxU8/2ZeffoXegj3KNCPGlWf0koAAAAADGQpcnn1/dZAAUZFnlZrTX+dD3tmsFmOvJvpJX3mrrGkdkyDGQMVLeNoNbGkFnDc51Lt7xa8uOt0Y9dqlPo3NKcLSlYsSTSIYrvVkyDGQAAGMjytWzmj67j3rc2zIxkBjWhwO50tsoN4N586Zgmrb2K2JZgBjIxk0rbWdMQW8NiPh36XYmxngc+WrvLrIADGmu+6re9MAAAAYcbmR40tkdbpUcxeylDHL5s8VbXTGmc7Y7vXKubGdY5tIMzb4xmBYAAAxVlkilp29ynzO7nG2M1PGWo9bu4AABR9N0gNdsZGMsZCPlU4qu1gAGs/pVDkwx1OhjGcgK9n026tYzFrHujxcxVsazAAAa1Llebdhlx9MdlzOZSlsdy1ny8E4AABh6sAABilyedZ2AACp1aeZ4QABrB6ufKGGbXSeSOBvHNMAAMZNK1ratPs08/1nCpYmnyq9/q48NfyAAAVuz2gAAHnqO24AACGTYAADFT096GavrPvFFvtDNMAAMZGKlurLLDPW4vfkcfzu9g3p9vu8PiXQAAGtG96rYDlcDo1s3OjeB53nzRV8W5QAAAAACp6Hpx5kginxFJNjOkgAADXWSvtNzK01W51EEuylDPcx4+XYAADTn+o6mWMhx+BcyitelBryq8vQ8xtKAAAAAAKnoeohzLSmnVJW0gAAAHNrxc2xzvV3xjI8ikAAAUfTdIAOPxNdLfa6YDXzdCeQAAAAAAFb1dhFlWt5q77TV7AAAAc/jcy9IxQ7m1jpbB5LcAAAq+zyAAAByuDZAAAAAAAYoesvR74VlnSTIAAAK3J7XjLw1xvFn0N1jPm624AANaPo+uxkAAAa+OsgAAAAAABB2uvWlgsw6WsgAABjHnPSReJ6QBXm63VzzfMXwAGsOk0l7t7AMZABFX3s+QsaZAAAAAAADTs9RVxPIAADDIOB1qPAs7gBV2s1JZQGlVYtSzXbmTGQwyAY4/PqTV5I9JLYAAAADFHE1oABZ6HR12AAAANeZQhI98gCPfIBE7t2UDGQAAr+bxuxkAMRTAAAArySUob0oAJJO/uAAAAObxdgAADGQYu9zYIOPa6iCbYACj5uwABVgztHvLaAAAAxruAYyZ6vQkAAAAOFTwAAAAFfW126Wca3+TU3odv0IAFPyl0I4Yy7BiWQY13BjIAAAABjPobIAAACDndLgwgAAAAMyR4GvO9b0AAKHmLgp9fq2HMp3YrllR50AAM7Sbb7VYwAAALHoNgAAAMRTacOoAAAAAAw6PaAAg8Zne5V9NeABitQ1333232k23zkMU+dTAAABd7mQAAABjjc8AAAAAAqd3sAAY15PN7XRYZABjIAAIufz5r80FevXwABnfrdAAAAAMczkgAAMZAADfo9TYAAAAAxkAABjIMQ1q9evqT9WWTbbIABjIGMgFDi4AAAAAG/Q6m5jIAYyAAAGMgAAADEVePobAAAMZAABV5BFGAAAAN+n0dxjIAMZAAAAwyAADGQAMZAMZDGQAADGTWKGGGKGLACa3iCHQN+n0dwBjIAAAMZAAAAAAAAAYyAAAAAGI4YYYs27coxpBBBL0dxpuANdgxkAAwyDGTDIBjJjIAAAAAAAAABjIYZAAAABjIAAAAxkAAAAAAAAAAMZAAxkYyDTcAAAAADGQABhkAAAAAAAAB//EABkBAQADAQEAAAAAAAAAAAAAAAABAgMEBf/aAAgBAhAAAAAAAAAAAAAAAAAAAAAAFbDn6AAAAAAAAAAADLUxp05ZdM1sAAAAAAAAAAOfazn1nDScOmZAAAAAAAAAM6xOfRzk20z1zo6JYzrlqAAAAAAAGG+OzldPP08Hbg6aZ6zzTTsznHppfPLpw1WAAAAAAI5+jg7zm2x6o5zpzx3xiadFN+PoW4+ucK9YiQAAABWxhvkty9fPG2WvJ20ozt18+mVMt+jm6+Tfmv1ZznOXVz9HL1gAAADm6RS/Nvz6ZzrgtG+Vdsps5+jDp04N75xn0xn04L5x0WAAAABnXakZxaaa15NOrI5+/CLZ4dNJ159HVx3rfO9Lb2znQAAAFbY7c/RzWvjpWlono5tcevltSLzFnLuNK10w3nLe2UXnn6a6AAABS+O2K3NbTn3Vjr5HRflXzvvPNtlh1xfn6mWPVy6Vno4dbU1tnuAAAM9Mlueejj6s+fcptTa05YzF+hlS91uSb03z0MLxF4vFJtqAAAOW0aTnNbbc0wr2UshaMsuqLVTCuHXy7UtLn6+ZTe+NenPRYAAGM1tGXXya52vGemPVgr1KaROWiYTCaVxnqObo5p2xUtfK/RTQAAFMtcrzScd8rt89Bja4TEwTCTmndFsUTXO1po6eLqtXQAGd8dsKzXSnTza3xp02QTjqBMTWYtETMGHRnpmzms2rnNtqNgAMZisVmrW9G+ekCJmJgSrMVtMJlAJVtnSbsNq7ZagAQ4+umekRoLwIlTSFL0umuddb1SCtLXmE897Z1dOFekADKKMuisaTpzdAIpFdLzGC2sxRnvMQtARFgnPDTflOiLgCKLYzne9NLphNZEJTGabzFK2vEkWgApN+bTOp0Vy6gBhtzXiu+DpnG15ictBMQmuRreCYi0TW1LwCYTW3JF6003mwAq5rw2uVkTndE5xOqma9rITDObJTCYmBNac+171hoAIztzNJ3hEomEkqzIqmSExNYi0xImiyYnCN1J5enQAUc+2O+kJgimgkhMSglWly0ExETJnSbaE46s89NJAGVaIx9AQSCLIjPLlr18u/NpovpbWSYrYRlFtCYtCuau9gFDnlbW8Uq1mCKYZ45KTWL1m0VtrWL9PZImLRGUa2IlPn9eO9b3AHLrXPqmGUNpQcPnAAtbMN/VuQmJmFbIi0Vtz02ta4AUwtuRNa3kSjPKzDl06tKZ30tTKcrdeggCYmISyrhrtpnoAOe/PPTaM9ZgEgAAABAJictYjnnbnnppoAM6Tha8bWBIAAAAAEAqxi1tMOkAynFZouImMq89e202SAAGHH3aSAQlzRac6xptoAji6crbzCXFyYxALRPsbAAjLkxwro29Wa5ctKUifR6ePs545+qY0uAjl2aTGfPzW9OniQAPR7wBlwckAOyOWAHo9+XJtNKb9AA566apyy0y6uHzgCev0rFOPHp7DDl28yoAAHd6FcabJ2ABHJ5/Z3WK+FAB0etYM89bjDy8gAADu9LLOI6FwAjyuVO/f0PEzBOmvX1gAjyOcAAAOz0MWuoAOXi54DbTmg26+nWQA5+OMmJMAA1vTIHq9UgAZ+JAAE+n0XjNOoHi4r6ZrpzoACYA9bqAAc/k1AAJg7/RA8rm7PRmEV4+W1rWtazj5wAt7GwADLyMwAAaevqBWwAAMufopjjjlG+19uoAAp5WdIAAnr79QAAAABGWwAAAZ555Z5z0655ZaehoAAAAAAAAAAAAAAAAAAAAP/xAAZAQEBAQEBAQAAAAAAAAAAAAAAAQIDBAX/2gAIAQMQAAAAAAAAAAAAAAAAAAAAABYN4AAAAAAAAAAANZNXGtc1gAAAAAAAAAA3mN5by1iAAAAAAAAADSzWNkZ1m6mDTOsgAAAAAABvGsujn059uWmLrM6TXKt87LvnvKAAAAAADpjrxXeN87oxd41ZrFx0ynTm25gAAAAFhrOk6c9s6z05W2znuXVzjfPpjpOdamue8dOYAAAA6cxZ0xvOmNpcaudSN43jPbE0uGue00xAAAAAaZrRNYvSc9G+O7nW+epncc+ubNZrE0yAAACzWd46TO82yzHTOufSVA3kZtzvDWJq5bwgAAAWazpOkm8K59GJ0SzDpjW+ab5ta59IY6yXM1gAAAazpNsdMXeLNYuY1omGrInRLjWTealipkAAAdJcqszuVeVlI1rnZUq659M2S659FxNXFiAAA0sa59M6kazvnpebWTWSost05m8dJnYlmUAABdZ1Fm8akzrI0kVLFQHSYqaWXUk059cRAAVrOqS8+mZq4iyzWbFJZWsUljeNZtolqZrIAGi0skrFiyksoiunTjkQLBZqyN4udZAAOvO2LkQXeL289nq83o88b9fo8HCkF6evl5ZY3Jpee2AANK1iozvMWV7d9fn8V+tOfzWu+vZ8hSVLNb5xpnWs56GEADUaWS5sg6c1m2LNeqTzR39HPyoqWKljq5blpi65gDeOkXG2J7ufmh7vERdZnb6NfP8APULCztxKhpmzpYswgArcXEN4qPX5KevpjxOntvm44A9eMc4AqWW7zM2sgBqdGWFS6yrNjeA0kAb6dPNOvOI7zklm2ZXTGQBW86zmwXt38UAAWOvbhGAddcpHr9mOHjhqTVkgA1ab4VNXNkWWBMzeNZt3OvTzwXpnMX6fTz+PAll0uIApsmZe3pnhhUyzEsWKlqXWwI19Dt8zlQnbnrKQAdM281e7W/llTPMACwNdFBB0xOt5LN3MkAC7mFO3Xy5WCCZtqKRLoKSoHXOZq7znNgA3NzB7fHksAAAAAFlQ9njXbG3PWQBqzclvOAAAAAAAssK1ZJrABqbRm5LCMtUAAAmdgAsdEWpnIB1xqYBnOQA60ACZmVXoTKQ6Xpz23iXMAOmWRnN24gB00AJnABuZAOmtdMzVxgAbuciJrPMAb2Gc60TOuQAADfRu5TIAE570HEAa6AShOcAAAN71awgAMYNbrlALdaAA55AAADfXTOQAZzkFuRd2gAmWUAAFsgOmgAE5AAHTRFAc8rYssgAADpoABnmAAA3sDGN7CZyqlYgAOtAATnAAAXpQAAAElkki23QABOaAAGt0AAAAAJQAAAkSRqyS7oAAAAAAAAAAAAAAAAAAAA//xAA/EAACAgEDAwMCBAUDAwIGAgMCAwEEBQAREgYTIRAUMSIyICMzQBUkMDRBQkNQFlFgUmElJjVTcHFigDZEY//aAAgBAQABDAD/APoHO+3j5/8AMmnK1kcRylDe8oWbbf8A4Mrvh4yURx/otjdZxqht7YfxTMR8/wD4GrRK7Dlf4/EZiAyZfC2C0IMPt1Q+kWL/ABZCZLtKjQjAjAx8f/gu3vNc4GN5QHbSAf51W8WLEfgVJTfb5nbVv+5r+hEIjJFO0CQmMEM7jqZiImZ8Qpy2xMrneP8A8A3mtUAEsttDPIYL8avF10fgBHF5u39Lvg0F6XP7ZmqsbV1+jv0j1jh2RM//AICvjyrTOklyUBfjD++Z6V7BOY0ZjYfXIR+UM+lgZNBiMbzXGRQEF4nT/wBFmqX9sHpYdKVSyI3lR9xYntt/5obAWPI52H58x6zMRG8+IEhMeQzExemYrFtpW/aDed5vMaoBJZcdDO4xPo8eaTHVIoKsHo+JlDIj5oFM1o386vxM1i20iZlC5md51HjIT6U/DrA/gvx/LToJ3AZ9RGB+PGrH6DNU/wC2X6ZCf5fSx4rEfxEQjHIpiIiYmN48x+EiEY3KYiP/ACa0qWpkR+5cSICM/JjyAhidtY6ZgWBPyY8wIZ1jp/LMJ+TATGQKNxiIGIGPEZCN6++lTusJ9aH0y1WnuhK5OY31EwYRP+KSjUmRONpu/wBseq/6C/Rni+qfRaODmN339K9nvycQMj6X/wC2LSf0g9D8gUb7ax0lKZ5bzFqdq7NVY2rr9Mj+hGo+I/FkC2r7aQHbSAejWMm4tSymI9bMwywpE+R/8eYwFjyOdhEhIYIZ3j+j+jf/AOw6X+VeMZ8R6WA5oMdUy5Vg9a/03HjrIRvXnStpUG3xqzHJDI1UnlWXPpY/u6/4KijX3JKNvS9/bFpX6QekxExtPwIiAwIxtF2dqx6k4RWgtt9JMmLEyHjOQ/t9Rvwjb5x5sOWEZSXrZa4WqWr0tpNwgI/HpVjnZe38EIn3MvmfH4GGxlwFAUwPqZisJMviuw2hzMeMf+KWl9xBx/miXKsPpMxEbz4jfeNxnVCSE2qP5/DfHYAbHyJQQwUfF6JHtviPIlBjBD8elCdhYv1idsjOmrFoSBfADAAIR8aONwKNUP7YfSz/AHVf8V2P5Y9J/RX+DIf2+h+2PS9/bFoJ/JEtY4dkkX4yniMlqhGyOU/P4rb2K4CuIkpnaN51SHlzfPz6u/PbCI+yIiI2j41ad2VSUfcgWCoYZPI//Eq35NhqPiNMCGAQT8Y8pgTSXyX5V8S/x+FoQxZBOqBySOE/Ll91RL320hXaUK9+Xqr6LzR/x6dlcM7vH6/RVkjsmmRiI1Q/SIfSz4s1/WXLFgqmfr9Ln9szSP0V+gOMrZq8cNZGfywH/Ppe/ti0wuNOZ1RHasHqRQIyRTtAGJjyCdx9LZca5zqsPBCx9DKAAin4qONy5M4iPwR+femf9F45FEjHysIBYhGpmIjefEacyVrmRjckq7QbT5IbAE6UjEzJFxGSnedFPubYjH6f/iDGgqIk52j0uDISFkI+oSgxgo+NCoBYTI+6+EyqGD9yzgwE49BYB78CgvSZiJiJnadL2XeYH+PR74QEHIzMAUGMHHwfi+H4hHjkS9KMzyePpcnixBepVxl4v3+rQMYVxgb/AJd3+2PSP0V+lOJJ7zn0u/U1Aamdo3n4W1bY5BPKMhP8vMau/TUmNIjZC41abKUyY/cgiNIEf3XS41j1UDhXCPW99XbTGrRyuuUjPGa8nKAk53Jy5aolxO0qCFrEI+PSy3spI/8ANNXbTEz9zEwxgHM+NOTDhgSmYj0tO7KZKPuqJ7St5++y7sqk/wDVVT2lfV9/9EWAUyIlEz/4A9woDmUTMAYsCDGdx9SYATEEURNlXdSQR9y5KQHlGxaIYIZEvimUxBoL7vQxgwIZ+KBT2iXPzqsPZuMVHgdXFGTUkMTPpdjgxLo9bg8qxxqoXKuE6seLaJ/Ez6b65/xqr9NmwGnWIUaw48pyEfkQX+YneInTWsG4tcT9HoCRBhsifN3+2PSP0F6mdo31jvKjL0P8zIDH+HTsk51jh2RvrITMisdZGf5fbQRxAY05IuDgW8REREREfGQmS7aY+YiBiIj49P1L8/8AZ6IeIjMzEfHiPW1YNRAAREzq3+a9SI+PWZiNt529f7i3/wB1amfc24j/AGtWm9pJFHgq/c7Idydz9bdiUjEB5YHPhHPaT1YdCVSc/NNMrXJn9/8AzDXLTEEydoiYmImPj8RCJRIlG8VwYkzVtur1vK7ieUfchkNUJx6FYWLRTO/LT/ybS3R9uQEpRyGZjSi5KAvSPyr8x/p1cGQNdmNRMTETHx6Xg5Vi/wC9c+aAKflrRUuWFvMRIsDf5GhOwmmfuuzxJB+jrPaaC+O/pfGSYmImY9LfhqD9A8ZA41YQTTUQzGrkb1j0md0hOm/36fwXv7Y9I/RXps7KOdY+I9vHoquYWGOIonVqdq7NVI2rL1c8urjq+LDlYCMyPqz68gsf8ekztG86oxJQx0/PpZYcGtK52I5kQIojeaQky1zPzOqv5r22P8eppYyyJlP5WrTu0qZj76yu0mBn7tQMRvtER6P3sWhRH2OaKVyZaApIIKY4zqZiImZnaK8TYeVkvt9B/m7HL/Z/5lyhcuQLVI54Sk/v/qI/IsGifs1YRDojzxLVpfcQQx5lJRYq7T81FsUmAZ86vjPbFo/cBwYCcfDAhiyCfikcyuVl9z4f31EveQ0yOSzHWPPkjj/mwHNJjqgfKvEf5FSxMjGNiyH6ETqJ3jfWQjYVs/z86YoGSMl86yEfkiXoXjIB6Wv7dmq07116atk3FMEdx0yz23gnjM+l3+2PVf8AQXpv6Z7/ABj/AO31JjBQMzHLVz+2PVf9BerU/wA3Xj8NeOdxzPW2zt1zn/NUOCAH/MztG86qmbBNhT9Koll5hl6UY5S1s6uM7aC2+ay+0gAnxPqJCW/GYnUztG8/Comy/vTH5XrYdCVSf+aSZBfcL73KNrlf/a9LZkZDVX9ywFYQA/GrjpEYUvyxCoSqAj5/5p6D7kPROzB3kYko2n8fZKLPeGfp1MxHzPpcXJLhgfqJbDViyPwV/wAmyxE/FtxJVzDbcZ3GJ0YwYEE/FA5lUrL50X5N0Sj7fWrELtuVHx86x/0k5f8AnV6N6xaVO6gnV0eVY9JLkkJ9b0b1i0E7gM6dPG8mfSxG6GRqnO9YPV8b3k+lz+2Zqv8AoL0/ylmsfP8ALRp8fz6Zj0vf2xaRGyVxpv1X1R6XHGs1CE7TopgYmZ+MfG6zOfnIScypYTqN4iIn5vTy7So+dXD4Vy/7pDtqAPSwztpM/wDNMOFcIn5fu22tMfbdaQLgAmYNQECxEp5FZcYSta/vtulSth++uqEqgP8ANphMKKyvuWArCAHxGpMYKBmYgtMn3VqFR5VprIUsjnVQnGrm2d9OaKVyc6pqKIJ7P1NPeCA5T5msk+Uvd5b/AOAcoidpmN/w2kS0IkZ2YElIDJRsWl/y1mVT4VqbG1mEFG0aubrNViNNULlyBfCglaxAp5TpZdm8YF4jV0OSJKPuUfcWJ6mN421RkgcxJz5d+XeWf+NL3XkDH/GrUb12RqpO9dc6dG6WRqiXKsPrajeuyNVp3QudXY2Yg/Q45AQ6x8719vV23vk/99XZ2rHrz7DxO0omSpRv5nH/ANvp/wDeon0yH9tOl+FhGl/XkDn0bEsyAD/jVouNdk6pDxrBqfzMh/7ad9V5I+luebkK9bk9xi60a+I/9qe7GtfPxKVk2GzG5TMREzPiK273FZKPpNQGQkUby5opXJzqoohGWs/UtNIeKVfqjEKVEEW8VhJzCtM1Zb2kkcfNJXbTEl9+nzNiwNePsiIiNo8QxHdaJHO69NZC1kc/FdRtP3L/AJ/58HNG1Km/bq6iWB3Q35obDlCcfOp32nbzNWxLoKDjiz8FtUsVMj99dveUJ/5ujMQDx+4Sghgh8xYX3EmH+ajO4gZn513l93s7/WxEMatu+06mImJifilMgTK8+nAOfPjHLIBJJg4+Un3FCerk9uylvo2N1nGqE71h1PmNtY+dhYufRMlF9ozO8HHICHVCd68RrI+FhOoneN9T8aqINAmJzE6IxGYgp2nVj6baC9L39seq+0117+Yb9KT2jWP/ALeNWY/mq86IhGNynaMjP5ERoY2GI1Wje6+fRf1ZE59L87Vp0oeKgHVTcrDz9F/XfZP+ND+ZkJn/AB6J/NuMb/pts7aCmPmsvtIAJ+dXDk5GsH3LAVhAD8ab/MWhV8rYYrCTLxFUJKZssj67CicHCC4wIwIwI+IZPu7MLH9LT2wlUnPzTTILkz/U/D8eZ0mwxzy4RHY/4OZmNto3/ZbxvMb+fUrAC6EzvBetpMtXuP6ldsOUJ/50P8rYkfhPo+Pb2BsD9kefMamYiYiZ9HWBSQCUTtpH5No0/AkMEMiXmKhSEnWKfOq26rLET8atbqtKf/p9bH5Npbv9PowIMCCfjHlMCaS+XIW4YE41EbRER8TG8TGsdP5Mj/nVPaH2I39I+nJT6UfHdXqwmHrkN9tKGVqESneUWAfBSMTEauzsSJj51d8EgvS7t7Y99V/0F6s/27NUI/lh1c8NrzqYgo2mN4yHmFD6UfLXlqZ2jfVCJM2un51kPKgHUzsMzEb6x4nEMIxkZ1QjlDHT86pfU17Pn0acLWRzqiEiiCn5b/MWxVH2aawVLky+KYSXKyf3ae2EqI51TVK1ci++yiXwI8th9HCRqIQnYqyIQqA+S1P81a2+U/isNJx+2T8qWKggB+P+SCyBOJPmC9LIElsWl7zAGJjBjO8elxMmuDD9RDYcoTj8Bfy1jl/taemHLkJ8aqukxlbPDZmBjeZ2hgCwJAvioZDJV2fddWRLhgffXdDlQcfNlPeVI/6qje6mN/vvDIwDx+RKDGCH4txKyCyPzEwUQUeYuR2zXZj5iYmN4+LCYeuQnxKgIFiBTymZiI3mdoiYKNxneLa+4goj5qt7qRL/ADeYxahIJmNRO8RMfHZYu73Fx+X61PosPV6LritxtiZ3076b6i9EzwvNCfSfMTrG/olHpYSxpq47cNX/AAC59L07Vi0iNkrjViYhDJnVD+2HV75T6WpgrSF+lDz3Z0zlKygfJUlmtHE42nV36nID1YJEBCM7TXV2VQvfeSmBGSn4x8T2SKfS+c8BUP3CMCMDHwiuS2tYUxPpennKkROhiBiBjxGmfzNuF/KvxW3SsIAPLK6YSqAj50y2IuFIRzKZiImZ8RWcbpYW35emvNx9ivpKQSHENS1cMhW/16X3mWCMtwX/AMhZQRyLVeG//v0mIKJGY3irMpYdYp3/AATHtX8o8J1YYalSYDylLIasWRpqxauQL4qtKYJDPDajjOCW2d22gICiyr7ihdpG0T9NSXQMraO02wKONhf3gYsCDH4VPt7ZKn7NM/lrMN/2mBDAIJ+KYOWEg2NoMIMCCfikc8SSf3OXDVkE6oskk8C+70eHNJhHzji3Rt6VvybTU/6bKpckgj5SJCoBL7vS1Yclocf09R9OQn0IoGJIp2hbAYMGE7jd+lyD9O0vud3j9ZtWExBlEToFguJgI4/gyH6QelhIuDiUzEDZVzhQbnNvxWZqpG1Zerv3oj0OtBWBfymJZJCBSMcioqNapk42n1b9WQVH4XiRJMQ8lWCQQAlG06j8+9//AA9VTFi4TY+zTmwpRMnVJcgrkX3zMDEzPiKzWvaTN5hPqRQIyRTtFYZc0rRx41afKxgF+W164pD/ALnc7xyKVxPFYCsIAfiy05KK6f1EpBIQA6tP7K948nVRK4ljPLfSSiPmYj/lJUBMFkx9XpyGCgZmOTFiwJAviqwhma7fvmImNp8wtQKHiEbRq2so42F/euFM2eMRy0H8rY7X+zqY38T8JmUWCRP6dxEtXuEfmVnQ5UF/qcoWrkC1TaRDKT/U9Hfk2wb8BqdkXt/gPWt+XZcn0uiQvU4YmZ/BeDlXLSD5pAtWZ4W0M/xMxETM+Igl2UlwncaS2LVIMjackM9oSjQTuMT6X4iTT+LIx+RE6GdxidEIlGxRvERERtEbRaAjQYjG5VAMK4Cfib0/mojTWgoeZztETvG8fgh4y+URHnQblkTn8TGCsJMvthwkmXD9uPiZWbJ+SIR25Tt6XH9pUxH31U9lMD/q0/8AmLIoj7NWzJhDVX9y1ioIAfiw+EjG0cjHeRiSjYtPKbDYrBP02HxXWILiJOWdtPcbtE1VkZTZb93pYfCQ/wC51kSuJM/LdEAFtyiJ9WMBYyZztArO4zus+lX/ACNiWIdFiJmVDMFEFE7x63EEWz1+GJbDlwcaspI4hi/DUPhwb/BacwlqIxHlKmC5cHHwn8h5In7NWEw5Uh/mq6TGVs8NYwVhJnOwvCLCYNU/VXdDlwXwTYmq7vj+lEwURMTvFkZS0bI/ETExEx5i93BAWgUxpwRZrfT81W91IlP3Xw3Rzj5Q3uqE/wDNxppVyD5AoIYKPiz+VZU//T68ogoH/Po0eajCPmkLATwZHGchGywPXgw/9sd4Bgz86uDBVjjVQuVcJ0bVrkYMtpciGyEzO34sj/b6V+kGgcBmSxn6vViVsISON5sIF4cJnbQxtER8/gqRytuZ6VfqtPOPjVd/fgp22jRub7wUhMcNWlG5cAExEXCFNXgPjVZfbQAz8iphWpaf2amssn98vM6nfaePzVrykZk/LGMFYEZfFMCmCsH9/wAedVv5h52C+3Vp/ZX9P6lVHZX58n7cfcd+Z3mf5t23+x6OcCQkz1XUZn7l33fga0FDyOdoBbLRw10bK+PEaKyAtFMbkX/FsY1VkZKd0+nx8+hDBDIlG8JKa7fbnP0arg1fMD8j6R/Ku2/2NWAJLPcq0BiwYMZ3GY38T8Kj21iU/wC3bTLF8g/UrOhyoP4nU1o9zD4LbRCJjIlG8VZlLSrHqf5a3v8A7ZgJjIFG41kEgSGS5CYQYSE/FMiHlXP7mBDAIC+FhCwEI8wn8m4avgSESGRKN4ERCOIxEQwBYEgcbiIiAwIxtF0YKse+qjYagZ/z6Obee9982fnV3C9C3DO4wYSXGCiS9LgSdc4GN5qyU1wk/mrsFp6/Qx5gQ6xxflEP+cjGwLLUTvG/rk8gvHVCsH9RdO5i7fsuVZISHRgDBkTjcYiBiBjxFTzZsTq2UjXOYnaaszNdcz86tXalMYOy2FxXsotLhtdkMDQMWyN1lBxqZ2iZ1jx/KJk/MztG+sdG4sZpvPtFwjcqSiWjYo2LVOJY5r59TWDNuY8v6FuZaxdYdRERERHxoFguNgiBiZiImZ8QjezYl8/Zq00t4Qr9RShUuAH40RCAyRTtCQKyz3DY+jUzAxMzO0A1tlsSqeCdOcCQky0pJvOH2PjToZKyhU7HWrwkfPln/Gw1i7crZO4GAmMgXwgirN9uydw09IuDjPiaziLdLfDdXO6HF65nZZwwBMfhqxaEgXxWYUTKG/qTETG0/AT7R8qKdk6tK7qp4/fXbDlQX+VVpU82CX0etpUkMMDwyeNyt4+6o6Wq+r7/AEtRKmBZGNCUEMFHmJIYmImYibwFEC8PuWwWAJj8esxExtPwICEcQiBjT3BXSx7PAYlyr+RtqfELitmH46pbxrZ2dVr2qmQx7ef5v4IUuGS2I+v0ozIvcufnIRvXmdK/SD/OpmIjefhNiu+JlDQbFpsZjJtM95x/SbE+9fzLZ/qmsCTMxmZlqgaEgfwIwIwMeI11bA+8qSzft2sfYxEjk8U2SqsyOXzcsXX2RXrMdTugOHM7LMBlbGQBwWRgXOiZUcR80x41wjVguKGTqiPGsPq4+2oz1RHjWHf518encDn2+Uc9U5kya6dEUCMkXiK7pcHOR4xqZgYmZnaKkSxjLJaY0FDyOdo9LbCMoqr+5axWEAPw5opXJlqsoo3c39W08lxAL8tUErXAkUlLJ903tDP5MRERtHiNOULlyspmIWsVhABG0OcCQ5nOkKNx+4fH4ZmIjefELatkTIFyj9nH7WCGd9pifWyiHL2+DrOlgyJ+GvSLg4l4lYkIQJlyLVtcxxsL+9TBasTH4mImNp8whEIghGZkdWESyIIJ4tQ+GjO8cTchbhgTjURERER8an+Ws7/CdXYZKeS5mJSyGKE/VVeFMMxmeLf5a1Dv9ufMeNVDaDjrumZIxgxkC8xTKR51zn6r4FEA8fkZFq4n5GmUqaysXnUzERvPiPRFnusNZDxL06gW1mJsQrfdKq9VeMyI766ixD32gt01k2GKAup6leI+nTHCswAvn8O0b77eb8TNYttV53QudZciHGWiGdprWn0z7tc+JVadn/pvjUCWO6fqkjPEmZgp/Hl8aGSqSqfDbV+0rGxiLAEtjW3gxwBCPb1Kl0krGhhVydnp2w2peLHvr7P9MiWyIHSRgVAMeuQPZMLj7gGBARj41fkpWKhjeVB21iHzpKiK2xxDIxoAEI4hG0GAmMgUbiACAwAxsOrplwFIfesIWAgPwcd+7AT5DTGQtZGXxSXMxNhnk9R/NWOXynVdZscdhsbTZaRFFZX6iVClcAPpLVwyFTP16fYBI7z5JVczPvWfJEUDEkU7REsuMnaZCuxoJDkc+BmCGCj4+PM6YZ3DlSp2StYLCACNo0gGjyJxbl+/87/+34mAVR3fCN1RMFETE7x6WQICiyqPqWYsCDHzHp8+J0veq+Vl+j6iYlvxmJ1ZWQFFlUfUtgsCDGd41ZsdiBmRkoasbCeOqjZYEif6mkbosmj/AEakwEoEpiC05QuXIFqk0pgkM++6ueMOD70s7qhZttq0MqaFoY319DV+PIoV2VQvflriPLltHJ6+6og1TZJp2L79HEJvCf8Ap1ScxsM5zvLAhizXPiKlVKL7cHf3ZXUoEqBS42CkgGdUWzdM93V6J4rkY3n8VoTKuYhG5VBIa4CcbFcTL6j0D92GxSskt1dpSpjWV8NjPH6fStUu27IN3lmnPShctccLW7qrFrmYDuN0fWFWJnhXYUU+pMbaMV8pSz1z6qnsDsWK8Pms+bfT0lWrxJY21brz7CouE3cXiFY+CYRS616WK4vgYkpjURtG0atHPdQsZ2nTvzbq17/ToGm29MAU9v0IwCORlAxEwUQQzvH4Ffn3CZ/p1AjEyURG+nz7h41x+yIiI2jxFtpREJX+olQqXAD6WX9oYgY5Mro7QzJTyZqzYhAePJ1UEO7m+WvcKVyRfNdBSXff5ZMwMSUztH13T2jca88EqnaNgQs7B99/2+Ij/wBjMrZ9pXhS1isYAI2H/iSGCiRKN4TM12+3Ofy9DYmLBJZHH0j+Vdt/sabDJWUL8HTsk2JWz9R6RcuQn5qOIolLP1NfPjUjNJ0GO8piYmN48wUTTbzj9CJiYiYneGLFgSBfFPurkkHE7WImu4bIx9MTExEx5jaN9/8AOriYYvnHg0GTEiRRsWnqaNkHpHf1MYMZAvMUmEBnWLz+Bcdu4wf8avL5oko+6u2GqE/8oWarbYgZ7eup6ZQKsnXjZtC2N2mqyPjV+RpdSVbO/EPSzar1Vy2wyFha6vSO41Ekcq6izbpJykixVbq4eXG7XleqmQp3R5Vmif4GsBKjac7B05K1UbOQeUBEBb6ku85mV49awWArWMCGrQzmM+VN7JXXp/w077MdWogSrIRl7x0KC1Jq38Lhjcuus5rXMblbmJsxj8nvCYmJjePMa6hZ28RYnfacGvt4mqO204oBu9RWrRTyH8MrAjhkx9eqMcza+flhcFkWseviqWT8+nUK22s1XpmyQVhnPxuSZh7BSYXMhUohB2WQGqtpFtMPrnzXp0GSihfg6yYSqB/1TMDEzM7QJCUQQzExZdCVSX+qomVK3L9RzRUuTL4qqKd7DfLNOcKVyZarJKSmw79TT3gkORfNZJsObL4+pzgSHM50hRuP3D49DIrjO0vwkREBgRjYdTMDEzM7RJMulIh9FdawWMAEbQ5y0jyOdJZ3Vwe0j6NaChkjnaK7jdEmQcQ/4Z6RcuQnxNZxFupnhrFLbx5xv6NWLQkC+KzCEprt+/TKwG0WxuJ6tqIZiyr70tFy4MfQhEhkSjeFkVVkJOd0kImMiUbikyrN9uydw9DAWBIFG41DJZFWP7tNI67xZJTKvwpeDoKR9No+fxTETG0/FOWLexHGe36PSD0sSf2dOmdLIWsU2d9dWII6K3jHmhZG3TTYHVmymognvLiunVs563767EjSy+DVbVDK0Qm1XB7YK9RHs5PH2aObrwb0gbsj0+6kRX8WwhnC5YMnXkpiAf6dUWpTj4QE/Xl+4unQwqI3bSqhTqqrB8aMpECKPM4CJdFzKt/MtdKzvStnvPd6SExr2jMZ36ZkLVi5cbG9nq+rutNsY84e17vGodP3a6rKBxcRPzU+iknfxHSUCRXHbefxFEyMxHzTUak8TjYr58USP+UB20gGu8HehPnlrqqP52jI7CfU0WKt6rkUxrILaLJ9+RtyGDolRxwKZ4b63ZNkhXCNLAVhAD8Ss32tziYVMxEbz4gd7j+c/oamYiJmfEJGbTZez9PTGCsJMp2hAFaZ7hsfQ1oKCTP4Uo7Bw98fTpjDtMlKZ2UtYLCACNo0RCAyRTsP13T/AMjXEREYEY2ixYFMbfcaa5GXesfUemkYrkgHkSqpEXdszzNrlpHkc7arssNZLCjin/g0WIaRhMcS9LCSmYcr9VLRcEGPrYT3B5B4bXfDh8+Gesfylnb4T6NULQkC+ENNZ+2d82Uw5Uj/AKqZOJcw4ZiSsALxTPidXVzHGwH3rMWAJj8OVDVkE6qMk18C8Hac1MgQxEriYKIKPMei661MJg77+jWglRuZPEF5BpDOcuMMK+Pt5BolZvgutXGzWPwDgKfwVZJbWIOeU66kAqd2rlExMSyE5CgUD9Suk3TNNtY/vyhzmMurHIKZQAAsBWEcQ1aGKvUtZq5gIa+pj+oQsIcPtpz+Hidpsjq5fRTy03MRIkNC6q/VCyrxGnMjIdQz3J/lMIs8nlHZZ07D6ZW7NCiyzAdyelqTorusmXFKukhHfnbPYenGqEuxkHgWBstqZKxjrccrHVA74g53210ocli9p9Or95rVgjV0uzjnlvxnpEIjHsP/AD+D5+PwOrrdxkt4mZiImZ+Kc91rbE+nVcmp9KzAcgquG5UTYINoxagu569aaEGOnOFXHeJmfwvVLVyvlx0tYqCAH49IiIiIiNoMhAZIp2EIO6fM9xRERERERtEJY90m+Nl6e0nFNdGlKBIQAR49IW62yZbEgqIgYgRjaH2ZguymObUVoXMsOebfV1yBLtpjuNVUIi7tmeZ+kOGXSmImZ0xgLGTOdorPJ/IpHiP7zkMFAzMctdlcN7sR9ejate3MoH0aM12d9cfQJQQwQzvFhjUlDY+pIkJjBDO8WVksosq+5bBaEGHxqHrlsp3+t6RcuQLVRxxM12+GelhEODj8FWfJxK2eG6uI7i+Y/qIOWJEyjaZiJiYnzFaZQ86xT9OrESh0WRj6CgWrmPkay2KVAMmJn8PVDpViiGJ2k5vZga9ekgoQvpa2/id63MlPRyNp2snuzDZvG/nUrEuHD9QrulFa1EJteluYVYS2PHpeqLu1WVmfHTNiVi7GPni8rg4XL344fT0vR7NQrjInvW8vbJhV8ZVN5ljM/agjuXorgnpei4YadplgTwOCppl1gJ7asJhSEWrriQ9PCorGRqGsYLAWGY7IuxdkoEcxkBx9I277NeptHHLqjEzcxtIaNJVeNuXp1ZYkhr0F7yxrnY1FGnXV3CMwWBMOeIYvKDkgawFEtd/uL6qqkERM+9s5PI3a/Du1Rxmcq0GPgzQnDZCcjSF5DxZ1dMQqp52nM5Gq7DPKs8WT08mU4lETEROsjmKeOkRfMyxeWzmWaa6EClf/AE7mHD/MXvOSxl/EJWxdozXieqYnZGR+YmJjePMekxExtPwpS1RxXG0a6sYQ40Qgd4Q8KeCW+fEdMVu1jYcX36ifcXN4ndfqbVgQgU7T6FZjvikB5F6fHmdGRXG9sJ2QACAwIxsNh/aGIGOTFQyFxDZ5G5xuP26J0pIJDgEaIhAZIp2gGAweQTyj0ZYNpdmt8oQCR2HyVmzCo4D9TawuFf5xbkZisZI52iTfb+lX5aU11pjYI8kQjEkU7Qm0DjIQidtbRp7wSHIvlaG2C7tnwERERtEbR+5sgYzFhU/WpotXBj8atoOSh6p/Mrvhy+UeJ9HoB4cS+ajCGZrN8HMRMTExvCymq3sn+lMRMbTG8RM02cZ819FE02cxjdAlBRBDO8XFkBjZXG5LYLQgx+CrATxfvMFozEBkjnYYmJiJid4tJKJiwrwxDhcuDH8DqwtMDmZGdGImMgXmKhysyqs+70zPUAY5goUENclsOStseI1duJo1jsun6atS11Db97b/AC6aUJrrhSQha/S9kqdBcMsHtrJroZQTuYuZ91gMr7+twbMRZ0a1s25jBeuaXFDI1cuEbDaIeoMuCExtUERAYAY2HG5UL9i0sIiAsJixXYgp4xRqDSqLqiUlDXY3Jk3GsLkaSXIyC4kQYS8V1PLDLgjK4qrl0+4rGHuF25/iCBz3OIornMZtmR3mavoxgqWbC+3Fc8vmGZJsTCbnVDbE+2qDFbWJQj2JV5tReirVr00wiuHBfUHukZercUmWxXq5lIy5FcVXLxZXH1WzetA9fTYdjDgbPojL2v4zYFVaNq0UZv24r45W8Ymg6hVhLXy6bVlNRB2HlxXWGeocyTmjMVbGCxhO2oWYq24/6npztMLvLPI/xGu7HvrMrWcRi6+VxjQmIC109fsV7Z4m6U/i6qnbEzG0TrKskOmV8dUFdijXVPyUTIzETtNZHYXx33L1Gv8Any8y5T6CsBMjiNi9LYvZspcfQpYqCAGPGu0Hc7u312AYapFZcSrIhC+P+rXFt0vqiVoABAYEI2EigYkinaCNtuZFf0IWoFDxCNoZz4T29pNFaFzJnPNrrYLLthHcYFU2FDLU8piIiNo8Q1q1DyOdoFbbc83bgkREI4jERHomqUsl1ieR/vGQVRndCN0xMFETHxpwlXb7hcbgJCYwQzvBlIhJDHKa7xeHKPE2Ud2IIJ2bXf3RmCjZjlC5cgWqziifbu8MYsWhIHG8V2Gs/bOnyQiYyJRvCSKq327P05jfxOv7N3j9DW8fH+WLFoSBxvFcXIZ2CiSVpm9J/MfKRKCGCj4cxlezBlMyn5/BZrmwwYqeJ6yNwaVNtgp83cbNfBnbtbnexq+1j6y5+dZ/e7lamM5SK0pWhQpUMCuZiI3mdoizWk5XDQljuqscs+ChY7WAVTu5K0dsBI7wV8d1BWKltGszXbiMgGTpxsurYXarrsL34ejXKSEsccLC5btZ9vsqAyNPH49GPrwhMayVW3aIVi6E0ujw82mxH0zMRG8ztGTDIkoDxxiLatWMe1mXy7xizhshayHuLDBgKtZQZjqB1iRg6mZsWcKpI44FqrLx+XzoLsWngNehRVQqjWVvI+lpROquUP3YPIOp1bdRa5O3SyWGyEEWTSlVpJdN445tpYsSqZuL9kV065mixZTVSTnnALNubyZTZoTNRCsFbafvM5Y/Ku3jype2qxKsc6DaxeLoR9eOxyMdXhKY+rXU2WG0yKdct0pjJY7EzaW0FouYhVTEV8gDC9wfVKV0FGMQ25iOoYyD/auV229KjwbfXrqejsscmjcX4y8F+muwP3fg6qmIxW066gKYwdQNttDGwxHpavVKfD3LYVosnjhjebStoy2MmdotK0F6mY8geshEwL7SifxMatQ8mmIDbyNSnW9y04leQz+WaAuUM1KuLOyePQdvy/1//WjMVjJnO0QLLkwRxIIEYGIEY2j1WhSpmQHadNtfV2kR3GLq/V3Xz3GasWe1MLAeba63xMm49y9GMBY8jKBgDhgQY77fvZiJiYnzATNNnAv0NRWkLPeXPEdWElBRYT+olwOCDHVlHdGCDw2tYhw7F4ZaR3g+nwys/vB58MekXrkJ+abjmSQ2J5sWLAkC+KzSUc1nT5tpOZh6pnmlsOULI9SESjYoiY01QNCQON4WsVhADvt+Gwz+K5tdQZiavVZ7Y0VxG5LgUVxgvpFnVJkTJp1Ccnp2PfWbWUeO7dQl+dy9lD7BArMYCvjqfuUOOTw1ZFfFpIYBTiqPZlvaudEuxfT9egcuYXuH5SsNqg9Jba6SuSaGUiid/TMYgMosBlkqPI4dmLpSZ3SmOn1Wl40JtGRmUchkfjVG1YwGRZXeO6rNetkakqOYNP8ABs9XjtVL26VdNOe4XZS1Nic5k1gqMRjo3dh8dGOpCmfLbVVNtB13RyWluR6cbCrA96hWt17aobXOGB65rC2gtfxPG/qTkqNiZ/iNKJcmz02ncprPaVXJX2wBgpWNx9jK0rQSCBh+mfxgSkk3CKGBlLhgu6czXt2QqKhSYjudP4oqSJe/za1mso5rYxWNnm/N0E4zGV6wbS1lSg3CITZOFJX09k2yutZsxNGuuhj+oDCxErQy461npu4xM2I6Snmy6yRgZcoXKNRxuPSr+2VnHH934OqwksVvGs1Pe6crNjVdkNQtkfGs7WPIZ1NMC20GORQbK8wlsBFLpNsDA2ZCR6dwbo3TbKYZ0nVmYmtaMJtLy+GmHhdh4YnJRkqcPiIBkb7efn0vSebyFkROYRj4W6RK8c+xx1BuYs/xO8PGr6NctI8jnVc3M3NgwAMYChkznaAWdo4a6OKvRltYHCxiTP0NgLHkc8YknWvAbrQpK0jxCNvXaN9/86iYmN4neNBVjl3HT3Gfv2LFgSBxvFcyWc1mzvPq4Crn7hX2iQmMEM7wysUvFyy4TqwMobFkPgSgxgh8xtG+/wDnVuv3Q5DH5leWkqJbGxwIjGwxERot+M7fNVptVyZGxamYj5nb1a1aVk1pQC6fU1S1cirCyCNZS0dOg6yERJ9JqkabXGEwedmHZLG1N99NDuKNe+2sS5tH3mIORXZxWVtotDi8mvg2JiZmInzkunzba99j2+3sDg8tfYM5azunIYapfQtJ7r1S6dx9FvuJkmHjstN+9aSuBmvlH8FQoZ+rp4+xnGqLx65DK08cG7z3ZVrPy1kMnk5FdRGToWLE1UOFjdZ2pj7FeJuMhJ1r2YxU8BjvVk9S23R9GOYU5S71ENQWWYGqllRlAfdVWF38VkQyNQXxsLMg5qKL3JjdkdVE5Eqt04bCFXm2GNxaWqGlFmKiotzE2NMYCgJjCgQ97ks2+V0imrRv4tZV4kY7hvrW6yycLRmKaItLGzaI3MiIiNo8Q+wquME2doh6pV3oKJXgKRXrx5J4z2tdQZhuOWC0D+b0/fw9NUk9kjb6kv0L8KOqyTYC0ZzCAoSiGPGxWMkWpYD62VB1kZzHCymnarPxlo8fXmuHSC9qLmbej4ih1SowiAX+DqEOeHsakfedJxA/OCfD8VXKPnSrXPP2ridpEcoJjIvVBDNTp+zP1oAJLpbFumZSbAj/AKQRE+LRxrI4cK15FBDCc+nTr0kQiuPEPRzVoWTWlAL6dUwslauIEoo51tZrhxWOUHLHU4o01VYLlPpNUCd3imSlzwSHI50pTLBQ6x4H0sd+YgExEaRXBA/T5LTrIqmAGObQrEwu7ZnkXiI/7QuwtjJANy9TMVjJnOw/n3J/ytC1isIAI2H98bADbmUD6NCWLIBLjNRrBIq7t+dpMmEGHhtd4vXBx4n0mImNp8wEzUb2yn8j0IYIZGY3hJFWd7c5mV6tNakRMIiRid4iY9DmRAiGN5q2IeEztsXrZT3l8YnYo32jf5Yxalk1hQIcH9Qu7jJlOKzGDrnTF+OXAOwuRjIUhZM/ndVO40ARG+6lipQKCNgO0hnVPNzBBcTExvHxl8OnJBBb9uweTzOLP2rxXZZhLTEwy2c95le0qwO4T5yIXDpsGicBYXmeoVBCDpkxsUuosrExbZ7ZFRePxFfsKLmebsvYBPVMjqh79TgzRJlyal2vcQFhEyQayXT9XI2IsGZrOelsdXEnWbBynpmp3brsgA9uvkMjWx6Za8vqWL7z/f3Z3J/6DNdP5GorELF7wXPUuToW6gJruhjAZDghm20YWuUZR7VkQRSy1DJqlfKAOsbenciSHxJURISGCGYkfTPPddtqw1WfPBeNx5ykImMNk5yVaXEELPqKp2arGB4XS/tE+mTjekzxvqlisrZqrFO0069dVVAITHFemKU0eLQExuVsPVSdmzXVAWLDslb7VVfbXVO109koiyO4FGJzCoiZW+F9OYdc79jlrM5KlXx76iGBDunkynEoifnXVMSu1QeH6msln+22KeOGLFvKzm4xXuHthDV5vtTj67FEc5FB2KD0L+/pdgOxRVy8zToXHm/GLsykyudSUFyuyj3IYu0hEGLS4ytqmRus4KNCRDO4zMSu/aD/AF8oxdtLcpZyt0xWtbFtCDUUGHpeNmaycY5MzFLLZP23DE4wP5rDYZeOVyPY7X4PawT5cyef4jexxyqtpNcEx4+o5mIiZn4k2XTkA3BC1gsYAI2iZiPmdvQhEo2KImPj/wDTLkyXbrj3DrqeJybmcp/due5DuRRyQJCYwQzvDVA0JA48V2EBzWb92rKO6MEE8W1390fMcWOGare+uN1iUEMEM7w0O4sg321UJoFNdozOmqFoSBfCGko/bOn6rTTUmTDbdTIYsTj4sIF65Gfupk6VyLomC/A0faWIcMflEYAEsIogG9R4hX+/zmrmsZbPtpfEn6WmHncjFFBT7B9Ku+pNKR4oOxkMBdGv3panpebDMhZeIcK3VgTFetYjfZTVuWLVlBBkem6y0XLncMz6eaTcRXkp3nPZP2FTiuf5nIYaKWMC1YMpt4holXlW/wBa2Go4MJ2IMqjjHMSgm5b/AAkNNt2G/ec7aZfpzLEsLx0pfASZQbMca5Dhc4dadxp+nVdm5yCqAFFapkb7q4U8TSlAZ7FRVrhZfYZYtAMAAiMbQ1cNUSymYhWJqr8nuyQrV1/YsY9McyKvUAx/pyfTlW6cvUXt3t6dzhh2CeLE4PH2sfWJVlsMn0XZmlknMczuFdydvIgOPqn3IwLzQiDD5y5Ls4WwY+YpgQ00SUbRq4EnVaI+Z6VPlioH1s2U1UE95cVui3nifc4l7LD5QMd3dqsus1rlTPmdO9W7T29JLEoKraJch03kYmYnIlAZrHVqaKuPqBu5SxUoFD9uupik8jQr/Gs3f9jj2MGdm9N0RRQGwY/zCb9e3bs441zyy1WrTcGVNnHWFdefRF17bng5mtnL1Q421fj+HdQV7v8As3bHtajrHzqgsMjiTuZCtFhlbpmxY5NZPshjB56oM9mRYLH5evE9+oURXyblc4fyZrGZLGIoHVvVyfrp5r/4qQ0YL2WsxkmAQ4+j9d57wwVMaFP83I4TDTT3t2p53dZLM08dHFxSTaeXoXIXCmj3fUmLEoEiiC1ZJ0BEJHclBCFzzOSkjZcmQXuCVrBQwARtHpEQMbRERDnrSO5z5gG2zg2xwVpr1Jjcy21+dbnzupC1LUPEI2jW8b7b+f3RDBRIlG8KSxDeIfUjVhPdGJGdm1390Zgo4s1YWQF7lP3gS3q3+RXLKtiEzuSvTkO8DvHKyiHBtHg67oeBJbH5iUikOA7zHrXW1T2DO8q9HLhqyXOiqwymVVk8hpVk4zJTVyqANV/pmjZCTqx7d2NzL6bv4dmNwZnbntMY1gT9fT9QKuMVtH16zEArPY95xHGBEY2GIiM0tDMZYh/2dK1boLmwxkxVzzxRirEz89LWUMx8Vgme7Yj3/U6k/crOG2++1ZgvyMliypgvK0ZiExmIJoRAbLmYiN5+AMGRyAoKDMFhJnPEUvU8ZJRcosjUXkNz3NFnHsbhE3lpBFh6hz2EW0fFjp7Ie8owthSVj16skibRXHx+G/MpdWuDoSExgxncfVpwCjOZ2jpaupuNbLgFg9P1UOZeuwEDrDT/ACxxp7zXSshH2YpA28HXiJ+shkZkSjaZjeJjXSDN61hPrkmnmsqGMRP8rl7Y1VJxONIV2FKhYxHyeaKxjs2vIIVyhGByWS5Wsg8kmnD9RpX7ULYgjG0JLqCV9wnrx+dRfuNqAsgnqLIjVpElbJC1ivc5DKVQayT11JQbcowSfJ1OqWV6q6xVubMBTuFasZS4HaPqB9CupTrSYsOWUmsCkeE56CoZSplA3gc9T97jD4eTvZT3XTQzJbuw6uzi6oay2Mdfu1CW6ADV9mUbmq1avzXWkAn5GJ106uu4bYmkCkFrXEwsYCMrl4rTFSpHevsIcFXlhz38vhcNKpjIX9zvaYwFLJhzsDHe5OxlbAwWlrZUUq6tkrs9OXTtVDFzDZY9F1YFpOYXM9MYCxkznaIBluYNm4IEYGIEY2j1ZaIi7VaOZpqQJdxs9xpFAxJFO0S9r5ka0bCqosJ5n+Yz0Zz4T29uaa8LmTOebfTaN9/8/tTasCETKIn1mYGNynaNWElyh6fDUOFwco8Tot6buUfoRMTETHmCKBGSLxC2raPJc7xbSwpFyv1AkpAZKOJW0kJRZT4NDweHIfmWBBwEzEF+PI46vkUSl0bTgL7INmKuFvYzWOVepMiQ5Pq4bOXa3Yacpr41Dq1FKHzBM11ebBOoQj9NJzLFNL2DwPqgyHEnETtqkAhTQARsL0pcolvCDV0wIc79sB4IxtiRq5XL7bGihMdKun/cwLRfiEb/AFau9PY+xXMEqFDToZ8Bmp2ZMSrX8KwDsL+h9xt7jVqLIijpNoCPZuSE5rDVcZWRASRsgB7fDb6OnZKrcu4sp8f/AEzqfYY2T65ye7na6/8AElAxuUxEMyFRfiWQUjZsuj+WpuZA0M874UFcR6eyZ/rXYDVnpgvbsmLbGn0ywjxQzzJheubfCMVZPfacKqa2DXJfPSsf/C3TrCz+W2NPHmhg66WvQFea5z9OSqco9wuPq+fEa6YZKslbrSMx6Z3IjRpEIzu+D/gOLhYxtkwoMjJUVtKSs6ydmalB9gZ4nhjtsx623C5NsvGvXY8/to23GtteruV7vTgsqwa8Q6We7x6RydwYdfxmKq48SJIzB66ivRSpbLLhZxhPjGoO2fJlNZ5zMndLxU1maUXse1Mff03dm1jRAy5M6gw5UnFYTERUx8iVKsYTuNuonKNK5irPbvYVuYljlZIfGRyVfHJFr95i1ZCvUZZn7elRhePdZaW0WMvcyDSqYUd4eVTp5MzEzYyeIw7pdGTyRdy1qZiI3mdo6lyQDVGmgxljJU5iqayjsQ8CZN5ozK+kZ5zdZttHq54JHcvlaWPKG2Y2HREIxJFO0Ltd5vBYzK2MBQ8jnaNnW/ndSFqWoeIDtDrKk+CncoS6zPOx9CxERiBGNo9CKBiSKdoiYmN4+NEQhG5TAxExMbx8fs5mBiZnxAmJjBDO46tVoeO8eGVHyYytnhvpYTD18JnbVZhqL2z/AJ09RAfuEx9ami0IMPghExkSjeFEVZvYZO65iJiYmN4Sple3IjG6vUFrXvwGB0+vLWLYM8S1lMknG1pcz6jO/nchucN9upWazVDb3I99ONylbJJ7iZ2O9kauPV3LB7axeUjJCwwSal6z9S6N+tkKKpM432jfxPrlctQrwVdjYiwjMqFEssTuK7tLqGq6nEkliFQhC0xMlFhUuQ1MTxmvas4yb2MYyO26IT0kod4iaCwnG11zG4dMHKjuY8vn0MAYMgYwQ9NrCuy9UmI7muqkQzFyz/VjX+5oV3T5m6RUepa9iI/L6sAxTWtBHmpYizWVYGNo9LK7t7P2vZcZKcVjKs88xd7zF5fC1p40aZM0ecyjPCKoJg7Wab99sVRKbp/qXnzqKjP82rE66ddNTJOx5eF+vUzjcdbFp/UJPGpKF66XdI4+4oo31ibKVwSTnicrYQTMARDhZnZ0f6VZ6khXC02JIcqbyIMLSI2Lq2sTmKZubyZMxEbzO0HZTey7sg0t6OBrHkrzctajlCt7XVjDj7ddSyTK1eoH3CMCMDHiMtTZdoMrqPgeIw38Lrm6Yhtwdzx93JtGJs5nuG7Di2efqURm+oNo81epbxQsMbX+qxjKI0KS68bcvSSThOoZgZ2rZSnF6i2v/q6bygqicXbmQbbxmTx147+M/NXPU1rbiOOZ3UY3IZm0NzJx2UdU3xhQ45P1NqdP5F6V17rOxUt5Wli1Rj8YEMsYjCGB+/yUy62RCIyRTEDe6kIme1xS5e7+B5XIcn5OzK9Krc1OcUyIGogADmY1WiXLI2THa6VR28Z3JjafR9qAntqjuNRWmC7r55t9GqFoSB/BuVWGFLjkaqxGUNszyIiEY3KdoJ7nlwrRsKaq1fVP1sMwWPI52ibbWnA1w3HTbS1zwjc2Qhr5g7M7D9Ix/iBO2RlK6w9wopSc87ByZREDERHx+zIYIZEvMRypN2nea/z6WUFMw5Ph1d8PDl8FoLcd4ksjhL0A8Np8Eh5QXYf4Zpm9R3cj9ESgoghneHpFy5AtVXFMyhvhujHmBBvtqsZodNds/T+BslkMxYa6ZNWn2Cac1qwwZlzxdhR02zL8b7TIZA2Zh094ABYwADAhrqPIWaNMCrTxNl6vVrLdbZC4WwGgLFlBB1FfsUaQnXnizG2StUEWD+/PY69NpttSFElQQKRCRgYymPLGkjLURgZq2V2q67Cp3DWSHuXss+PMZQP/AJXpzG+2NLlj6pfOuU0OqZk94X6nM0uphLb8vWRrxZovTMb66TtKmkVaSiG9WPWDanGf5gciOcxd1RK7bOlLMNx8okpk/SsNqy+yxTiSpVCqraYCCKIiPEfH4L5HWsV7yvBLYLVg0J3D0x8Ff6kfan6lRkac3PYwyJsdNkQZe6nbYWY2te6jsVijgmekqMfY5w6DpHHxO5MaWkdP4lEwQogyAAAYEBgR6oHiNKz/AJy5NHGWZTG7E2XHSnG11cjprjG4kIZG09KrYyLV9vk9XIh/UNJUz49HFwSw9LgY6WbJfOUKSbhJmPOszcKljmuD78S1lKlzEuGunknfvOytmOXqRCAyZzAjSrznsq288f5TWYwIZAoekoTZB/UuKmVmubKj6oycfTNOBIsl1HkI41a8oVSxNTED/EMk6DezIZPNESMaEoqY3C08eETAwx+s5afeuBhqZbaxeMTja8KCeZ6JKSAgIBkLuAqunmsI3sV317jaYxxKoiK1VVePPqtKlzMgO05PL1cYI93c2U7S7lZdlUTAasvJewLHkytWhUcz+pr7AJj6vJChtiYOz9IxAiO0bQJ2pMu3XHmQVN552C7htcpI/XO2t7Nn4/JUlCkx9EeWXBieColrIrOdPKyX0gAhHEIgY/bMAWBIF8LCFhARvMej1Gs/cI+5LgcEGGnV1u25/OnoBw7T4JDzg+w/wxiwYEgcbiACAwAxsOrSJOIavw2tZF8bfDNWq0PDx4NImC4Fhci9VKKvk79U5+qy43s9pXnae4QlFDGLljcNgF0trFn8y3ken6N2DYI9qzi8nYo2pxOTLzrqOv38S7aNyNDMz08jtxBWMRWbUx6a7p/NtVUW0kiwEGtKVoUCVRxXkse4LU3fcmS1jyYI6YsGrJTI3Dp5pVrNrEMnfWSvLoVDsM+aK1vwWSbJwdmEHkemFLVG7OlsiZAWOfMQfVaN6qbYzsxDYchbh+PTqZRQhN1X68dV0IrgZiXef1Jkbm66Ku1A02V7lSOX5nUuMvOvw9Ciauk1eCyj12Jkx6SrsBdiyQyC9XHQio506xEbVZn8d1PfrGEfd0vkAfTioU/nacRCkyH7unLK6uOvW2fdSx2WcA5apPJ3T+Ht0mus25iGYqZb1FkGzO+rNlFVJPeXBc9R4eJ29xpnUuMiNkkb2JMmKBhhKy6nVLMSZRq8829PG9c7kkKlhdGpRAvfdS2ZRijGPuxVeK2OrpiNp0ie91LZOPI+mVPt420UfLDAel1BMfVkuHewhD5PXVLJcVTHhP15youhjp2ZuWErzWxddcxsXp1HZa2VYmrHJ9KoqlWXWVH066oexFJZqaxTMbfzNiQFdys6dTMDEzPiKS/4/lG2bIyVNawWEAsYANX7YUqjbJ+Y6Zpt4NyVnaWrygnlGY3tFBaEhKNxmJjR1wb1bsa+Q+rWrSsmsKBCy33kWspaH6cQSadGtTc0Asa8fM6Zbki7daOZorQue4yebXWFq8T9Rwl1j6rE8QABAeIRtDOfAu399epMF3Xzza+wCYjf6j7Nmx5cXbWtK1RsA7ehmCx5HPGFNho84iYFYu7hEwo4+vn9s1JqOX148pcDg5j6A9TDIBLcnoFwcZ8TXcclKHeG6sk5DofBSShITGCGd4FKhOWCOx/i6hx5kv8AiNaeFiD2FdLHz3X9Kdj2jIFfF+sXQv1LNgrFjvJ6gTVvNioEFOQ6dyTLtSVu8vaoHKNTI5B0q01lboMnz63slL7raIjHbx6Ja+C/06v4DJW8m98NEFYOByFJ2NyI9wvZRjc37JE7oxeQfV6ecaQ5ttZE71iu9CpXfyVQ7uMYgojvYbqJFarFS9yEm9X04j8lLCJmfzVvxWX2gmhdszBXHzOlYyorzx5yIiMcRiIjIzAPqM9LmDx92zFlwzLAAFgIAMCGup7yk0CqwcQ+lkAQEV7AyvQMBgwYFBD6vvVkfee5Ny7f9oIEa93JWp7VdXdbUwWd7hN7o1SqIOvXBLGk4tWukzO1yrtgaqErrqBKh4rMoACOfjpTdpXbRfcQiUSJRExnlqqUCsV6yJPCpWOOrsgBhmsyPLFWo1heDcPWiYghq4+lTmZrJFc9YPLu16+2w0+pnQIg85iV9QKmREpWU4W8P8QvveJBoLdY/tYOomJ8x51nS44i1OnL/wDliue2+snO54Up+mNVZi71S42xEx1b9Q016iIGIGI2jTGApZMOdg6fQVp78w/eT9LWWeiw1GRoSdWpjsek4tV64rZrqS9Naj2Fz+diaA4+itEff6dTOOzZrYxMzJJUCVAlcbAWas1c42bxGNdTzzlKwuVsqpxWP/h1OK3PuTmbLa9AzQwFtv4y5UqfxM70sfh3usY1Dn+WemStuzN2MXRL+W6hotUFWtWXPtMXjLeRtc1FPB1laY2LyUKfZnk6ZWsQWoNhiBEnsdMhWjwmuCvq+4/wSIzMFMRJef8AHo6zIl2kjzaurJl3LM8z/bmUAMlPwlwODmHxYUTV8BLjMRMRETO8wxZHIQUST1Go/cI+UuBwch0+sQnFivH1gUkAkUcZsI7owQzxbXsdzcDji5gCwJAvIgArCAH4/A5y0KJzSgFh1eiXcTrkKDzGMCRgrK4nJ9uMVakdiHCwS8tUkvp1OHqrrWk1R7ZY6k7H0Jr92G2Rv2cYTbGQf37uFx70k3I35ibWMqXrl2yzHP8AbjBdTY7cjgbybGUQnJqyVKJE6712Eg9U7gqzXcRClosK1YCrXZYZ9uLB1jm8o5NrIhCYXHzirF+pmWY+82WxqlEI6otqHwNoSZ1XWjh4ep+DvlDORY/o9ozNhEjG+sj09jzRZcpXF+MBc1ALjElp1+qneCPcqVt2QuhWriICvEqj9Q5LXUtVKKSGrGB1ExMRMfHrlcFWyRQ0yJbsjh7uOjk2O/WQsiKCqN4si/kFDxamSn+KWznipHlrLsjPuXwkUpJ7l10DsTun6WPxrrNmZe/pmkNbHC2Y2Z+HIOhFGw6ddKJleM7k+nV1mQqprRO04eeWLqz6ZEeWPsjtvrpo+WHTHp1StU4sjKI5rDHvrrE5DmimFmyxaz4LYi1VsglTZIu5mFTuQ9yF5i2qfqSQzczl+xWOuQyKTRw6RiWfOR8YfFOj9QZ3GJ1ghkM9kBLyXVy59tXfA76rOGxXU8fjWYr2LWPbXrbdx/UCMWAUKqu6VawuyhdhU7gwIYslzMxGMxmRoP4Ta7tKJifj0iYyXU3/AKkeuJKb3UNm3MTIBaZic29Nls+39/gnWJcxyTZUydW1bOvUgjHXUk1XmmsCysXgxGVyJpXeEatJSwUsVLjiEzERvPiLeTtZewWOxc8UqTjsFSKCZx1UTczc9hc+2xtSoimga9ceK/bK73emJk3WFp8F5KEtsTysTxARgYgRjaNM7nCe3ESaK5iXdackzTWQpcnMTOkG1gyTBgPWOEzMjtv+5BQLkpCNvWxWKC76PDa7xcG8eCeo1H7hHylwOCDHTWQtcnPmFsBoQYTvFmv3PrX9LatmXRIlGzPXM5eaUDXrD3boYjP8Yf8AxCRfaV1LkIihYVArvYlTMUVKsAwS8dhUUXjasgd7AS+zgrCC3LUY20eJr3FAaruIzSMkuBnYLOXw0ZKVsF0pbj8JRxw95mzHZbNlemcbjAlusJi/4bU4F5eOcxpS+Jbw0t01mGx4BZrULT8W9mMtM2r4PFZBOUBhgS1Z/IRfcGMpzyjG0BqJGJj69dRVC4LyaI2sVLK7dddhU/RmamUZmodRWcFjMxcXd/h2WHi99dFhcqesWBUx1KlJzVVC51MRMTE+Yw1ARt3KDTkZDH1Qjbhy1T6fxtQpOF908SAj1Lej7fTqRXcxDtvnGt72PrM/DcQNmq5BfGMwf8Rom1TO3ZLHdR1vpEO6NNeYykHCDiIo9KJWfdvM9wVxIR1DjlAMCHVBzNFdYPLErhSgUPx+Hqux28dCYmOWJrzWx1dJRsWstB5DLvVG0r6cPnh0ejo5JYM/HSs74qI9M8EHiLMTG+qnT+Ns0UOkJE2dKAM8kMkZbgsipsNEyIpPKKnY1AzX8QYP6tZg6vX1WK/aCCE+oZleOx9QCgQyo1LGGSio8HN6eu+7xq+U7soiSuqrQn41l0FYxthQB3Dh+Vx1WlZJhiitnEPyJUJCVlvvG8edVagWMpl6pRG2EzlSlR9tcIhYnP4l3gbAjNjM277Tr4qOCel0WnWm3TaZKuWRq1W2C+OlkFFRtxkfmac5SFy1xwtdnq6N+NNHLWHt/wAJyBruxAhZp0cgse+AuCencPI7e3iNV61WintJEVKyfUJkz2WKjuuw2H9jBWLBdy76Zi+/I2RxWNLkLLVbAIjH0xmxeq4B9tsXM0yWsWtaghaxgAIhCORTERNhtgpCtGwpqrV9U/WxtzY+2ge4aobAfmzEn+IigRkinaOT7PgN1JUoFDwCNo76u7CuX1/tpMYmBmYidOtwlsAYzx1ZcaQghDlCmg4IMPiwkgL3CPvS4HBBjpyjSc2ERpTlvDcfOl1SU7ko9laiIiZmI2nXUOTZRqitE8bH/Tl4wg2ZFnfcjG4+yGRuOnunn8d3AUgistbDJWcKmIYnqO/XYarqIeOMiq3MAHal1cABYwADAhrKYRTckI0GduzFbq1EyIMlkDgs1f2/iVqRWiE4hxoSuO0l6nDyWW+upcYCbAZEVwSLOOGvXm1QibOLw7KF6Jxd6OY4pNVg38OonL0FbF4FSiMZI9FkHDnQoTEdkwFgEs43Dp8jqWLWIbO866pqydILYeGVXe4rKftt+C6MUs7Vu/C/QI7HVp7+I1eXDaVhc66ZZJ4hUT+LBT7bLZCjttBlxAi10kEeyc3ad9Wf/wDKKnnxkPzuoqCfmPxZ6Pd5ijSH59MKPurWWd8x0me+NMP86KNxmNdIsmab1T6ZNXdx9lesC3u4isWiyWPEpErKoJ2bxSR5FZAtKNF6uLoGeB42qfwMhOax0BaoqAuWslg3ZBFYO7CyaM4XKNGAIw6WXNXHutPmASCKGQtpydZvI3vBC+4fxcZN+eDh5A7AWHcLIM7Tem0Pr47t2AlZ4qlaXl79p4cRVjMMGRbBbNt38BjbK42Aa05CK6+nWRi9pr9OOS3FKFMSOup2wGIYP+cWrs46sv4nIX0Y+vL3z4KbWVZFm8X5SrNbv+1UOrNZdkODNMJ9CyaqrmrFOW6gYQVltEyu4m+ddl/I2oOcTmk4wYCa0GVK/VvKltY+Y66nyM1qkVlFs4XBhKoUaY97KYbDzT5Wrc9y96NStvHuRvqIgYiBjaNAta9+AwPoRCIyRTEDmM9ZtchpSSqmGc1+MrtdMyzTbUwfaQPcYhbh3Jp8pmImNpjeHWFJj658/wA3Z/8A+Kk1lJj6Y3L9tYrA+IneRNItFcQ2YImqBwcDjwlh1ziu+fExExtPmK9bsGciX0aaBVWd9cbqEhMYIZ3FVRanS0Zn1mYGJIp2h/VdIHStKzeDbQZ/L1BQBRXyj7o0pbjOLWUMbayj/f5kZ41cbRpzJVkistZNLsLdjJU95rU/aNUNqssRH0we1m1fyE+df9R0ZvxSGCn0yo7WInQMNZQQFIkFtV1RVLYbxirJ4e8zFXS2QGOoCSzBC4nKhGMy9fKhEwnqBEWsS0g+osZai3QQ/fecyXs8vQyBfpaygzSzFPID4DWXDuYy0O2+un2dzEV5md59c/VmzjWcP1MdbG7SVYid51miirmcfdnwOiGCGRLzHTJTXbcxp/d+G8z2PUlexvuDf0j10pMTi9vQp7vVQxpJ+46qaQ/b+G5nMdUKQNnM7uUY/KFkEFK5q9TZQ/planS7qLICk5mlC9dLIJWM5lG09ObLfka0eI9OmJgbGQT/AJ02OSjGfjpYpLEjGmYPFMaTTrjJ9N0kMvXGEsSD0ys75vFjPmNMSpu3dAT1k4t+z4USBbMRjbGPFoufDouhDhGvExDKtJdf6vuYXLjPDbnbuZMbTJbdWlvTuRe4m1LTZc63Xp1HszLeXPFZX+NBaruV2xthWw2DZW58p6VqsTjpaeuo6F28lC6owcAMAAjHiOpLHuTXiqy+9YuY+9iEqCHicqojj0BXnaX6JnNHt2gLFzjKUsk+3tFdSq3L24QuTWthSTBgixDf4ZmIRO8o1YfN3IW8iUwSOnMbxX/ErP12fwm57TJdeOMJV2g2kpKc7l7dV6qVGP5g8vlWJZiXblZsV1Mt1MFX24qUtKxUoeK9AsA3kYiNGwFxuZQMTYc/6aw7CmqC55l9bP3bkg4JA9IcSj9u/wC6xLoXun7xmZGJmNpmImNp8xvNNu3/APrxO/mPgWAczAlEzrNnIYqzIztPS41JxwmoR77KaZQ5KohMdInxC3Xn5rXatvn7dkM1kFWHUnKrFwdhK1qrjxVbnduSq+8our/6umsgCadivaLtwzq6iJ8QUwxrdR42yMjBSpvSpgWMmImOeSpNdlbx0xiIxN+MhRW/x3MuPlR6r1m2C2CPFaoqsP0+S6ltUrpLRVgn2um7c2MfCWTPevVAu1GVj+MBYl9I6NiPzenj9s23i2F9WWpe/oNrx9+AvTcoDDP1uoa/fxTZj7se/wBzRQ+Z3mzx9u3l9vSpyWKiJ/BMRMbT8Yjehk7WLLwrXUNP3eMZtG7MNcG5jktj7tMOanVIlMbB+HqseNatZiPqiYMIKPMdOZGlSRYVabCSLqbECW3dItIctnVIuWXNWCuVhy95jWCGnWa6Fd5zBBYZvEn8WgjSrVZ0RKmgyNdU3rlckoScqUjGVIGCme9r2lX/AO0GmYuofwMhN9B1lQMPMxw9D2VQYlhmZ5I8Tmb0wO8R1W4vsTJa/wCo8htv7WYjp24P8Wtd2IWWi+2ddKf/AEw/TpYY9o9vrlv/AK9jI9c4xmTyScQidoSoa1cF8pIaWSbfzEvCdqkzAxMzO0YvLDkxfKlkuDpVjMjNcSdex/Bcn34CSr+4x+TrMStwmAvw+AQSRZya68GVyK2ZEyRVoYm3TeptK53aWspkAx9WW7cm1FrwtRmRyE87uKa3J5BuVu+VmZMMjL59brJVVYcTtOMBg1oJhTM2R7mUoAEbnkn+3x9h3+YA14hKY8G3N4vGcaUkREXV1XeYVXaev+r0RP1VmRqt1Ni3zAkcpISEhghmCH1Yub/VAkkt15SMbimsyPDld6exhoWV6z5taIhGJIp2ibRsmQrDykKccuby7pxER4j406ytX0z9RqMzDkYcJ/bJs8zlZj22atxY4wSJ21WsQ8N/g3JBwcS0hxgft3/fqxaNDRiR/KMQauRnyNZhKP2zfl6iqs9wn7BmCGCj4z7e9l61OyUhStWl46Rx2BGCsIzffwVh9qYh3SY88g2JjcbAx03UZ7c+4/EXWBxp37IOuaTar2CYKTg5srn+MZCurzrFW8sVcUY2uuYtqssISyeL3i3ihqpRbS4iVjaU0eojqw45Bf8A8Dznb+2jYrBYgROZiLeSx+MXxYcDMRls5MwW9HHU6FWirtVggIKJxvUEHtEVtP8A/h3UKnD4T1JTP+KV2qKFkFTqlw9p1kUhVX/BM6NcjkkXOM1Hwf24y7k11TitbWtdN2Wal0ZMVqr4ikilSFaGd4PwdQoYuEZNEfm1rC7KF2F+QmN/E/GDL2WUu4svAttVkfrNBeupMnTedYqbOb46hzTGRAv8lZzLJiTuyOpLIzG3vna/+JbbRfdppXgCSbkXQLYyFsC7Z2LVfEZZP8KTLTgnNqKJhMREJmaZzO82GTNXJMx1neyubKnrQ4i9nWbGq+PCVbWImTbi+TN1lALKjXrjyaRlI22p29veasbWTu2FSp7xsLgzD7SmNLsT9Me5auU3KNatJttHbbfuULR1uzJjCMvjGxArtLmcoIuzrYS2IEcW2Y2ZZKYHFVY+7kemYiZZJJZwFWTyeMZAC+GrmzDMdNuI2jpXb+FRt866W/8ApzI9cj56lx8T8aIoEZIp2HplZ2LFvJsncuqMlCURRUezaTqdeFCtobZm2scTZYoxPXTa1hiEyEef4WsmkZlMi6hUfXmsxcSp/R4fNaxI6pYgRzg0j2sgynVYj2xqGU461UTaLDVwPYzBYEZzAghq7LmZ279FSzYY1DM1ajaUo9jjk1P938GWbEkqvJcRG3aPkVWqba3TvK5mDtEO0dWtkcetUb75ZVbH4unJ7xcwtWnK3ZTJTBJIcnl3zcx6vbq6eHGwbAtmUW7GBxVgOPYFc18rkcK4qR7GutYXarrsKncNZC4ujUZZPzrBQOPxbsjbnjOMqtzV0sneH8j0dWhzIJhTICIhECMbR6FEkMxE7SmspPmI5HMxEbz4g7g7yCRlppJhLiWjxP8AaPQLhj/SarBCfZsfSenpJZ+4RH1KaDggw+HoFwbT4Ku8pmUu8NYsGBIHG8VyJDvas8jZRDg2idjrPlm6XRs3WRxtbIp7L4nWLwdXGkTFyTGz0pXK4TjbPYkoxfU25fRXydKneTFeyUDNTHY7FKkwiA07K2cmU1MSBCKl1cPj5jf8vGSbrFjJHEidKxOIykTP9tExMRMTvHVCpPFEUaeztdQ0bE/p5nFjk60LguDRy2emgS1iQrwOMoFWXfn+Ye+/TrgRucARGVvZY5Tiw7Cc1SKxiyGCkn4y5F2imx/q6lrd7Gk0f1MuM5HBKuhH5uKuxeoqfvufUlGbNHvrj87D3Pf41bGbEacRYaVuWCSBrOzOSpqxqQ/lkJBCVpD7PRthCf1WAvTM/iF772RmXdR49wEpSW2YoZ2xilsq9mSCtmcrkIZA2U1YyaLAmNmLR2WrqYkRgjrm05pWQjgvtkNWmutHM5iWNlxyMV2BGpXkPkXBOk97tx39udmuNhfAp46FMwHZdcmUA+kmOAGIx72r/wDcjT3qbI9uz2oiSiY43onSmnBfmOUQQxc/BjreJ+NO70h+TIwe9+PkVnpRMPeGp4aKugvuWM6CuhcyQBAyQVGT5gCkVKEYARji4VwcwVOePChM/LEkIsn9C7vEjlY8iyTjk6TmLrHAGMqdOQwTba7p5a3UTjGkwo7fTeVoV6XtnthTV2qrZiFOA56bcpdA4YYhPpdas+qqojO/ply44u1O+2ulpCcTED84ymjK5a86wPcSjD1bWcbVTExTyFGrL7MUI41+lEvDHkxhT22ZQv4urHIgTjVuyFSs2wf29M1j7Lcg/wAu1g4h+SyN2I+nqbMDJfw9P1ClxZ+1XqCHYpZcVWszTpFEQhzSa0mF8+pFAjJFOwksix1jJMj6rwBjOnJREbFgcb7ClEFO7epOEuxwsmIXlGPykPyc/TVweLnKMkGnI1rf8QrvppxyhirfxNC/HKwGxzStUq7LGNykMRcVbsY1eTtTBHi22cZYppE+7R1fKcxmV48JmalmSz2UGoneKC1rUsVLGBCZiPM/ANWyZgCgvVluIPtpHusSp0F3HHuREIxuUxEFdGZ4pGWl7dzp5WS2EABY8QiBj9m160yMH49bNeHhA78ZAZEBGZ3neI+dNWaDl6Y3FTQaEGE7xZR3Yggni2tY7sSBxxbIjJQUxElq3X7g9xfhtVjGK3bGxevU2NO3WGwmJl3vMVl6yhvtmtdXjsTyibuU90BZ/CU0wuuUENu9dzjIDbs01LFSxUH23Kw2UyH+vpnJy9M0nz+flE9/HWVR83yNuBx1xe/KpZXbrLsKncJASGRmIkaAlh8sWPL+0HpvGRYKwYkycMHss3eo7SK9YmJx+VtY0vCstbphUdXc8Fn0rbh9RlJswWsQ+cbmX4w/CZiCiYmN4pe8xObOmgZNGazV0n2qPIPb0MxFaoqrSpPsxezucWI/yntoDM58hmJFQyxuVsDs+6UCNGvE8jiWFEVl+YFYSV5MRsT4mGNptODlZNKHmO/aqlELsPJkAdcgjT0d6IjmQRGPrf6ok5XVrqLmsOJehgJjIF8Bj6oR9nKYrV4+FDrsq/8AQOuyn/7Y6muiflY6mnVn/aHU0Kk/7emUaYRyKZCIx6txNbDj1dbUg4A9912UP8AcTM0Kk/7evYAPlTDXrs3Q+10HoDsScLekZgqlY/uWOv4eiN5AjXpLkAPAnwySr1HR9olpuMCQkVGQ6rGtIdm1jYfqpQw1pO7UlTdR6X5LYd4ykR9nESMDd5qbAnADkLNTX8HzBXJt1mhYPHZ4tnoykQmwzIZvLw72KYipjUvxuGKHiIt6ab7fGXbZax7fYYGxkCL+YsIJONoUIni38jHUv/SmnQy1s5zNSRht3I9SVkg5/aRrqNjHlSx0FsxSwUsVLjiFtvYqudrGhdR07LKY87VfpdjaDDsTxv4jEKxiJiJ5vr3JXkHFaPkcTExvE7x62+5YarHo/Uu1wZksdilR+Tm97uRpYsZ+nWeZOQuJxFcYI83FWjhooLHeaFdWFxJMZEyTslbyGP8AcYqeLa+UnK8qDKrkay9VeKxS8cguZ5pAU+nYrbxOr8mvG4axGs5lYpJ7CZ5XIJ1dI4ilsd7FY1WNqwkJ5HM7RM/OoU+0Uk/daxFFcfGwRExMRMTvGgWmsE7bDE2jbPGsHLUVOc8rBywhAQjYYgY/atUDg4HG8VltUMrZMEPraRL1cRnYqbyYMrZ+o1R1yl6I+lbBaEGPxaQUzD0+G1rAvDf4Ky5yWCcRumJ3jePj0sPCuhj2fZicsrKLYYBKyvWhp1G2S86XjURiGZG54dRpVrQTMkQnGPpVxlpDJai3bJ6iXsK/S33aVleRr+CqWk3awvVO63ufXq28TOwh02mFYlUwUl6Z2iVulJJj+Zxd4b9JdiPuzkTSv08qO/GJiY3jzHVItRFbIILg3Bpw9mGMyLOVlbum8SROQYyy5k4v5ZNtaCILnUmVE4QNaKzZXZsD/PWmtitNOoP5FUJNmRts8dyRhzbG/wBAdydrxfMrXr27ij67Bzr2KJ++SPQ06o/Cx0KlD9oDH7IhE4kSiCGIgYiIjaPXtL5QfGOUxvG2vYyM7g9g67d0ftaJ67lwfuUJ693tMQxTA9CQk9+SxnU0Ku+8DIz7Rg/pWDHRndQEmRAwVWrFrYKVcmsRgcw6d7Nn2w27tulwAKjLS56gw1oOzbGQ1TjKUoIcY2taX9UOsHmQcptF16gP/wAOtJtotZHOW6zav8OINSy1Uw01GgS4zDIOwjDDIKr1zXluoFEmZ9r1RbmELx6p3dQrRUppr+N8qcXeoqtSBkhx8xkeorFudiVrOt7WJslvtONT2KFdXx6Z637TGNIZ2PF4CsWKhdxe7bGMyeJmSTE2qiL9Z8REFxLTWCpZMPwPTtMuLMpYjZmEn3l+7lS8Dg4K7ft5U/tvWgp1G2T2104qRVZy9udivNvZEm5dEyNfH5Onlq8rnbuO6et49sW8O2ZMczn5jhOOnmkchkM3XTekSZ1YfNdWmHlmRyaKiV4ymEWbRieLLvP/AJnM4HElSUT7Mb2/R7ny3sIHaQpDM8nlLTmQWPmYEZssbMjWDlA04KebylpRERG0RtHo1oJDmfxD7T/CQ7YpSYTJsZJl+1tpcziSp2mtZh0cS+luuwvu96PB6MSqM7oRukSExghncZqkNqHKmBEhgokSjePXqDl/B7PGZicWL8ffot7kdrqtshjICI8ChdnOoqMHdLEfw3Mmj/bMBMCAvthFbHHB/U1q7MnaZX4+NOECUYs+zpEbMC+fPtMh09Tv2YsmRLNawUsVriBD0QX8JzZVJmIqZGoF2myue2/T16W1ZqWJ42cnlcNIxXsfzcvxdvKt7qKI0l1Ok6StitGTyQqlWnsVxWsswN6tlW27Cpka7u+kWbcf6P8An9/cuRWiIiOR4rENyRw+8UyiLuJoMij3ArkBqfEMS2CGwHU9JksU2Liv+olTMrytAlwCOl8gQwkoS08Lk0jMVL5GuxjbcDPuMUByF+9TMVostUN/Jxku1F6JUbcdjXU4nH2e9bxTbmPdNhdM3HiKtnKZU8hdGR9OmEG+/auu+pl3E3cIM361nxjrU3KSbMjxnqTdsU6MTqIiI2j41fiMjnEUfuQ+yisvuPZCwiYmImJ3i/gMfdkjkO051LMYwpHhNqvYv+5kEtAlrytynVx81yb2JslGJ6fXXQfdZjKg06Ka8fPUxNtuRjq25utZS22knESmUMPt4yiqqqILWQ7Vd0WKzezZo9RZjt7lXm2DepMkQH2aBL10spjZs5F+5My4WMnnpr1fvY/HYBZKrbWMlgcUEwOVsn37MtWJQJFEF6ttCJcFx3GDWNswdmeUkS1DuUwIqaLR5jvtqSEfmYj0mILxMRMf1+8Hf7Hnn/QfWhn1rng2vY7m62RxdqWBBwElEFMQUSMxvC5KpZ7Uzur0yeQHHVfcEEsjNZZiMWqzTmImvlaMVFG62ojuwFrH2BWUFETP8Ip3hmOfU8ieOrtidwQf/wA2vj511bX2Wi6P3AUGAnHxkEWCap6Igio1pQJSwuTpmIjefi5bZZFiqoySsHARiavCIiPwZrHKv05EiFbF5K096phM2r1bp2xcYVzKNIHQnE4lcHxXXiM3ZtzI4umThjHZi15u3u0FLE0aM8kL/M6gV3cRYjxvjiiagRvH7mbCB8SwYn3tWP8AcjR5OsP28i0i9Xd434F/TGueRyo11xyFCQQoVLjYbNKpbGBsqFsH07WA4bRaymyI2iI331kr1emsZsKNy10+ncwP8vEKatJoqwlJcz/i2crQRXMfzCvmcRkg7bZESd07iHj4T25tdIPGZmo4THuZ/EFxnuAFXq6xE7WUCY1uoMXZ2iHdo7ODtDZZexNmFyeN6hycCq60QTWrqqVwrpjZdvFWX5qvehke305oISbmTsHTiiJLsg39TMprvomt4kUYJ2das1UWBKa8PhIRYkZdkLE1qbXxO09N02uNuWt/U3IWGDk7LbqO8WHzVe1KaUhu3IWip1GWBHlrp5BvZYy742YdRM2wtMTDG5aVlw2KO4Fa57hgdjutwONPH05Bk/mdS2zbK8TXHk2TrYXGByiZVi6eVvMc6uXYXmMbWx9JNSsHcs1kDWrqQH2hTSLJZO5Fp1hSY3OfO1iz8/kqBaa4eNhibLGzxrDvAVB5c3FLTNi1DucwMe4e/wAVx4iNIZnk45YWmOWqNzKI0l0OHlAzEfuLFeG7EM8W17EnMqbHF1hAuD/saO72h7vg5EZmJmImfTLZimtzMdernKcNgf4kBPaZLr2cBg67VJa5oMtYW/iOVzHvk11m74K6rxrKzBdMqlpbHGXGMlUvAM883X9xi7AR5nGs7lMP++vchUtWSd5mpSyOZnf9CnSplRv3MQc7q6YZzxQhPz6WrtWmHOy0VxOZvXZkcTVkh/gVi1PPK2zdo7+FxI9mvAyzv9QZH9BY0EI6erdyHXWHdcRprq3ORUqvnqtq1FestrY1ZRmskTa5wFOnPSeM8/U2NXOm7lUe7jnE0VuyEDMtptmIvFM8YrtkvcW5+2k6Y9xkpKeNM9oLI7eaDt+eQmNxoOmDfkh+abBlOQd34U8IH8LGAoZJhQIzkWsKRrK5aGMyyJ7dfloqmfYP6Jjr2+eTH1VzZBXLKdos1TXI5KrPzMjqLlWf90dFarB8sHRZCTnhWXLCiles+GsmNI6cMvPaMoX0wPjkIDqOnKsR8xva6TUwZJDYBn8Mz1Of0pcupYGyzsTsp50LgfKinVq4usXA4mT/AIlU/wDVOpyNSP8AXvpT1OjdZQXq20hM7Gcbhae0ZmpTY6emsZZqQ6zaDgz8I0aYviyKQF2slkrNJgwqmyys7vTuRna4marwxWSqjDcPe7qQz12rERlKRrh2SK/w/hF1YNXh1XlSeSqAizPSy++Jd3vJLp51Yu5i7hokr3UdIdrFUbIp6poTsNkGV2IsIsBzQwWDrqV5BQGsuN2VkjXrrQPx1C4EY42T5ZhaHsKAKmNm6yxnk8gvEJmeytYKAVrjiF6ii+ia9jfhUo1KS+3WXARmbDsjeDC1ZjglK0KBK42BkyKyIfm7eJRwtUdyxh7N6MsAyfOLDhroY8/t6aTFpz8q84Y/qmxwpBUDyxWaTi1DQmq6dBenNZyoS1kKvRkHITC54mmoC5ljZ5sK3ElwQPdMapMnnZLlJGtQ7lMAM2XP3GsGwhTHl3HFLTiIiNo8RoomYmInaVU1gXNkyw/29l5IgSgJIQMTGDGdxY9S5gTKBmxXh0QQzxYixJT2XRxdrI3QoU2WSjlODzw3t0WpELWurhOaKSj7MbUYGGXVJsQRDjKCEKyDxc6jkX26ty5YGApIYpeNtLKZ7tnF+5woUdoFmNwNs7YPyawhbiAUsJn6dO77WWAAywWVs5ZQx3alKGY9KMZUyiilkhIyAyHgeoapFWC8neLPTNtZ3ril7wFzIU6IcrLYDX8QyuU+nGq9tXTg8fVmbV05stb1FX5yjHqO40sdm8nM+/dFWtUx2Pxq91AK9BmMc20NVToNurWKq3LAPs8mQtS1BC1DAB/T6nx9uz7exVCWEx+QIBh+JOWHYsr3JtNwD/Fa+32nvXffvT26dfkVfpY2FDci/mVfGUq8RC1RqIiI2jxHqzHUGzMsrKKX9O4l0T+T2yrdL41DOZ8n6GpVAYAUgIiIjGwxER+HKYarkQ3KO3YHGdT7lW9xPZo9MUa8Cdj+ZaWJxhRtNVWv4FiOMj7Udsh02aCi1iZkWI6jfEdk8fzsyHUmTmAIIpIpYHHUxj8uHM/pWqVS2HCyoWQ3AW6pSzEWiTH8Zy+PnjlavNUVen8yMEiYQ8E9QYyeKpjIVsddO6iWMQdcnma0mxa5adbqWsTOzdWdN2Upzlag+0cvVLp7JLsEEmdVnf6koT+cobyW5ddzNU2WBmulT0vHklgsG+P8Rz9elPlGstkIoVJZH1OwuPKnXlj/AKrfpkro0abLM7TPTdKVViuuj+Y9Mh03XtXZYqxCjxuGrY+Nx+szATAgOOQl02+qUtxdslGnD5KzfVcyjQKNCtYlJiAiTXOJkqrj5QDQDZpcy0ysTjmWsmVkyvWHaZgI79l36AcBCkPLm8pacRERtHiP3Te52y7W3OpZlkStvhuoiIjaPEPrreOxeCQ40l7d/wA2K4uH/wBJ1rDZZNdsbn1ABHiLAiHOarsayrFe5yU9WTzOPCO6EZCq/qPEXUHWtqaIUc7YxhSlRRZqtzeHukLr9Ipdby9i+A1EiNOlepQuquU/ONvLv1AeExvqYiY2nzCaFKvO6ULXMxBRIzG8FXlRZHCz9uEsxaxiDifqvZOhSGYssjlQxt1ru9jVlSV7LD4qO/fbD7P8Uyd/6cVW7SVdPy0oZlLJ2zNmOxaYgpXWWWZuXC4YmtJivAssFDctZKyQV6NMIIVrSL+oQM5RjElcdVN7K6zshC3EQjG5TER/WbTqO/VSs9JQlA8ELFY/tCGCGRneISoUrFYzMxtG+/8An+tMRMbTG8WsBjbMb9vss9r1Dj53rui8lfUq1H2sjXZVZkIuvqQWMaItPLPXMJztCCBWOqOjv4K7KDHM36MwvK1S2qZOjd/tnCZZi3YVbOLGOGzRTjcdciX4W2VZ9U8jhLbrF6ub4r53GWFG0W8NY1bMtcnLWR2R63mTm8yFBZfykRERERG0azWXcETCpmNDDIsrKs4ztjO4xPz6XbQU6rbJ/GDXZ9rNm0Uk7TGrUPI54wqyDjkQiZjTbalzx35n/OP+dkAuqlfnbkWm3FB9IfmGA2mFBsLthpz1Jjc58jPIYL41M7RM/OkLcTJe6ZGf2z60MmGBPBqLEkXabHFrhYS5hc8TqMduSnxPNyQcHE40lpqOEWNbR86a0EjzOdoKjjbap3QshPpNEFJVrLEzPTmUCJBdwDAcPmaowI1qtqMnjb9SBbaAFhVxyA4tkpbOmWHUXOnHMIQqNF1VLRKTH1zpVE3at3viD8bQzDO57QioUfY4bDB7mzPcaVzNZSNqCvZ1qWBp15hrt7Vmzbq0187DBUE5DKZTccYv29atgKizh1oiuWLN2pSCCsMFQjlMhfnbGV+CTxVcI9xmrkvlC0AoYriIq1bw6r1qHWmGxIAKwFYRsP8Ay9isi0uVWFwwFKWlYqVHFeWyFeioZsIN614zDZT+YxrSrOxlfIoWarzheN3A0bZdwRmu+V9Q44NlEOQSM4TKHEzvjshRjMIsRWt8bNbIdO0LkSQD7dyjzODCFGqLlHH5ijkI2SfFvpcqXMDfm/WiGV8flqeQCCSezNZLDw0pmVyxdLFFEwIr7SojaNo+NdTH3YqUBn6oiBiIjxGiATjYogobYSiOP+eNqz909lSq6kx9A+fVSFK34DtLrCkxuc+d7dnzH5K1U0rnlMcz9d432/z+0e7sr58ZLSmi0IMfh6AdHnwarBgcJseC9GqBwSBxvC2nXOEvncDAGDIHG4oruQ6YGd0TMRG8ztBFAjJT8V7APGZHxN2ou7VZWZEbDbPHSVK0EyfdsXV7Vw9tXxhY9vcxNJROr9PuNBPxL5/N1dz9KsfZVvZsQrOZPy4/4fWpYPH05gwX3HaDDqK8d60c2GWr1SmHKy0V6jJ5LIzI4tHaSjA1+5D7xldsWLVWovk9gqCcreyJSvEp4qq4Kss+/bKbln48R8WMbTtWAsWFww4iIjaPEf8AOrxVJVyLil9tukZCnYeyupsE65YOtXJ4KJ2or4vO1hskrVLF3aFgBRallHVqx7aub+BN0yph83HfpthFuMjl8QXDJrmzWqXqt1UNrMg4bK4Wct27fshOpYzEM9oNXO5ekpTLyZbVp5ahdiOw4ZP1UcZDqgijyr0s9+QgUfKKq1fVP1M06ypPgp3IZtOmJ/RXpr1Jj6y2mW2bE7JHtrVUWE8y3Yz1n4/763uunaIhIKqrWXOZk2ftVqWqJgI2jTVA0eJxvEMbVmAduavBh4nwsm1HQk5k1sWLAkDjeBM6hQDfqTExMbxO8OSLg4FvEIYVc/bv+1yirM9wmPpU0GhBhO8W6Va4o1uCJ0qEVDnCZiOdasbMBfKs/wD+ndQPTXtVrtVozdGtmMvHK2c0alPHUqAbIXAauZ4OU1saPu7eIr5NQsPIu7hXcnSojvYZEF7zNZGZGmn2dep0/TQfesTNuxey/ZeNKkv3Fx4WGVTFJwqxVwKROLF4yuWoiIjaPEf8vFhBH2xaEn/Tdjab3hYYqIfqIiPiNvV+TzNOy0305ZTBGGzcS6rM1rk3MpjI4ZNfu6c4ilcGLuFf7d2Tv5gEewyIwoaVT+MOX9MhiWpU5UpaEGu30tQdMnXIq5i/M4y9NOu/3hf9U31DvYpba/i3UGQWRUasLV0zj21KrG2AkH/h7a+fPjHN1hSY3OfPdtP/AEh7S1U1hPM/zD9GOWqNzKI1Nl7fFdfhIMANmFzOZiI3nxC2rZvwLl+2YD+6JrPcdXEMbAkudirWe5+Wz6WkMFEiUbwUMpzyDc66nqdG4Tv6EImMiUbjBFSZxLcq4lBRBDO8WEC9fCfEiPEICZ30qoCnS0ZmI1kq1GxVKL20KUdrIVyxdDk6pjMHUx0QQx3X3stSozwaXJ3ssplyk75zUpzOMw1b/Shfvcpl4kKATUq0cLSpbHA91/oFdC2G1axFk77ePmN9o3+ZmBjcp2ixnMVX352BKT6rTO/tqrWxPVL4iZ/h5xoOp7j5gK+PIzbl+oJnbtpRqb/UU7/zEapdT2UFCckvlFeyi0qHVzhi/wDhv8/+34beXx1TeHPHnY6rg4kKFcjIhyN/6r1owBtJQ369egbIOImIiJnef62SxSsgIlzJL8cF5daAvmLHBjqYW/eAuBfMRMbT5ixgl92bOPaVKxWZbsEWOy1WC0zBWqRy/DvkJx2fVYZ7W4HtbV64ulVZYZPjAViVXbk7cxDuDOoLXIpkcUAAsBAIgQ9DYC43MoGBtE04hITIaa5So3MojXds2PCh7S1VFLnkX5huepMbnPlTbDSguEAprlqjcyiNS6y/wgeAKpLCebPzDNi1jucwMTcNk8ay5PUVWt82WTMLWCx4hG0NtIVvBFuXctuj8sYUMfHn9lFti3SFgeAahgSchBRz0+sLdiieLFWSguzYjgyYiYmJ8wpK0jxXG0PsPQ7ch3QJCYwQzvDVC1cgXwpYqXCx+PXI5mtSnsjEvtpxV3JMizmSmFrUtQQtQwAaiuiHTYhY964VkKrCqjBvpYQ2s97mC79i51MC2xWxqvcH/Hs4oYY2uogp9SY6wrk04rstdU0EOha4J4h1djp+5bR1PVmL2mYhszY6ntWI4Y+vIaiqy23vZOww9V4w1baVU+RRmgGNhRtB5pkx9CoGXXLLv1GTMejVLaEgyN49tcpn3KTC0HUeWUMcx30PVV7bxWgpHq+Y8Nr7SPV1SfuSQ6HqzGz9wsjR9W42I+kGlJdXo2+iqydB1e2J/MqfSjqrGtiO5BqNL1vHmveRid/+Bz+SsWL0Y+qyQUrH1l+ZHnMRAxtEbRpjZp5Svcn9N2SpIrxZY4YUzq9EH+VWM11OosZZDcmwg/4jQ239yrZTlOHmkxYP9feP++pasY3IxiMlWxN9fCwxYscNiqysvInNrF3kOyi666rY/h6lLSoVKGBX6sqLa2GHMzo2pQP1TAx3bNj9GO2tVNYTyP8AMP0lK5Z3JGJM4khmBnjKqSxnmyZYZsBY7nMDHuXvnjXDYQpBvzdMtOIiI2iNoO6G8ikZaXbtujdh9oVVUq8iO5f1/q5f44+rVA0eBxvAkynPBm5oZXB0w9B8THfjHLzLnggYI99mKW9exeYFzaxQt/1LiYmN4neDATGRKNxABAYAY2H1e9NZROecLWWTyWWOU4oJTXoYqpjFm+ZljhyeTyzSXjBivWqpNCBUbScXr1DmxSsqNUuVimkaqeAffpmOrsPn5GVU66vIhG8gBeJGJgUJGdxAYn+pMRPzG+iroPySxmfZ1f8A7Q6Faw8AMD62aKnDPCIBnTGT4TOMsfSX7z/96u9T0q5SuvE2Wlns67clLFYjkOojjjL+EH/F2Rsd49VaMIOWGXNnq9APXIH8LxiQKCMpPUWnQvtDMCuxQQ6eW3bIcdQiIg1lOt34mzFulJe2rWF2UA9U7r/Fmc2rGhAL4ssx1BmlxDm1BJIdXVoj86s0Cb1eufCkFGi6ouFH5UEUzmM86Po/Ljnm2Tuy6Y6KvcZvDbjTiMWr/JnOoxdf/MlOv4ZV/wD5aTSBJTKzLgv3mNZ36BTIYzqGrdIUsjsWfWwyzz7SQ20qmAzzbPcZ6GYBHI5gYXaU1kgG8+hsBY8jKBibDneKwbCFMN+bplpixZFICUTPoIiP2xEejbqV+InmVdlhhTLA4B+zmImNp8wdc0l3K2kWAdG0fSbFgwZA43hFcklMQySWQiYyJRuM92kXj6662CwIMPt9chlKmOXzeX118fdy7Rt5XddYFgsIBYwATETG0+YAAAYEBgR1vEfPplHWEUHNqjzdjyogZtuMMWlkqkfZJHor1go3VXKYK/ZXEE1HEUthyhYPiP2t+tMx7lXhuDykZGruXh/4yLjG+0z6f+/9CN9vPz+Fz0oCWOMVhY6pxiigVc3yfU+QKYJNLYGdQ5o52XXAIfGUvf3liYCvSTXnkO5H/UMYYs1lG49LXCUxuMdPn0u5rHUt4a2CM+p7liOFCmUmx3UJRydbBJfxHqOI4w6J0mm9lkn3PqLUxE/Mb64B/wCmNRER8eP6Niop8bz9LMdnLVFoVcjPOuti2gLFlBh6NbYlvaSG2o328/Om11ukZZvOifXRHHeI13rLvCg7YhUCC5tmWm24pf0B+Yfbt2P1C7Qcl1j7NdfNsb7Rv8kYgMkU7Qd0znhWCSmQliuDPEpQlX6cfV+zPnwnhtyVbYsu3aHjPz5j4s15LZqfDl8+A9zbn6TETG0xvECIxsMREemQzcw2KOMGLFyvia1OJyOXbDrANvZpn5cnUxpsUod2HACTVAPMzERs9R45G8QUt1Y6lvWvy8entR7Nzpk7bzYYBfrzE1rjB0nqLI052vqh66TKOSRFsEjMCpYRsICMEAGPExgh6oiojt1kjMOqJJCBAp3n9rO207/HSET7qzI/Z+0mYiJmZ2ixmsZWjdlgJm51ZEz28emSIq9i4feyDSYSwWqOKghcfsbVdkmNmtPF9LqutNeIuiQ2Ludt5M5qUo7CK9TG1IiYV7pzLb2Dw5cV/sGLBgSBxuNW1dwzJJX51SlerXkw6uXIfWw6UhygZLQhbseWF2lrrIV5EY3ZbUJcQjuM7Vmx5cXbWpClRsA7ejGJTuZzAz7trZ2rrmYimTCg7J85OxXrxwHaZ/nLG/8Asrr1hRE8ZmZ/aMWDBkTjePzac/5ZXAxYMEE7xqxYahgzx3SBiYwQzuPo1q0rJrSgFsyOQzTTqY0ezVx2Lq45fFI7naxirdlbrBSxWVzV1t08fUL2y2YyDHkbzY4MWqI/MMikKFUPPDfUQIxxGIiPS+1ikxIDBR0/kAuU4GFwotTMREzM7RmbyrGWKwn6gid43/bXmwquXnYulqvZxvdKNi/adXWmLrprBO0Lp0krEQX3GCpQTuACM/tJq1yPnK4khERjYYgY/acLNCxFyjO2sVlq+TVJLjg3SvcywibsIaZdCC4KiWn2LD/1y4AtKlRsAxHo20lXiZ3Llcf9sdkF01DPI/zDbYSmNinzPu7Px+StNZSY+mNy/alEzExE7SuyxRdq14nwUf8AeDSxByyt8IsA6PHgpiCiYmN4VW7LJICnt6vX61BPesFtC03+oGQ2zvXxqEJrqFKQgF+mYwC75zZSfate8fUada6E9wLll07V68loMbmbPkt1C3prIguXqPk3H2qjZlN5s1nWHVE7SNlbRsXlsjsIGWn09hrNGSfZmILXVVw0UQSsuJVKK1iDDjkz9oZgAyRzAiy695duoM6x/TjrZC+6ye2ACsBAI4j+PM5VmNgCGImKubzi92xMvWPWCoXPOsUOPNZ6xPNUAgEZrOoj80AsCnqVsnA2KcrjH5VGQkoTE7fi6uqkddNodVramrGZKIPuL/8AVGiuVR+WRorrnl26YTOrVHJV0e7YRyNZ4vVBj8/8O+u1TYt05kH4bNKyIdtn5drT0y4OHKRhSVqHiEbejrSlTx+4+Nux9/5K1Vkq+0fqbZSrwRfVB2n/AGR2VpqrVPL7z1yHfjvG8zERvPiBekj4CcSX7QwBg8TjeEpFIyITO2rFblPdV9LqptNW7Y2LWXzK6EQlUd65SwzrDYv5gu6+71FQouKsQmbD6q4RBzRbC8f1BQvHComVO9GUqjWw5qQNggARsAwMelrEY64fcsIgmf8ATmH229vqpisfSnlXTAnqZiI3mdo6kvLu2wRWmGwETAjE/P7NrIWsmF8VkOvs7jpmQx+HBQiThiP6XUWKbkK4GiN3VLe38vYjttkRmd5iJn0syMV2bztrpCYkHR/n8RgDAJbBggZ0xiTLeFkvTOkqcz+WwhhHSuMXtLIN0yuhjq5NgASq5lLuYIkh+TSQhaA4BH/E2qx84s1p4PweX/iKSFsQNn0apjS27nBaq6k/YPnUlasMIB/KWmolXmI5F6Sm00phjOC01lJ8jG5NrLcUSe8wtKlfYMR+6zGXDHrha47lvE40afK9kmCV3JdSriJr43851BR1pJ7Ig7JsNhcjKSK/V4xFhMcSw2RjI0hbMx3v6N5lzK5ksa1vCutCU7wodo/aZRmyxTH3YbHLqVFSQxLv6edwg5AO9XiBtzUzyYgDRvrs5v57WhjNHPbhBxKumb1iIKwcxOHwq8Z3ChksP+gZisCM54g/qi08mLoV42YGTuDtctFIISCFwsPj/iimal1N1U8JrXEWQggKOXo6wpMfXPlLe6uD4yOmuWkeRztru2rPhQ9pYQQhAlPIpmBjcp2g7vIuFce4S2MUuStEMSix3+UwMwOksexpSQcFft8vllY1O+0HYOtZxlM8xZHu5Fdd18/dWmycqQpMbLGB9ZiJiYnzC2vxF2Hp37QdW0pj6lMkx6qrbSR1niFfqPEvKB7sqITExgwKCH8OeXNHNBcj7f2tBP8AEM2AT9Sv3XUV6KmOMY/UoI7NeN4+r/jJiCjaY3htV9ZsWqZTBYrKJyNcSGYh+iSozhhDElogA9uYwWiMFxucwMTcJk8awScxTNs8rJyUgsFxxAYGGV1MODOOUxERG0eI/cX7qqNU7LfMYam68+cxkPJ2ELsoNDY5A+u7D3/bGXcV+BiwYPA4ghWpSo2WMDqJmJ3idpfWRYHYwiCq3r2InivexToZWlkB3Qf1/g6xiOzVnbyqZlQTPz+yIxCORzAxYtm8oRU3mencdFOpLJ8s/cmYLAjOYEMnfLMXwBUT7X/jzQUMh9Y5TYxOZ9yXtLkQu5p1dzmeWcVduRV2wnaV0Y35PKWEIiMbDERDbiF+N+RQy2+foHsr/cmYLAmMKBAIPqHI8y8Y2IiIiIjaLVpVRJObMRF2wzKX+Y7zH9F1NLZ5R9DEZPNUth5RbVjc+q4UreuazYmJjeJ3jWcpIuUpFpws6BWgLsNCeH9YjAY3IoiDv1R/3N9Rf7k8UJY2RpZy1+mia4L6YuFPKyXOU4FgbRHbAQEQGAGNh/avtVq8bvaCo/jGL2mfdK2HJY4igRsqkt4mN4neH3jzl/2xu9vSYqoo+FQeKv8AkGVZs8IWXbsYfL+8ia1kZVe9G3djlSRlh+2e7zYZtC0JX9gRE/uuoXvsvTh6s7HTqpoVAQO0DkswYJIaEQbjr3LhQy+8jlalqHisYGP6dm0xDBjtyS1ZyERMrIxmOpck8dq1cN3Wcw1nNsCyUE8hmXBAT/QY1ao5MKBickJTMJUbJ7mSZP0LFcLxWcfO8FAwvprIlP512AgekkzP5tpp6DpXFj9/cZKcRjEbSusuJiIiNojaP2nxq71PQrMJS4J5t6iytmZ9ogUgVrqAZ7nuCKY6myZ15rQqPdJoCZS29JNYePoFEwCyCZxlbff6tpp2q886bzGaVUkwTGTu3/gXWntdC6fnR/xMRlkkMRRtTYCYPbuf1n1JyaRsVyheSw2U9+iRbHC1oQAZmRGIn92ZiAyZzxG1YxyrUWlrhtyxbfZndhePV3ekPyZiDjfaN/n+kxCWfeEFoREBgRjYf6NknCqZSPI8bhitFzeYk1OFrhEdwpPSqyE/prEf3V62NKoy0QyY2Ll7MxJlM1ccKKyxgUqEY/rEQjEkU7Qm0hxSKy3n9xbQ14iAHwBKVpCACNo0/HjO7ETKzXat1i2siRAqyl0zCy5T/UrvOu4Wh83hhNsM1j/rhLQcoHLnkH7zNvIKvbXtLA58Y57cv3hMYoZYreWYzqZDR7V6YS1bAaEMWUGH4bV2rTXLLDIWJ9XUoLZaWnFLqLHXGQqJJTPQ71JZwB2FiYMBkbgUFH9XNALMVaEvEY1xFjhVPx/VffSg5CYkibdtmMSlUgIU32Jhls52ZjFzMEkpWQsyNctiGWhOTkGSLFSMV76nn24iRL9z86BKllJAMDP9USICghnYv45Tr1RZYngVd67CQeqd1/u8wU37I42mPKwVVlTZDInf97ZqKsR5+kkov1o4VrhrCuzMgcunIBJ3brpr744lMeAdUOHfvV1a9h1JPzkAjX8IzBRMPyhCC8Hh0l3Lb5ssG7jK4QtO0DllYu8sjUqVWq+Qz6kikTGBmLlreL95nC+vHJiVVwMndK1WJFpnMxP9XLrlmMtBHziy3QQ/iN6VzsZiMnkaox4KTkclLJ4pQbCnJ8JkXJIC/iqNo+kt5IYHlM7C27WV4k4mQyFUy4wXGZQkj7khEn+CYifmN9CpQTuACM/i3neI28f8DMbxtqOG/wBYwY4+a/tFhWjiv91cKyNVk1BgrGAbSkGqr8mOs1lWV8GRq1UbVPicbj/wAkQ/bMxr3VmB4909iMy+4pL8LD4LI9Y5BGybJx4wwbViL+tm2knFWTH5xgxFbePn8DbLXsirRGWOodLVxDuZCZc5ONx6I2VXWOgAAjiAwMEIl90ROrvTmOtDPAPbsX0in4faYwVdOYhY8ezzm50xjrAj2YmudTF1qyAUQw0oQiPhYxo0JYPEwGRfhlF5QUhL6VlHkwmR/qQJT8RM6iu8vhZzqKVuf9ktRjrs/wC1OoxNyfkYjX8Ht/8AcNOrPROzQkf3VO2dVvKPILMGBBhO4/ufnVapXqLlVYIWGmqW4JWyOQ3aDKs8o+pX/DxEDEREbRUzNZNhGMgZI/6tyqu5WZWbvAEFrEuOvYXJD/Eyn7UFMIyKmnwOJWWrDGPYNKrHN2LxVfGpgFxyb/Vfj6r95kOJNwzhL8ooMYw1mfkgjUYVv+WjqMJ/6najCp/ywtRhqsfJHOoxNOP9MzqMZSj/AG99RRqR8JHUVq0fCgjUAA/AxH45iJjaY3h+Kqt8jHbJ+LtK8jHcGYmJ2mNp/b469Nc+2yfyYmJjePj94QiQyJRuN/GyndqY3V/w/tnose7ps7bqefWUor3Bldr+sxS2RxYMFGS6bqWxk0T2HR01mJntzZCF4rC18YMkM9x//BurIfGzQgtPw3+UHptWwouJrmJTi7bfMj2xTh0B5aUsksdTKNu1EaZhq8/YZBo8K+P0zEoPHXA+VTMEBh9wyP8AWgDn4GZ1Fd5fas51jCtCPZesoH99fxe+7q0eZjbxPz+z/wD1+yUlri4qGSmvhR3g7MxMgAgMAMbD/wA7MRMbTG8HSqM8kodzw9Uvtkg0eFZH6bILR4y4H+3yg1NXOxgQ/grU3WS2CNhViagDEHEsKMfSj4VGhq1h+FBGoWuPgIj/AIO9jRsbsVsLTAgKQOJEv+CWlrS4rGTmnieUc7MSOlrWoeKxgR/8FOpWZ96hnR4ioX28gleGSLORnJiIiAwIxED/AMPcortDv9rJxt2JmO3M6/ht3/7U6nG3Y/2p0VewH3LKNTEx4nx+8Ulri4qGSmth4jYrBbyta1DxWMCP/GRvPzG3/hZLA/uGC0dKof3KHR4mmXwMjo8KqfsYUaLCuj7GDOixVwfgYLRU7Q/Ki1IkP3RMf0VUrTduC52XhXTG7GCEnhrEfYYlo8bcD5XM6NLQ8GBD+JSWuLioZKa2HiNisFvpa1qHisYEf+D/AM7f+OzET4nzoqtY/uUE6LGUi/29tFhq8/aZDo8IX+hsTosRbH7eJaXh7JffIhCsPXHywiZK6yFfprEZ/CddB/esS0eMpF/t8dHhUz9jCHR4V0fYwS1Xwwx9Vgt9LWCx4rGBH8AS2TODGBD+kI8d/Mz+KP8A38f/ANCl93jPd25f8tO+3j5/5n//xABKEAABAwEFBAcFBAgEBgEFAQABAAIRIQMSMUFRECJhcQQygZGhscETIEJS0SNAYvAwM1BygpKi4UOywtIFFFNgcPHiNFSAk/KD/9oACAEBAA0/AP8A8dmiYRxHL/wYDEfoS0jwUnz/APBlHDt98VKOxjz+fD3nn+yAgf8AgyMuaArz2XgfP3A3Du2T6jYMSjgdgxQof/AU1RAPvkNPuPAEbA/6bIHmo2XT5IuP/gJpB9EWjy9+4PTYwwD7geNhBgIASNl0+Sr5nYIpzTgDH/emvuBHMIkT3q6K9imCiNhaYQkHv2XThyQJCBHmror2bDZ+uwO9T7gIRA24q6fJR67C4IAD3hiSjn72p/7nxHYgACiCJ0TXV/PYnAjvTXYc/wD0jiEKAIEH0RaPLa139vRYQnCY5ouJhU8wro8tjmkeex+WmxmZ2SPNXR5bINVepKhRsvDyPvuIHqgK89gguH54e51nDX/t/VHA/orYeOy1Ejn+Z2kU7EKd201/PegQVdHlsunwUR3U2V9xzyRy2U81dHlsKGACMDxTGiidWFeCiipQ7XGuwOlx0G0G6D+eXuXYA91m86PX3GiSj1eX/aoEjmE2QdpwKaZ9D71m4HsREjtVm6vJOEjax5/PhtLa9ydomgAdmwgqT57JPp71PMK6PL3CQo2U81dB8E53l74BPcnuJ9PfeYqhUq0cY5e4zetT5BDYaN5rEnn/ANpneYNjhCY7zVqIPP3nCEwkJ2aGe14DhtNJ2tmDnTY15GyT6bXYDbA81dHlsaPGmwuG2nmrkd4hGT47RUlHMbSI76KJ767GgnuUwI09yxEdqebo7U0AbTRo4lGr3akpoq7IcEBMDFWVTz/7RJgcztszvcQnCR27HxOlFZmU4A7BjB2HDZaCRzx2kxROEjtTmEefvObPlsD/AK7A/wCm0CI12MGHcqeYV0eWyY8TsLp8RsBhEgLdCujyWA7URJRp3oiT2122jq8goACIklOzTRG3BvMp+85Mrd1OwGSBntNG80/edKwaOKfvOPE/om4gZf8AYMxARw9xxgA5rFvMKBI47CIKsjA5bXAjvVm4jY4S3z2B0GMq47AYPntAnuqgI7qIyPec0jz2Ez+e9PMck1wKIlOFR37XxPYqeYV0eWwuPlss2yfz2oNPknOJ9E5yLggAFM0QoE939vVCg22Tacz/AO0DJGvuPOeybzuXuGg22Hi7ZY48TswbzKiT7j6NCisa7MGjUq0qf2ycEcPfOIK6zHenuWdRyzRFeeex2Gmx+49NNYRaPLZbDx/I2MMO5FGo2tgjsRFeyiGiePApjig/Y/PtjYTHlsD479jmD0THSZ0oqeaujyV0+vuU8wro8kGnyRJnY6gA0UKJ70XeoROI19xgnz2hWjqchteakZAIAkBNlxnXZN1vuMqB+LY6jeZRq7mdhqY2M3noYDUoiY02CpKbu2Y22WHE/to4HQqyMRw/Sv37P6bGmQ7YKjmERddzQJiNNlm4FOEpwhWRunlkphwGHbsII8Ewkd9UQYTSR6p3WKDgdjXfny2MN4c9jXA7Cz67IV0ICCe/Y+N7nsp5hXR5K6fJSUagZ7IHmro8lPr7rd0fns2kQO2iiT212Ocbg4BWdG+Wx7sfzzTt0dqiTzPuAwY12WdLManX3MGjirSpPBM3nc9r+sdAmiNlpQDghidT+2xQg4OCio4/oC2HjyjYcNlmbwRxGh9x2+xSBVESnAjvVmY7NlsIPMe4agfnnsa7+2wQfFFo8kIPcUWjy2iD4ogIiPPZdPkqjx2x5TsgeaujyV0+SBKI+uynmro8kBPnsca8co2ASnOTiaDM0jY939vXY7dHagBsApzNEantTN9ytDAhAVJT3c6J9GrFx4lO650ahQbDgMzss6u4nYBTiU4y0RFEMBqVaV5DYeq3Up2A+Uf9gnAe8yrVAkcdlrVnA6bCJa7U7GGHcijgU0ROy1q3nsZvA8k4SihUTwT9088NloCR57InuUR3UV0+SEjx2xPcro8EHRsIIQcRtg+uynmvZ49iunHtV4qPrskKB5JjY8hsaAe6uyI76I171ZN8fydjRPnsLrx2uN53IIJxhvJNEDRCpKG7Z8kyrUMBqVaVPAK0oOA1TBVxWFmDkFgOZT94+mxm9aFBMwZx47GiV8DcgP8AsB9bM7LOojRYO57MgmGCPdZvN7EceYVkZ7CiJCIpzFU3dPZsiYTPHYcUwy3kdhpezVmZ704V5rAnkf77C0jwQJHjsa7+3psInyhEEJpI9UHem0mRCcYHE7DI/Pfsp5q6PJBpgdikqStSi4eRQAQp47Gj0A2EgeKAAUx4nYxseWyzbH579rBcanbo7VEnmdjzvcAmiNllvP5polP6o0aid7iEBAVnVxyJ2ZDUq03ne+2l7j+xD9yGI9w4HL3GbzSsHDjstag6O2vMWmw4bHmL2Q2P3mIiCrMy3i07Dvt2Gh/PI+47ddtcIVmfNDAjFDYHHZe9Tsc2nd/bY15/PgsQU0QSmmK7A8bA/ZTzCujyV0ok+aveoWhRdhsLvU7HGB57HPCAwTjnTXY93589hdAPadjRKeS49qst53PYFaYcBsGA4q0N4oGXDXaRAKxceOyx7i73z+sdkAh+02658tuFoEajbZ1ajiND7lsYdwdsyOhVnRw14rUpwhWfVOrVZm8I0WDhxQq3mm7ruxWZE8k4SEww7i0o1BTTDuSKxB0KaIJQxKOBCG8OxCjuYV6sckap/W9fcm8Pz27H5bCI8xseJHntveg2MdLtdgeNhgeKDR5K6VJ81fGwG8e/+2wuUGBxRJMbC6veNpBAKGJ4lASnOJ2WhjsQEdyeZEaY7HurywQoBssqu0n330aM+axcdTsJAdwQxKBhh2fG/IBZnMlGoGyzJa1vzcf2izDiNpoQhvMPA+5a0OjXbBjy1Ry4o+BVnTmMimGDyTOuNWp4odCmUDtQrPHi1OEq1q3nstaO4HVOEJp3U4QrIx2IinNWZunlltIMJriPXY7eajUcwgADtOPHY9n58tgqSjmg71GwUlOoBrsJkxr7l8eR2AzIQpuiR3qFHqr/ANNjckBQapxmDj7jWz5+6QQAgKjZYjx9yzEN57BgOKtDeKFSUKNbx9wVJWFmNBsfRo04o9Z2qf1nJohOxPyhZnUp1Gjin9YnLhtOE/tRswee01AzRTMDq1HFYxss8Rq1OETstat4HTa/es/omVbqhRw4o4cCrKh4jbabr+ey2Hj7hN5v57dkgU4H3WkFEDvR3T+e1CpTgWyg4xxTXeaIB2Xo8veDh5FELQ7CKDtWmlVenxCmPdAknY1voPeGKgmvBPdKJgcTsfQDPmjV3M7Gbz9j6uOgQTqNbqoqOOwVtHDhkjRreCA3gNdE7qDQbXUY3Up9XH02AyJ12hN6jdR+0jDXt0RqD7jK8wEcRoUyrTrwQo5uh2NrCOIT96z+mzFp0Ks6OnPihihvMKFHDinmHt0OqOBXVtOSOCYagJwDm88UKO5hMIM+COPMKQOxOAPejuuPuET3bXAgIEwDomuCcPNB1dgE91UBHdROoEw3ufvXh6q6PJMx9xhkIGQQgIn3ASB2n+2wU8f7bGugcdkS4bCRenRHdaFEnmapgiz7c9gwGUjZlOEp5lxTRKtMOWxtGDTY6jBxTqvPFRAaclZmv4nbchmSj1G/KPdyGZ5IdSz157DjGXP9mWkNHA+6aEJ1bJx8tgMsdMmDttTT8Ltn+K3Uao4bLWrOB0TKtIxWDhx2Zt1pCNCF1rM6hW2PByNCp3RonCCrIwOIThCaIqrQXmjijiChkEcUKAIVHYhunmNvRXtJHy710XRoCFaNDgeYWk120MciojuoiZH57djgR3prvNB3uYWbMLzjkg2+2BBFQIEZbDkhQBT6lRA7So2OMNnE8gEcCPXZq0g+WwJ7vJBOd5f+1BgcUSSRsJgbWmRP6BxvOPBDDZjRDFMpZjZaYnRuqHidgqSh+qYfPYKkphq75uGzIalfBZ5AcdmRTqud+zn9TROoU6tm702CrToUzHiNdjOs3IhOEopmB+ZuqKtKsJwB2Nqw8QsHDiE74ePuWdW/ReTgmG67a2jwMwUago4DVWZqeCcPcK0GyzaXO5AL/iDHgRW64m+InkrC83o7xiDeukeN4LpRbacbr3Rvcx7pEE7ce4oEFXRXs2DEscHR3LoDXOdlN0f6iO5Wjfs2AQCJvO9PcfjKKAgbC0h0aBwmEQHOaflMRe+YFMZNq28GtjMuea1V37YAE2b3DGG03RqVYOAMCJBnEa0RaY7kRJ7Srp8U6TtAKdJ9w/Dnse6ByCAkqd3iNgqSnG6zkFhtf1zoE0IYDUq0qeA0T6N4cUMXHVWZl51OiGA2HRBZDMlf4bNOPujEoUMfskUMbRVp0KZR49Vi12YKGLtdlnjxanI4omQDlsZVrvRNo9uhQqIQ2W2OgdsYbxjREV557X/DkFaUfzRwKxaSZ7k4QVZmnEKzNU8eBQJLT7jDrjltADnAZtaQXeCNqRb6A2bxh/CrYAWl2okQGnkQuj2bGxNRcY5wk7LQkA+8cSgRPero8l7J1RyTmua4CcCIr6LptpvxEtZN3PLd8VYC0DnDCRu07T+gbvWT9HfQqyfUnNmIb3q0ID7RrSDbOFReccfJWwHtukEbxP4Z6rRqVbEutLUk36C8L3D67XHyqgBttCAOxAAd2y0MdyaIlAw2c8p2aBGhCGA2WpjsTRCshJHHY0K0ryGyywGTnbAbrAcgn9Y/KEMTqdjhIGw9VoxK+FmTUMSmnKhchTiURI2N6z9UNjshgBw/YGfvv640lGoO1nXHzNR9y0MsOQOnuChhN67fmajsOJ0ThLTxyVmbrhy2P3mbHdUa7DgdCrKnYrOvYnDBCjwNE8eBQmvasJzRFOeSYbrp2Wog89gdTgntLT2iFaODrFwkEPjdcNJbQqzaGtGNAICspfZAUEEBtf4XbA8e+cu1CaHmrSzc0ZVIgLo7w68ACTe3S0/yqwbdYDi5x+pVu4hpOYBlx7XbBi5xgIZtbA/qIWRJAnzTsBaCBOl7D3LKLtS01IHWFYTWFjLB2+3cpS91qLpDrr7a1oQMmtBFFa/rbZ2JmpA4bWmaIIuBMaDYwXjzx2MFRkfydoxJMBGoIwI92yF1vPYcTrGxm9aH0QVpQcBqhidTsdRrfVOq92x1GhP8AvhGpRwB+EIVJTT/ADFMGAQ/Vsy2Drv14Ifso0ITq2Tj5bD1DqNlqafhdsjdTPELI6FWdDxG19HcEVaHeHylGoKcEyrXZQnbtoPVHA7WVaURXYaPH12uEFNJg+5aAOHZjsZvBRXmE6s5TjssHC+4YxO67sKtGyRo4UI710hty059T6bRmfTVZPtDdE8hJVn12NYSADrBlfMyv9LoWbcHDm019yzaXOPACSra0c57jQBrf7kqxdAHD/cfBNENaMABssJDbOYLruMcT5Lo4+0t3AOhwy3gSa0xVj+vtwxsyNP7K2G5Zt3myB8Taxhqvhe6SWDAEHNqOGxwa0fxOA8kWXv5je9VYlzmTXO42OQ95tAdjnQPNNBKefAbbQMDMwC4kEwnVsHmchIgaEeKPVGLjyARpNRUZV2EQEauPFDElHAhGjRxT6uOaGA1KtKj8I02ZDUp2A+UbMhqUeo3QLIZlf4bDkNjeu7VDAbBiUKOObkFkMyjkdnmvg1I/Y+LToVZ0dx4ppkHYU3qn5m7AakZga7GdYahHEaHYaEJ36txy4FGhCd+rcfLacUyrDq3ZaGHA4NPvNMGf0BxQJM6bbRpa7k4QgbzDlLdB+IGVYPq7MNdTzhWjQTzwd4pmJ9ArM/Y2WToOHLUqxH2RaLoMVDfoV0SR0uwiBaDUNwyqFZC7aMc0G6T8s5GFZ73sh1mjO6c+Ss6WjAe5w4Hb0lwYAMboq76J4abRv4jh/USrNoBOpzPadgBIHJNMNLj1ZEkovlxivVp4yi8SSKkgV80XAVxa100HdCafZvPA1b4yrtx8/MzdJ7Y2OtGgdxKbZMmcoaEXNAPA3jHvkGFJJCeQPVACeaInhsrvGnxNiSmANvfiBLrp5gq1DSGDCzvGgPGMgnzaWg0Lsuwe4+pOUBBWWE5lDEqzO6PmOwVJTTFm07AhRjeSGA1K/w2cNTsHXfrwQ2DEpp7XFDAI9VoxKODcm7Mgsm5BZDM8kRuj9iMOHDaz+oaLMaHayrHeibR7fctfA7T4Ifq3fMEKtPFNMAnEpwkHLlsszXknCURTmrPdcOSmH6o1B2vxGW2zBc48AmuLOidEYYvk03tU4A2TCSHj94uouDgfX3RvNJzB2A3bQjO7Ud4kLpFnu/xCnaFYWhocg7+4KsCTbkYAg7/AHYDimANaBgAMNnSWEWwmAboNT3BdIBNvccC0F0zN3jBXJx/0p7Je26bsmb1KaSnUc04tcMRs/4cC8kYC4ATP8XkrJ0WTcpIIA/hG1sANNBLjFV0mWtsx+EkXp7wjNGC7XvKg3awJ4wVauve0AklwBcZdjBFQmvYedY9U20cBSKQD67HWh8v7pli6OBDU61M9jW/oW4EIVRN0cBss3EmcCQWuunnCtWh901gmqsTdZNQHA3WkTwbscYAHvHEpu0YAIVJTcG6lDAJh3Ga7PjfkAs+J2sNGaoUAR7m80cXHLl7hpAwCybkNoEuOQ2BA7p1++nAZ7CIJ2GgnY79a31RqCsHtzHFHApvXGoR2DJZHQpmB1G0Va7QplHDXjsZVpGPJEVRoUa2ew0tAPNPHmgTEae9auazs6x8l0RoY0zSYG851BkjVzRLz/M4+i+GWiBzqmVuNmf/ANZkFYNFbryMccDw2zDuX5Oy0EA6HI9hXR3ktac25xyPmrVl6yAwvHeb2VK6SZBONwYd5qmm6bZwLbNrgYIrE96xLLMmAP4bo8U4br2kQe3eTIvPe92Zj4YTwHNJLiCDUGpV4igEBpLmlo4Ik3Zwv0iP3mpwLbEavOfZiv8AiZD7XG8GTus7Sapol5HxOOJ22rr5aP5WjtJVoW2RdButgC843e9MBc4nAAJj7rXO+IRKeGUiKG8x3grGyeB0YmL7mG6DORmqYb7rEPIcdXXRogSx4GF4ZjvXtHGeQCcGs3SJ3iJEck8G0MZ3jI8NjgXNY0SSPJCCcJaOLnegTus289w9ESRuXmBhy+LNUDbcCn8Y9UcDtKNTsfaNBdpAJVnYNcOd2neV0hxeTnHVHlsscOfuPoNvxn5R7jOs7VDAJ1GN4rMr436BZnMoYkrCduDrTII9ZxxKd1WhEzyCGazecSs3HEoYkoCbxw2FfC3UrFtmtPvTBvNyLUctDsZlqEKOGh2/CdCmdU6hHEJ5mzdpwRxCeaH5Tsed4fKUagpnWHBFNyGewYlHApmI1CzGh9xmmewiCm9Q6jbQ2jSYDW/Uq0aHAY9YTsZgM3E4AJhutYDjHwt9Sm4NaI2kw1rauJHBWO9a2QaQ57Zi+BqFY0eNRk767BUTtvez6RGYIif5ZVh+stQKuANTPHAd6aAGgZAKwcAw5uFQXd4Vq1zCRiLwhWYi8aTJn1TDv2RlhJacsJVkfZgEEDdpScQraXOOQFoM/wCJNoy1aZa6PhcWrojYYC01dMhz9ezFdGIFkSCLxHV/3bWNLjFTAEro8ezbofgb5lPN11vaGrQaTQUVpe9ob14Q6haKzCbJipqeaY0BgANXAmhu81060c61tThZMo4C6ZiSSukWbrMWdA4PdSW0FAnF1oS6gice4Lo5M2x+IuiY7kwQ95Ju0+IkzCpAPVZTqt4Jgkn0HFWYkt0aOq2R8xr3ptQ1r7xnlN4IYVAd/pKtLN11rxu0F6bxjNWLyLO1GjhIDtRKZLbEOyc34QdCMPeNowVyzon2di08oafRMs2g84REToiZJ9zBo0G13WO13WchsiJRzRq48djcBmUMAhUlYOfm7lsynBOxcfRGl0eqysxgEMlkMysWszPNDIbZ3RkPvr+u0ZFGo2O/WtHmjUFASBqhRzdEyrD6JtHt4o4HQpvVPzBFD9W7UI0ITq2btlpj+E7SjVr9Oey06w0KcJHan0Og91tCeGxohg1eeqO9dIex9o51XNk0bwpim2TJ/lGww5/N015wEwQ0DYBeLLwkDWFMSwCOy8QnEvsrJ+pcXOocYTi0Wtkw4FxukcJBwVod8fDfPWaeDlaNDhONcjtbi5xgeKaftbdwoYNDy0GKxe84udqVBPSLsi0dGQOhRLWg0ykxrmhiSmOlzHRDxpJVoIujBoMCABieSvXejiIMDGSrAXYcLzXQLgx1q5OJvC6Sb/HmETLIiQMDAb6lNklxxcTmdtoxzRzcCEwm0s7MiQS2GvFKyMVNXFsNcMq6804Xdx5eY4NBKB+1t3Q0NpSBWU3FxTGkWTbTG1/FdLSrKpaXXqDIuwA5KzIBIobSMgMhwT6OjIYmTyqUYNo/N7tTssjNoRg5404BdKN0MibQzIkGKUnNPLHGojfF4XYzCc0B7KhrXZk/2RBLSDLTAkgymvaO4vCsC2+4Y3Z3TzBREPbo8Yj3XWjQPEpxsgeEMKAA2Whhk59y/fb9Ufxj6rCQ9uPeuBn3tXEAeKPUukEvOjdU8xZml58ZyfRObLicandmOHuhCrW5uQwHuHE4nZnoOa/pHLY7AIiLgwG3ijhNPvxxCed13ynTYeu3U7G9YfMFmNCmVYfRNo5qbVp9E2jxxXwnQqzz4IodRxzCs8uCOI47dDXYU3Cfe6CfaWsVDrQZdmHerS1aB2AlWTBeOQDQrKr7SSN0ZwGmO1OeG2RNbgg0aeVNliXXWiohrroAFO9BwaQ4iodSkAK2ZeNoQJLnVGOitLQNdbMN7ednkplr3CLvISa8UWEtJwDhVp71Ym812V12XftspLSBIrGIpoi6LOxaC0Occ+torQl7Q8yWtOAr3ogieaeQ12QiaWg7FaiQ5p7nAhDqhxLSOyHJpM2YkgjS8YjsCdFmRZ4MHyCMynb9qfxHLsTxB1ByI5J7qOblPy6HgUdMRzGXuA3nsbRwcPibrOYXxWtiTZuJ4twQO620cCO26QmGXOtPiANQ3q+SaQQ9zdwHVt4VIRM3Xk/3QMuAdQxnElOoxgy4wukQXzi0fLz12WkttXNxb+EHzVq+/avzcWtju3kGMuWjjduvih3o1VmZZddekH5Wqxn2d8SL10XS7zVkBSIkRdJ4Y0TnMMDASXmArRpa7kRCs3l7Ty3HeQ91lo0nxb6oeyd/Td9U9jXDtE7PZY4hvWdqiYZbWRBZ5fnRalxbPO+1DG7aMd6KKXodX+G6hAc1z57LjyfBAlto3EBw+qz29Cs3mzAE3nN4fiPguiS67q59Qxv7xCaY6PYDCGmgj5R47chmUeoM+1BDqWevE7dG5bdSs3HrO5LM5n3dRs49Uch+wChWzdqPcP61nqjUFDr8dhpaN1CIkbW1afRZrQU2RTmgSDlhsOG1glzjgAE9xbZ2hIg6SMp2MAug4SSG+qtbTruHWaBSO0lG0vvbwkR5FPaWzzEJxJsHWnUc+LsV1GCNLJ7QAMKCGUjiEMRoibzhUAu1BGE5phoxpknuAA5qxEWTm/CKCK8kzea60IAbFZpCsIuWgxcZidIOStMf3QrRr2jLO+PAbSJZZCrnfRMrY2JIgjEdms48k0TDaiBo7A7AYsrX4pPw8QmTDCJbBrIIqE4fZlpLgT/LgnENcbOA8zX5iQrN14vOJBoaeaG7aM+Vw9CrNhc2a1ATxDrpIa4Z0IPmpo2zLjdGQLqINHtCNezYwS5xoAAmmDbDruA4/RMbD3OAc58CLx1KZEENAdjrCMx7QlwxylDJEwKSom8rM/YzgXjD+Xz2W0w84NA041VqftHuaTA0BEqyJa7dcG3XZyeITWtBPyWrBmOKsY9iCd1oGnoQnMO9cF9mYqwAqzY5rCWhoeQ0wZGKfaxOe60fXZ0oC9kJfLT/AFCfdaGuHY4JllP/AOp1fBqY32Z5s3djNxs1yDP9KOMVB7CuRs/8kBZXXBzcMpBKy3R9VaxeJAAF4wKeKFTmSTmTtYJc44AK0vBrngAuJcCI5VTrUOtSwR9qd2Kc6pg3nauNTHCdowacAshmV8FnltdRz56oR6zjidhwYPVZM+EIICS4YbRiV4uQw+/uoJ2EUKbVpOYVnVp9Fg4aHaVaGWu+U6bSIITv1Z0nYDv6wjXYASBqhQj3AQWnis0wEuccAArF26MDaxn+cFYAOb7P42jzOcqz3LYfiGfare0A7G1+is2hrRwAhdGYWNLjdF6IivFx2Wf6u07cCmNLrO0O+5rQK1EHDVdJM2pMzTJDFpxCI3SfECcE2ntHWbyTGt2iNCw7sj9xtT2rF5FXOdx0QcJjENwVm433NiYi6aDRPMYVBGTtNkQ67BBAwxVmC50w0QNSmlzLJtcT9GqNyzHWeeAR/VWZwa3JXXeSsi5pDiAcb2BxxTLQOIaDEQRjEZp4mDxXRQ5/smVNoPkTpY+wtCA6tKaq3O7acPm5jMJwkEYEHPa+HW50HWAPIV7lYWZdGE3ROKa4tLQZ0Mp8dhkUVwbBGU5hdJcC8gjdumDemqsxDRs0cAR4oVcbjZJ0FMVaENsuj2dGwMCQKTqU9sPu1DmE9ZvKERIye0YcHBfic4jzVy42yYRQEhpwoKHBPl/8xkeGwOME4brmuFO3YTdNCWtOEUxKsny9ljIvNJaGy4HIyulWdmTaz8Tt3trirRjg3nGCs3Oa9p0fX1XRrQvbZmQ13w3hFdFdIFq3eI4ks9QnEQYpRcDOzUUWjqpkWdm51BedQR/C0o4OaZB7Rt6MZ6S8YOcMvQd6dDAGClmDpx8sU/8AWWmn4W+78LdPeHWtMhyR6zziUKlDrHMoI0E7NDs1yCcOqMB98OmSOBRz0Teo75m7GVY70TaPbxTqWjfVGoKcMUzB+UI+CHUd8wQImdE4ShVp0KYYBOfuvo8aJokuOAGq0Y0n0WTXAtJ5XgNvRyD0h7TR50HkO9FoaGtpAGEJkP8AZybrmumlcFaAl3yhxdLQOVVZWsH+IT/pTwHNcMwVDrWzBjd+Ig6poc2eDXEDwVtu2QxjV3YrVzZYSIaCCSDmSmkm7nBzQWYAp5rV30C+UUHhsaCHUx1AVrvWQObohze0LpkGz+VriadxptcA5zgOu6cOxNAHtrQy1up6oE/mFavDC53VwJMBNAACcIJGK44dwXIbOlNLTzOHi1Gri0S1x1IpVNMtDnuidYIRdLGtktYOBOu0uINvZmakyngue4UN1omCdNUHEEailFcvci0hycwEcRsLTA5Jlo5vk712sqT4Lowd7GyFC9wGGdde5PMSDF0fLF05pjSWOB3hEXoJEg8FNLwk97S1OEOi8ZGnWXSLTetDV7o3anm5MaGjkBGyQZieu4N9E/7OyIyc7PsxXSJc5x612d0eqsRvB4Ba9p4dq6Oy5YdHjdLwDcDdMVaEus4F03DhI/NE+85vY6R/S5dKHs7Q6GjT6FWTC4DUgUCBtCy4269zRkC2DjRO/U2Tjff24IHqtcD4PhZG46B21CdhqCnWntKAEYBsySDKcT7Rr63WfDP4tlvQQf1YPxH88VbQXkbxvOzj/KFazekzcnHtOewiW2TauI10CtBPsid8RiCPcdgNjjE6LFziUOs/N3JDbwWTcym1DczsyGa/qcvPZp97OITqwTVp2MqxybR7eOxvXb8wThUJ5F3hO04DNNq13FNo8arGvuO3geO04HQp7CwngaeCtaWds8S0QaOHA5rFpb1Dzb9E2Ay2cQaHAOIy496tB7OzPF+fdKth7V51vYdw2PFxxOGJH+paCiawuBzDh1Y7U9pFnZEk1nrAYBWjfZtHF9FYkm0BEddxIhdDaHOGUje8yF0FzbFo1LiQT3jyTmsNpZjK8BXkUYDycQT6LVagym1JQMHmiZtG2ZEzmJ5qwPtWNsgR9kTImcSMUwFzYH+I0Q5vJysN20vYkfCe73HOdTOZYPesLQE94PonAEEZg+41pJPISn2pkPF4Ua3VAus7Foo1oIM+EIPPkFaWbgR2YpgcGniHGiFCEUy0Dv5hH+nbYGbZ7cyOsezAcVaObZw0wbNp45EqAHviC4gYlPZGG650FpFM4hOJLWESe6RdCOJDiS0cN2R2LoUudaPOL4/3FWYJa53xBpg0yVrFwN612d48KKxN4OdvENYb8SrAl9z5hFY4qzaGNcHRMUEtjRdIkNsyCDBMkwagUomuPsLMkgTS8TwTmglhxbIw7E4hlrGcY97VZ/as/hFe8Jz22NrWst3p7Q1ezDu1296qxN60ZMOAkG80DPLYyH2jx1XCd6TypC4hWdu665zWzDssOCNSGgDyVpRlm2tyc3LplSTWCTXsr2lWhvbx6k+sd2xgLnHQCqcbtlZnCOqEXTZNAyVm77UvGF7ADu2k0Jy2BDqszdxKGAHuZuyCOZwHJDElDG1Pos3O2xScEcXH0+8uoB7mp2NyycNEKObmDstDUfKVkUBJOyzy1CIqNCm9aMwh1hoUagfoBVjx1mngujktsyfiY3KeHkrNpdYlsXrw+HkVZmWWdtIqNBE5qzbdJbhTDHhsYXEPyvbtPBWjA5zeJCc9jTymfRCzYB/KEaua4SKVRdDAMABedA5AhWhIszoXGfNwVt9uc6NII8GprTZuBr1TdjuRqy0aIgjhogLvtBBBB/ESrYVAMjlIzCtMWxJOcBEfaC6ceEOVo8h9q4xQDANwUXY4RCs3X7Pl1fEXV03LKXn/AHj3LOzDu2XO+iGZWjd7yRwddIHfVauIJ9Voxv8A/Ka0uax3VJAkDFNeWkPPUAjdbjSPccy4I1fu+qLHWkcHS4eCNo7/ACtQcPJFpHgr5unQmEOuNRrscCcMCx0V/m2WwLLJudaF3YuliYiSxs0nl5rpBbb2xdN4Sb0Gc4bsY03DjvGjceJVpL5IAN0ndw4KyaXkawJhf8QtItH13LPGZ43jPBMYLN17Nzmtc6I/EulOu2bLWTcs43t3U4RkrWri6pAxujgNlsYs3DrADrGeSuXnudQwa73ILozhdBzDeoIOuJ2AX7M/ibXxwVgbjpxu/D4K0cIAPVcQaR2I2TANKALo5uPIlsxgD9VZwGWhAF41wLaFOcGhrceJ7ExhfzpRWloSXOMCGgVQpadKdRoH4Z/Oi6QD9o6pk/EZwE96fvMafgnXjwy2DEldJIvEEQLPieKshetHA0MCSmEMsLMaj6JzmEf1n3D1WjEodSzy7dgxJQ6z9n9TtmTRivhsx6oYAbRiSjs1NEfugqSjgdjeqfRMo4HPjtxB4r/DdkRpsHXb8wR8CjiE79U4+SOIVpPYPcNTCYanhsNLOzmrj9Fi1rJYD3b3ehQl2P8AOPVN69mes0/RHqsFXOPAJpAa9+D+XLYyGktE7wNLw0Mwsx7gghsExPEClE1t4PaJvAcFQwYJABkOGvFWbQwOOJuiKq0Y5gdjF4RKbZ2l2gEvI3SDjUK1eC7jLi4eACNiwEagtEqxtC4d9w/5drhBaaggqxtetFS0yB5bLF7XDt3T5pzG3uYEHDiultFm7t3PChVhaY6TUeLVatDo0kbbMXC59A0NAafFCvsr0D+Vu8Vk6zswJ/idvL5rV17wbC0smDzNVwcW+qz+0KtRes5Obajvb7lu4Odym63xr2IWdxuXw3QrIl0Y9ZuH9Ke6QTgaYSoPVBKF0ycpkJtBd3yRhW7oiT9taDdbyyHaV0o/aluG+6HN44oVJK6AAW53oMMA/edVNd9m3K/H+kLozTx6rbnm7Z0m2a3sCAgDknQW1gGDN13BOaZjAZhjVaWwYxxHUJ33Fuh4pwZeGbnEsvHt29ExIwN0/wCp3gukkBwGN0mI/iKAm0cPiecTt6Q0Xmj4A86cHDuThLD+JtWpriLG9xxZzlWji60shjE3i0jMcqr4gS6P8kpnUsYgkAzF04cSVakF7W5N+FtMyVZmfYsMl5xLjln/AGR3WsZUNc7Nx+I8E+oa6tw6nU+SAkk0ACJj2kS3+FufNRIsm75p+FpDQrGmGLjgFaAkDOJhdFYS1mrjJqrZ7nTqBujy2nIZc0e5u06L4WDHtXwsyahiSsDaH0RxecUMypq9wpsyY2pQqLIeqHcs3ZBaCgCFB90IghPOPyna3xGiFHN0OwGGnVDquzBQ6p+YbLTrgZHVHAhZHQpn9Q2OBE80asJ92wcW2YOAAJDR67P8R5qxo+qpfYPiqCAQ3I6IkBlk+WtJ0JyjRNENaBAA5bLR92/EwIJpNE8CtauImgEp4lrhUEFWjwy/Q3RBOfJWjAXR82BTg0F1C51IqHHHkg0AsxA1Csbot7NogEYTTXAq0Ej6dmyyY1s8XOs2ehTfZE9rTj3o2TK/whdMENOUuj/U33On2d08HN//AJ8dj2OgfiAlvimPJuk1uujBWRNoBldkerVZMvBodMkbwIwzarBxEHJrqt9dtq8+0LSQ50kuinND4nVPvWbgCfH6p4DmngRI29GvBpOAjcbHiVBNwSYisE4SiHktGALXx6pjA6LOG1DWcNSubf8AatJaP9KFQXku8DRDAAQPBWVsBPPe/wBK9mY5HreCtrS+9zaueAN1sRkaqwsi+0A+YC85W74nlvO8XbLCzfbEcTQeW1rSe4J3SBd4nd9EQwmsYmz2QGs5uMT2J82jzAwGHgmm7ZE/N/8AFu1olxOAAVkQA35o6rfU7GjrRR8YXowjVCA0kG0bHBza964h+WKeboeAQR/G5CTJqAT8s1c5YO6Q6WkjMSPIL4rZwrP4dNjv17vGDwAqUTL7QgAun02P6zSBB5oCAw0EaBPcGXODiC0KyY1s6kCp2uxKf1bNsTGpnAK0EgHEQYI2P6vDinVc5HqtGJXw2Q9UO5Zu+ELT4R2LJox7kc/iKzccStBh3rKzbghkPu7qJogTt+NnzBZjMFDAjHYOq4YgodV2TgihQbGVEZ8EOs3Y3qlDE+453tmcRJPk/wAF/i2g+Eac0es4Vrmf7oia1azlx4p1faNmC78TcE0xY2xwM4AnQ5Hs2WUWg/hNfCVZQBJirNw94TAS4TMXiTCdiOOoKswGtGgCcbvsDgKZVwpoiQO8p4LXDUEQVYOL7I/hmD6FCjG/M84BWhv2rcwGm+D21QYLjZxNmbseCsq2U0dE7zY4KweADwd/cBWjQ4fxCdvRbQOb+7n4gJwl1k2sH95GhfF495oFaWjZIwBvBPa1oLBMEaoWV03c3lrXx30VsWizBzDb0xwrss2Od3BOeT5D34lvMVXR5oc2TQ9kxsa0kZ1ATXSdTTdHaSnWjjBgE6u3qEEkhWgu3Ab01DrxITQWTyc1o/ypuLoJx5LW4+P8qPVs2MdJOlQE5oJYcWkjAqyc1/jd9VaWDSTwcBe8JTnh3SLYTLf7DGitiLPsNT4BBgLv3nbzvE7Oj2DbOeLiHfXb7J47xCtOkGDymqu2U6RLI9dltaBxGfyN8ynxZsbEc89Ai2+7m83vXb0kgvjJk59yYKnU5k89ntAG+zMTSd5Or7N8h8ZyGtBpsFSVYi7ZtMgY7opHMptA1ogDsGyzFBq40aO9dKMtdndklx7SrNt72mRoDh27DgRUbAwPziQyju/3GAlzjkArQ+y6KyY3qVp8jfFXZ9k5wvS8lwEduzVZu+EI4uOXJHBgxWVk31QyCjdnVHWoCPVaM1/0249q1z2DMqaTnxWDWDzP3r42ZELMZg7G4hCrXZgpuB+Ya7DALcgjUFOxPv8AR2m+RS9ZkQe4K26zxiSfz2Jj7tq/EuzGy0M2YJJMk4kHDsVnZm0s4FC0VuE8cQujkNcTi4Hqk8aJ4LXDgVYuvNbpUtf5D3OjwTaTUuIqPFWZDieOQ2Wgo+YMACGFo5Loz4Acd5syMcaGV0yxcy64yQHA4/xNXR7QtrW6HQbxHCU2j32dfaOEXXBuqcwEDK+N6O9WIIY4CZAwaeK/FDQO4lHNo/1OWklx+i1dXwwQyFAm2kzyLTsEXgDAdd+YJohrRQADZbXdzO5NT4JuDudahHAj3PlbUo4F1T3BTi1uA45DtVpR7g7eIx/w/qmCDaO6zqzXHY90ua6bzQTUNpB4JghoGiaCT2K1eJPO84+aNCDUIOaHF1m0w00pTVPYHOc1oBM1yA2ezJ7qo2d1wNQYJaU7EjHvNU1ptJ1JN3whCl4gOB9QnUF10H1Vs8XTExdLt3uWhMeey5FOJAQt3V0m8PRezs6aVZs6MHCzGUsIYPOU60NP5R6oCAOGxgLnHQCpVs4tsQcmfmm299naMbfBblN6ieJBgggOGhw2dJNxoGN34vojvWp1ece7Da43ngTG9Rs8qlWbQ1o4AQmXmCya0GW/Acucp8CytTi4YzHYgS4uiMdBVOhrHPcG4mDdnNOLQHMcTLThD5qnNqdYJAPbtaZt7UYGMewZaldHsy51oAYDp3nPIzpKs3Av6QZxFRE1JRwaMSsrMYnmgsDanDsRxecT7owOaz2aZDmsm/CPvDRJhYEHEKQZQFSm4hf4jMiFmMwUDLm6oiSNE2rHJvWb6o4ptB7rBLnHIIugWkyY1LY9U8AiuTqidO1OsnkEGQZGKc4EE0kGR4rpQMmSBe+HkAU4Oc28d2/GDZrAVsLtn0RhkMrILooF0gSQf8NuMcE1xJdJALXON0Q2dEKmDLuzB3mng/8ANdHIIh0w4E4V81aAOaeaYYeGkEg8YVm0uPGMu1dKeXeKxcdSrUFzC4yCesC2cJANNltZ341cbrvUplnMnAgB5nvK6SXBzW4PY4ERX4myhFo12dd0jYWue0gmA4Vo3CqkyYE47B8LalGS5z5m6McFoKBMtmyc4IP0Rw9xrboe2CImagoGBaNy55t8lX7Nxh3YcCh8RBHlRHmfojS6IvH+FqtXAMtHUJy7AmsJBJLWBxoIAgmpzXSN9xzu/CO6vvNs3RziB4q2tHOHIQz02Wri537rP7leyaO4RsNk+n8JTS9v9ROxrm3HEVqagHkgwAmQHTCYTBNSU8bsUFTx5IcAf8tV+Eub6K0gOLqmhnGAi8WjOEvjyTbwDjjAKIQv73/+is7Qgu0vD+ytGB3eJ2WkCpikiV0aLO1cd1st60ZzKtGhw7U8FpIoaiKISRZkb0nDHDv29Bbjleb/APM+HuWYcGzl8DfCV0r7Rto+oaTUE+LUWhoL4o0ScXDimC8+2aPs5+XYCblkwndaa78Kx6tkyKDCAASZOpTAGtaMgEMSv8XpBkS3PkPNOq55j2j3Ybo8lZGS0EmTM4mrneCb2kk4klZTgEcGjFZWQ9UMBsynBHKd0bBkEeqM447cCR96cZIy2ipGqFHN0K/xGahZjMFN0RTeq4Z8Ezrae5bfq2ATE0vEeScJNmS66DpmPBAgvtQA1rozLpg8gmNHsi4DrCKzqYxQaTAf1HgUa2OtXFD2lnZTo5uHeV0EuF1zYLmhxfIBGUpo+0s/VvBWUhpiRB4SE3edb2mDeLZwVpuPtAMRo3hqVab1qRhOTRyXRzD7wiTJG6MTgumyH2tSakzdd8LhjCt2n2NsKtbf6to0/Kc1ZSX2nwuEYNdg6UHzbOGEjLjCgDkNNnRCHEjNgM15fVWgBHDUHkgxt21bQYEGXGiNGWsASTUA3aGciEa3XCRKtIvEEnDmTsNCrB95oAq5pzr2LV1VMh1rDrvIYIB8Dm5p2MLXjscPROs2zziD7toxze0iisrQtAd1SIBGFQogEFrx41VnAeaMxnQcFjcEhs5ycSrOzcQ0CAAL0QOxdItWta3WK+cJjQ0cgI962eBGd1u8SO2E1gLhxdvHz2dD6O81wm7Pm4Jt5p7HHYWkd4QtHjy2BocOwgq0s2OcQTEkCUMCDdPjKZRpeL8dtUPlMHxWor9EXCjhFE8AESAN1rQJ710Joc5rDJLWi686xWVY/ZvnE3RQ9ytGOc3iDdciwlreIrTig4t/5dwLeqSYIiocComzeTIfS92Uw2WjXQdCXXh4lWT3BrbpJumvZVaPBZ4uACZ+u6Y4Ua3MicPNNlkuJ+0dxmcFZtLo1OQ710m0Jvatb/8AKdjcXOMBSRetMDpAbWq6SGn2gq0E1BnSsFCrXA+TmrUOdP8AmQk456klO3TatF4T+DXmrUfaPJmATMD12mlu9uHEXtBmnwYiZc7N13wCOFiDQDQlvkE2jWtEAIZlZ2h9EcXnFYcAuGHvipK+b4nLzRyH3c4CanYR19k14BHEaFDrtycFmNCv8Rmo1R6zTkj1mHYcdlubrTm1o6xCNT1i0HP4pVwWTXPrJaILgAJkhWhAayybeNdZhXTcLsA6KSrMlptLHVuNcPJWryGttBUA5kNpRNENaBAA2Wn2j7MUFm0f4kjCuSGBL2PB/wD2VU1s7140/C3dRgk/GaZuzWYzHNOcB0hgJBLpx/iwXSGzbWEkub+NvFvhmmk/8pandfHySD3BAkF73AugG6boAEK0eGe1iXFxzOgGx9leFK3qmZ7E8FrhqDQqxd7SyOrXRPodnRnA3s7rjHnCtWNfH7wn3OlD/l7Tg74T5bekWdP5R/s2Os3jwKY57f6p9feLvaM5A/RwTQT3K0tT3AD67DYOgfzqya60I47x/wBPvt3ncA418G7bQlg5OL0y1cI5hp2EFWdrI/iA+mw2bo5gSmtufyEt9E0wQXtoe9aMN89zZTxu32w6JjBwX4T9VbWl3eGhb9VYSHAiQQ4io40TWuZZ36Xg9l29hUVT3XgTTdYKuVgXMLmYOp1XTpKwAGZU7tnkD9VZtdcY2jpg3d7KqvuIa6himXNPJFm7JwLpp2AK1m1Nlab10ONS0EappvF9mA2gxBVA5zcS29DyeKspZaA/P1ie2VaOY0d970Qs2k8yJPmsGNGLnaBY2Vg2jQE2eqBdEITdd8pKmA8zZ3h80Amic2WuPs5u4SXH1qrNs3Gm+6SbrRSGgTonEm1tcHxkBTJAw4QQQeIOzpFDBqGDHvwXSA02gFbriM+WQ7VayXOxuziAddTtbghgBsJk7AJJOAAVk4A2rTdc9xmK6UwTm1ccTBIB7Rsz0HNO+EYBaLJoxR/mIWbjj93HVcEMxosjoUf1b8o0RxTsG6bHfrGDLiEcEZhuQnaKknABN61o2AOwHFWG88vA1vGYkVuwmu3oh5ujG6Mymi7ZdHILRzjIJ1C6pPeZ2Wzo6RZfDP8AfLirYB14NDSZ1jbaWvs7Mn5GfkIuue1EXb+Ed+wtHmUMwrUXSRgZ8inm9Y2hwrgeTvNWRJsyGihNZCtT7PpMZSInu8lZRasI/Dj/AEkpzAH/ALwo7xVbK0OTQZr3OPdstj7C2nCDh+eGz2Tj2gT6JoLf5XEe5Yxa2cYyzHwlPaL/AAcKOHfsJNm93CYM9j9jhB5FWNoXN5dU+Q97pTBZv4Sbn0KunyQtHT3DZYWFO2f9y6PY3Tzp6u95uLWbx5JpHspxAaIWhNx3+b0Qad82kxxiFbPLxObYDR5KztqDgS4f6drbQHxeNhaR4Jto8Dz9U4kuqQCTWYBhWLrtmHAOukuMRPAbQXny2DC8AY704ht58ABppTirQh0Bt0B0bx7U43mzoMV8x9FBuzhOUrqltmTAikYJg9ox4hwuGPiHNWbLrgKgjqimquxumd18t705rmMmLz32mJjhKt3X2t/CN0HtTXkvBIBwgGvamgADknEPdGLNI0OvBdIN0WIG8HEVu8k7ft3j5jg3k3Zo8TGdCj8AJu+cp4uuIkkt03iU6jnEST2rpMMHNx3T2GmzoIiyBFCZLbL+reXSJc1zsQ12fN3vNMOtD6I1LjqrSDJaDN43Whs8Va2gsy8kXhJgs3aYqydPSLQfFaRLz2BMAa1oyA2OMnmuKztHYdiOLj6ffMjmCvgfk4KfBEVCOStD/KdjcY2FhHfQ+Cktt3RvTMgE6RCtgQ51mA0y4RKs3h3fLT/lVmbr7uRTmwx0xXnxV5zom8WgnCU9puz8wq3xXRCX72IacRHB3mvmgDwJUbrLSkng6oQtHXhnWF0b7V0UOALrvGZK6tqBk8Y9+KqPJDFxwCzecVZk71mLzbpxbTFdGJs3g4x8Mz3ditBAOjvhPYV0UmxtGnNuA+isLQus5zadO6UResz+JtQrA+ytBnTA9oVjFq3+HHwlPY0uP4o3vFXHXuUJlo8Dwd6+64+26MDocQPzlssftWfw9b+mU0Bj/wB5tD347Ol2YbORMfVo96ytRHIif9KcJHIr2pcGmcIinctQx0eS6RZSx2RFz/4q1LjZueQAd8mJK+cmnYuJu/5oRwuuB8tlq0l7m0LjMRPBH4pp4I/hC1afrKeY9m45BPa0uDzIaY6rRkFavk9u8PNcv7rUiF0iXBv4r0x47IK9s7ybstLd3OABtr57WG9auMwHRPg1WTYL3mSQMyU37OyBEE8fFCpKsiA0uqHSKeScZcTOKtG3SBk0xhxBCtWkODSA4AjGDgsXNEOtHHK9GCNGBom63n5lOIvWb6j2f4YkT3bHbtjZ5ucfoukGSMXEmtxvquij7Ng6rXHqgD81TjJ9yIB4miebwmsBXmkjhflNs3RzIgeK6fbyDqyz3B/U5WLQwhgkC6IgmRVDMwKeKGNQiJ+0EDleEhGoIqCPc6IGm0cMAWVu95VuCLJhqL2bwMuK6TvGcWtJnvOOwYkrAvPVC44Ds2nBjalfLw+7j4TmNjes0YlNo4LI5godV3zDYcXJwQ/Vu1Cwc3miJTS0uJkA3jvGfDgrUl1o8Q8iKgSZmncm3rGgi85zd2najYuDu1zV0t/2cijGtnnMSrQlzGsN8hsXoc5oicdli65aRk7SVattach7bzarP9Za3ReJkneLiE3rW3RaO7bpcD2rpFpdZu3XNacL1ccV7O9WPtJAMO7yul9WTQT/ALXeCaZpnwQG7ZNq49n1RxH+I8eGPdzRq44uceJX/ERdJyFoP7+ezp4uWgyviBPfC6SA0PJIAe2lSOYWBeIv00LBPiulMAvuzccCf4h4r2b73K6VZEfZWxaOt8l8R4o2Ti23aW5jGjiIhPPtPaZOvRhw93obgXRmwmqtGhw7dhcbSyH5/CQsd5wb5qwcXX24Nw76hOMBoa2K/wAKGTGgDwhc48lzP1QxN4/7kwS57w67u45uFFZi4WAy7dMNnSmqc4uMC9E5C9giLuOWiDA2xeGtvWcZAwnuvB1oRDQcgAPVSaSYAUYGSZRwLBhHehUNN70ojWrRIOrSWghHQr4pF5o7nT4JxEWbWFpaOLnoO+19oBDQS35ZlR8RunT4oVs0XbRpDgdwYdy0Ex5rifpCyEGnbKbT2ZN9sDKtWp1j7QAnCW3sV7R8+GwWz/Ju0Mmuu/sAkngFaOLGk8TePorb9ZHw2f8AdMIMyBNcU5lwFrgeubmXNPvOedXXiPRFxIaKYnVHIUMjORWVWloJ8WwrIXrc1DcJ11IQEBkCAOGi6M0uvuIIqQY1+JNEucaAALo8t6I0jIUvxqTh/ZO+y/4fYHBs/wCJGox/IVp9rbHi7Adnuk3rQ4wMEyQy0AIEN1gEBWbCYyBIuD1VraDuaCVYNu9Hun4yAXu7Measjda11b9p1jT4lYAssyw3DArFMTXknA2drYW8BjqzQOzpmsnWe6fDFWT96zdp+E5TirQBw+mxg3W/M44BW59oZxLR1Y/eJomECxs/hMGgriBnx2twYKV5oZDaREjJZvOOzhgsx91bVrhiCvhdk7YOuz5gsxoUKtdoUz+oaoo1s3cE2rXJuIOY2Ayx7es08E4RfdFG6ABOffFgBGNYvTgulAAaAOjyeETNkZAcHfhnFfFbWhE/zHBEltr0twhrW53fzKsWlz3ZvdmeZVo5wswMPtJv9woukUIyAJ9Eagqye15/y/6l0iyDb2pII9QrM3rNxwnMHmuji7a28G+K4FzsxwqrTedaWlS12YArnmm0NQTOkCqFLTpVpiODW6/mi6OBaMfgS5gqaaiU5sPAycKOXR3C0ac4wcrNrbYRlk/u9ERdtBo9tCujb7SMbvxD1UFlqNSKV5hdGY95Lm0JbgztQhptYIF0H4n4QFZtDW8miNuO84N80Jo0F2HIJzSHNayhByMq9es22hLXMB1ohldJdB+WjkRde8NLC3lXBFokvtDjH4Vk8tF8TxhHF2QHBYumpWhEei/DggZB0KbhZB8NjHCYXBdqGMTVcY+q1Bgz3rmNk/FhHYuBI80M5DgVyCiCeC7JQwEKaPZprRDUf+1o5xC1a4IY3Re83NCys7UezbwmaHvVqx1nZ3a3i5pgCFfcYdIEH8WCOAa4HyKNu8NDiBNG67bMNa7g7eMeOz2ThPMQhaPD+ePlCaXQCTi50NrwAXR/1kkkkihbPFy6E37a0c4uvPm7A7aK0dNkw5AUJ7SrrndIdm2ktA2WbS6NTkF0txMn5Z9TsdaezYeANfIIEHpBBiYPUnzXRmh1pZNMjdpjTHALo7DaWg/Djd7mhOPh7jRJPALpDxYWAOTQbziP5Y705gszxfadf1VtFpaUiJFG9i9tLycIBbPgrBwsrEQd4E/klWEFwGZccFMW1BAFMdKID9a03XADj9VYAlzHEOAjL4h4J1obNr4hzmQcY0LaL/iADmB1C1zontB2dGN63IwvDrf7V0Qw9zcDkSOcQO9MENaMABsbjHHbww70RAaMAtSuFAv+m3DtWg+6PwOXbtBkFAAE67D+ss/UIplWn0TaOb6oYHMbGVBGJ4IGNJ9zo8m6MXNOMclZC421rdP4vlqhVrDaQN3Gd5xTRu2dk0+sBMdIA8zqUwQEKsPFWHVnEt/sjZuI5gSPJdGdcLhi2N0HvaFaCe3MdhTusDgZ1XS5f0dxycPh9O5PcXXHulsmuAjxTx7SzGV0Gng7Za/b9HHA4gdnkrVjmNDqmSPlbVWR3GHEsdiIzEq1efZzSDiz+YURoQrR4Dm1j2ZqHzkWhF10Xam6KjeBzzTBBfF0EkySIDsynmGktLnSOdPBZOIr4Eo4tswGeLYWbnkklagNB71oDPkm0BDSUeTUcXTIGwY3TErVxPohnJ2uEFauMrkFyC5BfuhcBC5n6rCb0eakGQZkbSJkCQj8Joe5cz9VwNFo5seSM77ajuK4U8kflcpoXETyWraf5ViGky2UPjaXNce0SFJuj2jnDgZKbJshYODrwipqCgSGtBbQZfCnYe2abtOLX+iBvt6S17YcY810YXnEDrNGNK1UXCwhpmeL8TyVk19oQ3kXC8cymkuvalrZjxXS3ODHZl0lo8ZK6e/2tqf3iAye9dHZ2w0eZTrV7mtcYkcJpGSc661jQHPeeW9RW7gbUDCpDRPbKYA1o0AoFZsc6nASrYl413nXZriYCtTebeMhlZrE1OafBtbTKmQ4BPLmOtCZje8qI4Ee5bkXjo3Mrozfa2g4Nwnnd8UXe2teTZjwB2SH21pE+zHpSp7FaRZ2LMXEgglya32ttFSXEYDyVm4F9nQvgYtu9sq1s3NtLSKMMcl0q0l7jQuuxlzhMFmwEjF0gkhWZHfukf5VbC7ZMFSJpe+i6VXpdoDJaT/hz+EdZON60f8AM76IZIGlnhKOpx70ag7MyVm91GhaYNC0H3bLgU3qHOPcBkaJlDxR/WWeUahFM0zCHWCwcM0dtk0udGMAKzdDmkzQ4FWbSQNTg0d6tXE2AJu3hXAcSm9ZtO/BWYJN6uHBWp3LER1QYM7WkXxkefPBWgqMwc2nkrK1vyTvESBdAzycrUueZyJN2B3bLA+0sSMZGIHMIi7aDR46yaRY2wHymT5SjgVZONnfGO8JHkVeki1fdaQcwZElPpDHG1MaCphWRbdsx13hhvVhPgtD5c+DwMeSPWs2m631Hgv+pafaO8fRaN3fJHFznQuALivwgN8l+JxXGvmuAA+5HEFCgA9wYOgSitJlfibH+VfhdH+ZaxI7xsOJgLUEhaO3ghjO6foqXqbrZ1K+WzMn+kgeKa0B1swguJA+USU6hZbMkA9kp8u9nZuBMnO64tdK6Q0Nbbezo2CJMGNAKJxn2DjdfX8D7pB5FWjS1zodhnErpVreZIglrAL0543fFdGuj2pOZaCXO5LojWmzBEbtnGXFxXSXCWg1ug073KzYA6ML3xHvVgW34477uyF0YXbM4iRuNPbU7HNuD+Mhvqm2bZ5kSfHZaD2bObsfCVb/AGhdg5kjdAPJVMCS5o4jLyXyuodjRJVvSyn4bPXtRPsrKcLoifABOPsrH90R6AKzaS0HN3wjtKti43zSGDec7lPkuhvAsBFYBq4eBKLYtrF3Eb0ahNmbN8SQcgcD2o0DrrwJ1quib9pdwABvwYpMkBWtpIHLdH+ZNDWMbAcGObQEiu9wXSf1bet7Gczq7RW9XE1LQcp1125vOC44IdgCwL3UC0+EIZDbhRfO5ERGXd92ZWNSm9ZuyIMYHnsf12j4Uago9ceaOIPuQJjS8JXTIkNJghxuXXDgVa2jWk8gXei6F0dpaw9W9ArH8Q7la9Xk6oTgQeRTzds20kJjQb3ExTYQb3JE7hPzjGOzFGA8MiHRzGMJgDWtGQG3p32lnPw2hy76dyeNwnJ/wnvXRNx7XY3RQHswV4EWVmL+8MMwE4ybR5IJGpZ9AtBuN8K+KxuNgOPZinOPsn/DdwbBHBHLl+yHYDIJtQwUmck0TcgtFfxRHioIBYQW178FMAXQTGNW08EdW3gTxa8BO6oaTZOnkd1RSxtxfaRoZkeCH+L0V9w/yi9/lVfsulNoOHxeis5u2llD2lpNZYXCtNexNMvbafZlzdGtdmOac26wlroAOdBVWJDrpaQC74Wi9kI2MlsnG88m8fBB4EDdME0vDAq0bJboQYPkukWwnkKf6kNnRB7W3GV44A+CkNvOMCSjUEJ3+IyldS3AodV7QTTzCDwbUCriNBMK1syyxABLgIjALphMWrQQIfUx/DRNaC46uNXeKN60fZtNIA3Z7imXWWgAILwKNF2O9Bt0Xs/mcRxJTa3bMR/lwQMX2tIOGBLBHgmguL33iGgDE0arZ10OOfxO8YVkGtDpgNu7znTwJREOtTUMPpy71bb7DJ3Z11KOAmvuH4RlzWVmOqEKDJTAkROzjs4/cLt/Axdm7jh+hbg4eqbi3XiNhwCNCFaHd7dt4NugxiukFoa4wS1rgXSBrkrjb7rwBLoE7uKfZvAc0giY1XQbc2ZbwJ9qF7Zjp4Frk6yj+lh9FZuuHt3h5JwBHarPL1Vob1odlmL1raD5QfJFlY1z8fdZvWVq6gaeJ0Kst1ji4us92geGNivGVa9eyYGtJAoJc2Rlosnu6x7TLipgW9puWf57UetZdHF2ml/FHG1cbzz2n0TQHifwkHyQJHj95HEbOA+q+V1P0jOvoGjrEpqGF4VHI4oZscXCORPqtU8mQxgeBHzSqktZuPH8JoVZsu2brSskCBehNMF1kTrj8aMg2VuAPOiODrMkfULJtpuu7xIQwB37Mx3hCpdZktIHIypi7ai744eKtTNwGBXrbwkHuTHC8CWzzhgqYOasxDczzViBLSagiaNEZ7LNpc48AJXTLQuE43QTHjKJllyrg8AwQrKBFrgL09WkqPtCzqzwTGkh2IBih71bEizkZZu9Ag5zLNtpLWNAMN6sTTCvFXb24y7ZsuibokzTVMExwmF0lxFmNGA5eXYmi6y2HWaOI9U3FvAp8wSJAJPWVob7xpSIVuWl0HATQeqsQGm6BJc41PaSukE+1tzSQTLmtzXS7QNNq6rjdyGlSFZNDR2DFEyL1Y2ZNGJRy+IrNx+qztHdVccB2IL/AKjvRdw2ZDNZXhE/eW9VybiNeIQq1wxlZoYHTa5om0EVkTLRw1TSW2YFXHM40Vu67ZtkGT/ImSXtOIbnIwcELWyd/NT/AEossrucugUpwTbNrOkDC8RLDH8MINvt5s3vRM3T2YeGy0ILA2sgT3Kau15fN5Lpli72Lj8VDd9VZPewjtveu3KcTyGJX/3Ftus7B/dY+ys9yzEfnRH/AArEXnkj5nfUo/HaVtO6PRDO0O7/ACpgxMNaAETBtms3BxOcbDLTMPe8A49q/eH+1DGyd1uzIoUvNY4eYXyhtVrdd/tQoRccTXsXBrv9qz3Xf7VhVjvoE4xgQQe33RqtTJ8lq0T6rCjYK/dLv8tVxBbTk4LiPps4GfJamgXyME+S/Ebo9F2uK/cCyBbDTzgofKb47PiUwLN+7J0BNFw3vJZtzHNciuAK0z7tugqfBYBxabo7G496toDWnrAAkmeZ94SA8AA1xw2Fsm0ZgDOFGuRMF0XSDqXN9QiKWbyC08sW+SmDbMG73YeKE3rG1bBf/MPJTBfYui9+LdPmiftGWvXj8L2QhX2b95hPZ6gof4lnUx/B9FmHNvAdor4LVpB8tnS3izaM4mT9FZtDRHAQj9nZVwc+hPMNlP37X945dmGxhFp0twyA+H85pgDWtGQFApDgWmCCM1mcXHmcSpDukP0ipHIDxVm0NaOAQBI5wrTI1idUTFqBBaBpRWTS8/wiU5xaG4lgNf7DgukvAj8LTPnC6OLheAIc4VLq6ldHF5zXQYI3iacYG04ErEuOA71qOqO1ZMHVCGGXcv8AqO9Fxw2kUOi1d94PWIyRqE7BN6rgm+PLY0Q1urjQBSSyKB7eHEbBab3aDCcxwFrZHAPJILT2ro02lm+0M2kkzIbJPJAO9gCIJa0OvSV0h1m2zbGIYbzimWbLk5PaB/6ViwNayhv3Rdbeu6cU1pL/AN0CqefsxgfVMa5zgdwkASaHeKNp9s10FoIOEfw5ogXYpTJdDcLRhHyyLw9VaxbMacjO8Oy9CODcXHk0VRx6TbCp/dFfVCrra3O6Ow071FBZiGd+Pgj/AIFnBMYwY9SgN61fF7tcU+YuyWyK9bDZZiG2Rd9nOt1Nwa0QB3fpLKQ4N6wkgtICNDaWRLZ4wGuCBqS00HOAtIH1XxOxa3mTAC/6bMP5j9EMJr/b3TiSxs98L5mEtjswQwbaEXZ1gAIYANAHkuAj3gNy2GPJ2oREG0NpIjhi8IVJd1J/d+qOjAPJHHGewymV9iTP8pd5FNoQ0ESRq26SE7rEC4Y77yFTaWgvGeAwH6PKRUciKomTYvJuzzrPaFnasAz4jd8kawyGWgPFuBQwa4w8Dtr5pri0sfwzGCaCW2YMFxGUrR43e+AR3KZDi1r2uyi9WOxBt6zt7MywwatJaQQhi5nWjsg+C6P1m2hi6+pk9sLVhDh4Lorfa2o44wfDY/csWYlzzw4LpBv27jUyfh7NrRDGnNx6oXSiXlxxuEyO/Ha8S6xgEwKS2q+Y66pwIcDmCvlfgRxLforCLjGYEiugArsdQuAAJjUodZ5wCnHZkwU71k0Yr53rjghl97ikpuIwnaMHZhfA/IhDquGKb8X1UNMZiHAymv8Asuk2bRLW068EF1UR9nb2ZmnFzQfEJ4giAYOorkh1GulpHLRNEAtMgjIGrUSG3RmNCadwVh4g4lERaNHwuzGw4hataJ70aEHRPaekdEaT8u/dHZ5JrRZv/eZu/wB0R+rG84/w/VEOb7e2MvLXEdVscP7o1L7XfcT+FlVh/wAxbQB/C3/2sfZyW2YP55LICAT2CpWB6RbC6wch+eSx9k0ltmD2f2TPigNj+JYSAQwcZ/PNOEvYMAVqafp/xNB8wsbrAGiez7qREgwewhNES4lzjxJP3A4grK0stwzyFEP8O06/ZJ9VyvNjwKJDmuMEOboDBFUd32t3/wBg9hWPsrxLf4mO3h2oTPSbIXmnsw8exYlmDv5TBTQPtC2TXF18TGkKpNleII4EdYDvVtF+3Yb3GZ+sKzF57X7rgOWfYrM3eh2RyjF/5z5e50c3rQjMt65H+UIUA2OJbZgGP4igZfaDAH97NEbLNpIGpyHaV0p3tSCTDWnqgDKm0DrRTZ8rald7lm51Tsya2qBkMbj27MmjEoiYK0WDbPID7u2ocPVDLJ3ELIptQ7Uc1kcwvgtMjw2YJ9SQ0AzzFZUyBjHddRHxtrOlQ5TRzmtJE1xdc8VaOMCziGnGN1ULCcByGxwHt7glrK65J7GkOdiZGcZ+5YmHWTRefaN+WBhmK6q2eXgP68HC7ScOSMxaWpvvcfwtR/x7TruHD+3esXWtrvV4NMrATnGQCND0q1xP7jVm+2N4Twb9VG6DiY0C/wDubejf4WjFfK93s7KeDQiAW3AA0g502NADOjg3WA5kxVMAa0aAUH7YOTh5aJgDWt0ATzBDWhzRHzXqJtS1lC08WnDsKFLJwBvR+IlDC1st0zxGCFAHA+0A75PeUDWDcN4dwPgUQbvSRAcIFA4Zz+SvnYKHm3BM6rrOjmCeU9/es7J9H08+za8mprAdUsdpwK+KycQHjs02SS2Jls8kOsSIJ9Shs6RagkDQbv8AqQoBs4iVkxq0HWK+Y4+4c81k0YlHDUr5nV+8zgEfBDquGIXwvydtyOhRpZ2noUUamcVqUBKGLSniAcbrsndisXFsj5da+CP6zpdsbrQJ+HLzT2H/AJjpZpLiKGCO5dGcTZz8VmdPPt2GgsrLerxKd/hsrauB1OXhyQr7W03nTqMhsJ+xY4blm3IAarIE7x5NxKw/5q2oP4W5+K+a06o4BmiApJjuCwPS7YQ0futzRxtLWoH7rTTZZiGh3VxmrcCh+3oIIYbrTOrRTZZEteyoMjGJxTKljOtGZHJOpfIu2jS04SFW/Y2tXN0ukf22MEljBLjyQrebuvn8TKTzCwbbsqRzP+5Zj4m8HDJAEvvYXYrKFof+VYwY1wFRH/tWg3bQiHkfvfXFH/Dcbr/5T7nQmkDSW7v+Z204nQLNx14bMmipXe47MhmvndieS+Z3uaLXNfO77sTJ2eIXw2mY5pwxHFP6pRRox/y8CjgViCNUeo/JfGzgsxmE9t2/AvAcCr1/olsZArlIwxXSXF1la5MPHyPemG77Nu8XsOt3mjhYs/WOH4v79yA3rQ1cRxcUaC6Js28yrQgtswZuYzXtRws27zz/AAhH/Hthvx+Fv55rE2lrUTwanYsHVYPxkJzd18SGu7Vm60JLRyafVaftj5Q4T3fpLNwc20buukaluPb7oJDDZ1IaPikT4obxLN17TqQKHmEd3/mGCSB+Np9e8rEXCbs6Obi380Vo5oPSWihZn1cdV0PcsmYe0cMSRxzREFhEiixF3eb3GvimiTZbzxEXqg1BjQrXeYP6gUQQH5/wueWjwVs6od1rowntJ975s1kBij8bsUa3nbfFfO6gRqT6bG0MfdpAcw4RsZNNU3Eao4go4tzas6YbDiE7A/KjgQsQdCgIk5o4N57G1vkwW8QUHQ7pfSB1W5BvpnyWdq4V/hGSPVsWbzzOFEer0Zhh7h+P89i0+N5Hi4o0PSbTrOH4B+eaxdbWm84nUThtta2jgILo12ZoYkofCzfP9MoZndHheQqZcf8AYsXAEu8mrEYOI57zvJcAz/asPagQ4cSMD2I4OH7NAm43ed3BHqvfgD+62fNOr7NomP4QWtTy3fdF5riesLsICp1/T2c+ztWEyJ4JpID2Ztymgqrt2W0EchSUcQvwfqzzYrpPtmibJ4FOw1WJ6O8y13Cfr3oUuuo1x4TgeCYN0fM49VvaulTaOcaXWY9k49ysHQ0YG2eM+SaIa0YADbxU7zzQRs0zR+M49i+Z1UcAMSvxdYrIZo/4jkcS7Du2fMaBfI2g2D4RUo/E7H7m7qnTYKluexvVeFkcnI4omSjQQjUFFDX3HdSwZUycL0YIGbPojTAH70f+02ga0QBscADaQL0Dig37NpoCU6osidxg0gUPkhuzW7TJobUrQdbuDyfBDrMf6HNfE9mA5TiuTT/qWl0f7l/1XwY7MEcWWcd0mAO5D4rQhx/qlZC9HotSZ+i+UUHcNpWgMH+6wlw+oXI+izg/Vc59FwE+q0ugeblFJIFfFVwcQY7WrMOAI/mlEAgxQyJp+wmUtCw4u+KSNNFq6vgtBsDmlxxoKO8E8XmGZLprujEoTLiQ08KVUVZaU8cCv325dqPxNIcPD7icCSAgNy0Dmh7f7KxfLLSzhzTOF48NDlgrQ3re0ad4tEQwJghrRkB7gEXckMG/QL53Ynkji520UBRFDosy7BBf9Ry44IZBcMEfhbivmNT9wjtn3PJfC4Yt5rJwwKip4omKI1a4eiPVtPqjgQjiEMB7jcXFAw/pT6GPw/2ryUF1r0h9XRiY0TaO6TaCT2D89ibM2j+sZM+4/dtHNrcBxH7xVoB7V2Z/COHnsJkgGhWpqfFcQjoB+m5BclwAG3Iik80CTYk65s+n34YXaMn976BHAXf95WpDJ/pajiA5wHhCOfP3MjmCm/CcERBY1rQ2OQC1YAB3CiiCQ+DzqCpAc1xBmfgfHgYVoLzT77oizJ6o1dCfVoAIMHDM+S0EO87q1dB8AVpdb6Ao5kCfFZ3XEf5YRxF53q5cx9Fz/suaeCHsJlrgcipl9iatI5Z+aNLjuqT+E+4cX5LEk4Tt1KFb0U2cV/1HUHYuOCbiBlt4U2aNRG7r90OIXxWeR5IdZhxCKyYcu1HEFE4ZtRw9w9Szb1ndiFbHogp/N+ZTaBrRACKGDQIHh7jW7oFSJoXRwFUDuAMvVzJqKrS7BR+Igx4I8wnZH7sypjExn2KyhtqNTHWHP9BTDj9wHxOIA8Uc2CB/VCn4i4kjsuriCfEuRr7NvV/lEBfMfT9K8EGfPsQJdZafiA89op7Nm87uyTjDXGX/ANLQPNYizEXq5bjT4lawyf8AKibxJMlztdvL9EMHjFGjbepcNK5jxThLXCoI2jF7sFnsbgJQwa2p8EfifjHALV2HcsLrUfhGKOJJWaGZXzEUThvRrwQxdifukUnCUcHxTYDiKSo3o12nJDIbXUkVYzWeIQ3nPeZY05Boz/MBNIIeJbaW0aHIIDFxig4lfMSAEPlhZ2jt4+NAj+Imh/eTcGuJc3uw8ET+sbunwonkzfYL0jvWgAC0IkJ8PcB1Q2oHaSsTwnL7tmgwDtmn3UVJKiQ1hvn+mUaB9p6NCxDJoJy4di/CK9rsT9yszeBGJjDtTaOuiQ/iNEetaOMOI4kYDgF89r1J4Wf1Qws2C60DkPuJxCd17M5ceB4rAg0c06Ee4TEL5W4lD4jUrABtfFfI3HtWZz2HE5lfO7BD4RQL5Gao/wAydiTw+6leLUcxsOJGMo4Ha0S5xoAEJFp0gzVvPKdMU7r2jus4/TgrIbvRz+rvfOdUx1y/1XEtxM5DkvmcKeJJXCgXGShgBtJh0iRCsd0tbAaRq0bBUlMLQDrc+7uF1utV0hxd/CN0fdbYkv4hkQPFRv2lpWTndbgB4o5gAfdTitBT7qOvZjAjMRmEz9ZZHEcRqNmDWip5zs0GHev+m31Wuez5RUo5nrLV1Vk0Yo/zEL5jj92ihXw2mRRWLrLI8kOs04hHEFO+Djswa0dZx0aEDLLIUc/jx59ybQNG0ACT1XRhOYPFWZgkRKyJWjRHiYTa3Cd48k2m83dJGpyKOBYZPaFaboAFN7zTxDbIGbvF2Wy3dDoxuAbw8lE8BP3UZlZv/OCxgYu5EpoAaBgAMP0DwYBEye9OJN14kY5RBQ+EOF2eZr4IijYB7d+SiZrAd3tjyRnfDpFNd1NEk5e/ZOLXCuD8D3hRvAmKrmFwr5LAvITSJNQBPNfEND+yGG8QM4qmDfYaXvxN2ZxmFmczsya2pWYHWK+Y1K+UVKPxO6y+Z2OzTPZoPupyKJmCZjY2oIz5oEjSdlpRlkKxOBdHksWWB6jNJAp2JgEhgEA/LJIRwcTAP9KODLSBJ/CcNooHOaCVoBG3AuBLSed0ha33/wC5H4zLnd7p2alWVLzaguOMIAT90aJUw1ozOgQ6tkMB+9+isJIb8wMSB3Jm7vUwpB0K12lpFaZJsT2/+vfcIc01BBWjXGP6pWhqvxGB3NhWYlzgPyUDURUgGRe48FmTiT+yWG8CKEx6qypaNwvD5gNujcT2rU1OxpgnNfM6p2zQNxhZuNShkDAWuf3q1pZWQrjS8fzVWovONoR9mOZzTqe1HVby18k8zfcLxbynPitSZTKuu0/iom7tq0ZOGfb+iZaOgAQA1gJniYWpxPM/dXmY4D+6LZJ0nT9I3PC+0fCeOhWTqO8WlaQicbhHjgtXHDzKtAAcgAP0LQXOJwAFSUDDbV1aaxQBGps5p3CAszmTr+yw4C0mjSDjMI4smu3JoxRyKyGZR+M4oCrtVqVrkso0Q6rjnsFBOJP3h9LKy14ngrVwAvVFjf8AijXyT8fmMU7Frn37TQhOoRkR8pWjACD3kIfFdB76hHAWgujvqPFGocDIPvW4mmIMezd9fu1lV2kN/v8Ae+kTZsHAjePcn7x9P2acQVZ1AGLeXBNH2tnm0/TYBAnYKiVxXzGgC+VtAFwQEAZIZfeWCjc3OOACd/8AT2Z6rW5OA8u9WgLXBPgh2Eg5xkR7pyK4LgsntAa7wx7VMmzd1mzoVEusnUeOz3bz68ICLQfD7nqaI9Z40VsZLuAw+9NEucaAAKxO7IxHxOPOKftBuD20nmminy2g1bsGDW4oCA41Wk0QyC0bVfM6pP3polzjQABdFdAHzn+/khQAIYSYk6IUnIAHLT9FiHtoUMn9aOePmm13uof4jFUcDsZvWTz82nahMOOXCf0+pK4SUcLox7pWZfDT/VXwWhdT1KpN3GO5NEAcB92/G4DzQEneCIkC+36rVNJLRBqG5u4nKU0AA/E7i4/tGzN6wf8AiFbp5nxVkItGOpej4h67RThK+RuC1z7/AL3b71qfw5A8KSVZjedhLs3HmsnHqjWJxXyaegXD9IRVw1Q+DX0X/UdMd0gI8oHiFkAZ/Q8VlARzOP57Fqd0eIX4AXH/AErhA85X4nf7QEMCRePe6VoPuzTBLIuz+8V8zt4+MDwQM3QR/liF1fbAQR/DESnVIDoqdXQZRwN8mO9RhKHwhxae8J+PD9hM67qQe9NEkCMuxNxjMa/p+iwWuw9oBhKsTdt2RFdRsNSQMfvgxJTW3Q8EwBhyWTRRo9yRV2ELP9HrFe9DAD9EaDhxWbXmYHAZoZDdC1ivf96swCWjEyQPVA3bral5FYynyQ+I7zzzcfT9OMSUMsO77z8fELM5nns0GB+iOZxHIpuP6UYjUZhD/wCssRQlubo8+9WgDmnga/fbTAHCG181G9GE/fWiWwYMpvxHqmPIpwlrmmQR7wwBNTyGJWtB4SnGGttBEnmCRtJu3S8TPKVhIMj9MLMu7W7w8ky0eR2hv6YZBE0dEkr5B+aIdv8AdN4TPbihjWq+Gc/vbsSP0wwIQN24wTWMuCtAHNOFD98H6+2ysmHIxmmjE1njP375gKqZuiQO6VMGztLxYR/LA7ESJD3AADOhIR0F4+AcEdGj/Ys7gIPfLVmbR/o2qaIDWNgeiqWvAADj+OCmUaXhrjAymqPwWQAHm0eCMVe6SOxoGKcBLZoJw7f03s3ERwF70TXefvaE1WgH1Wgr5Shl/wC4WYoiJk0oVo2qObhAXzR72oAHvnP9h5tcJBCYLoZMlvP73H2YcQBOtdE03ukdIcKWjzo7yXwuGIKPVeMD+weBhaXiuJn3WgnuVbs5k5pzj3Afprl3+chvqnOJPl7rjEtr3J1S0Eho7RBK1ugnvNVoBA8FhUSpm/ZiO9uCHVDQGwO28vme4k+gTfibUEcQUwQbR4ElfuhaQvlNR9V8zaj9LyXBpXJcSB6ri4ei5n6LXLv+9Gj26hOEg/eyS4gannsOSODtOB/ZAwATgA60bUNe7eukfprQRIxBxB7CiZa4YEYXmniuf9kcJw2WpumPhBTv1lqRVx+nD9N8zaFcaELmfouRXBv91yC5j6L94riT9VxE+a/dC4D3ziCtW4dy1bj3IYg/eHH+U6/fjQgrEtzb/b9kAyAcK4jtVrTdEskmBWc/0/ESsji08CsJvONOV1PEPtSIpo0ZD9ia596+V31COFJB7lq6nhitOqFqJC4wQuMg+q1bB8lxEfpuAXBpQ6jiMOB+/wCLrMZ/u/sfghgxv1QoB+3tCtRTyXOR4rQgj6r8JBXEEe4MXnALMkkeAXGT5r90LgB+w89Hc0KEH9h8EDRmvMrQf9jaxB7wuBnzlD4YjvKGAGH7IHVf6FDMER5rmPquBB9VxB++8F8jcO0oZD/vjiJXAR5LgfrK4gH6LiCPquBHquAnyXEfoTmaDxWgF76LtC/CQVxBHvcF8jcO0oZD/wABfuhcCQuwr8QjyXA/Vc5Pgv5R4LWK9/vakBcCQuMH6LjI+q+RuHaUMh7oIuEGS4RUkRT9GSTXjl/+CF50XcLs7uOcftfL9tf/xABLEQACAQICBQcIBwUHAwQDAAABAgMAERIhBDFBUWEQEyIyQnGBI1JicpGhscEUIDAzU4LRQEOS4fAFJDRQY3OiYLLCFdLi8TVEg//aAAgBAgEBPwD/AKrLqGVCek98I34df1S7jSlS/QaFjb0lIz9//Q0kWJ43vYxMT3gixH1Mb/SObt0BFiJ9Imw+FTZT6O3pOntW/wAuSWXAYxa/OyYO7Im/urTs9HYecyKe4sAayUbgB4AUCCLjMGiyhgpIDNqF8zb/AKB0zF9GlKkqwTECMj0c6Q3VTruoPLpHWgP+v8VamkVWRD1pCQvgLmmRWKlhfA2Je/V8607/AA0h82zfwkGpwWhkA2xsPdUAtFGNVo1+FML6bGfNhc+8CndURnbJUBY9wpSGAYamFx4/50GfnWUr0AisrcTe4p5sM0cWHKUNZr7VztanmKzxxW6MqtnxXZQjYTtJfotGqW4gk/Ooy30uZSTYxxsovkNYNqdQysp7QI9taLKBoiO5wiNLMfUyPwplP0qJ1uVaJ1J2awRQH99bjo6/9xrSshEd08fvNvnU0bGWB1F+bdsXAMpFNIisqE2aS+Eb7a60z/DTf7TfCkN1U71FNKBNHHhvjVmDbsNf/uDho597Vpv+Fm9Q0osABsHKJidIaIDopGrFuLHVU0wiUEgtidUAGu7ck8vNpiAuxZUUb2Y2H+Yc6w0nmiBhaLGp23BsRyPOElijtcTYhi2AgX9/LpgIjEo1wOJPAdb3VpeUaTDPmXWTvXU3uNAggEZg0/R0uJvxI3j/AIbMPnyRoZNG0iEa+cmRfE3HxpAQqg6wBejlpanzoGH8LD9a0z7of7sX/eORowzo51x4rfmyrSv8PN/tP8Kj6i+qKsL3tnqvVi2kyhThYQIqm17XLZ1pSn6JKCcRETXY7bDXT3bSdFsTbBI5GzUAPjyIhV3YuzYyCFOpQBqFLGqu8g60mG/5dVTdLSIE83HKfAYR/wB3I8avhxC+Bg47xyLKrO6C94rYt3SzrRLsjSsT5Zy4vsTUo9nIjM8rEG0UfR9Z9vgvxqR1jRnY2VBc0jYlDEFcQBsdY4f5QGBJUEXXWN19VaUCoScDOBsR9Q5P7s6yZd6sPaDTxPFoi36TaM/OLbaqn/20CCARmCLigQRcEEcKYBgVOYIsfGtFs2jmJ+lzZaBuOHL4UTHDHn0UjW2+wFaSR5BxsmSx4P0fnyKqrfCAtzc22k7a0lnVoCpsGmCtxBBqU20nR+IkX3A/KtMygJ81429jjkjMhL4wAMfQt5u81JKJNGnIBGFZUz9EEGovu09RfhWiOzxF2JOKSQi+xcRAFQ56TpJ3c2vsW/zrSmU6LKykMDE1iMxqpBfSwPw9GA8Wb+VCZTKYc8QTGd1r2rnW+kcyAMIixk7bk2A5C4E80p6sEIX/AMz8qxs0WNAcRTEqnXe2QqFXWNBIcThRiPHbTuqIztkqAsfCkikOjPbKXSTib0Q+X/Faa6paNcRUWVb299MCVIBwki191YiJU0eKwVFxSHXZdg7zU15ZlhB6C2ll7geiPE/D6pIBAJ16v20OpYqCCy2uL5i+r6jNIsy36UTjDkM1bjwPJO7pEzoMTIMVjtA1+6kVGbnlP3iKOBGsfGgyPiUENhOFhx3Go5TDDKpBc6Lla+tNan2fCgQ6g7GX3GtDJERiOZgYxHuXq+61aIvNmWDVgfGnqPmPYbikhCSSODnKVJHqi1R9DSpk/FRJR4dFvlUmGRXhuMTRnLg2V6L4tBjc605q/ejAH4VJIka4nOEXA8TqrSJTFEZAuOxUYb21m1aXlGH/AA5Y3/5C9aTlJo7bpsP8SkVOJGj0sP1Al48ty3PvrSHbmomUlS0sWrLInk0hVXRpgosObkPiQSaQ4YVO6MH3VoQtosXFAfbnUaskWkO4ws7yPnuAsvuFOuH+zlQbUjH8RF/jUWelTtY9FY0Bt3k0EUMXA6TAAngNVaP0ptIk9MRDuQfqeTRkWSFmcXGkO0hB3E9H3AVLII0LEX1AAayTkBWmTOrRwR3DzsBjHZAOdaX0+bg/GcYvUTpN+nJIzKBhXGWYL3X1k8BUkixoztqQXNaMjKhd/vJTjfhfUv5RlUMJjxsxxvI+Jm+A7gKSTFNNKWwxQrzY824zdvDVQIIBG3OtJdujDGbSTGwPmqOs1AWAF72G2ovLy8/+7jukXE9p/kP22dGWRZ41xMvQdRrZDu4ihmL/AFEZjixLhwsQOI2Hkg8m76OdQ8pH6h1j8p+VCNQzOBZntiO+2qpQFnRj1J1MD9+tfmKiKJ5BSTzSLr3HVn4V93pfDSI/+cf8qn6EkU2y/NP6r9X2NSR4WdrsecYNY6hlbKprrpMD7Hxwt4jEvwqXo6ZA34iSRnw6QrSkA0WYKAOgzZb9daUMeiyEfh4x4dKhhkQXAIYBrH21pgvo0vCNj7M60o9CJv8AXiPtNTC8Ug3xt8KSMS6PDckWEcmW9bGtLd0RShsTKi6r5MbVpP8Ah5v9p/hSn+7g/wCl8qilMWiaOwGLFzSfxZVpRto8x/0n+FaQP7tEvnPCvvFTSrFGZGvYWGWvM2om2dRTczofPlSxdjJhGsmRsq0tyujyMMiVwjvbIfGiVggv2YY/+0UsBZIMbG8bCRr7Wt8iafpaVEv4SPIfzdFfnRlAkn0hrlYF5lQNp1t77CnlCR844IyHR1m52VzpMxiUXCLic7ieqKby8uH91AbtuaTYv5dZ40SBrNq0qVkjsn3kpwR952+FGCNIBGxskdmY78PSN+/bSzKYhMbopXH0tYFaMrMW0hxZpclB7MY1Dx1mp3Ln6PGekw6bDsIfmdlIiooRRZVFgOA/b4pVkDEAjAxRg2RBFLMeeaFlw2UOhv1l2+zkjkWRA6G6tQiUSNILguAGGw21HvrSUOESp14TjXiO0viKmlbmVmiOQwyHLrJ2h7K0lDJA2DrWxoR5y9Jai5uTDOo6TxgX4HO1TqZMDREM8Mw2+Dg+BqWMSRvGe2pH860eTnIlY9a2Fx6S5NUkDRaIQDjOjyc6m/Cpvb2XrTGHNxTjVHLG9/RbI/GpVxRuvnIw9oqDp6LH6UKj2itCbFo0ROsJhPeuXypZOfhlFsOckVu7KpWxf2eH3Rxt4raiLgjeK0QMmjRiQYCq4SG4ZCtM+7Q7poj/AMhWlf4eb/af4UseSy3yGjYMPvvQH9xg4GE/8hWmkDRpv9s++tIHS0VP9YH+FTWmi4gTz9IS/cM60psOjytujb4U62XRIPSUn/8Amt/jWl5iJPPnT2L0j8K0zNEj/FlRPC+I+4ckUgCz6S2oscPqR5D2m9aPCBAgkGJvvGv55OL3UfLaRh/d6ObnjIdQ/L8anbm1JjA52Zgi8WOVz3CmTCiaLESGYXd9oXtN3tRiLSoCLRQAFfSfZ/DUXltIabWkQ5qPcW7bD4VpHlXXRx1T05fUGpfzGnjVwFYXAINtmWqmBIIU2NsjrsahhWJLA4iek7nWzbSailWVSyXw4ityNdto4ftLSIpUMQMZwrfad3JHNikkjYYHjNwPOQ6mHJHI4meKS3nxnVddo7weSTyUom7D2SXh5r+Go1pEbMA6feRHEnHevc1RyrIgkXUd+zeD3Uo5mcr+70g4l4Sdofm10JvLNCwwnCHQ+cNvsNCR10ho3PRdccXh1l+daOAjS6OdSnGg9B9ngbitGODFAf3XU4xnq+zV4VovQMkH4T4l9R8x7MxUUbxzy5eTltIDubUw8ddXF7XzqLyekyR7JRzyd+px8DQkV3khI6ire+oh60VLwGCVbiNjH0hkyjqn2VbK1aCf7uinXHijP5TatFjaOLA4sQ727ixIrRMjpC7tIY/xAGtFVX0doXFwjSRMO4/pTOyTQQr1GR73zPQAtnWlqzQsqgliVtb1hWm/4dzuwn2MK0o20eb/AGn+FHLRs8rQ/wDjTf8A46PgkR9hFadbmGG2RkT2sKn/AMTow4yH/jUqltKgyOGNXe9sr6hWnH+7OPOwp/EQKeENLHJiPksVl2HELU6M2kRNboRq7X9Jsh7r1NnpOjr5vOP7rD41pEnNwu+0L0fWOQ99PA3NwQAXQEc6eCi+fea0mUxwu69a1l9Y5CoIhFGqazrY72Os1H5WdpexDeOPi3bb5Usah2cDpPbEfV1UyhlKnUwtlxomPR4dVkjWwHwHea0eNlUu/wB5Kcb8Ny9y6qSaNy4Vr82bNuB76jkWRA69U3tfhlenY6QTHGbRDKSQbd6J8zSYAMKWsnRsuy2yopOcBbCVAYqMW0Db+0SxrIhRtR3awRqI48ksRZ45EsGjaxvtRusPmOSaPGt0I5yI4kPpbjwO2lfnoiUJjYgrxRtoPdSqWjCy4WJXC9tR31GJBKsZfOG/W/eRNqPrKcv/ALqRRG5JF4dI6Eg2BzkG7m1Gjo4MHMlmNuq56wI6p8KdnkiWVR5fRnOJd5HXX8w1VIOeiWSI9IWkjPHce/UajCTGPSFLKwUrbv1q3ca0jybxz7FPNv6j7fA2qfyc0U+wnmZPVbqnwPJMMOlQSecGhPj0l+FabdBHOBcwyAm3mt0Wrm15wy9opg4WGfJEWXSpY2YsGVZUB2bGArRjZp0PZmLeDgNXOsZUCgNFIjNjGfSGrOoTbStJXfzbjxFj8K0h+ZieSNVviBI1XLEAk0VBIJAuNR3X5NO/wsvq06rIhUgMGGo6jUiycxKCQ7GN8IVbbMhtqbLQFuMPQhFj3rTrHIcDWYoVe18weyaaENKkpJ8mrBV2dLWaZ1XDiNsbYV4k7K0zMQr52kR+7pfLlZFEnPsbYYypvqC3uTUzCSSCMG6seeNtqp1fabVcar6ql8rpCRDqw+Vk9bsL860mUpH0PvHISMek36a6JXRtHAXpFQFUbWc6vaajvDDime5zd24nYPgKBuL1JKruZHPkNGP8Uv8A8fjU0jNhhjuryi7HaibT37BT6Oph5hSY0yHR122jxqU4yNGiOEADnGXsJ5o4mnjIiMcJEZw4VPm1FEsaBF1D2k7SeJqacoVjjXnJX1Je1l2sx2D9njlDl1sVMbYSD7j3HkniMiWVsLKQ6NuYavCoZecW5GF1OF181h/WVRsWUMVKE61OymXmZudGUctll4N2X+RqUGFzOoJU/fKN3njiNvCj0kOBrYl6LDPXqNFneNZLeX0ZrOo2+cB6wzFeTmi85JV9xqETCJka2NMSxs2eIdljb30kpBTSCMOM8zpC+a4yDe33GkR4pSqi8Ml29Rtvg1DyU+HsaRdhwkGv+IZ0YCZZb5xTRgML9oZZeFIDPorxMemuKJj6S6m+Bp0lMBQPaXm7Yxl0t9aUrHRycscQEo3YkzqNxIiuup1DDxonkMIMqy3IZVKW2EGly0uVbG0sSPw6JKmtAy0dV/DZ0P5WNN0dNjP4sLJ4ob/Og3OyTQyAFU5sjjfPPxHJetIjMkLxg2LqQL08jRtBGLdMlW/Kt8q54c9zNs+b5y/ja1MqsLMAw3HVlQRQ5cDpMACd4GrknGLSNHXzS8h/KLD3mp89I0ZfSdz+VbfOneT6RGi3wYHZzbLcovWksxeCNSVLyYiRl0EzapI1kRka+FhY23UsJE7Sm1hGI0A2DWajjCYrXONy5J13NJGiYsIC42xNxJ200as6yHMoCF3DFrNDyukk9jRshxkbWfyipYjJJESRgjJYrvbs+ytJkYBYozaSY4QfNXtN4VIkMUKgrdYiCi7S/Z7yagiKgs+cknSc/BRwFTSsCI485ZNV9SjazcBUUSxJYG/aZm1sdpNRTLKCyg4QbBtjcRwqaZg3NRANKc8+qg85v0qOIQqzdKRz0nbtMR/WQqHnSuKWwLG4Qdkbr7T+y3HIl0keGQ40lLNGWzyPWTw2cKgjeImLXEM42vqHmHu2VOpjb6QgzUWkUdpP1XZUiCaIhWIxAMrrv1qaQM0QEqjEy2ddY4+2omKNzEhv+Gx7S7j6Q/nUETxF0y5q9494vrXuGypfJSrN2HtHJ/4N4ajRC6PE5jQkLd8IPibUrBlDDMMLjuNSQEyEgYo51KTL4ZMPhWjSNhaJ85IDgPpDst4isQ0qAlQY3VsgdaSIcr0dJP0bnwuIr113WNn9lFhFpCsD5PSrDukA6J/MORpEVlRjYyXC8bUAALAWA3UeRmVRdjYXA9uQ5AoF7AC5ubbztqWHHJFIGw8yzG28MLUuWmyDfAh95om1bRyNGrMrEXZLlTuvWBcRewxEWJ22FHkFGIc6Jb5hClu83oHFpp/0YQPFzf4DkHT0s7oIgPzSG/wFTy81E0lsWEZDedg8aklkXmgqXaRwG9EWu2dPGWkja/RjxG3pEWHzoiTnQQQIwpuNpb9BU0oijZ9eEZDeTqHia6WjwIos0sr2z1c4+bE8BTMFBYmwUXJrRlLk6Q46UvUB7MfZHjrNR+Wk50/dxkrFxOpn+QpZSA+kvisx5uGPaRsy85jUEbLd5M5ZM24blHAUcEgZMmGaMPkaeQg8xAAXAHqRjYW+Qq66OAigzTSm/pMdrMdgFB1LFbjEBcjbY1POIwABjkfJEGtj+m80mPCMdsVulh1X4UjFlDFSl+y2vx/Y54ecUWOF0OKNtzfpvqGXnFzGF1OF13MP6ypsGkIyg2KNa/aR11GoZC4IcYZIzhccd44HZTTosqxMCDIDhPZJHZ76j8hJzJ+7kuYjuOsp8xyKfpCNHIOalibZ2T2XU1BKXujjDLHk4+DDgakL88Y5OnDOuEZdVrZg99aM5s0Lm7w5XPaXst41BFzSlL3XESo81T2fCllbnmiZbDCHRhtGo34g1pClHTSEBYg4JAouSh4eiaWHDM0itYSKMSekO17KJEOkYT93pXuk/wDkKVFVVUDooAF22tqqUYJ4pBqe8L9xzX3/ABrTEJixr1oWEq/l1jxFSOTJo0iEmNyQbemvRJ7qNGpo+djZL4cW2r1ccnMjnueub83zdtlr3vR18hrOr7+TbQ5BGgdnAsz2xHfh1VGjK8rsb42GHgoFq0Q3SSc/vnZ/yDJfcK0LGYecc9KZjLbcG1AeFGr8kqNJNEtvJxnnWO9h1V+ddG4BtfWBt76n8rINHHV68x9HYv5vhU7FiNHjNmcXdh2E2+J1CuhEmxEjXwAFRK0jc/ILfhIeyp7R9I+6p5WLCCE+VbW2yNfOPHcKPklGjwZyHMsc8N9bvxolNGjCIC8jnojtO+1mPxNRQFAzsQ0zjpPb3AeaN1XXRlCi800pv6TtvO5R7qjjEQaaZlMhHSfUqjzV4fGlYMoZTcMLg8DRlQSCO/TYXsM7Ded37HHIkih0OJTf3VMrI3Pxi5AtIo7Sf+5dnspMDDnEsecAOIbd1TIwYTRi7oLMvnp5vfuoc1PGrWDqbMOBHwIqaISoUOW1WGtWGoitHlZ1KvlJEcDjjsI4Gpoi1pI+jKnVOwjarcDTEyqJ4hhmhuCh1+lG3ypZ0MaSG6CSwAbI3OytJBTDpCC7RdcedGesPDWKmUyRBoz0ltJGRqvs8DX0hBCJzfDYE2FyN9+7bWvMU/O448GHBnzl9fAj6ujw8zHgxFwCSt9gOzwqGXnExWwm5VlOwqbEVwq+VW5NX1DrrOtlCtpoauUi4tvqWBhopgh14ObGI7NR91O/NtBDHa7G3ciDOjqoj30zKilmOFRt5EhCyvKTiZ7KPRUdkeNLGYlkZRzkjkvnlc9le4aq0eIopLnFJIcUjcdw4DZUsPOOmI+TQ4innN2b8BU85S0cYxzSdRdg9JuArA8EYSMGWaY9JyMsW1m4DYK6GjR3zkkkP55H/r2VBCVJllOKV9Z2KPNXhU8zJhVFMkkmSjZ3sdgpVTR1MkrY5JOs1s2OxUHwFLE8rCScWAzSLWB6Tb2+FMCQQDhuLXGzjSsqYotFXnHv5SVs1B3u3aPCoYyiWZ2ka9yzbzu3CldWvhN8JKnvGujKgkWInpuCQvAbTu+1lcohcKXw9YDXh2kb6VgyhlNwwuCN1JMeeaJ1wHrRnY6/qN1KqrfCALnEbbztozMkwRwBHJ92487arfKj5CS+qGVv4HPyb407NDJjYloZDnf922/1T7qf+7uZR9zIfKDzW88cD2vbWkSukYkQBlVgX29DaRUkZLxzwkEmyvnk0Z/TZRIAJJsBrJqYFG+kRjFYeUUdpN44rsp1jniIviSRciPcRSqQoVjiIUAnfxrRo3jDxkdBW8kb9k528KsIpih+60km24SbR+b41GgRFRb2QYRfPVy66sK1auSOJkkla4wyMrKNxtZvbR30OS3JqocnHlGur5kDkzNZUDyFVLBiBiXUbZi9HVyTI8kkSW8kDzjneV6q+3PklMgwiMAksASdSrtPJGJBiMjA3Y4QNSrsH61NIY0LBWcjUq6yTqqCEpeSQ4ppOsdgHmrwFESBnlN2wqQkaHZx9I+6oI2vz033rDVsRfNHzqGZ5WLKuGG3RZusx3gebUswjsAMcjdVF1n9BxpIiCZpvKSAGwXUo3IPntrEVPP6Q/NjqpEDqvvt1mrDLP18UMXmdt/WPZHCmkWK0MKBnt0UXIKPObcKiEgUCRg7bSosO6pmlFkiXpPfpnqoN53ncKhgWIG12ds3dusx+2ih5tnwt5NjiCW6p22O47qmi5xbA4XU4kbzW/rXUUpkUqehInRcead44HZSI8kRj0gZg4cQ7VtTjdUbXvo8/SbCbE6pE39++ozgP0aXpBgebZu2u1T6QqImNjo0nSUgmJm7S7UPFfhUZMEgga5jf7lt3+mflS/3eTmzlDKfJ+g/mdx2UJA8jwSJbK63zDpt/mKhiES4AxZQeji7I83wqGAxM4VvJMcSpbqk67HdUUciSSZ4on6a3OasdY7uSWJZUwPe1wcsiCNx5Nta8uTOhW3kPINXKfd9S9Fhe1FtgpRYUatV6GqhyHVyGr1nV+Q1e/IZk5wRA4nOZAzwje26pZipEcYxytqGxR5zcPjUUIjuxOORuu51n9BwpJUcsqsGKZNbYaYJk7hehchm7O/PZWOSbKK8ce2UjNvUB+JqOJIxZBa+ZOsk7ydtcyxl5x3ZgOog6Kjv848kZlYlnAReyutu9jq8PtiQBckAbzTEhSQMRAyG/hRfnEXSYgRJHcOm0gdZDxGykdXUOpurC4NTRCRbXwsvSRhrVt9Lh0hMMq2eJhiUHNWGog7jsqeLnEsDhdekjeaw1GoJudXMYXQ4XXzW/TdT83NzkDA5KL3HnaitKuBBcl8C2xHNjakkWVA6G6uMjUalUVWYuVFix1njQZsbKVsoAKtvvrHhy7aG2tVBgcqyq9uFA3N+TOgaL55Vs10jZ2o0BRq18jto3A8aGulW2fId1a61UNVDbV+TdTHLKsZ250HG0UOUckiMgP0dFxyv0mOoX1sd/dUMKxA2JZmzd26zHjUiY1K3ZcW1cjTNFo6Kqi2xETWTw/WhC0hx6RawzWIdReLecfdUczzNeKwhU9cjN+C8OPJLCZWAZrRAZouRY+kd3Ck0hCwjhUuq9EsvUW2y+3w+2mjd1sjmNlOJSNV9zDaKjJkjKyphPVdT1T3bwaiYxtzDm41xMdq+aeK/CpVMTmdBcH71BtA7Q4j3ig4gcMvS0ecjV2Hbb6rU8N5UlDFWXoncy7jU0bX56L7yPIr56+aflUTtJIJUbFC8dip1q6nd8aMI54TAlWw4WA1MNl+6nkKvGMBYSNhLDs7r8kUTRSuqjyL9NfRftL3HXVzV6FAVtrbRoqb5VjaszSi1CjR1clzSnOrDkIvlTZAV1hxHINVGrcnhV630OFba+Va6wisAoCiaFMbUGvyRNIwJkUJ0uit7nDx41KQs11xSzMtkjv0UG1juHH2VFDhJkc85KwsW3DzVGwchnZyY9GAa2TSH7te7zjQ5vR73LSyym52u5G4bAPZXNyzffeTj/CQ5n12+QqwRbKtgoyVR8BUbOy3dcBPZvfLjx+zuL2uL2vbbapjIsZMSh3GYVtu+opVlQOuo+0HaD3U5YIxQYmA6Kk2ue+lKaTFmCpBzGpkcfMVFI2IxS/eKLg7HXzh8xQ/u72P3EjdH/TY7PVPuNTSyRyR2XFG7YGI6yseqe6lhCyPIpPlLYl7Nx2u+lgZJi6EBJM5EPnbGX50jSFnDrZVIwMO0CKJAoEHkO+rclwD38h5NlYRQUWoa6+XI2qsG81ZBWJaBy5NtN1aW4oqDmMr0OQVto8vGhRrUeS9bKtV6OZpdfI87MxigszDrv2E/U8KihWMGxLM2bO3WY8and1YMXWKFbFjrZj5o/q9YXmF5bxQ68GpmHpnYOHtoSM4waKqqgy5wjoD1B2vhV4dHPSYvLJr7UjeA2e6nkSMYnYKOPyqOR3uxTm07OLrHiRso6SXJXR050jIuThjH5tvhQvbP7KeHHZ0OGWPNG/8AE8DS3sLixtmONSK0LmaMEq33qD/vXjv31IZHRJNHZTniseq67r7KliYNz0VuctZl2ONx47jT20iMNGcEsZuuLIq41q3ftoqHTDIAcS2YbONQyGN/o8pz/dOe2u71hWkxOSs0X3sXZvk67VpSSoJGEkXsdnDka96UGtVHlfXatVGttDkvasQvW2hqpzYfUjOVuS9bqsKyrfQoUOU0oN8+TXR5bUd3IQDQHIFVBhUBRuAsOSURpJzszYzqhjAvb1V2sd9Mpcc5pREcY1RXy/P5x4UDLKLKDBHvP3hHAdnxz4UVaIiPR4+k+bSvmBxY62PCugklgH0rSBrJ1LfjqT419HaTPSGxf6SZR+O1vH2UGQMIwQGtcKN3dSMWF8LLn2tff9mXUOEuMRGILtsKDNjK4bKACGvr3jwojmSZYunESecjXOx2svzFSzOESWECVL3e3WK714ipEYETwjpW6S6ucXd6w2VIBpMOKJyrA4kOqzjY3zFBcQUuqllsd9m225bimGYoVwoGhyAYmvsFa+ThRoua6RoIRmeVhesNYaC0FHJattHVybaOqr8SKxHhWLhyHZW08go/U4141socsolwgRYQxNizdkbTbbShI3Kxg6RpB6zMdXrN2RwFJB0uclPOSbPNX1R89dNIwdUVGa+bNqVR8zwpmCqWY2Ci5PCkZWUOoIDZ5ixqVXYBUfmyTmbXNvR41FCkYsozPWY5sx3k/ZkgazapXijCvLYYTYMRqLZeFSSLGhduqoubC/wojmj9Ig6Ub9KRF2+mvHfvr7ry8PThk6Touy/bT5ijJIWR47SwuLHDrB87iN9cwRMJY2wBvvFtk+48Dy2rKrV30TbgK2ZcmzKgAK21x5Qo3Vu+rYGgBW/6m2jQ1Vt5LCinGsJGzk3UzAUCdvJnWytlF91K16vwofUkSaRsOMRxeh128ezTQsqrHAVhXtEC7eHE7zSrhULcmwtdjc+Jq9SypGuJza+QAzJO4DbSszJiwlCb2D+69qxJG562k6QR2dY4bkH9Z1FzuG8uEMTkF2Ddfafs5I0kUo4xKdhrm1wc2emuHD0s7jjSEwMIpDeJso3Oz0G+RqJUjJhRWUKMXo9InIGj/dmxfuHPSH4bHb6p27qjSKIYUCoGJa287eTXWvk21cVsp+rSBvChur51srZyDVybaPIOXfQOVfrys1qx0pJzrFasQJ5BqoaqPJtoWJJ3U51UDcV8a2UaawGW2r2q530uujWQq45EhIcyO7SPna+SqNyryCpZYkK4s37CgYn8BWGeXrHmE81c5D3tqXw9tWh0dOzGg95+ZpWDKGF7HPMW9x+zlEuTRm+HWh1OO/Yd1XTSImVWZNht0XRhvqNiwOj6QAz28JE84fOo2aJxDISyt91IdvoNx3b6ZnjlKynnIdIOFb9hj2TwOynisV0dyVsb6NNtB808R7xVjbOs6HIxyoZirVwPKa21wocptRcAXJt30dIiUXLi3DP4U+loFJU4idQHzrn5jmZUS+wj+VDSZcdmk6N8yoBqTSrC0QZ234cqV9IJsS6jeV/lX0h1QDosQNdmHutR0mbETe19myo9Ie1y2frBfdavptjZlvxDA/Cvpqk/dkjfUemBiQEPtHztRmG3EnevzpXVtTqfGh30NVDVR5NnfQFr8asCBerChV8uQm55FGdBQDR20OQGjyFlBAJALahtNLEiszhek/WbWfbQ0hWfBGDJY2Zl6q/m2ngKmaBGV5SuMdS+bflWsU8vVH0dPOYXkPcupfH2UoKqASWsLXOs/Yy85gPNYcfZxaqjYsgZlKE61OsGhEgkaQCzuArHfbVUxDyiFwYyRihlHnDWP5baBEobR5xZwLm2phsdD/VqikIf6PNm46SMe2o2+sNtNzc/OQMCClr3FteplqNSqKrMXKi2I6zyGsdqYnbSGxo8orhW6pJo0tibDTabENWJu4frR0xyLrgT1mufYKadz1pj+Rf/AKoup1tK/ebfrXOLYDCSBsZifhamIJyUL3fzoSOBZWKjhlRZmN2Jbvz5DI51sx8axNvNB3Gpm9tCaQHN3t307s3WJNtV6UqD0hccDaseE+TLoDx/Si9/3iP66/Oxoh73Sy/7bfK9c5iFpCzey/vpZMPVlYcHGXuvSzztbAqOL6wf11Ut7DEAp4Z8l/rWoi+VFSORRYd9DfXzru5dfJGLOwhQzSgkPPNkL7QDr8BUkyR2Dm7tqRRiY9wpUmYBQBosY1Kti/6L76jhjjzVczrY5se9jnUmkRo2DpO5zCIMTfy8aRmZbspjPmk3Pu+zkidJGniuxa3ORk9a21dxpWjmVXFmANxfWrD4EVNGJR0WCyRm6sOy248DtFIV0lQJFwyQSDEB2XXcdxqw11ejW2nFjWy9AXoDLkNXrv5NOQYQ9s8QF+H2ojc6lY+FGGUDEUa3d9bR4w75jEFBJFRphUDK/DUOA+pbkFuTZy66wi/Id1NcAmlvvrbXGpDKMIjVTc9IsbWHzp3VFLOQqjWTRkklHk/JR7ZXGZHoqfifZUZXMaMnOE9aaQ9En1tbeGVNOsYCseckI6sY6R/LsHfUTSNcugjHZGLE3jbKucjx4MS4yL4dtuP2glQu0YPTQAkcDt4062dpICrOtudjv1u/c241EInbn4yQWXA66sx5w84Vlfvo1ajRvTi9Bd9WAGVDVye/6lqeCJ+sor6FDuPtNHRIT2feaGhwjs+81NoVzeOy8N9HRZh2PeKj0KQ5v0RuGZpdFXaqAeLH25V9HTUVS3q02hQnUCvca+gJfrNQ0TCegVX0iMTe+ho47UkjfmsPdSwxqbgEH1jUmixPmRY7xlQ0GO97sRuNfQI97e3+VNoGeT2HEUugC/SckcBao4UjFlFvq25MuTOiaPIOQUy3tSi1cKtSzh5Ciq5C3Be3RuNl9tTxgssgj551yUFrIvpEH9KcpitO5nfWIIhdR3rt72rDPL1j9GTzUzkP5tS+FRQxxiyLa+s6ye87aOeVRxRxDDGoUbd57zt+zmWUOssRLYRZor5MOHpUsquhkQFrA9HtXHZsdtYedA0mAc3LqZWyxW1q/HcaRVkcTRsYXBwzJbX6LDfuNY0L4MS47Xw3zt3VnWdFs6BvyH/I7Ufqd/IeTO+2/JtpsWE4LYtmLVQ0gr5O/wBIm2rGLAd52eJrmZZM5nwj8OLIeLaz7qSNEGFFCjhRR7GTSZQirngjOFB3trNc9NL9zHhU/vZch3hdZ91Z21+NRRiNcIJa5uWY5knafs7S87fEDGVsVIzB3g7akhbFzsJCybQeq43N8jUUwkBFisidaNtY/lxpTz15IxzOkRHA6tqPotvB2GlEU7q5BjmhPSGphwO9TUs5jkCsuGNxZZdYD7mFaPMxJhmsJk9jrsZfnWEX+vb/ACO3LegKvvq9CpExoVxMuLapsfCo4kjUIihVG6pFa7NNKI4l1KnRy9JtfgK5+STKBMvxZMl8Bral0ZcWOUmZ9YL9UequoVJpEaHCTifzEGJvYKViVBIKXF7HWKjgjJEhYztrDscQBHmgZD7KWdIsOO4DnDit0R6x2UqqhJxHyjXszZX9G9SQukhnhN2a3ORk5OBu3GiE0hQ8bFJEyDamU+aw3bxRmMMSvPruA5jBKjieFTxYwJIiBKgujbD6J3qaKc5HhlUdNbOuzjTQxsULLcxG6HdyW5PdQZTqIq4osoFybDjR0qHz1+NHS4R279ymn089hfFv0FJpzg9MAjhrpdIVs16Q4EX9htQkG4/H4VjXj/CaxCsQ13oG+Y/YZtJSLI5k7BT6c56qhe/OoHxRqxNyRnVx9jbkvyuZixLskEKHXrZhxJyUVz8kmUCdH8WTJfyjW1Joy4g8pMzja+oequoUUmLli9kXNUj1tbzmPwoieQEysNGj81D0rek+zw9tROoGHQ4gRtkbop7dbf1nUiItm0qUyYtUY6KnuQZt43oPMwCwxiFNjSjZ6KD52qOMpcs7yM2stq8FGQ+xZQwKsAwORBqWAouEqZoAbhf3kfFDtt7ax85FigZSSvRY5i/GjE7gSgcxOMj2la2xrax76inWTFE64JFHTjbPLeN6mgAAABYDIActuXSjM3RRThOsj4UkM6nonCx2YhemZ8RxE4hvNEk6zf64dxqZvbRJOukdkN1NjWjyrIlxlvG4/allGsgU2kRLrYfGpNPN7RjLe1HTJjtA7h+tO7O2Jjc8iyyKMKsQKE0oN8be2otLkDDGcS7aBuL8jOqi7EAcabSYgpIYG2wHOv8A1A36nvptMub2f+Ow91HS5OzZfeffR0mY9s1z8vnt7aTSZVbFivwO2oNJWTLU248ksUaNzsuOclrRpbEF4KvzPJI8iviZoooVtm2bN8AK595MoEJ/1JOins1n+s6JjL2OLTZV7K/dqf8AtHjc1BNjxK2HGhs2C+EejiOsjbU08UVsZu3ZVRdz3CpDOxURYEVhcu+bDgFqNCihWdpD5za/d9iRcEXtfdQaXR8pGM0X4nbT19440kSB2lQ/eAXAPQPpW30kyM7xg2ePWpyy3jeKsL3tnv5Xmjj6zWNPpqdkt4D9ak0yRur0B76h0qUuqkh7nblQNxepFU2JUMwzXwpmLMWOsm5+z0BTZm2E29n2c06RC7eA31LprtkgwfGmZmN2Jbv+xg0wxrhYFgNVqk02Rur0B7TTMzG7EseP2AJBuK0bTL9CTXsbkxNJGcGKEm4BZcxxwml5oN5NW0yYa3bMA+scl7hXMSSffvl+HH0V8TrajPDH5KJcbL+7iF7d+weNPz2EtK6aLHtC5t4sch4Co1Yk/R05sN1p5rlm7gcz41HoyKwkYtLINTudV9w1D7I1Ek0ZwFudj2M3XHA+d300TwkvB0k1tD803HhqpRHMI5cJuOkpPRYX2clwOSWFJBZhehoiDVhXwufa1/hTaOjABhjttP8AK1DQog2LPuvlQAGqtNkYAAHDiJvvsPsgCTYbajgAbCAJHGu/UXv30i4Ra9z7Pd9RwSuRIzGqn0uSNirp3WOyvp0t9S2qCeSQg6hiCkbPqaRAJQL5W1EUugi+Zf3fqa5lIhclVUa8rk95NPhxHBfDfK/7JoulYbI5y2Nu76ZVdSrZqwsdlHyaARx4rZBVsK5mWX758K/hxGw/M2s+6o3dhh0WNYowSMbiwy81BmfGk0ZQQ8hMzjUz6h6q6hSSyu2URRL5tIbHwUX99Ezl7ARrGDrJJYjgNnt+1JqbTES6qMTD2VJK8huxvw2VomkZYGzI6vEbqDA/zy+o2QvTMWJJNyfstFTFJe18HS8dlKoUWHjxO/60kSuMwDuuL0NGTaE/g/nSRhdpPuHsH1dJn5pLjMnICpJ5JMmOW4fs2i6TYhGaw1Z+7PZQN6UTc4xZl5vUqqM+8mpo3cnFLzcIGYTosd+Jtg7q+k4gF0dDNbLFeyD851+F6EWkjpmQPJsTqxLfhra1RhwgDtjba1re6o0KLZnaQ3Ju3H7M0SACTqFaZhLKwFmYG428L8gJGYyokk3Jz31HpUqHXiG5v1qLTI31nAdx/WgwOog0xABJNqlTC5FwdoI3fXAJ1Z0sErakb2WoaI4ze/cq4j+laNGQeoY1XVi6zHeftSbVpU3OSZdVch+v7RomklWCMbqchfYakiSVcL3w3vYEi/fatITRlwNNqXoomZBPBNpoNpDi0aLAuxpM2twQavE1FGUuWd5GbWW1eC6h9rLpccdwOm24avbTaZKw1gZ7BWbHM69p+skkYtdWBHaRrGn0tmywqVGxrn20Tc3sBwH1Iog2ZOs2Cr1jSaKfMRPXOM+zIUkCLrCn8oFBQNQ+0m0lI8tbbhR06QnIKBuqTTZGth6Hvr6TN55ptJlZMDG49/1ArEEgEgaz9qhRBiNnY6lOocTQeN1KuFjPZZV+NSQumfWXzlzH19FnDrhY9Ncu8b6sDYkatX20ysyFVtdss922mFmI3Hu+2BsQd1RaZGR0iVPHP30ssbamU+NXosBrIHeaM8Q/eJ7aOkQjtr7aEynDbtmy8eP2GkkmZ7/1lyrG7dVWPhX0abzPeKaJ0tiUi+quaktfA1u6lkiUC0eI7cRy9lfSJL3BwjYo6vsozIxu8SnLsnDTlCegCo3E3+1ucxfX9cVoukc4MLddR7eP2+kaMsouOi+w/rToyMVYWI/YAzDUSO7lGvOtGcy6QXOQVbAbvsJ9HlMjEC4Jve4p0ZDZhatF0XEMbjLsj51YclhWEXvYXoIo1Kvsp4Y3HSUHjtqTQNsbeDfrX0aa9sBoaJOex7xQ0KfcB40NAl3oPH+VD+z32utD+z98n/H+dD+z02u3soaBFtLnxFNoEZHRLKeOdSaJKmdsQ3rn9orFWDKbEVBMJUuMiOsPt5oUlWza9jbRUsLxNhYdx2H9jikdG6Gs7Nd6haVlvIoXx+X1yAcjnXNpYDCMtX7HJBHJ1lF94yNDQIwblmI3UsEQFgi+IvTaJA3Zt6ptTf2evZcjvzptAlGoq3uptHmXWjeGfwogjI5ckWjSSC4Fhvah/Z77WWh/Z++T/j/OodEETYg7fI/sEkayLhYXFTaLIjWALqdRAv7a5mXzH/hNEEawR3/bAEmwFzUWgu2bnAN22ooUjFlGfnbf8jKg6wD30dGhJvgX+uH7Pa+ujDEdaKfCjokB7Nu4mjoEexmHvo/2eey/tFHQZhqwt3Gjosw1ofDP4UmiTN2cPrZUn9njtv4L+tfQoLWse+9N/Z6dl2HfnTaA/ZZT35U2iTjs37jUWgu2chwDdtqOGOMdBbcdv/Xv/8QAPxEAAgEBBQUEBwcDBAIDAAAAAQIRABIhMUFRAyIyYXEQMJGhIEJSgbHB0QQTQGJyguEzUPAjQ2CykvEUg6L/2gAIAQMBAT8A/wCVwYJyHogD7snMMPD/AIMGgMPaHoQLE52o91LwOOh8+xVmfyia2XGDoCfKseyDExcP+A7KLazffFG4kdqYP+j5igpIJ9nGgSJjO6tlxjwpLmU/mFNxN1NA/wCk3NhQBJAGdEQY/vUCyDN84UFlWaeGLutBZRm9kjzq0LAXRiaMfdqYzIoGCDpW0X/UIGZu99Aj7tgcbQr/AGh+s/Ctn636DSkBXBzAjxoKSCRgMa2fGv6hRxNBd0tOBF1f7X7/AJVsv6i9aOPaV3A04mIpVLE8hPh2Itox7z7v7hZFi1mGg9gWVYzwxd27Lis+2LP0rZ4lT6ws+/LsF+zYeywPjd2MYdGPsqaOJof0zyceYrZ8X7W+HYGIBHtfKtnxr+oUcT17MEWbxbPyrZn/AFFOG9QuTadQOwkEAREedFiQBkvzpbkc6wvYCRMZiOwqQAfawraQCFHqiPfn2EAKPaa/oKAJIAzoiCRj/aIONbO+U9sR78qvB6UGDbQ5DaCD1P8ANG67sEgg1tLntD1oce+r2bUsaT1x+U+XYSTjSAENI9WRS8D/ALTWy4+ob4dhs3Rpf1pVsuk5lT403EetbQANAyUfCm4EH6jWzB+8UH2hR/p/qf4VZNm1lMVZFi1najsjcVfbaflUANBN0waYgsSBAm6gCSAM6LKNoPZS4e76mhebzjiaGOtRKl2zMDrS7qlszur8/wCxQYmLj6AClT7Qv6jsQAsATANEkCycjUEQcJvFFbTKcPvPjnWB6VteK0PXFqtobVl9RB6iixIUezTX7NT7JK/OlkENkD8KiNqRra8xSqWMCkW00TFbPiI1Vh5UmDj8s+BpLIbZkYzveNIBaYHJW7EJLrPtLRvY9a2v9RutEgsgF8BRQM7Ynm3lTf00HNjUmIyFPcqLytePY5KsAPUAH1pVLGK2SghnbBBhrWzutP7Iu6nDsUA4mLqUEkAZ05BMDBbh9aZpgAQFECisKqgbzb307NmBezcK+ZyHY26tjNr2+Q/GoQQUJibxyPokARBm7sfeAf3N1/mpJAGQwpb0IzQ2h86aTvn1iax2f6D5GkvVl/cOootIAjhpb0caQ1Lfs3GhDfKtmf8AUWdQK2d20HWKvB6VsuNetbPFh+VqXiHUUTZdv3CtmASZ9kmtnxr+oUeP91MtraODdFo+FbPjX9QpONjoGNKpYwOxltbWxMQI8BWzEuBznwq93/UfjRcAvA4t0dKF2zY+0QPC+rMqiDF94/KgstA8as7to5m750NxZ9Z8OQ/ns2agmTwreatsXtDFrvGipDWcTMXU5AhBguPM0gA32wGA1NEkkk5/j2UrE5iaK7oYGcjy7GUqYNFiVC6YVsyJsnBrvoaVRaKtzX30hhxPQ9DjTSJQ5GkNmQ1wZf8A1SmywOhpxZYgdR0pXDbTS2LJrZg2mTVSPeKUwwOhp7toeTVtRDt1nxoiwy3zg1KI2xHMjs2kF2IvkzWzxP6W+FbPjX9QoteVj15r/df93wrZDfXrSYbQ/l+JrZXWjohrZiXUfmFA37RuR/8A0a2frHRD9K2eJb2VJ7GF6bMf4Wp23yRhwjpXCk5v/wBf5pBaN/Cok9KBknaNlgOeQoNCn2nx5D+abdQLm283yFJugv7l6/xQYgyKGN9MxYz4DSmUqYP4kAmYGF57GWAGBkH46dhUWQw6N17F3ls5i9fmKRgDB4WuP1plIMGjvLOaXHpRXdDDWDyoqCgYYgw3yp7wr63HqKe+H9rHrnW0vsv7Qv6imIKL7S7vuy7GvRW9ndPyqyQA2p+FbQ7wdTxC1dkc+za8ZOt/jW0ILSNB8K2mCH8g8q2hIcMMwGFAAq7HEEedbMgMCefwrZcY99bPjX9Qr/c/d86H9Y9WrZcYOgJ8qXgf9vxpSBs31JArZcY5SfCg0KyxxR5UCAjDNiPCluRzrZFItpgKDi075+r76RbTAHDPpTtaYnw6U26gXNt49MhRJIAyFAwZ0q925sadgTA4VuH1oqREjiwplKmDQFiGbi9VfmaM4nO+mWLpm78QrFTI7FaAVOB+OXYpg38LY0RZa+8fEUSA0rdfdTWSpaOLyYfI0pLC7iS9elW9+1HUfGgArFTwbQXH4H3Uu6xDYcLUZW0hvvn+RSXhk1vHUUm8rJ+4e7HsUzs3XSGrZXkp7Q8xhVo2bOUz2MAdmpAiCVNPeEOqx4XVZFkzcwOFN/TQ/qFILTAEmp7Nl/UXrQJBkXRSkW1i68TJpf6xzvagWW8XTIoNCldT8KAJmMr62frHRD2gkiyM2mlFlXY4jd95x7F3ULZtur86RQWvwF56UJd77pvPIUd5oUch2KpAgce08l/mlUCWbBfM0HNq0d486W7fa/2eZoNvS29rTMWMmlSQSTZUZ/hysQfaE9iNZN4kG49KZbJ1BvB5URBiZoG0tnNeHpmKXeFg4+qflQuN4wN4qAGK+ptBcfh4VeraFTTWSwIwN5GmtFbimMbyHUUSGWSd5buoriSc0/6/xVvdWOJWu6UdzaBhgYYdDQK25jdnDlWzItxk277jTAgkHLttGyV1M0b9mp9liPG+ttxk6gHyoX7I/lYHxqLKqwN5ntRrLAnI0ACHOl48as7tr81mgSLxdUmAMh2IYRzrC+NJwOeg8TQAsMTjIApAIZiJhfM0pIIIyotuBedo0WJjkIokmJOFwoMQCNca4U5v/wBaVoDatdPLOkAvY8K+egoFmY38WJ5U7TcOFcKVQZZuEefKmYsfICmUrAOOmlKoi01y/HkKLWiBwjADIU1mYXLPX8Ob1DLcVuaPI05DQ3resPnSGRYOfCdDSmy14wuINGA26c7qYSLY/cNDTsGg+t631pd5SuY3l+YqS7CTjdNEQYOVK27jDIZX6VtAJDDB7/qKj7t77wR4g1Y37E44H4VFpCPW2fw/jsCkgkerj6ABNw7JJxpWhWWOKPKj/SH6z6AJAIBxxqTEZeha3bPOaw2Q/M3w7MNmPzN5ClW0wGtBQbUnhF3PSgYVh7UVu2TdvT5Uq2mAq53JNyqPIUBJgU5AhBguPNqbdWz6zcX0orgg6sf80p2BuHCuH1q8EHDMUF9dzd5mr3kndVfAcqgxOVItrkBiaMTdhzoiDEz+DRrJ1BuIplg6g3g8qEoQTmPEGmWDIwN4oISpYerjrTb62vWHF9ewiwQy7yt56g06xBF6nD6UIs2hcyGeopwLmGDeRzFM1oznF/Oioshgc4NIQQUJ5rOtFpUKRgbjy0riSfW2f/X+KJJJOuNLejLpvD51szvQcG3T76AEOp4h8se1WssDp2R2Wt2z+a13B7CxIAyGFEghQMhfW0xCD1QB762sWrI9UWfQUgKxzO6PnV9JugvngvXX3UgjfOWHM1ex1JpiFFgfuOtIoi23CPM6VxG2/DpryFCXMm4DHQCmeYAEKMBV7mTuqvgKJtQqi7Ia8zREGDlVkxay/BlSDBpSCLB9x0NGRccsqUiLLYHPQ1vISMDhSsVM/wCRTqAZHC14pWAuPCcfrQ3TYa9Wz+BFFDaIF8aVs75Q4Nh+qlNlobDBqsG1Zz7BZgzjl6LtaMxFMtkxjnPd5+mrD7y22s3UBIdjl8TQ7ACTA7C0qFwA8zRa0VB3QLqdpMC5VuFK1kGMTdOgpEm83KMT8hUhzJ3VXActBV7nRR4KKdgd1blHnzNIoMkmAMf4okuQqiFGXzNFgohPe30oY60QTDbQwMlHyFMZMgBeQogjHrQUwWyHeqJMTFEEGDlRXdDAzryNEk40FBWRiMRy1rjH5l8x/FABhA4h5j60N8WfWHDzGlIoJsm6cOtAgBkb3cm7Fgiw13snQ0CUbQqaJvkXU5DQ2ZG91riWRxbP4fxRMkk5+hPazAqozW7vbBs2jcMufpSYjtUgBj62A9+J7Fs32jl59jFboGV/WlEkCY5mnaYUXKuH1rdICi7VjTsOFeEeZ1plCiCd7QYClWb8AMTRYHdW5eefWoB3EFrVv8wFSqYbza5D60AW3mMDX6U0TuiBShbyxuGWZpnLcgMAMB3zNaAkXjPWlaydQbiOVMtkgi9TeKJCsGQ8+nKiMHS4f9TRvFtbvaAyOtNeLYuPrddabfFscQ4h86O+s+suPMa1ZhQ4P8Gma0Zi/Oma0BI3hidaYgqMmF3UdisVMj08vQPcLsHZbQFxpNiBL7SQq6iJraOXacBkNB+BsmLWVKs3tco/yBTNN2AGAoqQASInCgTgJvyqFXi3m9n60zFsatCzAHU59jBRcDJ1y76JwoY6VEE7Njc2B+BoggkHEUrWTqDiNRRlDKm5hdzFI1k6g3EcqdbJuvBvB5ULS2XFEydJOFEFTBxFEgkkCOVQIBm/T0m2TqoYjdImewKWMAFjyraqERUI3uI+/wBDZ/ZpQs1xxHZ9o2QUKy3SPQVrJBAmMedbNdmzgi9bEwb75pyQhIyE1ttudoAIiMevdbEKXFqLPOj9m2TXiR0NN9kb1WB63UdPRBBO+TCi4fKmYsdAMBpQMGYnrQDOSSepNWgLk/8ALP3aUyhRvcRy069isFFw3tdKKGLTGCb4OJ75SAbxaFHdMqZzBphaFsfuHPWlNoWD+0/KotiDxp5gfMUGhSpEg+RpSOFuE+R1pgAtkjeBx1FWjZs43yOVASCZiL417GYMoJ4hd1HcbHbpYCvlu9RQ+zbIHD3TQsruiByrbbQs0GN0kXdovIFQIjKl2aLgoFfaVnZn8t/arFTaGVbAh9qZAAZTcMKH+htYPC2dESCNRR7sMReDHSvv9pINo3egi2jHvJ5UyFccxPj2MFEQZ1pb1v3VBvOZ5UzTcLlGX17LAF7/APjmfpRl8AFVfAVaVeHePtH5CrybzjrTAA3Ge7jOlCkgMYFMpUkGhEiTAogo2vwIplEWl4T5HSuMT66jxH1pVBDXwwvFFiVAPq4GiwKwcVwPLSiBAg9aVWYwoJ6UylTBEdfRXZsyFhfZxGfX0fvHkGTK4U+0ZjJN/oIQGBOAM0ftZNyL41a+0tgCPKjsducfjTAgkHLt+zkDagkxW2CM6gkQQwnTSk252YKNvWeEijeZ7D3yEqMJDXnoDW0JKg+1f78+wIALT3DIZmmYtyAwAypQCCACzHDQc6kLcu82uQ6VZAv2hk+zn763nwEKPcooKSYAmmAEAGTnGFWIvc2eWfh3aNFxvU4ijyoEMLJxHCflS2QSHB+YpWEWWwyOlDcaGvVseY1FTBkZYUwtC2v7hofpSML1bhbyOtHHXs+zsq7KQLyY5k5Vt9oDcd5vJeQ9H7KoCFqN9D0IJr7trJbIZ9v2dA7wcMaCqtwAHTt+1JDz7Q73askKqRcLzESe5VyuHgbxTGeySTJM9iyVsqI9o0DG7s95va+lbq477eX81Ibedrhgo/y6ryL42afH61bC8Aj8xx/ioJFrzoiM56d3BiYuqBEz7qG9utc3qk/A0qiSrbpy686UjgbDI6Gl3GhhIwPSpiYJg/D0EcqDGeenT0D2W/u9gF9Z/gc/R2f2YEBi2Im4UfuExhjz3jW2+0bNkKLJnw7dltChkY0v2gRLKRzF4o/aFyDN7oHiaf7S5wNnp9abaO1zEmPTFFeEFdntLWEXHyr7pbwV2gIMGLxTbNReHnkVI/ALZnemOVGWEtuJkB8taL3QosjzPWgBBJMcs6AkwM6IIMHKlIBki1TMWx8Mu8UMZC+FAEmBiam1uPcwuUn4GuLca5hcCfgasiCG3WGvwq3u2SJjhOnpojOSFvIE0QQYNx7BE30zFjJ9H7x4C2jAy9IMwwJHSmZjiSevdByMgeopdvGRWfZb5GabbhlItm+6GX5ijEkDIns2WxZ5IuGpp1AYgG0Bn6KfZyQCxCWsNTW22J2Z1BwPpKVAmLTc8B9aDAkl5Y5aUTJnsVSxgUQAYmelQWGSJ/njTWZ3Z9/dhipkGDVozIuON1EWxaHEOIfMUxJ3iZm7ndXGI9ceY+tEs15vjuPsxjajmCK+0tsohhL5RiPwey2RcmIu1Nf/ABG1B8q2qhTZAI1kzQSb5UUdmRz8fTZmVFUEhWEmthsy9qIujE69KdSrEHI+h9lUs1o3hMOpplVhDAHrR2ezUFrK3X4V9oAbZK9myfrQ9AtIgCyP8z7VVjMYZ6VKLhvnnh4VvOc2NEQY7tbODZ56VejAkA+YNMIh0uHwNEBhaXEcQ+YoAFZW5kvPMa0Gxcf/AGL8/RRSzBRnTqVMHp2AkEEGCO7JGtFhGtWjqBVozeaLaX0C3OrRAq01BjrX3kUNu4Mgt40dqXMm81s9rs1EOrH30dtsCICfKmgm4R6W02luzdZsiLqV2WbJiakn0NklhAuefXs+0vZSzm3wpto7CGMj04ONFiQATcMqsECW3dBmaUOQQsxn/JqEXHfPLDxomTOHcrEi1hyphBIBnnVowFyFKIUsL8mXlRFmHQ3f5caZQRbXDMaH6ULSWXGf+QaYySQI5dgvupfs7sA0izrNfZ1QA2ZnU/KvtOzkWxlj9fTJAxouKtdBVo6+FSOZqeVGpPbJ1qak61aOpokmhU6SKnmD1q/LyqdaB0PjUtlHdK1khtDWz26PdwnQ0TF5ra7S25OWA7g3gWjZXJFoKThhqaJUGT/qNqcP5pmY4n3ZUqEicBqcKIANxnuwwICNdGDadaIZSRh9KViuI3WxGooyh3TKuPL0fszWlZff440oYPYUWVBnlEVtdoqKSc8Br6bi6e9g6VZOnpKJNDvPvXs2bV3bsVDbRQRIrawSWs2OXaLN9onlFAEmAJqyq8W83sj5mmn1zZ0UfTKghaSBC6nCmCjA2tbqsmJi7vCpADZGgZAV8PVbT+Ka0BYOV4/j0vs+0CPJwIitp9pBIsDhOJ+lMzMZYz3BAOIqyulWV0qyulFJwqw2lBDnVkaCoGgqytWBqasxhUczUUVBqwKsLzqxoasamgAMPwGy2thWAxaL+VbRgYvtc+0rAkkX5Z0jXEWrION1/ShMbgsD22+v0qUXDfOpwpmLYnsZixkme7UrBVrpwbT+KKkGDdzy61Nnce9ciMuYokqLLC0PVP0qDExdr2XVsvs+4wYXt76dbLEaHL+2DsKowm0o2YA687te0RN+FWJv4F/NVpV4RPNvpRYkyTNSMEWSczefCrKrxGT7K/XsZixnyHdytmI3pxpWEWWvHmOlMscwcCKO7uneRrwR8RRLKCOJW8P/AHSoCJBkjFeXKnUcS8J8jpX320s2bVw/uEdgMGYB60zFjJM0CLgqy3O/wFWFXjP7Vxo7QxCiyOXzNKjG/Aam4URBjGmduGLHIfPulQtMZUSTF3CKVgVsNgMG0oShgiVOWR5igoZiE91rGlaDZbhOI+dTDSpwNxoMwmDxY9xaXWrS0X0FB9RUzhf+GJAxouchQMgd+LMCAXY+AqwF4z+0Y0doYhRZHL5mgVAgC84k5dBUoLlFs6nDwpgcdo37Rj/FKSbtmsc8/GiFF7NaOi/WmacAF6dyCQZFK8mZsvr6rdaiGhx1q0Bu8afCmQrDAyuR9NpwFAMMKM59xJ17ASMKBkT30jWi+gq2aJJx7ASMDUnWg5m/t61I1q3yq2NPOrZq02tWjrQY0GB7FYkWVhLrz/PYApEAMzHwFWAOM+4XmhIF0bJdfWNOsQRgcJxpUZsMMzlS2BJaTyGHjTGTIAXkO6IV+EWW0yPSixgKfV8elFSAGyPoEgY0WFFzldQYzGPae7TA92SBjRfS6p7lXgQaLnK7ulabj2QFYTDdDRtReRs10/jOrYXgH7mxqwzbzGBq1CzMKDtG54eFMR65mPUXCjtCRAAVdB3bFSJAstoMKDBrmxyb60Syys8jp6BAONWRUA431ZHY5gd2BB1Pl6RYg3irZ0FAk3+gRNWBzoADkKMTd+EVsjQJBkZVxHePvNWlXhEn2m+lEAX7RrR0HzNFzEDdGg+ZoqoHFJ0X61uRiSfLviwHOiScaVsvwCC/p6ZvqBoPSJgTRYn8MrZE9hs2QADazNKwAuW03P6VYi9zZ5Z+FWkwswNcWpiCbhApjJuEdO9fL0AxHOgwPLtIg9xZOlWNaHSB3zGT0/EK2RpWKmRSFzIXO8n+ahBxEufy4eNM04AKBp3pYCrZ9MEaeFFuXogUF5D331HelgKtnSi5yuq02tFiRHoR3ogX41INxuoqR6atIjvzhR78MOlSNey6rS61I1qR3DcR7QCcqsNpRBGNQdKkDKrRq0MxRjL8KrT179lmiI/CAy09wymTRBGNKuZ9GKIBoppVk6VZOlWDVg1YOtWOdWBrVgVYFFSO9Bkd+RNER+DBIwoTmPxZANWBUDSrIqxzqwasnTtCk1YNWOdBYz/AETRUioOn4AIc6AA/slkafiIGlWRVgVY51YNWTpQU0E1NWRVgVYNWTQQ50AB/z3//2Q==" /> <p class="version"> <strong>Rails version:</strong> <%= Rails.version %><br /> diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 0f9bf98737..81537d813e 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -12,7 +12,12 @@ require "rails/generators/test_case" require "active_support/testing/autorun" if defined?(ActiveRecord::Base) - ActiveRecord::Migration.maintain_test_schema! + begin + ActiveRecord::Migration.maintain_test_schema! + rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 + end module ActiveSupport class TestCase diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb index 8decdb0f4f..5d0cf1305f 100644 --- a/railties/lib/rails/test_unit/minitest_plugin.rb +++ b/railties/lib/rails/test_unit/minitest_plugin.rb @@ -6,7 +6,7 @@ require "shellwords" module Minitest class SuppressedSummaryReporter < SummaryReporter # Disable extra failure output after a run if output is inline. - def aggregated_results + def aggregated_results(*) super unless options[:output_inline] end end @@ -134,7 +134,7 @@ module Minitest end end - mattr_reader(:run_via) { RunVia.new } + mattr_reader :run_via, default: RunVia.new end # Put Rails as the first plugin minitest initializes so other plugins diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb index 9cc3f73a9c..443e743421 100644 --- a/railties/lib/rails/test_unit/railtie.rb +++ b/railties/lib/rails/test_unit/railtie.rb @@ -1,7 +1,7 @@ require "rails/test_unit/line_filtering" if defined?(Rake.application) && Rake.application.top_level_tasks.grep(/^(default$|test(:|$))/).any? - ENV["RAILS_ENV"] ||= "test" + ENV["RAILS_ENV"] ||= Rake.application.options.show_tasks ? "development" : "test" end module Rails diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb index fe11664d5e..1cc27f7b6c 100644 --- a/railties/lib/rails/test_unit/reporter.rb +++ b/railties/lib/rails/test_unit/reporter.rb @@ -3,8 +3,7 @@ require "minitest" module Rails class TestUnitReporter < Minitest::StatisticsReporter - class_attribute :executable - self.executable = "bin/rails test" + class_attribute :executable, default: "bin/rails test" def record(result) super diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index ef19bd7626..33408081f1 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -48,6 +48,7 @@ namespace :test do Minitest.rake_run(["test/controllers", "test/mailers", "test/functional"]) end + desc "Run system tests only" task system: "test:prepare" do $: << "test" Minitest.rake_run(["test/system"]) diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 76de2b4639..2df303750c 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index e4b2d0457d..2d4c7a0f0b 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -12,7 +12,7 @@ require "rails/all" module TestApp class Application < Rails::Application - config.root = File.dirname(__FILE__) + config.root = __dir__ secrets.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" end end diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index 72f340df34..057d473870 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -136,9 +136,9 @@ class FullStackConsoleTest < ActiveSupport::TestCase assert_output "> " end - def spawn_console + def spawn_console(options) Process.spawn( - "#{app_path}/bin/rails console --sandbox", + "#{app_path}/bin/rails console #{options}", in: @slave, out: @slave, err: @slave ) @@ -146,18 +146,26 @@ class FullStackConsoleTest < ActiveSupport::TestCase end def test_sandbox - spawn_console + spawn_console("--sandbox") write_prompt "Post.count", "=> 0" write_prompt "Post.create" write_prompt "Post.count", "=> 1" @master.puts "quit" - spawn_console + spawn_console("--sandbox") write_prompt "Post.count", "=> 0" write_prompt "Post.transaction { Post.create; raise }" write_prompt "Post.count", "=> 0" @master.puts "quit" end + + def test_environment_option_and_irb_option + spawn_console("test -- --verbose") + + write_prompt "a = 1", "a = 1" + write_prompt "puts Rails.env", "puts Rails.env\r\ntest" + @master.puts "quit" + end end diff --git a/railties/test/application/current_attributes_integration_test.rb b/railties/test/application/current_attributes_integration_test.rb new file mode 100644 index 0000000000..5653ec0be1 --- /dev/null +++ b/railties/test/application/current_attributes_integration_test.rb @@ -0,0 +1,84 @@ +require "isolation/abstract_unit" +require "rack/test" + +class CurrentAttributesIntegrationTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include Rack::Test::Methods + + setup do + build_app + + app_file "app/models/current.rb", <<-RUBY + class Current < ActiveSupport::CurrentAttributes + attribute :customer + + resets { Time.zone = "UTC" } + + def customer=(customer) + super + Time.zone = customer.try(:time_zone) + end + end + RUBY + + app_file "app/models/customer.rb", <<-RUBY + class Customer < Struct.new(:name) + def time_zone + "Copenhagen" + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get "/customers/:action", controller: :customers + end + RUBY + + app_file "app/controllers/customers_controller.rb", <<-RUBY + class CustomersController < ApplicationController + def set_current_customer + Current.customer = Customer.new("david") + render :index + end + + def set_no_customer + render :index + end + end + RUBY + + app_file "app/views/customers/index.html.erb", <<-RUBY + <%= Current.customer.try(:name) || 'noone' %>,<%= Time.zone.name %> + RUBY + + require "#{app_path}/config/environment" + end + + teardown :teardown_app + + test "current customer is assigned and cleared" do + get "/customers/set_current_customer" + assert_equal 200, last_response.status + assert_match(/david,Copenhagen/, last_response.body) + + get "/customers/set_no_customer" + assert_equal 200, last_response.status + assert_match(/noone,UTC/, last_response.body) + end + + test "resets after execution" do + assert_nil Current.customer + assert_equal "UTC", Time.zone.name + + Rails.application.executor.wrap do + Current.customer = Customer.new("david") + + assert_equal "david", Current.customer.name + assert_equal "Copenhagen", Time.zone.name + end + + assert_nil Current.customer + assert_equal "UTC", Time.zone.name + end +end diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb index 206e42703b..cee198bd01 100644 --- a/railties/test/application/initializers/i18n_test.rb +++ b/railties/test/application/initializers/i18n_test.rb @@ -245,7 +245,7 @@ fr: end test "[shortcut] config.i18n.fallbacks = [{ :ca => :'es-ES' }] initializes fallbacks with a mapping ca => es-ES" do - I18n::Railtie.config.i18n.fallbacks.map = { ca: :'es-ES' } + I18n::Railtie.config.i18n.fallbacks = [{ ca: :'es-ES' }] load_app assert_fallbacks ca: [:ca, :"es-ES", :es, :en] end diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index 959a629ede..a14ea589ed 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -162,6 +162,11 @@ module ApplicationTests end RUBY + add_to_config <<-RUBY + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true + RUBY + require "#{app_path}/config/environment" get "/foo/write_session" @@ -171,9 +176,9 @@ module ApplicationTests get "/foo/read_encrypted_cookie" assert_equal "1", last_response.body - secret = app.key_generator.generate_key("encrypted cookie") - sign_secret = app.key_generator.generate_key("signed encrypted cookie") - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret) + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) get "/foo/read_raw_cookie" assert_equal 1, encryptor.decrypt_and_verify(last_response.body)["foo"] @@ -209,6 +214,9 @@ module ApplicationTests add_to_config <<-RUBY secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" + + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true RUBY require "#{app_path}/config/environment" @@ -220,9 +228,9 @@ module ApplicationTests get "/foo/read_encrypted_cookie" assert_equal "1", last_response.body - secret = app.key_generator.generate_key("encrypted cookie") - sign_secret = app.key_generator.generate_key("signed encrypted cookie") - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret) + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) get "/foo/read_raw_cookie" assert_equal 1, encryptor.decrypt_and_verify(last_response.body)["foo"] @@ -264,6 +272,73 @@ module ApplicationTests add_to_config <<-RUBY secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" + + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true + RUBY + + require "#{app_path}/config/environment" + + get "/foo/write_raw_session" + get "/foo/read_session" + assert_equal "1", last_response.body + + get "/foo/write_session" + get "/foo/read_session" + assert_equal "2", last_response.body + + get "/foo/read_encrypted_cookie" + assert_equal "2", last_response.body + + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) + + get "/foo/read_raw_cookie" + assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"] + end + + test "session upgrading from AES-CBC-HMAC encryption to AES-GCM encryption" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get ':controller(/:action)' + end + RUBY + + controller :foo, <<-RUBY + class FooController < ActionController::Base + def write_raw_session + # AES-256-CBC with SHA1 HMAC + # {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1} + cookies[:_myapp_session] = "TlgrdS85aUpDd1R2cDlPWlR6K0FJeGExckwySjZ2Z0pkR3d2QnRObGxZT25aalJWYWVvbFVLcHF4d0VQVDdSaFF2QjFPbG9MVjJzeWp3YjcyRUlKUUU2ZlR4bXlSNG9ZUkJPRUtld0E3dVU9LS0xNDZXbGpRZ3NjdW43N2haUEZJSUNRPT0=--3639b5ce54c09495cfeaae928cd5634e0c4b2e96" + head :ok + end + + def write_session + session[:foo] = session[:foo] + 1 + head :ok + end + + def read_session + render plain: session[:foo] + end + + def read_encrypted_cookie + render plain: cookies.encrypted[:_myapp_session]['foo'] + end + + def read_raw_cookie + render plain: cookies[:_myapp_session] + end + end + RUBY + + add_to_config <<-RUBY + # Use a static key + secrets.secret_key_base = "known key base" + + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true RUBY require "#{app_path}/config/environment" @@ -279,9 +354,9 @@ module ApplicationTests get "/foo/read_encrypted_cookie" assert_equal "2", last_response.body - secret = app.key_generator.generate_key("encrypted cookie") - sign_secret = app.key_generator.generate_key("signed encrypted cookie") - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret) + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) get "/foo/read_raw_cookie" assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"] diff --git a/railties/test/application/per_request_digest_cache_test.rb b/railties/test/application/per_request_digest_cache_test.rb index 6c003e9bcc..6e6996a6ba 100644 --- a/railties/test/application/per_request_digest_cache_test.rb +++ b/railties/test/application/per_request_digest_cache_test.rb @@ -18,6 +18,10 @@ class PerRequestDigestCacheTest < ActiveSupport::TestCase class Customer < Struct.new(:name, :id) extend ActiveModel::Naming include ActiveModel::Conversion + + def cache_key + [ name, id ].join("/") + end end RUBY diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index 23b259b503..8e0712fca2 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -469,7 +469,7 @@ module ApplicationTests def test_run_app_without_rails_loaded # Simulate a real Rails app boot. app_file "config/boot.rb", <<-RUBY - ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) + ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. RUBY diff --git a/railties/test/code_statistics_calculator_test.rb b/railties/test/code_statistics_calculator_test.rb index 1bd4225f34..25a8a40d27 100644 --- a/railties/test/code_statistics_calculator_test.rb +++ b/railties/test/code_statistics_calculator_test.rb @@ -317,7 +317,7 @@ class Animal private def temp_file(name, content) - dir = File.expand_path "../fixtures/tmp", __FILE__ + dir = File.expand_path "fixtures/tmp", __dir__ path = "#{dir}/#{name}" FileUtils.mkdir_p dir diff --git a/railties/test/code_statistics_test.rb b/railties/test/code_statistics_test.rb index 965b6eeb79..e6e3943117 100644 --- a/railties/test/code_statistics_test.rb +++ b/railties/test/code_statistics_test.rb @@ -3,7 +3,7 @@ require "rails/code_statistics" class CodeStatisticsTest < ActiveSupport::TestCase def setup - @tmp_path = File.expand_path(File.join(File.dirname(__FILE__), "fixtures", "tmp")) + @tmp_path = File.expand_path("fixtures/tmp", __dir__) @dir_js = File.join(@tmp_path, "lib.js") FileUtils.mkdir_p(@dir_js) end diff --git a/railties/test/commands/secrets_test.rb b/railties/test/commands/secrets_test.rb index 00b0343397..be610f3b47 100644 --- a/railties/test/commands/secrets_test.rb +++ b/railties/test/commands/secrets_test.rb @@ -18,11 +18,12 @@ class Rails::Command::SecretsCommandTest < ActiveSupport::TestCase end test "edit secrets" do - run_setup_command + # Runs setup before first edit. + assert_match(/Adding config\/secrets\.yml\.key to store the encryption key/, run_edit_command) # Run twice to ensure encrypted secrets can be reread after first edit pass. 2.times do - assert_match(/external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289…/, run_edit_command) + assert_match(/external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289/, run_edit_command) end end @@ -30,8 +31,4 @@ class Rails::Command::SecretsCommandTest < ActiveSupport::TestCase def run_edit_command(editor: "cat") Dir.chdir(app_path) { `EDITOR="#{editor}" bin/rails secrets:edit` } end - - def run_setup_command - Dir.chdir(app_path) { `bin/rails secrets:setup` } - end end diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index 2d1f071969..722323efdc 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -140,6 +140,18 @@ class Rails::ServerTest < ActiveSupport::TestCase end def test_argument_precedence_over_environment_variable + switch_env "PORT", "1234" do + args = ["-p", "5678"] + options = parse_arguments(args) + assert_equal 5678, options[:Port] + end + + switch_env "PORT", "1234" do + args = ["-p", "3000"] + options = parse_arguments(args) + assert_equal 3000, options[:Port] + end + switch_env "HOST", "1.2.3.4" do args = ["-b", "127.0.0.1"] options = parse_arguments(args) @@ -153,6 +165,12 @@ class Rails::ServerTest < ActiveSupport::TestCase server_options = parse_arguments(["--port", 3001]) assert_equal [:Port], server_options[:user_supplied_options] + + server_options = parse_arguments(["-p3001", "-C", "--binding", "127.0.0.1"]) + assert_equal [:Port, :Host, :caching], server_options[:user_supplied_options] + + server_options = parse_arguments(["--port=3001"]) + assert_equal [:Port], server_options[:user_supplied_options] end def test_default_options diff --git a/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb b/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb index 21b0ff6c28..701515440a 100644 --- a/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb +++ b/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb @@ -1,5 +1,5 @@ require "rails/generators" class UsageTemplateGenerator < Rails::Generators::Base - source_root File.expand_path("templates", File.dirname(__FILE__)) + source_root File.expand_path("templates", __dir__) end diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index 360e8e97d7..03b29be907 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -278,9 +278,12 @@ class ActionsTest < Rails::Generators::TestCase end def test_capify_should_run_the_capify_command - assert_called_with(generator, :run, ["capify .", verbose: false]) do - action :capify! + content = capture(:stderr) do + assert_called_with(generator, :run, ["capify .", verbose: false]) do + action :capify! + end end + assert_match(/DEPRECATION WARNING: `capify!` is deprecated/, content) end def test_route_should_add_data_to_the_routes_block_in_config_routes diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb index 2edb39c8e8..a19e0f0dd8 100644 --- a/railties/test/generators/api_app_generator_test.rb +++ b/railties/test/generators/api_app_generator_test.rb @@ -70,7 +70,6 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase assert_no_file "config/initializers/cookies_serializer.rb" assert_no_file "config/initializers/assets.rb" - assert_no_file "config/initializers/new_framework_defaults_5_1.rb" end def test_app_update_does_not_generate_unnecessary_bin_files diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 8a51beb380..8a8c9a35ce 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -157,7 +157,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_new_application_doesnt_need_defaults - assert_no_file "config/initializers/new_framework_defaults_5_1.rb" + assert_no_file "config/initializers/new_framework_defaults_5_2.rb" end def test_new_application_load_defaults @@ -203,14 +203,14 @@ class AppGeneratorTest < Rails::Generators::TestCase app_root = File.join(destination_root, "myapp") run_generator [app_root] - assert_no_file "#{app_root}/config/initializers/new_framework_defaults_5_1.rb" + assert_no_file "#{app_root}/config/initializers/new_framework_defaults_5_2.rb" stub_rails_application(app_root) do generator = Rails::Generators::AppGenerator.new ["rails"], { update: true }, destination_root: app_root, shell: @shell generator.send(:app_const) quietly { generator.send(:update_config_files) } - assert_file "#{app_root}/config/initializers/new_framework_defaults_5_1.rb" + assert_file "#{app_root}/config/initializers/new_framework_defaults_5_2.rb" end end @@ -412,13 +412,6 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_generator_if_skip_yarn_is_given - run_generator [destination_root, "--skip-yarn"] - - assert_no_file "package.json" - assert_no_file "bin/yarn" - end - def test_generator_if_skip_action_cable_is_given run_generator [destination_root, "--skip-action-cable"] assert_file "config/application.rb", /#\s+require\s+["']action_cable\/engine["']/ @@ -475,7 +468,7 @@ class AppGeneratorTest < Rails::Generators::TestCase if defined?(JRUBY_VERSION) assert_gem "therubyrhino" else - assert_file "Gemfile", /# gem 'therubyracer', platforms: :ruby/ + assert_file "Gemfile", /# gem 'mini_racer', platforms: :ruby/ end end @@ -524,6 +517,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_generator_for_yarn_skipped run_generator([destination_root, "--skip-yarn"]) assert_no_file "package.json" + assert_no_file "bin/yarn" assert_file "config/initializers/assets.rb" do |content| assert_no_match(/node_modules/, content) @@ -569,9 +563,9 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator assert_file "config/environments/development.rb" do |content| if RbConfig::CONFIG["host_os"] =~ /darwin|linux/ - assert_match(/^\s*config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + assert_match(/^\s*config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) else - assert_match(/^\s*# config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) end end end @@ -639,7 +633,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile" do |content| assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) - assert_no_match(/\Agem 'web-console', '>= 3.3.0'\z/, content) + assert_no_match(/\Agem 'web-console', '>= 3\.3\.0'\z/, content) end end @@ -648,7 +642,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile" do |content| assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) - assert_no_match(/\Agem 'web-console', '>= 3.3.0'\z/, content) + assert_no_match(/\Agem 'web-console', '>= 3\.3\.0'\z/, content) end end @@ -787,7 +781,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_psych_gem run_generator - gem_regex = /gem 'psych',\s+'~> 2.0',\s+platforms: :rbx/ + gem_regex = /gem 'psych',\s+'~> 2\.0',\s+platforms: :rbx/ assert_file "Gemfile" do |content| if defined?(Rubinius) @@ -870,7 +864,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_gem "spring-watcher-listen" assert_file "config/environments/development.rb" do |content| - assert_match(/^\s*config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + assert_match(/^\s*config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) end end @@ -880,7 +874,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end assert_file "config/environments/development.rb" do |content| - assert_match(/^\s*# config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) end end diff --git a/railties/test/generators/channel_generator_test.rb b/railties/test/generators/channel_generator_test.rb index a1d54200ba..af68a9c49f 100644 --- a/railties/test/generators/channel_generator_test.rb +++ b/railties/test/generators/channel_generator_test.rb @@ -25,7 +25,7 @@ class ChannelGeneratorTest < Rails::Generators::TestCase end assert_file "app/assets/javascripts/channels/chat.js" do |channel| - assert_match(/App.chat = App.cable.subscriptions.create\("ChatChannel/, channel) + assert_match(/App\.chat = App\.cable\.subscriptions\.create\("ChatChannel/, channel) end end @@ -39,7 +39,7 @@ class ChannelGeneratorTest < Rails::Generators::TestCase end assert_file "app/assets/javascripts/channels/chat.js" do |channel| - assert_match(/App.chat = App.cable.subscriptions.create\("ChatChannel/, channel) + assert_match(/App\.chat = App\.cable\.subscriptions\.create\("ChatChannel/, channel) assert_match(/,\n\n speak/, channel) assert_match(/,\n\n mute: function\(\) \{\n return this\.perform\('mute'\);\n \}\n\}\);/, channel) end diff --git a/railties/test/generators/create_migration_test.rb b/railties/test/generators/create_migration_test.rb index ddd40e4d02..c7b0237f02 100644 --- a/railties/test/generators/create_migration_test.rb +++ b/railties/test/generators/create_migration_test.rb @@ -46,7 +46,7 @@ class CreateMigrationTest < Rails::Generators::TestCase def test_invoke create_migration - assert_match(/create db\/migrate\/1_create_articles.rb\n/, invoke!) + assert_match(/create db\/migrate\/1_create_articles\.rb\n/, invoke!) assert_file @migration.destination end @@ -67,7 +67,7 @@ class CreateMigrationTest < Rails::Generators::TestCase migration_exists! create_migration - assert_match(/identical db\/migrate\/1_create_articles.rb\n/, invoke!) + assert_match(/identical db\/migrate\/1_create_articles\.rb\n/, invoke!) assert @migration.identical? end @@ -84,8 +84,8 @@ class CreateMigrationTest < Rails::Generators::TestCase create_migration(dest, force: true) { "different content" } stdout = invoke! - assert_match(/remove db\/migrate\/1_migration.rb\n/, stdout) - assert_match(/create db\/migrate\/2_migration.rb\n/, stdout) + assert_match(/remove db\/migrate\/1_migration\.rb\n/, stdout) + assert_match(/create db\/migrate\/2_migration\.rb\n/, stdout) assert_file @migration.destination assert_no_file @existing_migration.destination end @@ -97,8 +97,8 @@ class CreateMigrationTest < Rails::Generators::TestCase end stdout = invoke! - assert_match(/remove db\/migrate\/1_create_articles.rb\n/, stdout) - assert_match(/create db\/migrate\/2_create_articles.rb\n/, stdout) + assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, stdout) + assert_match(/create db\/migrate\/2_create_articles\.rb\n/, stdout) assert_no_file @migration.destination end @@ -106,7 +106,7 @@ class CreateMigrationTest < Rails::Generators::TestCase migration_exists! create_migration(default_destination_path, {}, skip: true) { "different content" } - assert_match(/skip db\/migrate\/2_create_articles.rb\n/, invoke!) + assert_match(/skip db\/migrate\/2_create_articles\.rb\n/, invoke!) assert_no_file @migration.destination end @@ -114,7 +114,7 @@ class CreateMigrationTest < Rails::Generators::TestCase migration_exists! create_migration - assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!) + assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, revoke!) assert_no_file @existing_migration.destination end @@ -122,13 +122,13 @@ class CreateMigrationTest < Rails::Generators::TestCase migration_exists! create_migration(default_destination_path, {}, pretend: true) - assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!) + assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, revoke!) assert_file @existing_migration.destination end def test_revoke_when_no_exists create_migration - assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!) + assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, revoke!) end end diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb index 2cdddc8713..5fb331e197 100644 --- a/railties/test/generators/generators_test_helper.rb +++ b/railties/test/generators/generators_test_helper.rb @@ -9,7 +9,7 @@ module Rails class << self remove_possible_method :root def root - @root ||= Pathname.new(File.expand_path("../../fixtures", __FILE__)) + @root ||= Pathname.new(File.expand_path("../fixtures", __dir__)) end end end @@ -41,7 +41,7 @@ module GeneratorsTestHelper end def copy_routes - routes = File.expand_path("../../../lib/rails/generators/rails/app/templates/config/routes.rb", __FILE__) + routes = File.expand_path("../../lib/rails/generators/rails/app/templates/config/routes.rb", __dir__) destination = File.join(destination_root, "config") FileUtils.mkdir_p(destination) FileUtils.cp routes, destination diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb index 7d69d7470d..2ff03ea65e 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -9,13 +9,13 @@ class MailerGeneratorTest < Rails::Generators::TestCase run_generator assert_file "app/mailers/notifier_mailer.rb" do |mailer| assert_match(/class NotifierMailer < ApplicationMailer/, mailer) - assert_no_match(/default from: "from@example.com"/, mailer) + assert_no_match(/default from: "from@example\.com"/, mailer) assert_no_match(/layout :mailer_notifier/, mailer) end assert_file "app/mailers/application_mailer.rb" do |mailer| assert_match(/class ApplicationMailer < ActionMailer::Base/, mailer) - assert_match(/default from: 'from@example.com'/, mailer) + assert_match(/default from: 'from@example\.com'/, mailer) assert_match(/layout 'mailer'/, mailer) end end @@ -48,11 +48,11 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_match(/class NotifierMailerPreview < ActionMailer::Preview/, preview) assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier_mailer\/foo/, preview) assert_instance_method :foo, preview do |foo| - assert_match(/NotifierMailer.foo/, foo) + assert_match(/NotifierMailer\.foo/, foo) end assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier_mailer\/bar/, preview) assert_instance_method :bar, preview do |bar| - assert_match(/NotifierMailer.bar/, bar) + assert_match(/NotifierMailer\.bar/, bar) end end end @@ -137,12 +137,12 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_file "app/mailers/notifier_mailer.rb" do |mailer| assert_instance_method :foo, mailer do |foo| - assert_match(/mail to: "to@example.org"/, foo) + assert_match(/mail to: "to@example\.org"/, foo) assert_match(/@greeting = "Hi"/, foo) end assert_instance_method :bar, mailer do |bar| - assert_match(/mail to: "to@example.org"/, bar) + assert_match(/mail to: "to@example\.org"/, bar) assert_match(/@greeting = "Hi"/, bar) end end diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index f46278cefe..6fe6e4ca07 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -204,8 +204,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| assert_match(/create_join_table :artists, :musics/, change) - assert_match(/# t.index \[:artist_id, :music_id\]/, change) - assert_match(/ t.index \[:music_id, :artist_id\], unique: true/, change) + assert_match(/# t\.index \[:artist_id, :music_id\]/, change) + assert_match(/ t\.index \[:music_id, :artist_id\], unique: true/, change) end end end @@ -265,8 +265,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| assert_match(/create_join_table :artist, :music/, change) - assert_match(/# t.index \[:artist_id, :music_id\]/, change) - assert_match(/ t.index \[:music_id, :artist_id\], unique: true/, change) + assert_match(/# t\.index \[:artist_id, :music_id\]/, change) + assert_match(/ t\.index \[:music_id, :artist_id\], unique: true/, change) end end end diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index 99490af3a9..f41969fc46 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -10,7 +10,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase run_generator assert_file "app/models/application_record.rb" do |record| assert_match(/class ApplicationRecord < ActiveRecord::Base/, record) - assert_match(/self.abstract_class = true/, record) + assert_match(/self\.abstract_class = true/, record) end end @@ -253,7 +253,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_with_timestamps run_generator - assert_migration "db/migrate/create_accounts.rb", /t.timestamps/ + assert_migration "db/migrate/create_accounts.rb", /t\.timestamps/ end def test_migration_timestamps_are_skipped @@ -261,7 +261,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/create_accounts.rb" do |m| assert_method :change, m do |up| - assert_no_match(/t.timestamps/, up) + assert_no_match(/t\.timestamps/, up) end end end @@ -269,19 +269,19 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_is_skipped_with_skip_option run_generator output = run_generator ["Account", "--skip"] - assert_match %r{skip\s+db/migrate/\d+_create_accounts.rb}, output + assert_match %r{skip\s+db/migrate/\d+_create_accounts\.rb}, output end def test_migration_is_ignored_as_identical_with_skip_option run_generator ["Account"] output = run_generator ["Account", "--skip"] - assert_match %r{identical\s+db/migrate/\d+_create_accounts.rb}, output + assert_match %r{identical\s+db/migrate/\d+_create_accounts\.rb}, output end def test_migration_is_skipped_on_skip_behavior run_generator output = run_generator ["Account"], behavior: :skip - assert_match %r{skip\s+db/migrate/\d+_create_accounts.rb}, output + assert_match %r{skip\s+db/migrate/\d+_create_accounts\.rb}, output end def test_migration_error_is_not_shown_on_revoke diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index afb37b6a99..f8512f9157 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -48,7 +48,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator [File.join(destination_root, "hyphenated-name")] assert_no_file "hyphenated-name/lib/hyphenated-name.rb" assert_no_file "hyphenated-name/lib/hyphenated_name.rb" - assert_file "hyphenated-name/lib/hyphenated/name.rb", /module Hyphenated\n module Name\n # Your code goes here...\n end\nend/ + assert_file "hyphenated-name/lib/hyphenated/name.rb", /module Hyphenated\n module Name\n # Your code goes here\.\.\.\n end\nend/ end def test_correct_file_in_lib_folder_of_camelcase_plugin_name @@ -63,11 +63,13 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_no_file "config/routes.rb" assert_no_file "app/assets/config/bukkits_manifest.js" assert_file "test/test_helper.rb" do |content| - assert_match(/require.+test\/dummy\/config\/environment/, content) + assert_match(/require_relative.+test\/dummy\/config\/environment/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+test\/dummy\/db\/migrate/, content) assert_match(/Minitest\.backtrace_filter = Minitest::BacktraceFilter\.new/, content) assert_match(/Rails::TestUnitReporter\.executable = 'bin\/test'/, content) end + assert_file "lib/bukkits/railtie.rb", /module Bukkits\n class Railtie < ::Rails::Railtie\n end\nend/ + assert_file "lib/bukkits.rb", /require "bukkits\/railtie"/ assert_file "test/bukkits_test.rb", /assert_kind_of Module, Bukkits/ assert_file "bin/test" assert_no_file "bin/rails" @@ -152,7 +154,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "--skip-active-record"] assert_file "bukkits.gemspec" do |contents| - assert_no_match(/s.add_development_dependency "sqlite3"/, contents) + assert_no_match(/s\.add_development_dependency "sqlite3"/, contents) end end @@ -294,7 +296,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "hyphenated-name/config/routes.rb", /Rails.application.routes.draw do/ assert_file "hyphenated-name/lib/hyphenated/name/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n end\n end\nend/ assert_file "hyphenated-name/lib/hyphenated/name.rb", /require "hyphenated\/name\/engine"/ - assert_file "hyphenated-name/bin/rails", /\.\.\/\.\.\/lib\/hyphenated\/name\/engine/ + assert_file "hyphenated-name/bin/rails", /\.\.\/lib\/hyphenated\/name\/engine/ end def test_creating_engine_with_hyphenated_and_underscored_name_in_full_mode @@ -308,14 +310,14 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "my_hyphenated-name/app/helpers" assert_file "my_hyphenated-name/app/mailers" assert_file "my_hyphenated-name/bin/rails" - assert_file "my_hyphenated-name/config/routes.rb", /Rails.application.routes.draw do/ + assert_file "my_hyphenated-name/config/routes.rb", /Rails\.application\.routes\.draw do/ assert_file "my_hyphenated-name/lib/my_hyphenated/name/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n end\n end\nend/ assert_file "my_hyphenated-name/lib/my_hyphenated/name.rb", /require "my_hyphenated\/name\/engine"/ - assert_file "my_hyphenated-name/bin/rails", /\.\.\/\.\.\/lib\/my_hyphenated\/name\/engine/ + assert_file "my_hyphenated-name/bin/rails", /\.\.\/lib\/my_hyphenated\/name\/engine/ end def test_being_quiet_while_creating_dummy_application - assert_no_match(/create\s+config\/application.rb/, run_generator) + assert_no_match(/create\s+config\/application\.rb/, run_generator) end def test_create_mountable_application_with_mountable_option @@ -323,13 +325,13 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "app/assets/javascripts/bukkits" assert_file "app/assets/stylesheets/bukkits" assert_file "app/assets/images/bukkits" - assert_file "config/routes.rb", /Bukkits::Engine.routes.draw do/ + assert_file "config/routes.rb", /Bukkits::Engine\.routes\.draw do/ assert_file "lib/bukkits/engine.rb", /isolate_namespace Bukkits/ assert_file "test/dummy/config/routes.rb", /mount Bukkits::Engine => "\/bukkits"/ assert_file "app/controllers/bukkits/application_controller.rb", /module Bukkits\n class ApplicationController < ActionController::Base/ assert_file "app/models/bukkits/application_record.rb", /module Bukkits\n class ApplicationRecord < ActiveRecord::Base/ assert_file "app/jobs/bukkits/application_job.rb", /module Bukkits\n class ApplicationJob < ActiveJob::Base/ - assert_file "app/mailers/bukkits/application_mailer.rb", /module Bukkits\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n/ + assert_file "app/mailers/bukkits/application_mailer.rb", /module Bukkits\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n/ assert_file "app/helpers/bukkits/application_helper.rb", /module Bukkits\n module ApplicationHelper/ assert_file "app/views/layouts/bukkits/application.html.erb" do |contents| assert_match "<title>Bukkits</title>", contents @@ -350,15 +352,15 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "hyphenated-name/app/assets/javascripts/hyphenated/name" assert_file "hyphenated-name/app/assets/stylesheets/hyphenated/name" assert_file "hyphenated-name/app/assets/images/hyphenated/name" - assert_file "hyphenated-name/config/routes.rb", /Hyphenated::Name::Engine.routes.draw do/ - assert_file "hyphenated-name/lib/hyphenated/name/version.rb", /module Hyphenated\n module Name\n VERSION = '0.1.0'\n end\nend/ + assert_file "hyphenated-name/config/routes.rb", /Hyphenated::Name::Engine\.routes\.draw do/ + assert_file "hyphenated-name/lib/hyphenated/name/version.rb", /module Hyphenated\n module Name\n VERSION = '0\.1\.0'\n end\nend/ assert_file "hyphenated-name/lib/hyphenated/name/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Hyphenated::Name\n end\n end\nend/ assert_file "hyphenated-name/lib/hyphenated/name.rb", /require "hyphenated\/name\/engine"/ assert_file "hyphenated-name/test/dummy/config/routes.rb", /mount Hyphenated::Name::Engine => "\/hyphenated-name"/ assert_file "hyphenated-name/app/controllers/hyphenated/name/application_controller.rb", /module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\nend\n/ assert_file "hyphenated-name/app/models/hyphenated/name/application_record.rb", /module Hyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\nend/ assert_file "hyphenated-name/app/jobs/hyphenated/name/application_job.rb", /module Hyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ - assert_file "hyphenated-name/app/mailers/hyphenated/name/application_mailer.rb", /module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\nend/ + assert_file "hyphenated-name/app/mailers/hyphenated/name/application_mailer.rb", /module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n end\n end\nend/ assert_file "hyphenated-name/app/helpers/hyphenated/name/application_helper.rb", /module Hyphenated\n module Name\n module ApplicationHelper\n end\n end\nend/ assert_file "hyphenated-name/app/views/layouts/hyphenated/name/application.html.erb" do |contents| assert_match "<title>Hyphenated name</title>", contents @@ -372,15 +374,15 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name" assert_file "my_hyphenated-name/app/assets/stylesheets/my_hyphenated/name" assert_file "my_hyphenated-name/app/assets/images/my_hyphenated/name" - assert_file "my_hyphenated-name/config/routes.rb", /MyHyphenated::Name::Engine.routes.draw do/ - assert_file "my_hyphenated-name/lib/my_hyphenated/name/version.rb", /module MyHyphenated\n module Name\n VERSION = '0.1.0'\n end\nend/ + assert_file "my_hyphenated-name/config/routes.rb", /MyHyphenated::Name::Engine\.routes\.draw do/ + assert_file "my_hyphenated-name/lib/my_hyphenated/name/version.rb", /module MyHyphenated\n module Name\n VERSION = '0\.1\.0'\n end\nend/ assert_file "my_hyphenated-name/lib/my_hyphenated/name/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace MyHyphenated::Name\n end\n end\nend/ assert_file "my_hyphenated-name/lib/my_hyphenated/name.rb", /require "my_hyphenated\/name\/engine"/ assert_file "my_hyphenated-name/test/dummy/config/routes.rb", /mount MyHyphenated::Name::Engine => "\/my_hyphenated-name"/ assert_file "my_hyphenated-name/app/controllers/my_hyphenated/name/application_controller.rb", /module MyHyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\nend\n/ assert_file "my_hyphenated-name/app/models/my_hyphenated/name/application_record.rb", /module MyHyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\nend/ assert_file "my_hyphenated-name/app/jobs/my_hyphenated/name/application_job.rb", /module MyHyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ - assert_file "my_hyphenated-name/app/mailers/my_hyphenated/name/application_mailer.rb", /module MyHyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\nend/ + assert_file "my_hyphenated-name/app/mailers/my_hyphenated/name/application_mailer.rb", /module MyHyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n end\n end\nend/ assert_file "my_hyphenated-name/app/helpers/my_hyphenated/name/application_helper.rb", /module MyHyphenated\n module Name\n module ApplicationHelper\n end\n end\nend/ assert_file "my_hyphenated-name/app/views/layouts/my_hyphenated/name/application.html.erb" do |contents| assert_match "<title>My hyphenated name</title>", contents @@ -394,15 +396,15 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "deep-hyphenated-name/app/assets/javascripts/deep/hyphenated/name" assert_file "deep-hyphenated-name/app/assets/stylesheets/deep/hyphenated/name" assert_file "deep-hyphenated-name/app/assets/images/deep/hyphenated/name" - assert_file "deep-hyphenated-name/config/routes.rb", /Deep::Hyphenated::Name::Engine.routes.draw do/ - assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/version.rb", /module Deep\n module Hyphenated\n module Name\n VERSION = '0.1.0'\n end\n end\nend/ + assert_file "deep-hyphenated-name/config/routes.rb", /Deep::Hyphenated::Name::Engine\.routes\.draw do/ + assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/version.rb", /module Deep\n module Hyphenated\n module Name\n VERSION = '0\.1\.0'\n end\n end\nend/ assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/engine.rb", /module Deep\n module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Deep::Hyphenated::Name\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/lib/deep/hyphenated/name.rb", /require "deep\/hyphenated\/name\/engine"/ assert_file "deep-hyphenated-name/test/dummy/config/routes.rb", /mount Deep::Hyphenated::Name::Engine => "\/deep-hyphenated-name"/ assert_file "deep-hyphenated-name/app/controllers/deep/hyphenated/name/application_controller.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\n end\nend\n/ assert_file "deep-hyphenated-name/app/models/deep/hyphenated/name/application_record.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/app/jobs/deep/hyphenated/name/application_job.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ - assert_file "deep-hyphenated-name/app/mailers/deep/hyphenated/name/application_mailer.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\n end\nend/ + assert_file "deep-hyphenated-name/app/mailers/deep/hyphenated/name/application_mailer.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/app/helpers/deep/hyphenated/name/application_helper.rb", /module Deep\n module Hyphenated\n module Name\n module ApplicationHelper\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/app/views/layouts/deep/hyphenated/name/application.html.erb" do |contents| assert_match "<title>Deep hyphenated name</title>", contents @@ -413,16 +415,16 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_creating_gemspec run_generator - assert_file "bukkits.gemspec", /s.name\s+= "bukkits"/ - assert_file "bukkits.gemspec", /s.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.md"\]/ - assert_file "bukkits.gemspec", /s.version\s+ = Bukkits::VERSION/ + assert_file "bukkits.gemspec", /s\.name\s+= "bukkits"/ + assert_file "bukkits.gemspec", /s\.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.md"\]/ + assert_file "bukkits.gemspec", /s\.version\s+ = Bukkits::VERSION/ end def test_usage_of_engine_commands run_generator [destination_root, "--full"] - assert_file "bin/rails", /ENGINE_PATH = File.expand_path\('..\/..\/lib\/bukkits\/engine', __FILE__\)/ - assert_file "bin/rails", /ENGINE_ROOT = File.expand_path\('..\/..', __FILE__\)/ - assert_file "bin/rails", %r|APP_PATH = File.expand_path\('../../test/dummy/config/application', __FILE__\)| + assert_file "bin/rails", /ENGINE_PATH = File\.expand_path\('\.\.\/lib\/bukkits\/engine', __dir__\)/ + assert_file "bin/rails", /ENGINE_ROOT = File\.expand_path\('\.\.', __dir__\)/ + assert_file "bin/rails", %r|APP_PATH = File\.expand_path\('\.\./test/dummy/config/application', __dir__\)| assert_file "bin/rails", /require 'rails\/all'/ assert_file "bin/rails", /require 'rails\/engine\/commands'/ end @@ -438,7 +440,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "spec/dummy/config/application.rb" assert_no_file "test/dummy" assert_file "test/test_helper.rb" do |content| - assert_match(/require.+spec\/dummy\/config\/environment/, content) + assert_match(/require_relative.+spec\/dummy\/config\/environment/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+spec\/dummy\/db\/migrate/, content) end end @@ -449,7 +451,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "spec/fake/config/application.rb" assert_no_file "test/dummy" assert_file "test/test_helper.rb" do |content| - assert_match(/require.+spec\/fake\/config\/environment/, content) + assert_match(/require_relative.+spec\/fake\/config\/environment/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+spec\/fake\/db\/migrate/, content) end end @@ -469,7 +471,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator assert_file "test/dummy/config/environments/development.rb" do |contents| - assert_match(/^\s*# config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, contents) + assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, contents) end end @@ -687,7 +689,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "#{destination_root}/app/models/bukkits/application_record.rb" do |record| assert_match(/module Bukkits/, record) assert_match(/class ApplicationRecord < ActiveRecord::Base/, record) - assert_match(/self.abstract_class = true/, record) + assert_match(/self\.abstract_class = true/, record) end end @@ -741,7 +743,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase quietly { Rails::Engine::Updater.run(:create_bin_files) } assert_file "#{destination_root}/bin/rails" do |content| - assert_match(%r|APP_PATH = File\.expand_path\('\.\./\.\./test/dummy/config/application', __FILE__\)|, content) + assert_match(%r|APP_PATH = File\.expand_path\('\.\./test/dummy/config/application', __dir__\)|, content) end ensure Object.send(:remove_const, "ENGINE_ROOT") diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index cc9d3629e9..5e75879964 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -45,14 +45,14 @@ module SharedGeneratorTests reserved_words = %w[application destroy plugin runner test] reserved_words.each do |reserved| content = capture(:stderr) { run_generator [File.join(destination_root, reserved)] } - assert_match(/Invalid \w+ name #{reserved}. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n/, content) + assert_match(/Invalid \w+ name #{reserved}\. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n/, content) end end def test_name_raises_an_error_if_name_already_used_constant %w{ String Hash Class Module Set Symbol }.each do |ruby_class| content = capture(:stderr) { run_generator [File.join(destination_root, ruby_class)] } - assert_match(/Invalid \w+ name #{ruby_class}, constant #{ruby_class} is already in use. Please choose another \w+ name.\n/, content) + assert_match(/Invalid \w+ name #{ruby_class}, constant #{ruby_class} is already in use\. Please choose another \w+ name\.\n/, content) end end diff --git a/railties/test/generators/system_test_generator_test.rb b/railties/test/generators/system_test_generator_test.rb index e8e561ec49..4622360244 100644 --- a/railties/test/generators/system_test_generator_test.rb +++ b/railties/test/generators/system_test_generator_test.rb @@ -9,4 +9,9 @@ class SystemTestGeneratorTest < Rails::Generators::TestCase run_generator assert_file "test/system/users_test.rb", /class UsersTest < ApplicationSystemTestCase/ end + + def test_namespaced_system_test_skeleton_is_created + run_generator %w(admin/user) + assert_file "test/system/admin/users_test.rb", /class Admin::UsersTest < ApplicationSystemTestCase/ + end end diff --git a/railties/test/generators/test_runner_in_engine_test.rb b/railties/test/generators/test_runner_in_engine_test.rb index 4b5fb3ba3f..680dc2608e 100644 --- a/railties/test/generators/test_runner_in_engine_test.rb +++ b/railties/test/generators/test_runner_in_engine_test.rb @@ -17,7 +17,7 @@ class TestRunnerInEngineTest < ActiveSupport::TestCase create_test_file "post", pass: false output = run_test_command("test/post_test.rb") - expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/post_test.rb:6\]:\nwups!\n\nbin/rails test test/post_test.rb:4} + expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/post_test\.rb:6\]:\nwups!\n\nbin/rails test test/post_test\.rb:4} assert_match expect, output end diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index c3c16b6f86..e07627f36d 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -233,7 +233,7 @@ class GeneratorsTest < Rails::Generators::TestCase end def test_usage_with_embedded_ruby - require File.expand_path("fixtures/lib/generators/usage_template/usage_template_generator", File.dirname(__FILE__)) + require_relative "fixtures/lib/generators/usage_template/usage_template_generator" output = capture(:stdout) { Rails::Generators.invoke :usage_template, ["--help"] } assert_match(/:: 2 ::/, output) end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 2e742a005e..7496b5f84a 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -14,7 +14,7 @@ require "active_support/testing/autorun" require "active_support/testing/stream" require "active_support/test_case" -RAILS_FRAMEWORK_ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../..") +RAILS_FRAMEWORK_ROOT = File.expand_path("../../..", __dir__) # These files do not require any others and are needed # to run the tests diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb index 9f4c5bb025..383adcc55d 100644 --- a/railties/test/rails_info_test.rb +++ b/railties/test/rails_info_test.rb @@ -39,7 +39,7 @@ class InfoTest < ActiveSupport::TestCase def test_rails_version assert_property "Rails version", - File.read(File.realpath("../../../RAILS_VERSION", __FILE__)).chomp + File.read(File.realpath("../../RAILS_VERSION", __dir__)).chomp end def test_html_includes_middleware diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 52d691b73b..e382a7a873 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -89,16 +89,16 @@ module RailtiesTest assert File.exist?("#{app_path}/db/migrate/2_create_users.bukkits.rb") assert File.exist?("#{app_path}/db/migrate/3_add_last_name_to_users.bukkits.rb") - assert_match(/Copied migration 2_create_users.bukkits.rb from bukkits/, output) - assert_match(/Copied migration 3_add_last_name_to_users.bukkits.rb from bukkits/, output) - assert_match(/NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/, output) + assert_match(/Copied migration 2_create_users\.bukkits\.rb from bukkits/, output) + assert_match(/Copied migration 3_add_last_name_to_users\.bukkits\.rb from bukkits/, output) + assert_match(/NOTE: Migration 3_create_sessions\.rb from bukkits has been skipped/, output) assert_equal 3, Dir["#{app_path}/db/migrate/*.rb"].length output = `bundle exec rake railties:install:migrations`.split("\n") assert_no_match(/2_create_users/, output.join("\n")) - bukkits_migration_order = output.index(output.detect { |o| /NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/ =~ o }) + bukkits_migration_order = output.index(output.detect { |o| /NOTE: Migration 3_create_sessions\.rb from bukkits has been skipped/ =~ o }) assert_not_nil bukkits_migration_order, "Expected migration to be skipped" migrations_count = Dir["#{app_path}/db/migrate/*.rb"].length @@ -135,8 +135,8 @@ module RailtiesTest Dir.chdir(app_path) do output = `bundle exec rake railties:install:migrations`.split("\n") - assert_match(/Copied migration \d+_create_users.bukkits.rb from bukkits/, output.first) - assert_match(/Copied migration \d+_create_blogs.blog_engine.rb from blog_engine/, output.last) + assert_match(/Copied migration \d+_create_users\.bukkits\.rb from bukkits/, output.first) + assert_match(/Copied migration \d+_create_blogs\.blog_engine\.rb from blog_engine/, output.last) end end @@ -171,8 +171,8 @@ module RailtiesTest Dir.chdir(app_path) do output = `bundle exec rake railties:install:migrations`.split("\n") - assert_match(/Copied migration \d+_create_users.core_engine.rb from core_engine/, output.first) - assert_match(/Copied migration \d+_create_keys.api_engine.rb from api_engine/, output.last) + assert_match(/Copied migration \d+_create_users\.core_engine\.rb from core_engine/, output.first) + assert_match(/Copied migration \d+_create_keys\.api_engine\.rb from api_engine/, output.last) end end @@ -202,7 +202,7 @@ module RailtiesTest Dir.chdir(@plugin.path) do output = `bundle exec rake app:bukkits:install:migrations` assert File.exist?("#{app_path}/db/migrate/0_add_first_name_to_users.bukkits.rb") - assert_match(/Copied migration 0_add_first_name_to_users.bukkits.rb from bukkits/, output) + assert_match(/Copied migration 0_add_first_name_to_users\.bukkits\.rb from bukkits/, output) assert_equal 1, Dir["#{app_path}/db/migrate/*.rb"].length end end diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb index e22c939981..98201394cd 100644 --- a/railties/test/test_unit/reporter_test.rb +++ b/railties/test/test_unit/reporter_test.rb @@ -16,7 +16,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(failed_test) @reporter.report - assert_match %r{^bin/rails test .*test/test_unit/reporter_test.rb:\d+$}, @output.string + assert_match %r{^bin/rails test .*test/test_unit/reporter_test\.rb:\d+$}, @output.string assert_rerun_snippet_count 1 end @@ -52,7 +52,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(failed_test) @reporter.report - assert_match %r{^bin/test .*test/test_unit/reporter_test.rb:\d+$}, @output.string + assert_match %r{^bin/test .*test/test_unit/reporter_test\.rb:\d+$}, @output.string ensure Rails::TestUnitReporter.executable = original_executable end @@ -62,7 +62,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(failed_test) @reporter.report - expect = %r{\AF\n\nFailure:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nboo\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z} + expect = %r{\AF\n\nFailure:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nboo\n\nbin/rails test test/test_unit/reporter_test\.rb:\d+\n\n\z} assert_match expect, @output.string end @@ -70,7 +70,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(errored_test) @reporter.report - expect = %r{\AE\n\nError:\nTestUnitReporterTest::ExampleTest#woot:\nArgumentError: wups\n No backtrace\n\nbin/rails test .*test/test_unit/reporter_test.rb:\d+\n\n\z} + expect = %r{\AE\n\nError:\nTestUnitReporterTest::ExampleTest#woot:\nArgumentError: wups\n No backtrace\n\nbin/rails test .*test/test_unit/reporter_test\.rb:\d+\n\n\z} assert_match expect, @output.string end @@ -79,7 +79,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase verbose.record(skipped_test) verbose.report - expect = %r{\ATestUnitReporterTest::ExampleTest#woot = 10\.00 s = S\n\n\nSkipped:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nskipchurches, misstemples\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z} + expect = %r{\ATestUnitReporterTest::ExampleTest#woot = 10\.00 s = S\n\n\nSkipped:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nskipchurches, misstemples\n\nbin/rails test test/test_unit/reporter_test\.rb:\d+\n\n\z} assert_match expect, @output.string end diff --git a/tasks/release.rb b/tasks/release.rb index b021535245..038fdc584a 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -1,7 +1,7 @@ FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack activejob actionmailer actioncable railties ) FRAMEWORK_NAMES = Hash.new { |h, k| k.split(/(?<=active|action)/).map(&:capitalize).join(" ") } -root = File.expand_path("../../", __FILE__) +root = File.expand_path("..", __dir__) version = File.read("#{root}/RAILS_VERSION").strip tag = "v#{version}" gem_version = Gem::Version.new(version) diff --git a/tools/README.md b/tools/README.md index b2e7e4b0ae..f133b27128 100644 --- a/tools/README.md +++ b/tools/README.md @@ -6,3 +6,5 @@ They aren't used by Rails apps directly. * `console` drops you in irb and loads local Rails repos * `profile` profiles `Kernel#require` to help reduce startup time * `line_statistics` provides CodeTools module and LineStatistics class to count lines + * `test` is loaded by every major component of Rails to simplify testing, for example: + `cd ./actioncable; bin/test ./path/to/actioncable_test_with_line_number.rb:5` diff --git a/tools/test.rb b/tools/test.rb index 71349a5974..52e9c19198 100644 --- a/tools/test.rb +++ b/tools/test.rb @@ -7,11 +7,12 @@ require "rails/test_unit/minitest_plugin" require "rails/test_unit/line_filtering" require "active_support/test_case" -module Rails - # Necessary to get rerun-snippts working. - def self.root +class << Rails + # Necessary to get rerun-snippets working. + def root @root ||= Pathname.new(COMPONENT_ROOT) end + alias __root root end ActiveSupport::TestCase.extend Rails::LineFiltering |