diff options
52 files changed, 943 insertions, 91 deletions
diff --git a/.travis.yml b/.travis.yml index 6e43023365..985956363a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ env: - "GEM=ar:mysql2" - "GEM=ar:sqlite3" - "GEM=ar:postgresql" + - "GEM=aj:integration" matrix: allow_failures: - rvm: 1.9.3 @@ -46,3 +47,8 @@ notifications: bundler_args: --path vendor/bundle --without test services: - memcached + - redis + - rabbitmq +addons: + postgresql: "9.3" + @@ -45,12 +45,14 @@ group :job do gem 'sidekiq', require: false gem 'sucker_punch', require: false gem 'delayed_job', require: false - gem 'queue_classic', require: false, platforms: :ruby + gem 'queue_classic', "< 3.0.0", require: false, platforms: :ruby gem 'sneakers', '0.1.1.pre', require: false gem 'que', require: false gem 'backburner', require: false gem 'qu-rails', github: "bkeepers/qu", branch: "master", require: false gem 'qu-redis', require: false + gem 'delayed_job_active_record', require: false + gem 'sequel', require: false end # Add your own local bundler stuff diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 9450be838c..b9d5009683 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -45,8 +45,8 @@ module Mime # # respond_to do |format| # format.html - # format.ics { render text: post.to_ics, mime_type: Mime::Type["text/calendar"] } - # format.xml { render xml: @people } + # format.ics { render text: @post.to_ics, mime_type: Mime::Type["text/calendar"] } + # format.xml { render xml: @post } # end # end # end diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index 19eca60f70..7361e6c44b 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -1,3 +1,3 @@ require 'active_support/deprecation' -ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::SelectorAssertions has been has been extracted to the rails-dom-testing gem.")
\ No newline at end of file +ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::SelectorAssertions has been extracted to the rails-dom-testing gem.") diff --git a/actionpack/lib/action_dispatch/testing/assertions/tag.rb b/actionpack/lib/action_dispatch/testing/assertions/tag.rb index d5348d80e1..5c2905d1ac 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/tag.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/tag.rb @@ -1,3 +1,3 @@ require 'active_support/deprecation' -ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::TagAssertions has been has been extracted to the rails-dom-testing gem.") +ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::TagAssertions has been extracted to the rails-dom-testing gem.") diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index 7edfc436a6..af34d2ce5a 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -158,8 +158,7 @@ module ActionView # Need to experiment if this priority is the best one: rendered => output_buffer def document_root_element - @html_document ||= Nokogiri::HTML::Document.parse(@rendered.blank? ? @output_buffer : @rendered) - @html_document.root + Nokogiri::HTML::Document.parse(@rendered.blank? ? @output_buffer : @rendered).root end def say_no_to_protect_against_forgery! diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb index 4582fa13ee..5697ffa317 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -293,6 +293,17 @@ module ActionView assert_select 'li', :text => 'foo' end end + + test "do not memoize the document_root_element in view tests" do + concat form_tag('/foo') + + assert_select 'form' + + concat content_tag(:b, 'Strong', class: 'foo') + + assert_select 'form' + assert_select 'b.foo' + end end class RenderTemplateTest < ActionView::TestCase diff --git a/activejob/.gitignore b/activejob/.gitignore new file mode 100644 index 0000000000..b3aaf55871 --- /dev/null +++ b/activejob/.gitignore @@ -0,0 +1 @@ +test/dummy diff --git a/activejob/Rakefile b/activejob/Rakefile index 484cd1d0b8..e918428459 100644 --- a/activejob/Rakefile +++ b/activejob/Rakefile @@ -34,6 +34,12 @@ namespace :test do tasks = ACTIVEJOB_ADAPTERS.map{|a| "isolated_test_#{a}" } run_without_aborting(*tasks) end + + desc 'Run all adapter integration tests' + task :integration do + tasks = ACTIVEJOB_ADAPTERS.map{|a| "integration_test_#{a}" } + run_without_aborting(*tasks) + end end @@ -53,6 +59,15 @@ ACTIVEJOB_ADAPTERS.each do |adapter| end or raise 'Failures' end end + + namespace :integration do + Rake::TestTask.new(adapter => "#{adapter}:env") do |t| + t.description = "" + t.libs << 'test' + t.test_files = FileList['test/integration/**/*_test.rb'] + t.verbose = true + end + end end namespace adapter do @@ -60,6 +75,17 @@ ACTIVEJOB_ADAPTERS.each do |adapter| task isolated_test: "isolated_test_#{adapter}" task(:env) { ENV['AJADAPTER'] = adapter } + + namespace :isolated do + task(:env) { ENV['AJADAPTER'] = adapter } + end + + namespace :integration do + task(:env) do + ENV['AJADAPTER'] = adapter + ENV['AJ_INTEGRATION_TESTS'] = "1" + end + end end @@ -67,7 +93,10 @@ ACTIVEJOB_ADAPTERS.each do |adapter| task "test_#{adapter}" => ["#{adapter}:env", "test:#{adapter}"] desc "Run #{adapter} tests in isolation" - task "isolated_test_#{adapter}" => ["#{adapter}:env", "test:isolated:#{adapter}"] + task "isolated_test_#{adapter}" => ["#{adapter}:isolated:env", "test:isolated:#{adapter}"] + + desc "Run #{adapter} integration tests" + task "integration_test_#{adapter}" => ["#{adapter}:integration:env", "test:integration:#{adapter}"] end diff --git a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb index 60633639f4..1ab0a87485 100644 --- a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb @@ -21,7 +21,7 @@ module ActiveJob class JobWrapper include Sneakers::Worker - from_queue 'active_jobs_default' + from_queue 'default' def work(msg) job_data = ActiveSupport::JSON.decode(msg) diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb index 85094387ef..3386cef6f3 100644 --- a/activejob/test/helper.rb +++ b/activejob/test/helper.rb @@ -1,6 +1,7 @@ require File.expand_path('../../../load_paths', __FILE__) require 'active_job' +require 'support/job_buffer' GlobalID.app = 'aj' @@ -17,8 +18,13 @@ end # Sidekiq doesn't work with MRI 1.9.3 exit if sidekiq? && ruby_193? -require "adapters/#{@adapter}" +if ENV['AJ_INTEGRATION_TESTS'] + require 'support/integration/helper' +else + require "adapters/#{@adapter}" +end require 'active_support/testing/autorun' ActiveJob::Base.logger.level = Logger::DEBUG + diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb new file mode 100644 index 0000000000..779dedb53f --- /dev/null +++ b/activejob/test/integration/queuing_test.rb @@ -0,0 +1,46 @@ +require 'helper' +require 'jobs/logging_job' +require 'active_support/core_ext/numeric/time' + +class QueuingTest < ActiveSupport::TestCase + test 'should run jobs enqueued on a listenting queue' do + TestJob.perform_later @id + wait_for_jobs_to_finish_for(5.seconds) + assert job_executed + end + + test 'should not run jobs queued on a non-listenting queue' do + begin + skip if adapter_is?(:inline) || adapter_is?(:sucker_punch) + old_queue = TestJob.queue_name + TestJob.queue_as :some_other_queue + TestJob.perform_later @id + wait_for_jobs_to_finish_for(2.seconds) + assert_not job_executed + ensure + TestJob.queue_name = old_queue + end + end + + test 'should not run job enqueued in the future' do + begin + TestJob.set(wait: 10.minutes).perform_later @id + wait_for_jobs_to_finish_for(5.seconds) + assert_not job_executed + rescue NotImplementedError + skip + end + end + + test 'should run job enqueued in the future at the specified time' do + begin + TestJob.set(wait: 3.seconds).perform_later @id + wait_for_jobs_to_finish_for(2.seconds) + assert_not job_executed + wait_for_jobs_to_finish_for(10.seconds) + assert job_executed + rescue NotImplementedError + skip + end + end +end diff --git a/activejob/test/support/integration/adapters/backburner.rb b/activejob/test/support/integration/adapters/backburner.rb new file mode 100644 index 0000000000..0cda36a273 --- /dev/null +++ b/activejob/test/support/integration/adapters/backburner.rb @@ -0,0 +1,38 @@ +module BackburnerJobsManager + def setup + ActiveJob::Base.queue_adapter = :backburner + Backburner.configure do |config| + config.logger = Rails.logger + end + unless can_run? + puts "Cannot run integration tests for backburner. To be able to run integration tests for backburner you need to install and start beanstalkd.\n" + exit + end + end + + def clear_jobs + tube.clear + end + + def start_workers + @thread = Thread.new { Backburner.work "integration-tests" } # backburner dasherizes the queue name + end + + def stop_workers + @thread.kill + end + + def tube + @tube ||= Beaneater::Tube.new(Backburner::Worker.connection, "backburner.worker.queue.integration-tests") # backburner dasherizes the queue name + end + + def can_run? + begin + Backburner::Worker.connection.send :connect! + rescue => e + return false + end + true + end +end + diff --git a/activejob/test/support/integration/adapters/delayed_job.rb b/activejob/test/support/integration/adapters/delayed_job.rb new file mode 100644 index 0000000000..dbd0d1a4db --- /dev/null +++ b/activejob/test/support/integration/adapters/delayed_job.rb @@ -0,0 +1,20 @@ +require 'delayed_job' +require 'delayed_job_active_record' + +module DelayedJobJobsManager + def setup + ActiveJob::Base.queue_adapter = :delayed_job + end + def clear_jobs + Delayed::Job.delete_all + end + + def start_workers + @worker = Delayed::Worker.new(quiet: false, sleep_delay: 0.5, queues: %w(integration_tests)) + @thread = Thread.new { @worker.start } + end + + def stop_workers + @worker.stop + end +end diff --git a/activejob/test/support/integration/adapters/inline.rb b/activejob/test/support/integration/adapters/inline.rb new file mode 100644 index 0000000000..83c38f706f --- /dev/null +++ b/activejob/test/support/integration/adapters/inline.rb @@ -0,0 +1,15 @@ +module InlineJobsManager + def setup + ActiveJob::Base.queue_adapter = :inline + end + + def clear_jobs + end + + def start_workers + end + + def stop_workers + end +end + diff --git a/activejob/test/support/integration/adapters/qu.rb b/activejob/test/support/integration/adapters/qu.rb new file mode 100644 index 0000000000..e913f04a24 --- /dev/null +++ b/activejob/test/support/integration/adapters/qu.rb @@ -0,0 +1,38 @@ +module QuJobsManager + def setup + require 'qu-rails' + require 'qu-redis' + ActiveJob::Base.queue_adapter = :qu + ENV['REDISTOGO_URL'] = "tcp://127.0.0.1:6379/12" + backend = Qu::Backend::Redis.new + backend.namespace = "active_jobs_int_test" + Qu.backend = backend + Qu.logger = Rails.logger + Qu.interval = 0.5 + unless can_run? + puts "Cannot run integration tests for qu. To be able to run integration tests for qu you need to install and start redis.\n" + exit + end + end + + def clear_jobs + Qu.clear "integration_tests" + end + + def start_workers + @thread = Thread.new { Qu::Worker.new("integration_tests").start } + end + + def stop_workers + @thread.kill + end + + def can_run? + begin + Qu.backend.connection.client.connect + rescue => e + return false + end + true + end +end diff --git a/activejob/test/support/integration/adapters/que.rb b/activejob/test/support/integration/adapters/que.rb new file mode 100644 index 0000000000..a5b9b3ec0a --- /dev/null +++ b/activejob/test/support/integration/adapters/que.rb @@ -0,0 +1,37 @@ +module QueJobsManager + def setup + require 'sequel' + ActiveJob::Base.queue_adapter = :que + que_url = ENV['QUE_DATABASE_URL'] || 'postgres://localhost/active_jobs_que_int_test' + uri = URI.parse(que_url) + user = uri.user||ENV['USER'] + pass = uri.password + db = uri.path[1..-1] + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database "#{db}"' -U #{user} -t template1} + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} + Que.connection = Sequel.connect(que_url) + Que.migrate! + Que.mode = :off + Que.worker_count = 1 + rescue Sequel::DatabaseConnectionError + puts "Cannot run integration tests for que. To be able to run integration tests for que you need to install and start postgresql.\n" + exit + end + + def clear_jobs + Que.clear! + end + + def start_workers + @thread = Thread.new do + loop do + Que::Job.work("integration_tests") + sleep 0.5 + end + end + end + + def stop_workers + @thread.kill + end +end diff --git a/activejob/test/support/integration/adapters/queue_classic.rb b/activejob/test/support/integration/adapters/queue_classic.rb new file mode 100644 index 0000000000..81d1935132 --- /dev/null +++ b/activejob/test/support/integration/adapters/queue_classic.rb @@ -0,0 +1,45 @@ +module QueueClassicJobsManager + def setup + ENV['QC_DATABASE_URL'] ||= 'postgres://localhost/active_jobs_qc_int_test' + ENV['QC_LISTEN_TIME'] = "0.5" + uri = URI.parse(ENV['QC_DATABASE_URL']) + user = uri.user||ENV['USER'] + pass = uri.password + db = uri.path[1..-1] + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database "#{db}"' -U #{user} -t template1} + %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1} + ActiveJob::Base.queue_adapter = :queue_classic + QC::Setup.create + rescue PG::ConnectionBad + puts "Cannot run integration tests for queue_classic. To be able to run integration tests for queue_classic you need to install and start postgresql.\n" + exit + end + + def clear_jobs + QC::Queue.new("integration_tests").delete_all + retried = false + rescue => e + puts "Got exception while trying to clear jobs: #{e.inspect}" + if retried + puts "Already retried. Raising exception" + raise e + else + puts "Retrying" + retried = true + QC::Conn.connection = QC::Conn.connect + retry + end + end + + def start_workers + @pid = fork do + QC::Conn.connection = QC::Conn.connect + worker = QC::Worker.new(q_name: 'integration_tests') + worker.start + end + end + + def stop_workers + Process.kill 'HUP', @pid + end +end diff --git a/activejob/test/support/integration/adapters/resque.rb b/activejob/test/support/integration/adapters/resque.rb new file mode 100644 index 0000000000..03ffd3fd62 --- /dev/null +++ b/activejob/test/support/integration/adapters/resque.rb @@ -0,0 +1,47 @@ +module ResqueJobsManager + def setup + ActiveJob::Base.queue_adapter = :resque + Resque.redis = Redis::Namespace.new 'active_jobs_int_test', redis: Redis.connect(url: "tcp://127.0.0.1:6379/12", :thread_safe => true) + Resque.logger = Rails.logger + unless can_run? + puts "Cannot run integration tests for resque. To be able to run integration tests for resque you need to install and start redis.\n" + exit + end + end + + def clear_jobs + Resque.queues.each { |queue_name| Resque.redis.del "queue:#{queue_name}" } + Resque.redis.keys("delayed:*").each { |key| Resque.redis.del "#{key}" } + Resque.redis.del "delayed_queue_schedule" + end + + def start_workers + @resque_thread = Thread.new do + Resque::Worker.new("integration_tests").work(0.5) + end + @scheduler_thread = Thread.new do + Resque::Scheduler.configure do |c| + c.poll_sleep_amount = 0.5 + c.dynamic = true + c.verbose = true + c.logfile = nil + end + Resque::Scheduler.master_lock.release! + Resque::Scheduler.run + end + end + + def stop_workers + @resque_thread.kill + @scheduler_thread.kill + end + + def can_run? + begin + Resque.redis.client.connect + rescue => e + return false + end + true + end +end diff --git a/activejob/test/support/integration/adapters/sidekiq.rb b/activejob/test/support/integration/adapters/sidekiq.rb new file mode 100644 index 0000000000..b3c3dcff22 --- /dev/null +++ b/activejob/test/support/integration/adapters/sidekiq.rb @@ -0,0 +1,58 @@ +require 'sidekiq/cli' +require 'sidekiq/api' + +module SidekiqJobsManager + + def setup + ActiveJob::Base.queue_adapter = :sidekiq + unless can_run? + puts "Cannot run integration tests for sidekiq. To be able to run integration tests for sidekiq you need to install and start redis.\n" + exit + end + end + + def clear_jobs + Sidekiq::ScheduledSet.new.clear + Sidekiq::Queue.new("integration_tests").clear + end + + def start_workers + fork do + sidekiq = Sidekiq::CLI.instance + logfile = Rails.root.join("log/sidekiq.log").to_s + pidfile = Rails.root.join("tmp/sidekiq.pid").to_s + sidekiq.parse([ "--require", Rails.root.to_s, + "--queue", "integration_tests", + "--logfile", logfile, + "--pidfile", pidfile, + "--environment", "test", + "--concurrency", "1", + "--timeout", "1", + "--daemon", + "--verbose" + ]) + require 'celluloid' + require 'sidekiq/scheduled' + Sidekiq.poll_interval = 0.5 + Sidekiq::Scheduled.const_set :INITIAL_WAIT, 1 + sidekiq.run + end + sleep 1 + end + + def stop_workers + pidfile = Rails.root.join("tmp/sidekiq.pid").to_s + Process.kill 'TERM', File.open(pidfile).read.to_i + FileUtils.rm_f pidfile + rescue + end + + def can_run? + begin + Sidekiq.redis { |conn| conn.connect } + rescue => e + return false + end + true + end +end diff --git a/activejob/test/support/integration/adapters/sneakers.rb b/activejob/test/support/integration/adapters/sneakers.rb new file mode 100644 index 0000000000..f21bb38a32 --- /dev/null +++ b/activejob/test/support/integration/adapters/sneakers.rb @@ -0,0 +1,90 @@ +require 'sneakers/runner' +require 'sneakers/publisher' +require 'timeout' + +module Sneakers + class Publisher + def safe_ensure_connected + @mutex.synchronize do + ensure_connection! unless connected? + end + end + end +end + + +module SneakersJobsManager + def setup + ActiveJob::Base.queue_adapter = :sneakers + Sneakers.configure :heartbeat => 2, + :amqp => 'amqp://guest:guest@localhost:5672', + :vhost => '/', + :exchange => 'active_jobs_sneakers_int_test', + :exchange_type => :direct, + :daemonize => true, + :threads => 1, + :workers => 1, + :pid_path => Rails.root.join("tmp/sneakers.pid").to_s, + :log => Rails.root.join("log/sneakers.log").to_s + unless can_run? + puts "Cannot run integration tests for sneakers. To be able to run integration tests for sneakers you need to install and start rabbitmq.\n" + exit + end + end + + def clear_jobs + bunny_queue.purge + end + + def start_workers + @pid = fork do + queues = %w(integration_tests) + workers = queues.map do |q| + worker_klass = "ActiveJobWorker"+Digest::MD5.hexdigest(q) + Sneakers.const_set(worker_klass, Class.new(ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper) do + from_queue q + end) + end + Sneakers::Runner.new(workers).run + end + begin + Timeout.timeout(10) do + while bunny_queue.status[:consumer_count] == 0 + sleep 0.5 + end + end + rescue Timeout::Error + stop_workers + raise "Failed to start sneakers worker" + end + end + + def stop_workers + Process.kill 'TERM', @pid + Process.kill 'TERM', File.open(Rails.root.join("tmp/sneakers.pid").to_s).read.to_i + rescue + end + + def can_run? + begin + bunny_publisher + rescue => e + return false + end + true + end + + protected + def bunny_publisher + @bunny_publisher ||= begin + p = ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper.send(:publisher) + p.safe_ensure_connected + p + end + end + + def bunny_queue + @queue ||= bunny_publisher.exchange.channel.queue "integration_tests", durable: true + end + +end diff --git a/activejob/test/support/integration/adapters/sucker_punch.rb b/activejob/test/support/integration/adapters/sucker_punch.rb new file mode 100644 index 0000000000..691ba35c90 --- /dev/null +++ b/activejob/test/support/integration/adapters/sucker_punch.rb @@ -0,0 +1,5 @@ +module SuckerPunchJobsManager + def setup + ActiveJob::Base.queue_adapter = :sucker_punch + end +end diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb new file mode 100644 index 0000000000..28aae0f884 --- /dev/null +++ b/activejob/test/support/integration/dummy_app_template.rb @@ -0,0 +1,21 @@ +if ENV['AJADAPTER'] == 'delayed_job' + generate "delayed_job:active_record" + rake("db:migrate") +end + +initializer 'activejob.rb', <<-CODE +require "#{File.expand_path("../jobs_manager.rb", __FILE__)}" +JobsManager.current_manager.setup +CODE + +file 'app/jobs/test_job.rb', <<-CODE +class TestJob < ActiveJob::Base + queue_as :integration_tests + + def perform(x) + File.open(Rails.root.join("tmp/\#{x}"), "w+") do |f| + f.write x + end + end +end +CODE diff --git a/activejob/test/support/integration/helper.rb b/activejob/test/support/integration/helper.rb new file mode 100644 index 0000000000..ccd5036b36 --- /dev/null +++ b/activejob/test/support/integration/helper.rb @@ -0,0 +1,32 @@ +puts "\n\n" +puts "*** Running integration tests for #{ENV['AJADAPTER']} ***" +puts "\n\n" + +ENV["RAILS_ENV"] = "test" +ActiveJob::Base.queue_name_prefix = nil + +require 'rails/generators/rails/app/app_generator' + +dummy_app_path = Dir.mktmpdir + "/dummy" +dummy_app_template = File.expand_path("../dummy_app_template.rb", __FILE__) +args = Rails::Generators::ARGVScrubber.new(["new", dummy_app_path, "--skip-gemfile", "--skip-bundle", + "--skip-git", "--skip-spring", "-d", "sqlite3", "--skip-javascript", "--force", "--quite", + "--template", dummy_app_template]).prepare! +Rails::Generators::AppGenerator.start args + +require "#{dummy_app_path}/config/environment.rb" + +ActiveRecord::Migrator.migrations_paths = [ Rails.root.join('db/migrate').to_s ] +require 'rails/test_help' + +Rails.backtrace_cleaner.remove_silencers! + +require_relative 'test_case_helpers' +ActiveSupport::TestCase.send(:include, TestCaseHelpers) + +JobsManager.current_manager.start_workers + +Minitest.after_run do + JobsManager.current_manager.stop_workers + JobsManager.current_manager.clear_jobs +end diff --git a/activejob/test/support/integration/jobs_manager.rb b/activejob/test/support/integration/jobs_manager.rb new file mode 100644 index 0000000000..4df34aaeb1 --- /dev/null +++ b/activejob/test/support/integration/jobs_manager.rb @@ -0,0 +1,27 @@ +class JobsManager + @@managers = {} + attr :adapter_name + + def self.current_manager + @@managers[ENV['AJADAPTER']] ||= new(ENV['AJADAPTER']) + end + + def initialize(adapter_name) + @adapter_name = adapter_name + require_relative "adapters/#{adapter_name}" + extend "#{adapter_name.camelize}JobsManager".constantize + end + + def setup + ActiveJob::Base.queue_adapter = nil + end + + def clear_jobs + end + + def start_workers + end + + def stop_workers + end +end diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb new file mode 100644 index 0000000000..ee2f6aebea --- /dev/null +++ b/activejob/test/support/integration/test_case_helpers.rb @@ -0,0 +1,48 @@ +require 'active_support/concern' +require 'support/integration/jobs_manager' + +module TestCaseHelpers + extend ActiveSupport::Concern + + included do + self.use_transactional_fixtures = false + + setup do + clear_jobs + @id = "AJ-#{SecureRandom.uuid}" + end + + teardown do + clear_jobs + end + end + + protected + + def jobs_manager + JobsManager.current_manager + end + + def clear_jobs + jobs_manager.clear_jobs + end + + def adapter_is?(adapter) + ActiveJob::Base.queue_adapter.name.split("::").last.gsub(/Adapter$/, '').underscore==adapter.to_s + end + + def wait_for_jobs_to_finish_for(seconds=60) + begin + Timeout.timeout(seconds) do + while !job_executed do + sleep 0.25 + end + end + rescue Timeout::Error + end + end + + def job_executed + Dummy::Application.root.join("tmp/#{@id}").exist? + end +end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 001338b46c..7d4869e113 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,31 @@ +* Include default column limits in schema.rb. Allows defaults to be changed + in the future without affecting old migrations that assumed old defaults. + + *Jeremy Kemper* + +* MySQL: schema.rb now includes TEXT and BLOB column limits. + + *Jeremy Kemper* + +* MySQL: correct LONGTEXT and LONGBLOB limits from 2GB to their true 4GB. + + *Jeremy Kemper* + +* SQLite3Adapter now checks for views in `table_exists?`. Fixes #14041. + + *Girish Sonawane* + +* Introduce `connection.supports_views?` to check wether the current adapter + has support for SQL views. Connection adapters should define this method. + + *Yves Senn* + +* Allow included modules to override association methods. + + Fixes #16684. + + *Yves Senn* + * Schema loading rake tasks (like `db:schema:load` and `db:setup`) maintain the database connection to the current environment. diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 7519fec10a..46bccbf15a 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -2,33 +2,42 @@ module ActiveRecord module Associations # Implements the details of eager loading of Active Record associations. # - # Note that 'eager loading' and 'preloading' are actually the same thing. - # However, there are two different eager loading strategies. + # Suppose that you have the following two Active Record models: # - # The first one is by using table joins. This was only strategy available - # prior to Rails 2.1. Suppose that you have an Author model with columns - # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using - # this strategy, Active Record would try to retrieve all data for an author - # and all of its books via a single query: + # class Author < ActiveRecord::Base + # # columns: name, age + # has_many :books + # end # - # SELECT * FROM authors - # LEFT OUTER JOIN books ON authors.id = books.author_id - # WHERE authors.name = 'Ken Akamatsu' + # class Book < ActiveRecord::Base + # # columns: title, sales + # end # - # However, this could result in many rows that contain redundant data. After - # having received the first row, we already have enough data to instantiate - # the Author object. In all subsequent rows, only the data for the joined - # 'books' table is useful; the joined 'authors' data is just redundant, and - # processing this redundant data takes memory and CPU time. The problem - # quickly becomes worse and worse as the level of eager loading increases - # (i.e. if Active Record is to eager load the associations' associations as - # well). + # When you load an author with all associated books Active Record will make + # multiple queries like this: + # + # Author.includes(:books).where(:name => ['bell hooks', 'Homer').to_a + # + # => SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer') + # => SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5) + # + # Active Record saves the ids of the records from the first query to use in + # the second. Depending on the number of associations involved there can be + # arbitrarily many SQL queries made. + # + # However, if there is a WHERE clause that spans across tables Active + # Record will fall back to a slightly more resource-intensive single query: + # + # Author.includes(:books).where(books: {title: 'Illiad'}).to_a + # => SELECT `authors`.`id` AS t0_r0, `authors`.`name` AS t0_r1, `authors`.`age` AS t0_r2, + # `books`.`id` AS t1_r0, `books`.`title` AS t1_r1, `books`.`sales` AS t1_r2 + # FROM `authors` + # LEFT OUTER JOIN `books` ON `authors`.`id` = `books`.`author_id` + # WHERE `books`.`title` = 'Illiad' + # + # This could result in many rows that contain redundant data and it performs poorly at scale + # and is therefore only used when necessary. # - # The second strategy is to use multiple database queries, one for each - # level of association. Since Rails 2.1, this is the default strategy. In - # situations where a table join is necessary (e.g. when the +:conditions+ - # option references an association's column), it will fallback to the table - # join strategy. class Preloader #:nodoc: extend ActiveSupport::Autoload diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 43a3993898..36d0d63c71 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -69,6 +69,8 @@ module ActiveRecord @generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m } @attribute_methods_generated = false include @generated_attribute_methods + + super end # Generates all the attribute related methods for columns in the database diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 9bd0401e40..b05a4f8440 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -19,12 +19,16 @@ module ActiveRecord spec = {} spec[:name] = column.name.inspect spec[:type] = column.type.to_s - spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] + spec[:null] = 'false' unless column.null + + limit = column.limit || types[column.type][:limit] + spec[:limit] = limit.inspect if limit spec[:precision] = column.precision.inspect if column.precision spec[:scale] = column.scale.inspect if column.scale - spec[:null] = 'false' unless column.null - spec[:default] = schema_default(column) if column.has_default? - spec.delete(:default) if spec[:default].nil? + + default = schema_default(column) if column.has_default? + spec[:default] = default unless default.nil? + spec end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 2ee5a88f2a..a0d9086875 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -240,6 +240,11 @@ module ActiveRecord false end + # Does this adapter support views? + def supports_views? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) 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 3a24ecac77..037fb69dfb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -197,6 +197,10 @@ module ActiveRecord true end + def supports_views? + version[0] >= 5 + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -636,18 +640,21 @@ module ActiveRecord def initialize_type_map(m) # :nodoc: super + m.register_type(%r(enum)i) do |sql_type| limit = sql_type[/^enum\((.+)\)/i, 1] .split(',').map{|enum| enum.strip.length - 2}.max Type::String.new(limit: limit) end - m.register_type %r(tinytext)i, Type::Text.new(limit: 255) - m.register_type %r(tinyblob)i, Type::Binary.new(limit: 255) - m.register_type %r(mediumtext)i, Type::Text.new(limit: 16777215) - m.register_type %r(mediumblob)i, Type::Binary.new(limit: 16777215) - m.register_type %r(longtext)i, Type::Text.new(limit: 2147483647) - m.register_type %r(longblob)i, Type::Binary.new(limit: 2147483647) + m.register_type %r(tinytext)i, Type::Text.new(limit: 2**8 - 1) + m.register_type %r(tinyblob)i, Type::Binary.new(limit: 2**8 - 1) + m.register_type %r(text)i, Type::Text.new(limit: 2**16 - 1) + m.register_type %r(blob)i, Type::Binary.new(limit: 2**16 - 1) + m.register_type %r(mediumtext)i, Type::Text.new(limit: 2**24 - 1) + m.register_type %r(mediumblob)i, Type::Binary.new(limit: 2**24 - 1) + m.register_type %r(longtext)i, Type::Text.new(limit: 2**32 - 1) + m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1) m.register_type %r(^bigint)i, Type::Integer.new(limit: 8) m.register_type %r(^int)i, Type::Integer.new(limit: 4) m.register_type %r(^mediumint)i, Type::Integer.new(limit: 3) @@ -779,10 +786,6 @@ module ActiveRecord full_version =~ /mariadb/i end - def supports_views? - version[0] >= 5 - end - def supports_rename_index? mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6 end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 52a03d0e6d..80461f3910 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -158,6 +158,10 @@ module ActiveRecord true end + def supports_views? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 4481df974d..ebb311df57 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -183,6 +183,10 @@ module ActiveRecord true end + def supports_views? + true + end + def active? @active != false end @@ -369,7 +373,7 @@ module ActiveRecord sql = <<-SQL SELECT name FROM sqlite_master - WHERE type = 'table' AND NOT name = 'sqlite_sequence' + WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence' SQL sql << " AND name = #{quote_table_name(table_name)}" if table_name diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 2e9c9e3197..83859e474a 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -183,8 +183,6 @@ module ActiveRecord end def initialize_generated_modules - super - generated_association_methods end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index b2bf9480dd..8c1c22d3bf 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -297,7 +297,7 @@ module ActiveRecord def test_tables_logs_name sql = <<-SQL SELECT name FROM sqlite_master - WHERE type = 'table' AND NOT name = 'sqlite_sequence' + WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence' SQL assert_logged [[sql.squish, 'SCHEMA', []]] do @conn.tables('hello') @@ -316,7 +316,7 @@ module ActiveRecord with_example_table do sql = <<-SQL SELECT name FROM sqlite_master - WHERE type = 'table' + WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence' AND name = \"ex\" SQL assert_logged [[sql.squish, 'SCHEMA', []]] do diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 3f5858714a..b6333240a8 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -5,7 +5,9 @@ if ActiveRecord::Base.connection.supports_migrations? class ActiveRecordSchemaTest < ActiveRecord::TestCase self.use_transactional_fixtures = false - def setup + setup do + @original_verbose = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false @connection = ActiveRecord::Base.connection ActiveRecord::SchemaMigration.drop_table end @@ -16,6 +18,7 @@ if ActiveRecord::Base.connection.supports_migrations? @connection.drop_table :nep_schema_migrations rescue nil @connection.drop_table :has_timestamps rescue nil ActiveRecord::SchemaMigration.delete_all rescue nil + ActiveRecord::Migration.verbose = @original_verbose end def test_has_no_primary_key diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb index 24a332bb4a..321fb6c8dd 100644 --- a/activerecord/test/cases/associations/required_test.rb +++ b/activerecord/test/cases/associations/required_test.rb @@ -18,8 +18,8 @@ class RequiredAssociationsTest < ActiveRecord::TestCase end teardown do - @connection.execute("DROP TABLE parents") if @connection.table_exists? 'parents' - @connection.execute("DROP TABLE children") if @connection.table_exists? 'children' + @connection.drop_table 'parents' if @connection.table_exists? 'parents' + @connection.drop_table 'children' if @connection.table_exists? 'children' end test "belongs_to associations are not required by default" do diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 635c657d9b..9b0cf4c18f 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -357,4 +357,18 @@ class GeneratedMethodsTest < ActiveRecord::TestCase def test_model_method_overrides_association_method assert_equal(comments(:greetings).body, posts(:welcome).first_comment) end + + module MyModule + def comments; :none end + end + + class MyArticle < ActiveRecord::Base + self.table_name = "articles" + include MyModule + has_many :comments, inverse_of: false + end + + def test_included_module_overwrites_association_methods + assert_equal :none, MyArticle.new.comments + end end diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb index 644876752e..53bd58e22e 100644 --- a/activerecord/test/cases/attribute_decorators_test.rb +++ b/activerecord/test/cases/attribute_decorators_test.rb @@ -28,7 +28,7 @@ module ActiveRecord teardown do return unless @connection - @connection.execute 'DROP TABLE attribute_decorators_model' if @connection.table_exists? 'attribute_decorators_model' + @connection.drop_table 'attribute_decorators_model' if @connection.table_exists? 'attribute_decorators_model' Model.attribute_type_decorations.clear Model.reset_column_information end diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 4741ee8799..e38b32d7fc 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -13,6 +13,7 @@ module ActiveRecord def self.superclass; Base; end def self.base_class; self; end def self.decorate_matching_attribute_types(*); end + def self.initialize_generated_modules; end include ActiveRecord::AttributeMethods diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index ba28d55e4c..406dd70c37 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -29,8 +29,8 @@ module ActiveRecord teardown do if defined?(@connection) - @connection.execute "DROP TABLE astronauts" if @connection.table_exists? 'astronauts' - @connection.execute "DROP TABLE rockets" if @connection.table_exists? 'rockets' + @connection.drop_table "astronauts" if @connection.table_exists? 'astronauts' + @connection.drop_table "rockets" if @connection.table_exists? 'rockets' end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index d5584ad06c..d7ee56374d 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -10,12 +10,11 @@ class SchemaDumperTest < ActiveRecord::TestCase end def standard_dump - @stream = StringIO.new - old_ignore_tables, ActiveRecord::SchemaDumper.ignore_tables = ActiveRecord::SchemaDumper.ignore_tables, [] - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, @stream) - @stream.string - ensure - ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables + @@standard_dump ||= perform_schema_dump + end + + def perform_schema_dump + dump_all_table_schema [] end def test_dump_schema_information_outputs_lexically_ordered_versions @@ -31,8 +30,7 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_magic_comment - output = standard_dump - assert_match "# encoding: #{@stream.external_encoding.name}", output + assert_match "# encoding: #{Encoding.default_external.name}", standard_dump end def test_schema_dump @@ -100,7 +98,11 @@ 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 + 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 @@ -111,6 +113,8 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{c_int_4.*}, output assert_no_match %r{c_int_4.*limit:}, output elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) + assert_match %r{c_int_without_limit.*limit: 4}, 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 @@ -118,13 +122,13 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{c_int_4.*}, output assert_no_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 assert_match %r{c_int_4.*limit: 4}, output end - assert_match %r{c_int_without_limit.*}, output - assert_no_match %r{c_int_without_limit.*limit:}, output if current_adapter?(:SQLite3Adapter) assert_match %r{c_int_5.*limit: 5}, output @@ -203,9 +207,9 @@ class SchemaDumperTest < ActiveRecord::TestCase end if current_adapter?(:MysqlAdapter, :Mysql2Adapter) - def test_schema_dump_should_not_add_default_value_for_mysql_text_field + def test_schema_dump_should_add_default_value_for_mysql_text_field output = standard_dump - assert_match %r{t.text\s+"body",\s+null: false$}, output + assert_match %r{t.text\s+"body",\s+limit: 65535,\s+null: false$}, output end def test_schema_dump_includes_length_for_mysql_binary_fields @@ -217,13 +221,13 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_includes_length_for_mysql_blob_and_text_fields output = standard_dump assert_match %r{t.binary\s+"tiny_blob",\s+limit: 255$}, output - assert_match %r{t.binary\s+"normal_blob"$}, output + assert_match %r{t.binary\s+"normal_blob",\s+limit: 65535$}, output assert_match %r{t.binary\s+"medium_blob",\s+limit: 16777215$}, output - assert_match %r{t.binary\s+"long_blob",\s+limit: 2147483647$}, output + assert_match %r{t.binary\s+"long_blob",\s+limit: 4294967295$}, output assert_match %r{t.text\s+"tiny_text",\s+limit: 255$}, output - assert_match %r{t.text\s+"normal_text"$}, output + assert_match %r{t.text\s+"normal_text",\s+limit: 65535$}, output assert_match %r{t.text\s+"medium_text",\s+limit: 16777215$}, output - assert_match %r{t.text\s+"long_text",\s+limit: 2147483647$}, output + assert_match %r{t.text\s+"long_text",\s+limit: 4294967295$}, output end def test_schema_dumps_index_type @@ -249,12 +253,12 @@ class SchemaDumperTest < ActiveRecord::TestCase connection = ActiveRecord::Base.connection connection.stubs(:extensions).returns(['hstore']) - output = standard_dump + output = perform_schema_dump assert_match "# These are extensions that must be enabled", output assert_match %r{enable_extension "hstore"}, output connection.stubs(:extensions).returns([]) - output = standard_dump + output = perform_schema_dump assert_no_match "# These are extensions that must be enabled", output assert_no_match %r{enable_extension}, output end @@ -354,7 +358,7 @@ class SchemaDumperTest < ActiveRecord::TestCase match = output.match(%r{create_table "goofy_string_id"(.*)do.*\n(.*)\n}) assert_not_nil(match, "goofy_string_id table not found") assert_match %r(id: false), match[1], "no table id not preserved" - assert_match %r{t.string[[:space:]]+"id",[[:space:]]+null: false$}, match[2], "non-primary key id column not preserved" + assert_match %r{t.string\s+"id",.*?null: false$}, match[2], "non-primary key id column not preserved" end def test_schema_dump_keeps_id_false_when_id_is_false_and_unique_not_null_column_added @@ -395,7 +399,7 @@ class SchemaDumperTest < ActiveRecord::TestCase migration = CreateDogMigration.new migration.migrate(:up) - output = standard_dump + output = perform_schema_dump assert_no_match %r{create_table "foo_.+_bar"}, output assert_no_match %r{add_index "foo_.+_bar"}, output assert_no_match %r{create_table "schema_migrations"}, output @@ -433,7 +437,7 @@ class SchemaDumperDefaultsTest < ActiveRecord::TestCase def test_schema_dump_defaults_with_universally_supported_types output = dump_table_schema('defaults') - assert_match %r{t\.string\s+"string_with_default",\s+default: "Hello!"}, output + assert_match %r{t\.string\s+"string_with_default",.*?default: "Hello!"}, output assert_match %r{t\.date\s+"date_with_default",\s+default: '2014-06-05'}, output assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: '2014-06-05 07:17:04'}, output assert_match %r{t\.time\s+"time_with_default",\s+default: '2000-01-01 07:17:04'}, output diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb new file mode 100644 index 0000000000..357265aa05 --- /dev/null +++ b/activerecord/test/cases/view_test.rb @@ -0,0 +1,95 @@ +require "cases/helper" +require "models/book" + +if ActiveRecord::Base.connection.supports_views? +class ViewWithPrimaryKeyTest < ActiveRecord::TestCase + fixtures :books + + class Ebook < ActiveRecord::Base + self.primary_key = "id" + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute <<-SQL + CREATE VIEW ebooks + AS SELECT id, name, status FROM books WHERE format = 'ebook' + SQL + end + + teardown do + @connection.execute "DROP VIEW ebooks" if @connection.table_exists? "ebooks" + end + + def test_reading + books = Ebook.all + assert_equal [books(:rfr).id], books.map(&:id) + assert_equal ["Ruby for Rails"], books.map(&:name) + end + + def test_table_exists + view_name = Ebook.table_name + assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" + end + + def test_column_definitions + assert_equal([["id", :integer], + ["name", :string], + ["status", :integer]], Ebook.columns.map { |c| [c.name, c.type] }) + end + + def test_attributes + assert_equal({"id" => 2, "name" => "Ruby for Rails", "status" => 0}, + Ebook.first.attributes) + end + + def test_does_not_assume_id_column_as_primary_key + model = Class.new(ActiveRecord::Base) do + self.table_name = "ebooks" + end + assert_nil model.primary_key + end +end + +class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase + fixtures :books + + class Paperback < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute <<-SQL + CREATE VIEW paperbacks + AS SELECT name, status FROM books WHERE format = 'paperback' + SQL + end + + teardown do + @connection.execute "DROP VIEW paperbacks" if @connection.table_exists? "paperbacks" + end + + def test_reading + books = Paperback.all + assert_equal ["Agile Web Development with Rails"], books.map(&:name) + end + + def test_table_exists + view_name = Paperback.table_name + assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" + end + + def test_column_definitions + assert_equal([["name", :string], + ["status", :integer]], Paperback.columns.map { |c| [c.name, c.type] }) + end + + def test_attributes + assert_equal({"name" => "Agile Web Development with Rails", "status" => 0}, + Paperback.first.attributes) + end + + def test_does_not_have_a_primary_key + assert_nil Paperback.primary_key + end +end +end diff --git a/ci/travis.rb b/ci/travis.rb index db6e41ecfa..3ccbd1dad4 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -48,6 +48,7 @@ class Build heading = [gem] heading << "with #{adapter}" if activerecord? heading << "in isolation" if isolated? + heading << "integration" if integration? heading.join(' ') end @@ -55,7 +56,7 @@ class Build if activerecord? ['db:mysql:rebuild', "#{adapter}:#{'isolated_' if isolated?}test"] else - ["test#{':isolated' if isolated?}"] + ["test", ('isolated' if isolated?), ('integration' if integration?)].compact.join(":") end end @@ -74,6 +75,10 @@ class Build options[:isolated] end + def integration? + component.split(':').last == 'integration' + end + def gem MAP[component.split(':').first] end @@ -93,11 +98,17 @@ class Build end end +if ENV['GEM']=='aj:integration' + ENV['QC_DATABASE_URL'] = 'postgres://postgres@localhost/active_jobs_qc_int_test' + ENV['QUE_DATABASE_URL'] = 'postgres://postgres@localhost/active_jobs_que_int_test' +end + results = {} ENV['GEM'].split(',').each do |gem| [false, true].each do |isolated| next if gem == 'railties' && isolated + next if gem == 'aj:integration' && isolated build = Build.new(gem, :isolated => isolated) results[build.key] = build.run! diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 15d743fb33..2a8940684f 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -208,9 +208,7 @@ precompiling works. NOTE: You must have an ExecJS supported runtime in order to use CoffeeScript. If you are using Mac OS X or Windows, you have a JavaScript runtime installed in -your operating system. Check -[ExecJS](https://github.com/sstephenson/execjs#readme) documentation to know all -supported JavaScript runtimes. +your operating system. Check [ExecJS](https://github.com/sstephenson/execjs#readme) documentation to know all supported JavaScript runtimes. You can also disable generation of controller specific asset files by adding the following to your `config/application.rb` configuration: diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 1023598aa4..0761d5b39c 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -676,6 +676,22 @@ en: <div><%= t('title.html') %></div> ``` +Interpolation escapes as needed though. For example, given: + +```yaml +en: + welcome_html: "<b>Welcome %{username}!</b>" +``` + +you can safely pass the username as set by the user: + +```erb +<%# This is safe, it is going to be escaped if needed. %> +<%= t('welcome_html', username: @current_user.username %> +``` + +Safe strings on the other hand are interpolated verbatim. + NOTE: Automatic conversion to HTML safe translate text is only available from the `translate` view helper method. ![i18n demo html safe](images/i18n/demo_html_safe.png) diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 71358ad3f5..120dea51e9 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -8,7 +8,7 @@ This guide provides steps to be followed when you upgrade your applications to a General Advice -------------- -Before attempting to upgrade an existing application, you should be sure you have a good reason to upgrade. You need to balance out several factors: the need for new features, the increasing difficulty of finding support for old code, and your available time and skills, to name a few. +Before attempting to upgrade an existing application, you should be sure you have a good reason to upgrade. You need to balance several factors: the need for new features, the increasing difficulty of finding support for old code, and your available time and skills, to name a few. ### Test Coverage @@ -55,7 +55,11 @@ a [pull request](https://github.com/rails/rails/edit/master/guides/source/upgrad ### Web Console -TODO: setup instructions for web console on existing apps. +First, add `gem 'web-console', '~> 2.0.0.beta3'` to the `:development` group in your Gemfile and run `bundle install` (it won't have been included when you upgraded Rails). Once it's been installed, you can simply drop a reference to the console helper (i.e., `<%= console %>`) into any view you want to enable it for. A console will also be provided on any error page you view in your development environment. + +Additionally, you can tell Rails to automatically mount a VT100-compatible console on a predetermined path by setting `config.web_console.automount = true` in your `config/environments/development.rb`. You can specify the path by setting `config.web_console.default_mount_path` (note that this defaults to `/console`). + +TODO: Update `web-console` version to release version. ### Responders @@ -63,7 +67,22 @@ TODO: mention https://github.com/rails/rails/pull/16526 ### Error handling in transaction callbacks -TODO: mention https://github.com/rails/rails/pull/16537 +Currently, Active Record suppresses errors raised +within `after_rollback` or `after_commit` callbacks and only prints them to +the logs. In the next version, these errors will no longer be suppressed. +Instead, the errors will propagate normally just like in other Active +Record callbacks. + +When you define a `after_rollback` or `after_commit` callback, you +will receive a deprecation warning about this upcoming change. When +you are ready, you can opt into the new behvaior and remove the +deprecation warning by adding following configuration to your +`config/application.rb`: + + config.active_record.raise_in_transactional_callbacks = true + +See [#14488](https://github.com/rails/rails/pull/14488) and +[#16537](https://github.com/rails/rails/pull/16537) for more details. ### Ordering of test cases diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 2c16b83f8a..786dcee007 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -93,9 +93,10 @@ module Rails # Loads and returns the entire raw configuration of database from # values stored in `config/database.yml`. def database_configuration - yaml = Pathname.new(paths["config/database"].existent.first || "") + path = paths["config/database"].existent.first + yaml = Pathname.new(path) if path - config = if yaml.exist? + config = if yaml && yaml.exist? require "yaml" require "erb" YAML.load(ERB.new(yaml.read).result) || {} @@ -104,7 +105,7 @@ module Rails # by Active Record. {} else - raise "Could not load database configuration. No such file - #{yaml}" + raise "Could not load database configuration. No such file - #{paths["config/database"].instance_variable_get(:@paths)}" end config diff --git a/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb b/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb index feb250e89a..f5351d0ec6 100644 --- a/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb +++ b/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb @@ -2,7 +2,6 @@ require 'test_helper' <% module_namespacing do -%> class <%= class_name %>JobTest < ActiveJob::TestCase - # test "the truth" do # assert true # end diff --git a/railties/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb index 977feb922b..1dc1d70f8d 100644 --- a/railties/lib/rails/templates/rails/mailers/email.html.erb +++ b/railties/lib/rails/templates/rails/mailers/email.html.erb @@ -2,6 +2,10 @@ <html><head> <meta name="viewport" content="width=device-width" /> <style type="text/css"> + body { + margin: 0; + } + header { width: 100%; padding: 10px 0 0 0; @@ -95,4 +99,4 @@ <iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe> </body> -</html>
\ No newline at end of file +</html> diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index db2106aed3..f8b4ee30d8 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -980,6 +980,15 @@ module ApplicationTests assert_kind_of Hash, Rails.application.config.database_configuration end + test 'raises with proper error message if no database configuration found' do + FileUtils.rm("#{app_path}/config/database.yml") + require "#{app_path}/config/environment" + err = assert_raises RuntimeError do + Rails.application.config.database_configuration + end + assert_match 'config/database', err.message + end + test 'config.action_mailer.show_previews defaults to true in development' do Rails.env = "development" require "#{app_path}/config/environment" |