aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rw-r--r--Gemfile20
-rw-r--r--README.md2
-rw-r--r--Rakefile2
-rw-r--r--actionmailer/CHANGELOG.md13
-rw-r--r--actionmailer/actionmailer.gemspec1
-rw-r--r--actionmailer/lib/action_mailer.rb2
-rw-r--r--actionmailer/lib/action_mailer/base.rb8
-rw-r--r--actionmailer/lib/action_mailer/delivery_job.rb11
-rw-r--r--actionmailer/lib/action_mailer/message_delivery.rb45
-rw-r--r--actionmailer/lib/action_mailer/preview.rb2
-rw-r--r--actionmailer/lib/action_mailer/test_case.rb7
-rw-r--r--actionmailer/test/abstract_unit.rb2
-rw-r--r--actionmailer/test/assert_select_email_test.rb47
-rw-r--r--actionmailer/test/base_test.rb8
-rw-r--r--actionmailer/test/mailers/delayed_mailer.rb6
-rw-r--r--actionmailer/test/message_delivery_test.rb82
-rw-r--r--actionpack/CHANGELOG.md49
-rw-r--r--actionpack/Rakefile24
-rw-r--r--actionpack/actionpack.gemspec1
-rw-r--r--actionpack/lib/abstract_controller/helpers.rb3
-rw-r--r--actionpack/lib/action_controller.rb1
-rw-r--r--actionpack/lib/action_controller/base.rb1
-rw-r--r--actionpack/lib/action_controller/caching.rb2
-rw-r--r--actionpack/lib/action_controller/metal/conditional_get.rb37
-rw-r--r--actionpack/lib/action_controller/metal/etag_with_template_digest.rb50
-rw-r--r--actionpack/lib/action_controller/metal/live.rb10
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb244
-rw-r--r--actionpack/lib/action_controller/metal/responder.rb297
-rw-r--r--actionpack/lib/action_controller/test_case.rb17
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb36
-rw-r--r--actionpack/lib/action_dispatch/middleware/request_id.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb2
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb116
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb27
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb5
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions.rb18
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/dom.rb28
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/selector.rb431
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/tag.rb135
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb4
-rw-r--r--actionpack/test/abstract_unit.rb17
-rw-r--r--actionpack/test/controller/action_pack_assertions_test.rb29
-rw-r--r--actionpack/test/controller/assert_select_test.rb356
-rw-r--r--actionpack/test/controller/integration_test.rb7
-rw-r--r--actionpack/test/controller/live_stream_test.rb2
-rw-r--r--actionpack/test/controller/mime/respond_with_test.rb737
-rw-r--r--actionpack/test/controller/mime/responders_test.rb32
-rw-r--r--actionpack/test/controller/render_test.rb36
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb3
-rw-r--r--actionpack/test/controller/selector_test.rb629
-rw-r--r--actionpack/test/controller/test_case_test.rb179
-rw-r--r--actionpack/test/dispatch/cookies_test.rb89
-rw-r--r--actionpack/test/dispatch/mapper_test.rb2
-rw-r--r--actionpack/test/dispatch/request_test.rb2
-rw-r--r--actionpack/test/fixtures/respond_with/edit.html.erb1
-rw-r--r--actionpack/test/fixtures/respond_with/new.html.erb1
-rw-r--r--actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb1
-rw-r--r--actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb1
-rw-r--r--actionpack/test/fixtures/respond_with/using_resource.js.erb1
-rw-r--r--actionpack/test/fixtures/respond_with/using_resource_with_block.html.erb1
-rw-r--r--actionview/CHANGELOG.md6
-rw-r--r--actionview/actionview.gemspec1
-rw-r--r--actionview/lib/action_view.rb1
-rw-r--r--actionview/lib/action_view/helpers/sanitize_helper.rb170
-rw-r--r--actionview/lib/action_view/helpers/tags/placeholderable.rb32
-rw-r--r--actionview/lib/action_view/helpers/tags/text_area.rb4
-rw-r--r--actionview/lib/action_view/helpers/tags/text_field.rb4
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb1
-rw-r--r--actionview/lib/action_view/routing_url_for.rb4
-rw-r--r--actionview/lib/action_view/test_case.rb17
-rw-r--r--actionview/lib/action_view/vendor/html-scanner.rb20
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/document.rb68
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/node.rb532
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb188
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/selector.rb830
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb107
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/version.rb11
-rw-r--r--actionview/test/activerecord/controller_runtime_test.rb4
-rw-r--r--actionview/test/template/atom_feed_helper_test.rb10
-rw-r--r--actionview/test/template/date_helper_test.rb29
-rw-r--r--actionview/test/template/form_collections_helper_test.rb88
-rw-r--r--actionview/test/template/form_helper_test.rb168
-rw-r--r--actionview/test/template/form_tag_helper_test.rb2
-rw-r--r--actionview/test/template/html-scanner/cdata_node_test.rb15
-rw-r--r--actionview/test/template/html-scanner/document_test.rb148
-rw-r--r--actionview/test/template/html-scanner/node_test.rb89
-rw-r--r--actionview/test/template/html-scanner/sanitizer_test.rb330
-rw-r--r--actionview/test/template/html-scanner/tag_node_test.rb243
-rw-r--r--actionview/test/template/html-scanner/text_node_test.rb50
-rw-r--r--actionview/test/template/html-scanner/tokenizer_test.rb131
-rw-r--r--actionview/test/template/sanitize_helper_test.rb22
-rw-r--r--actionview/test/template/text_helper_test.rb2
-rw-r--r--actionview/test/template/url_helper_test.rb2
-rw-r--r--activejob/CHANGELOG.md1
-rw-r--r--activejob/MIT-LICENSE21
-rw-r--r--activejob/README.md141
-rw-r--r--activejob/Rakefile84
-rw-r--r--activejob/activejob.gemspec22
-rw-r--r--activejob/lib/active_job.rb33
-rw-r--r--activejob/lib/active_job/arguments.rb62
-rw-r--r--activejob/lib/active_job/base.rb22
-rw-r--r--activejob/lib/active_job/callbacks.rb144
-rw-r--r--activejob/lib/active_job/enqueuing.rb71
-rw-r--r--activejob/lib/active_job/execution.rb33
-rw-r--r--activejob/lib/active_job/gem_version.rb15
-rw-r--r--activejob/lib/active_job/identifier.rb15
-rw-r--r--activejob/lib/active_job/logging.rb90
-rw-r--r--activejob/lib/active_job/queue_adapter.rb24
-rw-r--r--activejob/lib/active_job/queue_adapters/backburner_adapter.rb25
-rw-r--r--activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb23
-rw-r--r--activejob/lib/active_job/queue_adapters/inline_adapter.rb15
-rw-r--r--activejob/lib/active_job/queue_adapters/qu_adapter.rb30
-rw-r--r--activejob/lib/active_job/queue_adapters/que_adapter.rb23
-rw-r--r--activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb23
-rw-r--r--activejob/lib/active_job/queue_adapters/resque_adapter.rb41
-rw-r--r--activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb35
-rw-r--r--activejob/lib/active_job/queue_adapters/sneakers_adapter.rb34
-rw-r--r--activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb25
-rw-r--r--activejob/lib/active_job/queue_name.rb18
-rw-r--r--activejob/lib/active_job/railtie.rb23
-rw-r--r--activejob/lib/active_job/version.rb8
-rw-r--r--activejob/lib/rails/generators/job/job_generator.rb22
-rw-r--r--activejob/lib/rails/generators/job/templates/job.rb9
-rw-r--r--activejob/test/adapters/backburner.rb3
-rw-r--r--activejob/test/adapters/delayed_job.rb7
-rw-r--r--activejob/test/adapters/inline.rb1
-rw-r--r--activejob/test/adapters/qu.rb3
-rw-r--r--activejob/test/adapters/que.rb2
-rw-r--r--activejob/test/adapters/queue_classic.rb2
-rw-r--r--activejob/test/adapters/resque.rb2
-rw-r--r--activejob/test/adapters/sidekiq.rb2
-rw-r--r--activejob/test/adapters/sneakers.rb2
-rw-r--r--activejob/test/adapters/sucker_punch.rb2
-rw-r--r--activejob/test/cases/adapter_test.rb8
-rw-r--r--activejob/test/cases/callbacks_test.rb22
-rw-r--r--activejob/test/cases/job_serialization_test.rb15
-rw-r--r--activejob/test/cases/logging_test.rb96
-rw-r--r--activejob/test/cases/parameters_test.rb77
-rw-r--r--activejob/test/cases/queue_naming_test.rb23
-rw-r--r--activejob/test/cases/queuing_test.rb44
-rw-r--r--activejob/test/cases/rescue_test.rb30
-rw-r--r--activejob/test/helper.rb50
-rw-r--r--activejob/test/jobs/callback_job.rb32
-rw-r--r--activejob/test/jobs/gid_job.rb6
-rw-r--r--activejob/test/jobs/hello_job.rb5
-rw-r--r--activejob/test/jobs/logging_job.rb10
-rw-r--r--activejob/test/jobs/nested_job.rb10
-rw-r--r--activejob/test/jobs/rescue_job.rb25
-rw-r--r--activejob/test/models/person.rb20
-rw-r--r--activejob/test/support/backburner/inline.rb8
-rw-r--r--activejob/test/support/delayed_job/delayed/backend/test.rb113
-rw-r--r--activejob/test/support/delayed_job/delayed/serialization/test.rb (renamed from actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb)0
-rw-r--r--activejob/test/support/queue_classic/inline.rb11
-rw-r--r--activejob/test/support/sneakers/inline.rb12
-rw-r--r--activemodel/lib/active_model/dirty.rb18
-rw-r--r--activerecord/CHANGELOG.md47
-rw-r--r--activerecord/Rakefile24
-rw-r--r--activerecord/activerecord.gemspec2
-rw-r--r--activerecord/lib/active_record/associations.rb12
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb2
-rw-r--r--activerecord/lib/active_record/attribute.rb15
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb17
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb25
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb24
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb2
-rw-r--r--activerecord/lib/active_record/enum.rb4
-rw-r--r--activerecord/lib/active_record/persistence.rb5
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb18
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/timestamp.rb7
-rw-r--r--activerecord/lib/active_record/type/value.rb11
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/active_schema_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/timestamp_test.rb8
-rw-r--r--activerecord/test/cases/ar_schema_test.rb57
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb8
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb20
-rw-r--r--activerecord/test/cases/attribute_test.rb30
-rw-r--r--activerecord/test/cases/dirty_test.rb21
-rw-r--r--activerecord/test/cases/finder_test.rb2
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb7
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb4
-rw-r--r--activerecord/test/cases/migration/columns_test.rb3
-rw-r--r--activerecord/test/cases/migration/helper.rb2
-rw-r--r--activerecord/test/cases/migration/index_test.rb6
-rw-r--r--activerecord/test/cases/migration_test.rb2
-rw-r--r--activerecord/test/cases/relation/where_test.rb9
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb4
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb8
-rw-r--r--activerecord/test/cases/test_case.rb2
-rw-r--r--activerecord/test/cases/timestamp_test.rb19
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb13
-rw-r--r--activerecord/test/cases/transactions_test.rb24
-rw-r--r--activerecord/test/cases/types_test.rb6
-rw-r--r--activerecord/test/cases/validations/i18n_validation_test.rb1
-rw-r--r--activerecord/test/cases/validations_repair_helper.rb2
-rw-r--r--activerecord/test/schema/schema.rb10
-rw-r--r--activesupport/CHANGELOG.md11
-rw-r--r--activesupport/lib/active_support/cache.rb2
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb4
-rw-r--r--activesupport/lib/active_support/message_encryptor.rb3
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb4
-rw-r--r--activesupport/lib/active_support/test_case.rb11
-rw-r--r--activesupport/lib/active_support/testing/time_helpers.rb6
-rw-r--r--activesupport/test/dependencies_test.rb23
-rw-r--r--activesupport/test/multibyte_chars_test.rb26
-rw-r--r--activesupport/test/multibyte_test_helpers.rb6
-rw-r--r--activesupport/test/test_test.rb13
-rwxr-xr-xci/travis.rb3
-rw-r--r--guides/source/4_2_release_notes.md6
-rw-r--r--guides/source/active_job_basics.md253
-rw-r--r--guides/source/caching_with_rails.md7
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md2
-rw-r--r--guides/source/engines.md4
-rw-r--r--guides/source/generators.md15
-rw-r--r--guides/source/layouts_and_rendering.md2
-rw-r--r--guides/source/plugins.md2
-rw-r--r--guides/source/ruby_on_rails_guides_guidelines.md22
-rw-r--r--guides/source/security.md8
-rw-r--r--guides/source/testing.md8
-rw-r--r--guides/source/upgrading_ruby_on_rails.md38
-rw-r--r--install.rb2
-rw-r--r--rails.gemspec1
-rw-r--r--railties/lib/rails/all.rb1
-rw-r--r--railties/lib/rails/api/task.rb7
-rw-r--r--railties/lib/rails/application.rb3
-rw-r--r--railties/lib/rails/engine.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile9
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt3
-rw-r--r--railties/lib/rails/generators/rails/controller/controller_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/Gemfile2
-rw-r--r--railties/lib/rails/rubyprof_ext.rb35
-rw-r--r--railties/test/generators/app_generator_test.rb17
-rw-r--r--railties/test/generators/model_generator_test.rb2
-rw-r--r--railties/test/generators_test.rb10
-rw-r--r--railties/test/path_generation_test.rb88
-rw-r--r--tasks/release.rb2
-rwxr-xr-xtools/line_statistics41
-rwxr-xr-xtools/profile162
248 files changed, 4025 insertions, 6423 deletions
diff --git a/.travis.yml b/.travis.yml
index 43b08044d3..4c45265db0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,7 +12,7 @@ rvm:
env:
- "GEM=railties"
- "GEM=ap"
- - "GEM=am,amo,as,av"
+ - "GEM=am,amo,as,av,aj"
- "GEM=ar:mysql"
- "GEM=ar:mysql2"
- "GEM=ar:sqlite3"
diff --git a/Gemfile b/Gemfile
index 04cef17458..df4c1e77ed 100644
--- a/Gemfile
+++ b/Gemfile
@@ -7,12 +7,14 @@ gemspec
# ensure correct loading order
gem 'mocha', '~> 0.14', require: false
-gem 'rack', github: 'rack/rack'
+gem 'rack', github: 'rack/rack', branch: 'master'
gem 'rack-cache', '~> 1.2'
gem 'jquery-rails', '~> 3.1.0'
gem 'turbolinks', github: 'rails/turbolinks', branch: 'master'
gem 'coffee-rails', '~> 4.0.0'
-gem 'arel', github: 'rails/arel', branch: 'master'
+gem 'rails-html-sanitizer', github: 'rails/rails-html-sanitizer'
+#temporary gem until a new version of loofah is released
+gem 'loofah', github: 'kaspth/loofah', branch: 'single-scrub'
gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master'
gem 'i18n', github: 'svenfuchs/i18n', branch: 'master'
@@ -35,6 +37,20 @@ end
# AS
gem 'dalli', '>= 2.2.1'
+# ActiveJob
+gem 'globalid', github: 'rails/globalid'
+gem 'resque', require: false
+gem 'resque-scheduler', require: false
+gem 'sidekiq', require: false
+gem 'sucker_punch', require: false
+gem 'delayed_job', require: false
+gem 'queue_classic', require: false
+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
+
# Add your own local bundler stuff
local_gemfile = File.dirname(__FILE__) + "/.Gemfile"
instance_eval File.read local_gemfile if File.exist? local_gemfile
diff --git a/README.md b/README.md
index 6a73727eed..d786914d6d 100644
--- a/README.md
+++ b/README.md
@@ -67,7 +67,7 @@ independently outside Rails.
* [Getting Started with Rails](http://guides.rubyonrails.org/getting_started.html)
* [Ruby on Rails Guides](http://guides.rubyonrails.org)
* [The API Documentation](http://api.rubyonrails.org)
- * [Ruby on Rails Tutorial](http://ruby.railstutorial.org/ruby-on-rails-tutorial-book)
+ * [Ruby on Rails Tutorial](http://www.railstutorial.org/book)
## Contributing
diff --git a/Rakefile b/Rakefile
index 0737afd089..db27105cc3 100644
--- a/Rakefile
+++ b/Rakefile
@@ -11,7 +11,7 @@ task :build => "all:build"
desc "Release all gems to rubygems and create a tag"
task :release => "all:release"
-PROJECTS = %w(activesupport activemodel actionpack actionview actionmailer activerecord railties)
+PROJECTS = %w(activesupport activemodel actionpack actionview actionmailer activerecord railties activejob)
desc 'Run all tests by default'
task :default => %w(test test:isolated)
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md
index ab93745f60..e6952a4b3b 100644
--- a/actionmailer/CHANGELOG.md
+++ b/actionmailer/CHANGELOG.md
@@ -1,3 +1,16 @@
+* Added #deliver_later in addition to #deliver, which will enqueue a job to render and
+ deliver the mail instead of delivering it right at that moment. The job is enqueued
+ using the new Active Job framework in Rails, and will use whatever queue is configured for Rails.
+
+ *DHH/Abdelkader Boudih/Cristian Bica*
+
+
+* Make ActionMailer::Previews methods class methods. Previously they were
+ instance methods and ActionMailer tries to render a message when they
+ are called.
+
+ *Cristian Bica*
+
* Deprecate `*_path` helpers in email views. When used they generate
non-working links and are not the intention of most developers. Instead
we recommend to use `*_url` helper.
diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec
index 01d97b7213..8452348e11 100644
--- a/actionmailer/actionmailer.gemspec
+++ b/actionmailer/actionmailer.gemspec
@@ -23,4 +23,5 @@ Gem::Specification.new do |s|
s.add_dependency 'actionview', version
s.add_dependency 'mail', ['~> 2.5', '>= 2.5.4']
+ s.add_dependency 'rails-dom-testing'
end
diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb
index 83969d4074..b994ef3182 100644
--- a/actionmailer/lib/action_mailer.rb
+++ b/actionmailer/lib/action_mailer.rb
@@ -45,4 +45,6 @@ module ActionMailer
autoload :Previews, 'action_mailer/preview'
autoload :TestCase
autoload :TestHelper
+ autoload :MessageDelivery
+ autoload :DeliveryJob
end
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index bc540aece0..9aae14ec8c 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -548,8 +548,8 @@ module ActionMailer
end
def method_missing(method_name, *args) # :nodoc:
- if respond_to?(method_name)
- new(method_name, *args).message
+ if action_methods.include?(method_name.to_s)
+ MessageDelivery.new(self, method_name, *args)
else
super
end
@@ -586,6 +586,10 @@ module ActionMailer
class NullMail #:nodoc:
def body; '' end
+ def respond_to?(string, include_all=false)
+ true
+ end
+
def method_missing(*args)
nil
end
diff --git a/actionmailer/lib/action_mailer/delivery_job.rb b/actionmailer/lib/action_mailer/delivery_job.rb
new file mode 100644
index 0000000000..b2cfa245fd
--- /dev/null
+++ b/actionmailer/lib/action_mailer/delivery_job.rb
@@ -0,0 +1,11 @@
+require 'active_job'
+
+module ActionMailer
+ class DeliveryJob < ActiveJob::Base
+ queue_as :mailers
+
+ def perform(mailer, mail_method, delivery_method, *args)
+ mailer.constantize.public_send(mail_method, *args).send(delivery_method)
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb
new file mode 100644
index 0000000000..80a0517bff
--- /dev/null
+++ b/actionmailer/lib/action_mailer/message_delivery.rb
@@ -0,0 +1,45 @@
+require 'delegate'
+
+module ActionMailer
+ class MessageDelivery < Delegator
+ def initialize(mailer, mail_method, *args)
+ @mailer = mailer
+ @mail_method = mail_method
+ @args = args
+ end
+
+ def __getobj__
+ @obj ||= @mailer.send(:new, @mail_method, *@args).message
+ end
+
+ def __setobj__(obj)
+ @obj = obj
+ end
+
+ def message #:nodoc:
+ __getobj__
+ end
+
+ def deliver_later!(options={})
+ enqueue_delivery :deliver!, options
+ end
+
+ def deliver_later(options={})
+ enqueue_delivery :deliver, options
+ end
+
+ private
+ def enqueue_delivery(delivery_method, options={})
+ args = @mailer.name, @mail_method.to_s, delivery_method.to_s, *@args
+ enqueue_method = :enqueue
+ if options[:at]
+ enqueue_method = :enqueue_at
+ args.unshift options[:at]
+ elsif options[:in]
+ enqueue_method = :enqueue_in
+ args.unshift options[:in]
+ end
+ ActionMailer::DeliveryJob.send enqueue_method, *args
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb
index 33de1dc049..44cf6665ba 100644
--- a/actionmailer/lib/action_mailer/preview.rb
+++ b/actionmailer/lib/action_mailer/preview.rb
@@ -22,7 +22,9 @@ module ActionMailer
# :nodoc:
mattr_accessor :preview_interceptors, instance_writer: false
self.preview_interceptors = []
+ end
+ module ClassMethods
# Register one or more Interceptors which will be called before mail is previewed.
def register_preview_interceptors(*interceptors)
interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) }
diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb
index a5442c0316..d507032838 100644
--- a/actionmailer/lib/action_mailer/test_case.rb
+++ b/actionmailer/lib/action_mailer/test_case.rb
@@ -1,4 +1,5 @@
require 'active_support/test_case'
+require 'rails-dom-testing'
module ActionMailer
class NonInferrableMailerError < ::StandardError
@@ -15,6 +16,7 @@ module ActionMailer
include ActiveSupport::Testing::ConstantLookup
include TestHelper
+ include Rails::Dom::Testing::Assertions::SelectorAssertions
included do
class_attribute :_mailer_class
@@ -55,14 +57,13 @@ module ActionMailer
protected
def initialize_test_deliveries
- @old_delivery_method = ActionMailer::Base.delivery_method
+ set_delivery_method :test
@old_perform_deliveries = ActionMailer::Base.perform_deliveries
- ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
end
def restore_test_deliveries
- ActionMailer::Base.delivery_method = @old_delivery_method
+ restore_delivery_method
ActionMailer::Base.perform_deliveries = @old_perform_deliveries
ActionMailer::Base.deliveries.clear
end
diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb
index 98d266bd73..c549545674 100644
--- a/actionmailer/test/abstract_unit.rb
+++ b/actionmailer/test/abstract_unit.rb
@@ -26,7 +26,7 @@ I18n.enforce_available_locales = false
FIXTURE_LOAD_PATH = File.expand_path('fixtures', File.dirname(__FILE__))
ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH
-class Rails
+module Rails
def self.root
File.expand_path('../', File.dirname(__FILE__))
end
diff --git a/actionmailer/test/assert_select_email_test.rb b/actionmailer/test/assert_select_email_test.rb
new file mode 100644
index 0000000000..57ae3436e1
--- /dev/null
+++ b/actionmailer/test/assert_select_email_test.rb
@@ -0,0 +1,47 @@
+require 'abstract_unit'
+
+class AssertSelectEmailTest < ActionMailer::TestCase
+ class AssertSelectMailer < ActionMailer::Base
+ def test(html)
+ mail body: html, content_type: "text/html",
+ subject: "Test e-mail", from: "test@test.host", to: "test <test@test.host>"
+ end
+ end
+
+ class AssertMultipartSelectMailer < ActionMailer::Base
+ def test(options)
+ mail subject: "Test e-mail", from: "test@test.host", to: "test <test@test.host>" do |format|
+ format.text { render text: options[:text] }
+ format.html { render text: options[:html] }
+ end
+ end
+ end
+
+ #
+ # Test assert_select_email
+ #
+
+ def test_assert_select_email
+ assert_raise ActiveSupport::TestCase::Assertion do
+ assert_select_email {}
+ end
+
+ AssertSelectMailer.test("<div><p>foo</p><p>bar</p></div>").deliver
+ assert_select_email do
+ assert_select "div:root" do
+ assert_select "p:first-child", "foo"
+ assert_select "p:last-child", "bar"
+ end
+ end
+ end
+
+ def test_assert_select_email_multipart
+ AssertMultipartSelectMailer.test(html: "<div><p>foo</p><p>bar</p></div>", text: 'foo bar').deliver
+ assert_select_email do
+ assert_select "div:root" do
+ assert_select "p:first-child", "foo"
+ assert_select "p:last-child", "bar"
+ end
+ end
+ end
+end
diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb
index 6116d1e29f..fc24639bf4 100644
--- a/actionmailer/test/base_test.rb
+++ b/actionmailer/test/base_test.rb
@@ -11,6 +11,8 @@ require 'mailers/asset_mailer'
class BaseTest < ActiveSupport::TestCase
setup do
+ @original_delivery_method = ActionMailer::Base.delivery_method
+ ActionMailer::Base.delivery_method = :test
@original_asset_host = ActionMailer::Base.asset_host
@original_assets_dir = ActionMailer::Base.assets_dir
end
@@ -19,6 +21,7 @@ class BaseTest < ActiveSupport::TestCase
ActionMailer::Base.asset_host = @original_asset_host
ActionMailer::Base.assets_dir = @original_assets_dir
BaseMailer.deliveries.clear
+ ActionMailer::Base.delivery_method = @original_delivery_method
end
test "method call to mail does not raise error" do
@@ -240,7 +243,7 @@ class BaseTest < ActiveSupport::TestCase
end
end
- e = assert_raises(RuntimeError) { LateAttachmentMailer.welcome }
+ e = assert_raises(RuntimeError) { LateAttachmentMailer.welcome.message }
assert_match(/Can't add attachments after `mail` was called./, e.message)
end
@@ -252,7 +255,7 @@ class BaseTest < ActiveSupport::TestCase
end
end
- e = assert_raises(RuntimeError) { LateInlineAttachmentMailer.welcome }
+ e = assert_raises(RuntimeError) { LateInlineAttachmentMailer.welcome.message }
assert_match(/Can't add attachments after `mail` was called./, e.message)
end
@@ -468,7 +471,6 @@ class BaseTest < ActiveSupport::TestCase
end
test "calling deliver on the action should increment the deliveries collection if using the test mailer" do
- BaseMailer.delivery_method = :test
BaseMailer.welcome.deliver
assert_equal(1, BaseMailer.deliveries.length)
end
diff --git a/actionmailer/test/mailers/delayed_mailer.rb b/actionmailer/test/mailers/delayed_mailer.rb
new file mode 100644
index 0000000000..62d4baa434
--- /dev/null
+++ b/actionmailer/test/mailers/delayed_mailer.rb
@@ -0,0 +1,6 @@
+class DelayedMailer < ActionMailer::Base
+
+ def test_message(*)
+ mail(from: 'test-sender@test.com', to: 'test-receiver@test.com', subject: 'Test Subject', body: 'Test Body')
+ end
+end
diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb
new file mode 100644
index 0000000000..a097d0e84d
--- /dev/null
+++ b/actionmailer/test/message_delivery_test.rb
@@ -0,0 +1,82 @@
+# encoding: utf-8
+gem 'activejob'
+require 'active_job'
+require 'abstract_unit'
+require 'minitest/mock'
+require_relative 'mailers/delayed_mailer'
+
+class MessageDeliveryTest < ActiveSupport::TestCase
+
+ setup do
+ @previous_logger = ActiveJob::Base.logger
+ @previous_delivery_method = ActionMailer::Base.delivery_method
+ ActionMailer::Base.delivery_method = :test
+ ActiveJob::Base.logger = Logger.new(nil)
+ @mail = DelayedMailer.test_message(1, 2, 3)
+ ActionMailer::Base.deliveries.clear
+ end
+
+ teardown do
+ ActiveJob::Base.logger = @previous_logger
+ ActionMailer::Base.delivery_method = @previous_delivery_method
+ end
+
+ test 'should have a message' do
+ assert @mail.message
+ end
+
+ test 'its message should be a Mail::Message' do
+ assert_equal Mail::Message , @mail.message.class
+ end
+
+ test 'should respond to .deliver' do
+ assert_respond_to @mail, :deliver
+ end
+
+ test 'should respond to .deliver!' do
+ assert_respond_to @mail, :deliver!
+ end
+
+ test 'should respond to .deliver_later' do
+ assert_respond_to @mail, :deliver_later
+ end
+
+ test 'should respond to .deliver_later!' do
+ assert_respond_to @mail, :deliver_later!
+ end
+
+ test 'should enqueue and run correctly in activejob' do
+ @mail.deliver_later!
+ assert_equal 1 , ActionMailer::Base.deliveries.size
+ end
+
+ test 'should enqueue the email with :deliver delivery method' do
+ ret = ActionMailer::DeliveryJob.stub :enqueue, ->(*args){ args } do
+ @mail.deliver_later
+ end
+ assert_equal ['DelayedMailer', 'test_message', 'deliver', 1, 2, 3], ret
+ end
+
+ test 'should enqueue the email with :deliver! delivery method' do
+ ret = ActionMailer::DeliveryJob.stub :enqueue, ->(*args){ args } do
+ @mail.deliver_later!
+ end
+ assert_equal ['DelayedMailer', 'test_message', 'deliver!', 1, 2, 3], ret
+ end
+
+ test 'should enqueue a delivery with a delay' do
+ ret = ActionMailer::DeliveryJob.stub :enqueue_in, ->(*args){ args } do
+ @mail.deliver_later in: 600
+ end
+ assert_equal [600, 'DelayedMailer', 'test_message', 'deliver', 1, 2, 3], ret
+ end
+
+ test 'should enqueue a delivery at a specific time' do
+ later_time = Time.now.to_i + 3600
+ ret = ActionMailer::DeliveryJob.stub :enqueue_at, ->(*args){ args } do
+ @mail.deliver_later at: later_time
+ end
+ assert_equal [later_time, 'DelayedMailer', 'test_message', 'deliver', 1, 2, 3], ret
+ end
+
+end
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 44b8fa843d..ddacbe2aa2 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,52 @@
+* Deleted the deprecated TagAssertions.
+
+ *Kasper Timm Hansen*
+
+* Use the Active Support JSON encoder for cookie jars using the `:json` or
+ `:hybrid` serializer. This allows you to serialize custom Ruby objects into
+ cookies by defining the `#as_json` hook on such objects.
+
+ Fixes #16520.
+
+ *Godfrey Chan*
+
+* Add `config.action_dispatch.cookies_digest` option for setting custom
+ digest. The default remains the same - 'SHA1'.
+
+ *Łukasz Strzałkowski*
+
+* Move `respond_with` (and the class-level `respond_to`) to
+ the `responders` gem.
+
+ *José Valim*
+
+* When your templates change, browser caches bust automatically.
+
+ New default: the template digest is automatically included in your ETags.
+ When you call `fresh_when @post`, the digest for `posts/show.html.erb`
+ is mixed in so future changes to the HTML will blow HTTP caches for you.
+ This makes it easy to HTTP-cache many more of your actions.
+
+ If you render a different template, you can now pass the `:template`
+ option to include its digest instead:
+
+ fresh_when @post, template: 'widgets/show'
+
+ Pass `template: false` to skip the lookup. To turn this off entirely, set:
+
+ config.action_controller.etag_with_template_digest = false
+
+ *Jeremy Kemper*
+
+* Remove deprecated `AbstractController::Helpers::ClassMethods::MissingHelperError`
+ in favor of `AbstractController::Helpers::MissingHelperError`.
+
+ *Yves Senn*
+
+* Fix `assert_template` not being able to assert that no files were rendered.
+
+ *Guo Xiang Tan*
+
* Extract source code for the entire exception stack trace for
better debugging and diagnosis.
diff --git a/actionpack/Rakefile b/actionpack/Rakefile
index 7eab972595..d12213a2d8 100644
--- a/actionpack/Rakefile
+++ b/actionpack/Rakefile
@@ -40,27 +40,9 @@ task :release => :package do
end
task :lines do
- lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
-
- FileList["lib/**/*.rb"].each do |file_name|
- next if file_name =~ /vendor/
- File.open(file_name, 'r') do |f|
- while line = f.gets
- lines += 1
- next if line =~ /^\s*$/
- next if line =~ /^\s*#/
- codelines += 1
- end
- end
- puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
-
- total_lines += lines
- total_codelines += codelines
-
- lines, codelines = 0, 0
- end
-
- puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
+ load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics'
+ files = FileList["lib/**/*.rb"]
+ CodeTools::LineStatistics.new(files).print_loc
end
rule '.rb' => '.y' do |t|
diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec
index d509891fe3..5834e79668 100644
--- a/actionpack/actionpack.gemspec
+++ b/actionpack/actionpack.gemspec
@@ -23,6 +23,7 @@ Gem::Specification.new do |s|
s.add_dependency 'rack', '~> 1.6.0.alpha'
s.add_dependency 'rack-test', '~> 0.6.2'
+ s.add_dependency 'rails-deprecated_sanitizer'
s.add_dependency 'actionview', version
s.add_development_dependency 'activemodel', version
diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb
index e77e4e01e9..95c67d482b 100644
--- a/actionpack/lib/abstract_controller/helpers.rb
+++ b/actionpack/lib/abstract_controller/helpers.rb
@@ -27,9 +27,6 @@ module AbstractController
end
module ClassMethods
- MissingHelperError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('AbstractController::Helpers::ClassMethods::MissingHelperError',
- 'AbstractController::Helpers::MissingHelperError')
-
# When a class is inherited, wrap its helper module in a new module.
# This ensures that the parent class's module can be changed
# independently of the child class's.
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index 50bc26a80f..7f1aeafe8b 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -17,6 +17,7 @@ module ActionController
autoload :ConditionalGet
autoload :Cookies
autoload :DataStreaming
+ autoload :EtagWithTemplateDigest
autoload :Flash
autoload :ForceSSL
autoload :Head
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index e6fe6b0b00..7bbf938987 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -213,6 +213,7 @@ module ActionController
Rendering,
Renderers::All,
ConditionalGet,
+ EtagWithTemplateDigest,
RackDelegation,
Caching,
MimeResponds,
diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb
index 12d798d0c1..de85e0c1a7 100644
--- a/actionpack/lib/action_controller/caching.rb
+++ b/actionpack/lib/action_controller/caching.rb
@@ -16,7 +16,7 @@ module ActionController
# All the caching stores from ActiveSupport::Cache are available to be used as backends
# for Action Controller caching.
#
- # Configuration examples (MemoryStore is the default):
+ # Configuration examples (FileStore is the default):
#
# config.action_controller.cache_store = :memory_store
# config.action_controller.cache_store = :file_store, '/path/to/cache/directory'
diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb
index 6e0cd51d8b..a93727df90 100644
--- a/actionpack/lib/action_controller/metal/conditional_get.rb
+++ b/actionpack/lib/action_controller/metal/conditional_get.rb
@@ -41,6 +41,11 @@ module ActionController
# * <tt>:last_modified</tt>.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cachable by other devices (proxy caches).
+ # * <tt>:template</tt> By default, the template digest for the current
+ # controller/action is included in ETags. If the action renders a
+ # different template, you can include its digest instead. If the action
+ # doesn't render a template at all, you can pass <tt>template: false</tt>
+ # to skip any attempt to check for a template digest.
#
# === Example:
#
@@ -66,18 +71,24 @@ module ActionController
# @article = Article.find(params[:id])
# fresh_when(@article, public: true)
# end
+ #
+ # When rendering a different template than the default controller/action
+ # style, you can indicate which digest to include in the ETag:
+ #
+ # before_action { fresh_when @article, template: 'widgets/show' }
+ #
def fresh_when(record_or_options, additional_options = {})
if record_or_options.is_a? Hash
options = record_or_options
- options.assert_valid_keys(:etag, :last_modified, :public)
+ options.assert_valid_keys(:etag, :last_modified, :public, :template)
else
record = record_or_options
options = { etag: record, last_modified: record.try(:updated_at) }.merge!(additional_options)
end
- response.etag = combine_etags(options[:etag]) if options[:etag]
- response.last_modified = options[:last_modified] if options[:last_modified]
- response.cache_control[:public] = true if options[:public]
+ response.etag = combine_etags(options) if options[:etag] || options[:template]
+ response.last_modified = options[:last_modified] if options[:last_modified]
+ response.cache_control[:public] = true if options[:public]
head :not_modified if request.fresh?(response)
end
@@ -93,6 +104,11 @@ module ActionController
# * <tt>:last_modified</tt>.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cachable by other devices (proxy caches).
+ # * <tt>:template</tt> By default, the template digest for the current
+ # controller/action is included in ETags. If the action renders a
+ # different template, you can include its digest instead. If the action
+ # doesn't render a template at all, you can pass <tt>template: false</tt>
+ # to skip any attempt to check for a template digest.
#
# === Example:
#
@@ -133,6 +149,14 @@ module ActionController
# end
# end
# end
+ #
+ # When rendering a different template than the default controller/action
+ # style, you can indicate which digest to include in the ETag:
+ #
+ # def show
+ # super if stale? @article, template: 'widgets/show'
+ # end
+ #
def stale?(record_or_options, additional_options = {})
fresh_when(record_or_options, additional_options)
!request.fresh?(response)
@@ -168,8 +192,9 @@ module ActionController
end
private
- def combine_etags(etag)
- [ etag, *etaggers.map { |etagger| instance_exec(&etagger) }.compact ]
+ def combine_etags(options)
+ etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact
+ etags.unshift options[:etag]
end
end
end
diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
new file mode 100644
index 0000000000..3ca0c6837a
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
@@ -0,0 +1,50 @@
+module ActionController
+ # When our views change, they should bubble up into HTTP cache freshness
+ # and bust browser caches. So the template digest for the current action
+ # is automatically included in the ETag.
+ #
+ # Enabled by default for apps that use Action View. Disable by setting
+ #
+ # config.action_controller.etag_with_template_digest = false
+ #
+ # Override the template to digest by passing `:template` to `fresh_when`
+ # and `stale?` calls. For example:
+ #
+ # # We're going to render widgets/show, not posts/show
+ # fresh_when @post, template: 'widgets/show'
+ #
+ # # We're not going to render a template, so omit it from the ETag.
+ # fresh_when @post, template: false
+ #
+ module EtagWithTemplateDigest
+ extend ActiveSupport::Concern
+
+ include ActionController::ConditionalGet
+
+ included do
+ class_attribute :etag_with_template_digest
+ self.etag_with_template_digest = true
+
+ ActiveSupport.on_load :action_view, yield: true do |action_view_base|
+ etag do |options|
+ determine_template_etag(options) if etag_with_template_digest
+ end
+ end
+ end
+
+ private
+ def determine_template_etag(options)
+ if template = pick_template_for_etag(options)
+ lookup_and_digest_template(template)
+ end
+ end
+
+ def pick_template_for_etag(options)
+ options.fetch(:template) { "#{controller_name}/#{action_name}" }
+ end
+
+ def lookup_and_digest_template(template)
+ ActionView::Digestor.digest name: template, finder: lookup_context
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
index 706ce04062..c9ef3a3dad 100644
--- a/actionpack/lib/action_controller/metal/live.rb
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -303,10 +303,12 @@ module ActionController
logger = ActionController::Base.logger
return unless logger
- message = "\n#{exception.class} (#{exception.message}):\n"
- message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
- message << " " << exception.backtrace.join("\n ")
- logger.fatal("#{message}\n\n")
+ logger.fatal do
+ message = "\n#{exception.class} (#{exception.message}):\n"
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << " " << exception.backtrace.join("\n ")
+ "#{message}\n\n"
+ end
end
def response_body=(body)
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index 00e7e980f8..dc572f13d2 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -5,56 +5,22 @@ module ActionController #:nodoc:
module MimeResponds
extend ActiveSupport::Concern
- included do
- class_attribute :responder, :mimes_for_respond_to
- self.responder = ActionController::Responder
- clear_respond_to
- end
-
module ClassMethods
- # Defines mime types that are rendered by default when invoking
- # <tt>respond_with</tt>.
- #
- # respond_to :html, :xml, :json
- #
- # Specifies that all actions in the controller respond to requests
- # for <tt>:html</tt>, <tt>:xml</tt> and <tt>:json</tt>.
- #
- # To specify on per-action basis, use <tt>:only</tt> and
- # <tt>:except</tt> with an array of actions or a single action:
- #
- # respond_to :html
- # respond_to :xml, :json, except: [ :edit ]
- #
- # This specifies that all actions respond to <tt>:html</tt>
- # and all actions except <tt>:edit</tt> respond to <tt>:xml</tt> and
- # <tt>:json</tt>.
- #
- # respond_to :json, only: :create
- #
- # This specifies that the <tt>:create</tt> action and no other responds
- # to <tt>:json</tt>.
- def respond_to(*mimes)
- options = mimes.extract_options!
-
- only_actions = Array(options.delete(:only)).map(&:to_s)
- except_actions = Array(options.delete(:except)).map(&:to_s)
-
- new = mimes_for_respond_to.dup
- mimes.each do |mime|
- mime = mime.to_sym
- new[mime] = {}
- new[mime][:only] = only_actions unless only_actions.empty?
- new[mime][:except] = except_actions unless except_actions.empty?
- end
- self.mimes_for_respond_to = new.freeze
+ def respond_to(*)
+ raise NoMethodError, "The controller-level `respond_to' feature has " \
+ "been extracted to the `responders` gem. Add it to your Gemfile to " \
+ "continue using this feature:\n" \
+ " gem 'responders', '~> 2.0'\n" \
+ "Consult the Rails upgrade guide for details."
end
+ end
- # Clear all mime types in <tt>respond_to</tt>.
- #
- def clear_respond_to
- self.mimes_for_respond_to = Hash.new.freeze
- end
+ def respond_with(*)
+ raise NoMethodError, "The `respond_with' feature has been extracted " \
+ "to the `responders` gem. Add it to your Gemfile to continue using " \
+ "this feature:\n" \
+ " gem 'responders', '~> 2.0'\n" \
+ "Consult the Rails upgrade guide for details."
end
# Without web-service support, an action which collects the data for displaying a list of people
@@ -217,7 +183,7 @@ module ActionController #:nodoc:
# format.html.phone { redirect_to progress_path }
# format.html.none { render "trash" }
# end
- #
+ #
# Variants also support common `any`/`all` block that formats have.
#
# It works for both inline:
@@ -253,189 +219,13 @@ module ActionController #:nodoc:
def respond_to(*mimes, &block)
raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
- if collector = retrieve_collector_from_mimes(mimes, &block)
- response = collector.response
- response ? response.call : render({})
- end
- end
-
- # For a given controller action, respond_with generates an appropriate
- # response based on the mime-type requested by the client.
- #
- # If the method is called with just a resource, as in this example -
- #
- # class PeopleController < ApplicationController
- # respond_to :html, :xml, :json
- #
- # def index
- # @people = Person.all
- # respond_with @people
- # end
- # end
- #
- # then the mime-type of the response is typically selected based on the
- # request's Accept header and the set of available formats declared
- # by previous calls to the controller's class method +respond_to+. Alternatively
- # the mime-type can be selected by explicitly setting <tt>request.format</tt> in
- # the controller.
- #
- # If an acceptable format is not identified, the application returns a
- # '406 - not acceptable' status. Otherwise, the default response is to render
- # a template named after the current action and the selected format,
- # e.g. <tt>index.html.erb</tt>. If no template is available, the behavior
- # depends on the selected format:
- #
- # * for an html response - if the request method is +get+, an exception
- # is raised but for other requests such as +post+ the response
- # depends on whether the resource has any validation errors (i.e.
- # assuming that an attempt has been made to save the resource,
- # e.g. by a +create+ action) -
- # 1. If there are no errors, i.e. the resource
- # was saved successfully, the response +redirect+'s to the resource
- # i.e. its +show+ action.
- # 2. If there are validation errors, the response
- # renders a default action, which is <tt>:new</tt> for a
- # +post+ request or <tt>:edit</tt> for +patch+ or +put+.
- # Thus an example like this -
- #
- # respond_to :html, :xml
- #
- # def create
- # @user = User.new(params[:user])
- # flash[:notice] = 'User was successfully created.' if @user.save
- # respond_with(@user)
- # end
- #
- # is equivalent, in the absence of <tt>create.html.erb</tt>, to -
- #
- # def create
- # @user = User.new(params[:user])
- # respond_to do |format|
- # if @user.save
- # flash[:notice] = 'User was successfully created.'
- # format.html { redirect_to(@user) }
- # format.xml { render xml: @user }
- # else
- # format.html { render action: "new" }
- # format.xml { render xml: @user }
- # end
- # end
- # end
- #
- # * for a JavaScript request - if the template isn't found, an exception is
- # raised.
- # * for other requests - i.e. data formats such as xml, json, csv etc, if
- # the resource passed to +respond_with+ responds to <code>to_<format></code>,
- # the method attempts to render the resource in the requested format
- # directly, e.g. for an xml request, the response is equivalent to calling
- # <code>render xml: resource</code>.
- #
- # === Nested resources
- #
- # As outlined above, the +resources+ argument passed to +respond_with+
- # can play two roles. It can be used to generate the redirect url
- # for successful html requests (e.g. for +create+ actions when
- # no template exists), while for formats other than html and JavaScript
- # it is the object that gets rendered, by being converted directly to the
- # required format (again assuming no template exists).
- #
- # For redirecting successful html requests, +respond_with+ also supports
- # the use of nested resources, which are supplied in the same way as
- # in <code>form_for</code> and <code>polymorphic_url</code>. For example -
- #
- # def create
- # @project = Project.find(params[:project_id])
- # @task = @project.comments.build(params[:task])
- # flash[:notice] = 'Task was successfully created.' if @task.save
- # respond_with(@project, @task)
- # end
- #
- # This would cause +respond_with+ to redirect to <code>project_task_url</code>
- # instead of <code>task_url</code>. For request formats other than html or
- # JavaScript, if multiple resources are passed in this way, it is the last
- # one specified that is rendered.
- #
- # === Customizing response behavior
- #
- # Like +respond_to+, +respond_with+ may also be called with a block that
- # can be used to overwrite any of the default responses, e.g. -
- #
- # def create
- # @user = User.new(params[:user])
- # flash[:notice] = "User was successfully created." if @user.save
- #
- # respond_with(@user) do |format|
- # format.html { render }
- # end
- # end
- #
- # The argument passed to the block is an ActionController::MimeResponds::Collector
- # object which stores the responses for the formats defined within the
- # block. Note that formats with responses defined explicitly in this way
- # do not have to first be declared using the class method +respond_to+.
- #
- # Also, a hash passed to +respond_with+ immediately after the specified
- # resource(s) is interpreted as a set of options relevant to all
- # formats. Any option accepted by +render+ can be used, e.g.
- # respond_with @people, status: 200
- # However, note that these options are ignored after an unsuccessful attempt
- # to save a resource, e.g. when automatically rendering <tt>:new</tt>
- # after a post request.
- #
- # Two additional options are relevant specifically to +respond_with+ -
- # 1. <tt>:location</tt> - overwrites the default redirect location used after
- # a successful html +post+ request.
- # 2. <tt>:action</tt> - overwrites the default render action used after an
- # unsuccessful html +post+ request.
- def respond_with(*resources, &block)
- if self.class.mimes_for_respond_to.empty?
- raise "In order to use respond_with, first you need to declare the " \
- "formats your controller responds to in the class level."
- end
-
- if collector = retrieve_collector_from_mimes(&block)
- options = resources.size == 1 ? {} : resources.extract_options!
- options = options.clone
- options[:default_response] = collector.response
- (options.delete(:responder) || self.class.responder).call(self, resources, options)
- end
- end
-
- protected
-
- # Collect mimes declared in the class method respond_to valid for the
- # current action.
- def collect_mimes_from_class_level #:nodoc:
- action = action_name.to_s
-
- self.class.mimes_for_respond_to.keys.select do |mime|
- config = self.class.mimes_for_respond_to[mime]
-
- if config[:except]
- !config[:except].include?(action)
- elsif config[:only]
- config[:only].include?(action)
- else
- true
- end
- end
- end
-
- # Returns a Collector object containing the appropriate mime-type response
- # for the current request, based on the available responses defined by a block.
- # In typical usage this is the block passed to +respond_with+ or +respond_to+.
- #
- # Sends :not_acceptable to the client and returns nil if no suitable format
- # is available.
- def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc:
- mimes ||= collect_mimes_from_class_level
collector = Collector.new(mimes, request.variant)
block.call(collector) if block_given?
- format = collector.negotiate_format(request)
- if format
+ if format = collector.negotiate_format(request)
_process_format(format)
- collector
+ response = collector.response
+ response ? response.call : render({})
else
raise ActionController::UnknownFormat
end
diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb
deleted file mode 100644
index 5096558c67..0000000000
--- a/actionpack/lib/action_controller/metal/responder.rb
+++ /dev/null
@@ -1,297 +0,0 @@
-require 'active_support/json'
-
-module ActionController #:nodoc:
- # Responsible for exposing a resource to different mime requests,
- # usually depending on the HTTP verb. The responder is triggered when
- # <code>respond_with</code> is called. The simplest case to study is a GET request:
- #
- # class PeopleController < ApplicationController
- # respond_to :html, :xml, :json
- #
- # def index
- # @people = Person.all
- # respond_with(@people)
- # end
- # end
- #
- # When a request comes in, for example for an XML response, three steps happen:
- #
- # 1) the responder searches for a template at people/index.xml;
- #
- # 2) if the template is not available, it will invoke <code>#to_xml</code> on the given resource;
- #
- # 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it.
- #
- # === Built-in HTTP verb semantics
- #
- # The default \Rails responder holds semantics for each HTTP verb. Depending on the
- # content type, verb and the resource status, it will behave differently.
- #
- # Using \Rails default responder, a POST request for creating an object could
- # be written as:
- #
- # def create
- # @user = User.new(params[:user])
- # flash[:notice] = 'User was successfully created.' if @user.save
- # respond_with(@user)
- # end
- #
- # Which is exactly the same as:
- #
- # def create
- # @user = User.new(params[:user])
- #
- # respond_to do |format|
- # if @user.save
- # flash[:notice] = 'User was successfully created.'
- # format.html { redirect_to(@user) }
- # format.xml { render xml: @user, status: :created, location: @user }
- # else
- # format.html { render action: "new" }
- # format.xml { render xml: @user.errors, status: :unprocessable_entity }
- # end
- # end
- # end
- #
- # The same happens for PATCH/PUT and DELETE requests.
- #
- # === Nested resources
- #
- # You can supply nested resources as you do in <code>form_for</code> and <code>polymorphic_url</code>.
- # Consider the project has many tasks example. The create action for
- # TasksController would be like:
- #
- # def create
- # @project = Project.find(params[:project_id])
- # @task = @project.tasks.build(params[:task])
- # flash[:notice] = 'Task was successfully created.' if @task.save
- # respond_with(@project, @task)
- # end
- #
- # Giving several resources ensures that the responder will redirect to
- # <code>project_task_url</code> instead of <code>task_url</code>.
- #
- # Namespaced and singleton resources require a symbol to be given, as in
- # polymorphic urls. If a project has one manager which has many tasks, it
- # should be invoked as:
- #
- # respond_with(@project, :manager, @task)
- #
- # Note that if you give an array, it will be treated as a collection,
- # so the following is not equivalent:
- #
- # respond_with [@project, :manager, @task]
- #
- # === Custom options
- #
- # <code>respond_with</code> also allows you to pass options that are forwarded
- # to the underlying render call. Those options are only applied for success
- # scenarios. For instance, you can do the following in the create method above:
- #
- # def create
- # @project = Project.find(params[:project_id])
- # @task = @project.tasks.build(params[:task])
- # flash[:notice] = 'Task was successfully created.' if @task.save
- # respond_with(@project, @task, status: 201)
- # end
- #
- # This will return status 201 if the task was saved successfully. If not,
- # it will simply ignore the given options and return status 422 and the
- # resource errors. You can also override the location to redirect to:
- #
- # respond_with(@project, location: root_path)
- #
- # To customize the failure scenario, you can pass a block to
- # <code>respond_with</code>:
- #
- # def create
- # @project = Project.find(params[:project_id])
- # @task = @project.tasks.build(params[:task])
- # respond_with(@project, @task, status: 201) do |format|
- # if @task.save
- # flash[:notice] = 'Task was successfully created.'
- # else
- # format.html { render "some_special_template" }
- # end
- # end
- # end
- #
- # Using <code>respond_with</code> with a block follows the same syntax as <code>respond_to</code>.
- class Responder
- attr_reader :controller, :request, :format, :resource, :resources, :options
-
- DEFAULT_ACTIONS_FOR_VERBS = {
- :post => :new,
- :patch => :edit,
- :put => :edit
- }
-
- def initialize(controller, resources, options={})
- @controller = controller
- @request = @controller.request
- @format = @controller.formats.first
- @resource = resources.last
- @resources = resources
- @options = options
- @action = options.delete(:action)
- @default_response = options.delete(:default_response)
- end
-
- delegate :head, :render, :redirect_to, :to => :controller
- delegate :get?, :post?, :patch?, :put?, :delete?, :to => :request
-
- # Undefine :to_json and :to_yaml since it's defined on Object
- undef_method(:to_json) if method_defined?(:to_json)
- undef_method(:to_yaml) if method_defined?(:to_yaml)
-
- # Initializes a new responder and invokes the proper format. If the format is
- # not defined, call to_format.
- #
- def self.call(*args)
- new(*args).respond
- end
-
- # Main entry point for responder responsible to dispatch to the proper format.
- #
- def respond
- method = "to_#{format}"
- respond_to?(method) ? send(method) : to_format
- end
-
- # HTML format does not render the resource, it always attempt to render a
- # template.
- #
- def to_html
- default_render
- rescue ActionView::MissingTemplate => e
- navigation_behavior(e)
- end
-
- # to_js simply tries to render a template. If no template is found, raises the error.
- def to_js
- default_render
- end
-
- # All other formats follow the procedure below. First we try to render a
- # template, if the template is not available, we verify if the resource
- # responds to :to_format and display it.
- #
- def to_format
- if get? || !has_errors? || response_overridden?
- default_render
- else
- display_errors
- end
- rescue ActionView::MissingTemplate => e
- api_behavior(e)
- end
-
- protected
-
- # This is the common behavior for formats associated with browsing, like :html, :iphone and so forth.
- def navigation_behavior(error)
- if get?
- raise error
- elsif has_errors? && default_action
- render :action => default_action
- else
- redirect_to navigation_location
- end
- end
-
- # This is the common behavior for formats associated with APIs, such as :xml and :json.
- def api_behavior(error)
- raise error unless resourceful?
- raise MissingRenderer.new(format) unless has_renderer?
-
- if get?
- display resource
- elsif post?
- display resource, :status => :created, :location => api_location
- else
- head :no_content
- end
- end
-
- # Checks whether the resource responds to the current format or not.
- #
- def resourceful?
- resource.respond_to?("to_#{format}")
- end
-
- # Returns the resource location by retrieving it from the options or
- # returning the resources array.
- #
- def resource_location
- options[:location] || resources
- end
- alias :navigation_location :resource_location
- alias :api_location :resource_location
-
- # If a response block was given, use it, otherwise call render on
- # controller.
- #
- def default_render
- if @default_response
- @default_response.call(options)
- else
- controller.default_render(options)
- end
- end
-
- # Display is just a shortcut to render a resource with the current format.
- #
- # display @user, status: :ok
- #
- # For XML requests it's equivalent to:
- #
- # render xml: @user, status: :ok
- #
- # Options sent by the user are also used:
- #
- # respond_with(@user, status: :created)
- # display(@user, status: :ok)
- #
- # Results in:
- #
- # render xml: @user, status: :created
- #
- def display(resource, given_options={})
- controller.render given_options.merge!(options).merge!(format => resource)
- end
-
- def display_errors
- controller.render format => resource_errors, :status => :unprocessable_entity
- end
-
- # Check whether the resource has errors.
- #
- def has_errors?
- resource.respond_to?(:errors) && !resource.errors.empty?
- end
-
- # Check whether the necessary Renderer is available
- def has_renderer?
- Renderers::RENDERERS.include?(format)
- end
-
- # By default, render the <code>:edit</code> action for HTML requests with errors, unless
- # the verb was POST.
- #
- def default_action
- @action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol]
- end
-
- def resource_errors
- respond_to?("#{format}_resource_errors", true) ? send("#{format}_resource_errors") : resource.errors
- end
-
- def json_resource_errors
- {:errors => resource.errors}
- end
-
- def response_overridden?
- @default_response.present?
- end
- end
-end
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index eb5d824cbc..8c10c3e7b0 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -3,6 +3,8 @@ require 'active_support/core_ext/object/to_query'
require 'active_support/core_ext/module/anonymous'
require 'active_support/core_ext/hash/keys'
+require 'rails-dom-testing'
+
module ActionController
module TemplateAssertions
extend ActiveSupport::Concern
@@ -91,6 +93,13 @@ module ActionController
# # assert that no partials were rendered
# assert_template partial: false
#
+ # # assert that a file was rendered
+ # assert_template file: "README.rdoc"
+ #
+ # # assert that no file was rendered
+ # assert_template file: nil
+ # assert_template file: false
+ #
# In a view test case, you can also assert that specific locals are passed
# to partials:
#
@@ -140,6 +149,8 @@ module ActionController
if options[:file]
assert_includes @_files.keys, options[:file]
+ elsif options.key?(:file)
+ assert @_files.blank?, "expected no files but #{@_files.keys} was rendered"
end
if expected_partial = options[:partial]
@@ -433,6 +444,7 @@ module ActionController
extend ActiveSupport::Concern
include ActionDispatch::TestProcess
include ActiveSupport::Testing::ConstantLookup
+ include Rails::Dom::Testing::Assertions
attr_reader :response, :request
@@ -675,6 +687,11 @@ module ActionController
end
private
+
+ def document_root_element
+ html_document.root
+ end
+
def check_required_ivars
# Sanity check for required instance variables so we can give an
# understandable error message.
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index a519d6c1fc..8c035c3c6c 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -225,7 +225,7 @@ module ActionDispatch
@remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s
end
- # Returns the unique request id, which is based off either the X-Request-Id header that can
+ # Returns the unique request id, which is based on either the X-Request-Id header that can
# be generated by a firewall, load balancer, or web server or by the RequestId middleware
# (which sets the action_dispatch.request_id environment variable).
#
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index ac9e5effe2..83ac62a83d 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -3,6 +3,7 @@ require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/object/blank'
require 'active_support/key_generator'
require 'active_support/message_verifier'
+require 'active_support/json'
module ActionDispatch
class Request < Rack::Request
@@ -90,6 +91,7 @@ module ActionDispatch
SECRET_TOKEN = "action_dispatch.secret_token".freeze
SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
+ COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
# Cookies can typically store 4096 bytes.
MAX_COOKIE_SIZE = 4096
@@ -173,10 +175,14 @@ module ActionDispatch
end
end
+ # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream
+ # to the Message{Encryptor,Verifier} allows us to handle the
+ # (de)serialization step within the cookie jar, which gives us the
+ # opportunity to detect and migrate legacy cookies.
module VerifyAndUpgradeLegacySignedMessage
def initialize(*args)
super
- @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: NullSerializer)
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
def verify_and_upgrade_legacy_signed_message(name, signed_message)
@@ -212,7 +218,8 @@ module ActionDispatch
secret_token: env[SECRET_TOKEN],
secret_key_base: env[SECRET_KEY_BASE],
upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?,
- serializer: env[COOKIES_SERIALIZER]
+ serializer: env[COOKIES_SERIALIZER],
+ digest: env[COOKIES_DIGEST]
}
end
@@ -385,24 +392,11 @@ module ActionDispatch
class JsonSerializer
def self.load(value)
- JSON.parse(value, quirks_mode: true)
+ ActiveSupport::JSON.decode(value)
end
def self.dump(value)
- JSON.generate(value, quirks_mode: true)
- end
- end
-
- # Passing the NullSerializer downstream to the Message{Encryptor,Verifier}
- # allows us to handle the (de)serialization step within the cookie jar,
- # which gives us the opportunity to detect and migrate legacy cookies.
- class NullSerializer
- def self.load(value)
- value
- end
-
- def self.dump(value)
- value
+ ActiveSupport::JSON.encode(value)
end
end
@@ -441,6 +435,10 @@ module ActionDispatch
serializer
end
end
+
+ def digest
+ @options[:digest] || 'SHA1'
+ end
end
class SignedCookieJar #:nodoc:
@@ -451,7 +449,7 @@ module ActionDispatch
@parent_jar = parent_jar
@options = options
secret = key_generator.generate_key(@options[:signed_cookie_salt])
- @verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer)
+ @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
def [](name)
@@ -508,7 +506,7 @@ module ActionDispatch
@options = options
secret = key_generator.generate_key(@options[:encrypted_cookie_salt])
sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt])
- @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: NullSerializer)
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
def [](name)
diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb
index 5d1740d0d4..25658bac3d 100644
--- a/actionpack/lib/action_dispatch/middleware/request_id.rb
+++ b/actionpack/lib/action_dispatch/middleware/request_id.rb
@@ -5,7 +5,7 @@ module ActionDispatch
# Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through
# ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header.
#
- # The unique request id is either based off the X-Request-Id header in the request, which would typically be generated
+ # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
# by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
# header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
#
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
index 36b01bf952..c0b53068f7 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
@@ -3,7 +3,7 @@ Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unse
<% @traces.each do |name, trace| %>
<% if trace.any? %>
<%= name %>
-<%= trace.map(&:trace).join("\n") %>
+<%= trace.map { |t| t[:trace] }.join("\n") %>
<% end %>
<% end %>
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index cd94f35e8f..e92baa5aa7 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -63,7 +63,7 @@ module ActionDispatch
attr_reader :requirements, :conditions, :defaults
attr_reader :to, :default_controller, :default_action, :as, :anchor
- def self.build(scope, set, path, options)
+ def self.build(scope, set, path, as, options)
options = scope[:options].merge(options) if scope[:options]
options.delete :only
@@ -74,10 +74,10 @@ module ActionDispatch
defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}
- new scope, set, path, defaults, options
+ new scope, set, path, defaults, as, options
end
- def initialize(scope, set, path, defaults, options)
+ def initialize(scope, set, path, defaults, as, options)
@requirements, @conditions = {}, {}
@defaults = defaults
@set = set
@@ -85,7 +85,7 @@ module ActionDispatch
@to = options.delete :to
@default_controller = options.delete(:controller) || scope[:controller]
@default_action = options.delete(:action) || scope[:action]
- @as = options.delete :as
+ @as = as
@anchor = options.delete :anchor
formatted = options.delete :format
@@ -1046,8 +1046,6 @@ module ActionDispatch
VALID_ON_OPTIONS = [:new, :collection, :member]
RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
CANONICAL_ACTIONS = %w(index create new show update destroy)
- RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
- RESOURCE_SCOPES = [:resource, :resources]
class Resource #:nodoc:
attr_reader :controller, :path, :options, :param
@@ -1521,7 +1519,7 @@ module ActionDispatch
if on = options.delete(:on)
send(on) { decomposed_match(path, options) }
else
- case @scope[:scope_level]
+ case @scope.scope_level
when :resources
nested { decomposed_match(path, options) }
when :resource
@@ -1544,13 +1542,13 @@ module ActionDispatch
action = nil
end
- if !options.fetch(:as, true) # if it's set to nil or false
- options.delete(:as)
- else
- options[:as] = name_for_action(options[:as], action)
- end
+ as = if !options.fetch(:as, true) # if it's set to nil or false
+ options.delete(:as)
+ else
+ name_for_action(options.delete(:as), action)
+ end
- mapping = Mapping.build(@scope, @set, URI.parser.escape(path), options)
+ mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options)
app, conditions, requirements, defaults, as, anchor = mapping.to_route
@set.add_route(app, conditions, requirements, defaults, as, anchor)
end
@@ -1564,7 +1562,7 @@ module ActionDispatch
raise ArgumentError, "must be called with a path and/or options"
end
- if @scope[:scope_level] == :resources
+ if @scope.resources?
with_scope_level(:root) do
scope(parent_resource.path) do
super(options)
@@ -1631,15 +1629,15 @@ module ActionDispatch
end
def resource_scope? #:nodoc:
- RESOURCE_SCOPES.include? @scope[:scope_level]
+ @scope.resource_scope?
end
def resource_method_scope? #:nodoc:
- RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
+ @scope.resource_method_scope?
end
def nested_scope? #:nodoc:
- @scope[:scope_level] == :nested
+ @scope.nested?
end
def with_exclusive_scope
@@ -1655,7 +1653,7 @@ module ActionDispatch
end
def with_scope_level(kind)
- @scope = @scope.new(:scope_level => kind)
+ @scope = @scope.new_level(kind)
yield
ensure
@scope = @scope.parent
@@ -1699,8 +1697,8 @@ module ActionDispatch
@scope[:constraints][parent_resource.param]
end
- def canonical_action?(action, flag) #:nodoc:
- flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
+ def canonical_action?(action) #:nodoc:
+ resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
end
def shallow_scope(path, options = {}) #:nodoc:
@@ -1714,7 +1712,7 @@ module ActionDispatch
end
def path_for_action(action, path) #:nodoc:
- if canonical_action?(action, path.blank?)
+ if path.blank? && canonical_action?(action)
@scope[:path].to_s
else
"#{@scope[:path]}/#{action_path(action, path)}"
@@ -1729,15 +1727,17 @@ module ActionDispatch
def prefix_name_for_action(as, action) #:nodoc:
if as
prefix = as
- elsif !canonical_action?(action, @scope[:scope_level])
+ elsif !canonical_action?(action)
prefix = action
end
- prefix.to_s.tr('-', '_') if prefix
+
+ if prefix && prefix != '/' && !prefix.empty?
+ Mapper.normalize_name prefix.to_s.tr('-', '_')
+ end
end
def name_for_action(as, action) #:nodoc:
prefix = prefix_name_for_action(as, action)
- prefix = Mapper.normalize_name(prefix) if prefix
name_prefix = @scope[:as]
if parent_resource
@@ -1747,22 +1747,9 @@ module ActionDispatch
member_name = parent_resource.member_name
end
- name = case @scope[:scope_level]
- when :nested
- [name_prefix, prefix]
- when :collection
- [prefix, name_prefix, collection_name]
- when :new
- [prefix, :new, name_prefix, member_name]
- when :member
- [prefix, name_prefix, member_name]
- when :root
- [name_prefix, collection_name, prefix]
- else
- [name_prefix, member_name, prefix]
- end
+ name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
- if candidate = name.select(&:present?).join("_").presence
+ if candidate = name.compact.join("_").presence
# If a name was not explicitly given, we check if it is valid
# and return nil in case it isn't. Otherwise, we pass the invalid name
# forward so the underlying router engine treats it and raises an exception.
@@ -1897,11 +1884,48 @@ module ActionDispatch
:controller, :action, :path_names, :constraints,
:shallow, :blocks, :defaults, :options]
- attr_reader :parent
+ RESOURCE_SCOPES = [:resource, :resources]
+ RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
+
+ attr_reader :parent, :scope_level
- def initialize(hash, parent = {})
+ def initialize(hash, parent = {}, scope_level = nil)
@hash = hash
@parent = parent
+ @scope_level = scope_level
+ end
+
+ def nested?
+ scope_level == :nested
+ end
+
+ def resources?
+ scope_level == :resources
+ end
+
+ def resource_method_scope?
+ RESOURCE_METHOD_SCOPES.include? scope_level
+ end
+
+ def action_name(name_prefix, prefix, collection_name, member_name)
+ case scope_level
+ when :nested
+ [name_prefix, prefix]
+ when :collection
+ [prefix, name_prefix, collection_name]
+ when :new
+ [prefix, :new, name_prefix, member_name]
+ when :member
+ [prefix, name_prefix, member_name]
+ when :root
+ [name_prefix, collection_name, prefix]
+ else
+ [name_prefix, member_name, prefix]
+ end
+ end
+
+ def resource_scope?
+ RESOURCE_SCOPES.include? scope_level
end
def options
@@ -1909,7 +1933,15 @@ module ActionDispatch
end
def new(hash)
- self.class.new hash, self
+ self.class.new hash, self, scope_level
+ end
+
+ def new_level(level)
+ self.class.new(self, self, level)
+ end
+
+ def fetch(key, &block)
+ @hash.fetch(key, &block)
end
def [](key)
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index bd3696cda1..cd8b1ab066 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -142,22 +142,27 @@ module ActionDispatch
%w(edit new).each do |action|
module_eval <<-EOT, __FILE__, __LINE__ + 1
- def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {})
- polymorphic_url( # polymorphic_url(
- record_or_hash, # record_or_hash,
- options.merge(:action => "#{action}")) # options.merge(:action => "edit"))
- end # end
- #
- def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {})
- polymorphic_url( # polymorphic_url(
- record_or_hash, # record_or_hash,
- options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path))
- end # end
+ def #{action}_polymorphic_url(record_or_hash, options = {})
+ polymorphic_url_for_action("#{action}", record_or_hash, options)
+ end
+
+ def #{action}_polymorphic_path(record_or_hash, options = {})
+ polymorphic_path_for_action("#{action}", record_or_hash, options)
+ end
EOT
end
private
+ def polymorphic_url_for_action(action, record_or_hash, options)
+ polymorphic_url(record_or_hash, options.merge(:action => action))
+ end
+
+ def polymorphic_path_for_action(action, record_or_hash, options)
+ options = options.merge(:action => action, :routing_type => :path)
+ polymorphic_path(record_or_hash, options)
+ end
+
class HelperMethodBuilder # :nodoc:
CACHE = { 'path' => {}, 'url' => {} }
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 5b3651aaee..f51bee3b31 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -101,6 +101,11 @@ module ActionDispatch
@path_helpers.include?(key) || @url_helpers.include?(key)
end
+ def helpers
+ ActiveSupport::Deprecation.warn("`named_routes.helpers` is deprecated, please use `route_defined?(route_name)` to see if a named route was defined.")
+ @path_helpers + @url_helpers
+ end
+
def helper_names
@path_helpers.map(&:to_s) + @url_helpers.map(&:to_s)
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb
index 226baf9ad0..f325c35b57 100644
--- a/actionpack/lib/action_dispatch/testing/assertions.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions.rb
@@ -1,18 +1,22 @@
+require 'rails-dom-testing'
+
module ActionDispatch
module Assertions
- autoload :DomAssertions, 'action_dispatch/testing/assertions/dom'
autoload :ResponseAssertions, 'action_dispatch/testing/assertions/response'
autoload :RoutingAssertions, 'action_dispatch/testing/assertions/routing'
- autoload :SelectorAssertions, 'action_dispatch/testing/assertions/selector'
- autoload :TagAssertions, 'action_dispatch/testing/assertions/tag'
extend ActiveSupport::Concern
- include DomAssertions
include ResponseAssertions
include RoutingAssertions
- include SelectorAssertions
- include TagAssertions
+ include Rails::Dom::Testing::Assertions
+
+ def html_document
+ @html_document ||= if @response.content_type =~ /xml$/
+ Nokogiri::XML::Document.parse(@response.body)
+ else
+ Nokogiri::HTML::Document.parse(@response.body)
+ end
+ end
end
end
-
diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb
index 241a39393a..fb579b52fe 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb
@@ -1,27 +1,3 @@
-require 'action_view/vendor/html-scanner'
+require 'active_support/deprecation'
-module ActionDispatch
- module Assertions
- module DomAssertions
- # \Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
- #
- # # assert that the referenced method generates the appropriate HTML string
- # assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com")
- def assert_dom_equal(expected, actual, message = nil)
- expected_dom = HTML::Document.new(expected).root
- actual_dom = HTML::Document.new(actual).root
- assert_equal expected_dom, actual_dom, message
- end
-
- # The negated form of +assert_dom_equivalent+.
- #
- # # assert that the referenced method does not generate the specified HTML string
- # assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com")
- def assert_dom_not_equal(expected, actual, message = nil)
- expected_dom = HTML::Document.new(expected).root
- actual_dom = HTML::Document.new(actual).root
- assert_not_equal expected_dom, actual_dom, message
- end
- end
- end
-end
+ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::DomAssertions has been extracted to the rails-dom-testing gem.") \ No newline at end of file
diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb
index 12023e6f77..19eca60f70 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb
@@ -1,430 +1,3 @@
-require 'action_view/vendor/html-scanner'
-require 'active_support/core_ext/object/inclusion'
+require 'active_support/deprecation'
-#--
-# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
-# Under MIT and/or CC By license.
-#++
-
-module ActionDispatch
- module Assertions
- NO_STRIP = %w{pre script style textarea}
-
- # Adds the +assert_select+ method for use in Rails functional
- # test cases, which can be used to make assertions on the response HTML of a controller
- # action. You can also call +assert_select+ within another +assert_select+ to
- # make assertions on elements selected by the enclosing assertion.
- #
- # Use +css_select+ to select elements without making an assertions, either
- # from the response HTML or elements selected by the enclosing assertion.
- #
- # In addition to HTML responses, you can make the following assertions:
- #
- # * +assert_select_encoded+ - Assertions on HTML encoded inside XML, for example for dealing with feed item descriptions.
- # * +assert_select_email+ - Assertions on the HTML body of an e-mail.
- #
- # Also see HTML::Selector to learn how to use selectors.
- module SelectorAssertions
- # Select and return all matching elements.
- #
- # If called with a single argument, uses that argument as a selector
- # to match all elements of the current page. Returns an empty array
- # if no match is found.
- #
- # If called with two arguments, uses the first argument as the base
- # element and the second argument as the selector. Attempts to match the
- # base element and any of its children. Returns an empty array if no
- # match is found.
- #
- # The selector may be a CSS selector expression (String), an expression
- # with substitution values (Array) or an HTML::Selector object.
- #
- # # Selects all div tags
- # divs = css_select("div")
- #
- # # Selects all paragraph tags and does something interesting
- # pars = css_select("p")
- # pars.each do |par|
- # # Do something fun with paragraphs here...
- # end
- #
- # # Selects all list items in unordered lists
- # items = css_select("ul>li")
- #
- # # Selects all form tags and then all inputs inside the form
- # forms = css_select("form")
- # forms.each do |form|
- # inputs = css_select(form, "input")
- # ...
- # end
- def css_select(*args)
- # See assert_select to understand what's going on here.
- arg = args.shift
-
- if arg.is_a?(HTML::Node)
- root = arg
- arg = args.shift
- elsif arg == nil
- raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
- elsif defined?(@selected) && @selected
- matches = []
-
- @selected.each do |selected|
- subset = css_select(selected, HTML::Selector.new(arg.dup, args.dup))
- subset.each do |match|
- matches << match unless matches.any? { |m| m.equal?(match) }
- end
- end
-
- return matches
- else
- root = response_from_page
- end
-
- case arg
- when String
- selector = HTML::Selector.new(arg, args)
- when Array
- selector = HTML::Selector.new(*arg)
- when HTML::Selector
- selector = arg
- else raise ArgumentError, "Expecting a selector as the first argument"
- end
-
- selector.select(root)
- end
-
- # An assertion that selects elements and makes one or more equality tests.
- #
- # If the first argument is an element, selects all matching elements
- # starting from (and including) that element and all its children in
- # depth-first order.
- #
- # If no element if specified, calling +assert_select+ selects from the
- # response HTML unless +assert_select+ is called from within an +assert_select+ block.
- #
- # When called with a block +assert_select+ passes an array of selected elements
- # to the block. Calling +assert_select+ from the block, with no element specified,
- # runs the assertion on the complete set of elements selected by the enclosing assertion.
- # Alternatively the array may be iterated through so that +assert_select+ can be called
- # separately for each element.
- #
- #
- # ==== Example
- # If the response contains two ordered lists, each with four list elements then:
- # assert_select "ol" do |elements|
- # elements.each do |element|
- # assert_select element, "li", 4
- # end
- # end
- #
- # will pass, as will:
- # assert_select "ol" do
- # assert_select "li", 8
- # end
- #
- # The selector may be a CSS selector expression (String), an expression
- # with substitution values, or an HTML::Selector object.
- #
- # === Equality Tests
- #
- # The equality test may be one of the following:
- # * <tt>true</tt> - Assertion is true if at least one element selected.
- # * <tt>false</tt> - Assertion is true if no element selected.
- # * <tt>String/Regexp</tt> - Assertion is true if the text value of at least
- # one element matches the string or regular expression.
- # * <tt>Integer</tt> - Assertion is true if exactly that number of
- # elements are selected.
- # * <tt>Range</tt> - Assertion is true if the number of selected
- # elements fit the range.
- # If no equality test specified, the assertion is true if at least one
- # element selected.
- #
- # To perform more than one equality tests, use a hash with the following keys:
- # * <tt>:text</tt> - Narrow the selection to elements that have this text
- # value (string or regexp).
- # * <tt>:html</tt> - Narrow the selection to elements that have this HTML
- # content (string or regexp).
- # * <tt>:count</tt> - Assertion is true if the number of selected elements
- # is equal to this value.
- # * <tt>:minimum</tt> - Assertion is true if the number of selected
- # elements is at least this value.
- # * <tt>:maximum</tt> - Assertion is true if the number of selected
- # elements is at most this value.
- #
- # If the method is called with a block, once all equality tests are
- # evaluated the block is called with an array of all matched elements.
- #
- # # At least one form element
- # assert_select "form"
- #
- # # Form element includes four input fields
- # assert_select "form input", 4
- #
- # # Page title is "Welcome"
- # assert_select "title", "Welcome"
- #
- # # Page title is "Welcome" and there is only one title element
- # assert_select "title", {count: 1, text: "Welcome"},
- # "Wrong title or more than one title element"
- #
- # # Page contains no forms
- # assert_select "form", false, "This page must contain no forms"
- #
- # # Test the content and style
- # assert_select "body div.header ul.menu"
- #
- # # Use substitution values
- # assert_select "ol>li#?", /item-\d+/
- #
- # # All input fields in the form have a name
- # assert_select "form input" do
- # assert_select "[name=?]", /.+/ # Not empty
- # end
- def assert_select(*args, &block)
- # Start with optional element followed by mandatory selector.
- arg = args.shift
- @selected ||= nil
-
- if arg.is_a?(HTML::Node)
- # First argument is a node (tag or text, but also HTML root),
- # so we know what we're selecting from.
- root = arg
- arg = args.shift
- elsif arg == nil
- # This usually happens when passing a node/element that
- # happens to be nil.
- raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
- elsif @selected
- root = HTML::Node.new(nil)
- root.children.concat @selected
- else
- # Otherwise just operate on the response document.
- root = response_from_page
- end
-
- # First or second argument is the selector: string and we pass
- # all remaining arguments. Array and we pass the argument. Also
- # accepts selector itself.
- case arg
- when String
- selector = HTML::Selector.new(arg, args)
- when Array
- selector = HTML::Selector.new(*arg)
- when HTML::Selector
- selector = arg
- else raise ArgumentError, "Expecting a selector as the first argument"
- end
-
- # Next argument is used for equality tests.
- equals = {}
- case arg = args.shift
- when Hash
- equals = arg
- when String, Regexp
- equals[:text] = arg
- when Integer
- equals[:count] = arg
- when Range
- equals[:minimum] = arg.begin
- equals[:maximum] = arg.end
- when FalseClass
- equals[:count] = 0
- when NilClass, TrueClass
- equals[:minimum] = 1
- else raise ArgumentError, "I don't understand what you're trying to match"
- end
-
- # By default we're looking for at least one match.
- if equals[:count]
- equals[:minimum] = equals[:maximum] = equals[:count]
- else
- equals[:minimum] = 1 unless equals[:minimum]
- end
-
- # Last argument is the message we use if the assertion fails.
- message = args.shift
- #- message = "No match made with selector #{selector.inspect}" unless message
- if args.shift
- raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type"
- end
-
- matches = selector.select(root)
- # If text/html, narrow down to those elements that match it.
- content_mismatch = nil
- if match_with = equals[:text]
- matches.delete_if do |match|
- text = ""
- stack = match.children.reverse
- while node = stack.pop
- if node.tag?
- stack.concat node.children.reverse
- else
- content = node.content
- text << content
- end
- end
- text.strip! unless NO_STRIP.include?(match.name)
- text.sub!(/\A\n/, '') if match.name == "textarea"
- unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s)
- content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, text)
- true
- end
- end
- elsif match_with = equals[:html]
- matches.delete_if do |match|
- html = match.children.map(&:to_s).join
- html.strip! unless NO_STRIP.include?(match.name)
- unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s)
- content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, html)
- true
- end
- end
- end
- # Expecting foo found bar element only if found zero, not if
- # found one but expecting two.
- message ||= content_mismatch if matches.empty?
- # Test minimum/maximum occurrence.
- min, max, count = equals[:minimum], equals[:maximum], equals[:count]
-
- # FIXME: minitest provides messaging when we use assert_operator,
- # so is this custom message really needed?
- message = message || %(Expected #{count_description(min, max, count)} matching "#{selector.to_s}", found #{matches.size})
- if count
- assert_equal count, matches.size, message
- else
- assert_operator matches.size, :>=, min, message if min
- assert_operator matches.size, :<=, max, message if max
- end
-
- # If a block is given call that block. Set @selected to allow
- # nested assert_select, which can be nested several levels deep.
- if block_given? && !matches.empty?
- begin
- in_scope, @selected = @selected, matches
- yield matches
- ensure
- @selected = in_scope
- end
- end
-
- # Returns all matches elements.
- matches
- end
-
- def count_description(min, max, count) #:nodoc:
- pluralize = lambda {|word, quantity| word << (quantity == 1 ? '' : 's')}
-
- if min && max && (max != min)
- "between #{min} and #{max} elements"
- elsif min && max && max == min && count
- "exactly #{count} #{pluralize['element', min]}"
- elsif min && !(min == 1 && max == 1)
- "at least #{min} #{pluralize['element', min]}"
- elsif max
- "at most #{max} #{pluralize['element', max]}"
- end
- end
-
- # Extracts the content of an element, treats it as encoded HTML and runs
- # nested assertion on it.
- #
- # You typically call this method within another assertion to operate on
- # all currently selected elements. You can also pass an element or array
- # of elements.
- #
- # The content of each element is un-encoded, and wrapped in the root
- # element +encoded+. It then calls the block with all un-encoded elements.
- #
- # # Selects all bold tags from within the title of an Atom feed's entries (perhaps to nab a section name prefix)
- # assert_select "feed[xmlns='http://www.w3.org/2005/Atom']" do
- # # Select each entry item and then the title item
- # assert_select "entry>title" do
- # # Run assertions on the encoded title elements
- # assert_select_encoded do
- # assert_select "b"
- # end
- # end
- # end
- #
- #
- # # Selects all paragraph tags from within the description of an RSS feed
- # assert_select "rss[version=2.0]" do
- # # Select description element of each feed item.
- # assert_select "channel>item>description" do
- # # Run assertions on the encoded elements.
- # assert_select_encoded do
- # assert_select "p"
- # end
- # end
- # end
- def assert_select_encoded(element = nil, &block)
- case element
- when Array
- elements = element
- when HTML::Node
- elements = [element]
- when nil
- unless elements = @selected
- raise ArgumentError, "First argument is optional, but must be called from a nested assert_select"
- end
- else
- raise ArgumentError, "Argument is optional, and may be node or array of nodes"
- end
-
- fix_content = lambda do |node|
- # Gets around a bug in the Rails 1.1 HTML parser.
- node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { Rack::Utils.escapeHTML($1) }
- end
-
- selected = elements.map do |elem|
- text = elem.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join
- root = HTML::Document.new(CGI.unescapeHTML("<encoded>#{text}</encoded>")).root
- css_select(root, "encoded:root", &block)[0]
- end
-
- begin
- old_selected, @selected = @selected, selected
- assert_select ":root", &block
- ensure
- @selected = old_selected
- end
- end
-
- # Extracts the body of an email and runs nested assertions on it.
- #
- # You must enable deliveries for this assertion to work, use:
- # ActionMailer::Base.perform_deliveries = true
- #
- # assert_select_email do
- # assert_select "h1", "Email alert"
- # end
- #
- # assert_select_email do
- # items = assert_select "ol>li"
- # items.each do
- # # Work with items here...
- # end
- # end
- def assert_select_email(&block)
- deliveries = ActionMailer::Base.deliveries
- assert !deliveries.empty?, "No e-mail in delivery list"
-
- deliveries.each do |delivery|
- (delivery.parts.empty? ? [delivery] : delivery.parts).each do |part|
- if part["Content-Type"].to_s =~ /^text\/html\W/
- root = HTML::Document.new(part.body.to_s).root
- assert_select root, ":root", &block
- end
- end
- end
- end
-
- protected
- # +assert_select+ and +css_select+ call this to obtain the content in the HTML page.
- def response_from_page
- html_document.root
- end
- end
- end
-end
+ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::SelectorAssertions has been has been extracted to the rails-dom-testing gem.") \ No newline at end of file
diff --git a/actionpack/lib/action_dispatch/testing/assertions/tag.rb b/actionpack/lib/action_dispatch/testing/assertions/tag.rb
deleted file mode 100644
index e5fe30ba82..0000000000
--- a/actionpack/lib/action_dispatch/testing/assertions/tag.rb
+++ /dev/null
@@ -1,135 +0,0 @@
-require 'action_view/vendor/html-scanner'
-
-module ActionDispatch
- module Assertions
- # Pair of assertions to testing elements in the HTML output of the response.
- module TagAssertions
- # Asserts that there is a tag/node/element in the body of the response
- # that meets all of the given conditions. The +conditions+ parameter must
- # be a hash of any of the following keys (all are optional):
- #
- # * <tt>:tag</tt>: the node type must match the corresponding value
- # * <tt>:attributes</tt>: a hash. The node's attributes must match the
- # corresponding values in the hash.
- # * <tt>:parent</tt>: a hash. The node's parent must match the
- # corresponding hash.
- # * <tt>:child</tt>: a hash. At least one of the node's immediate children
- # must meet the criteria described by the hash.
- # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
- # meet the criteria described by the hash.
- # * <tt>:descendant</tt>: a hash. At least one of the node's descendants
- # must meet the criteria described by the hash.
- # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
- # meet the criteria described by the hash.
- # * <tt>:after</tt>: a hash. The node must be after any sibling meeting
- # the criteria described by the hash, and at least one sibling must match.
- # * <tt>:before</tt>: a hash. The node must be before any sibling meeting
- # the criteria described by the hash, and at least one sibling must match.
- # * <tt>:children</tt>: a hash, for counting children of a node. Accepts
- # the keys:
- # * <tt>:count</tt>: either a number or a range which must equal (or
- # include) the number of children that match.
- # * <tt>:less_than</tt>: the number of matching children must be less
- # than this number.
- # * <tt>:greater_than</tt>: the number of matching children must be
- # greater than this number.
- # * <tt>:only</tt>: another hash consisting of the keys to use
- # to match on the children, and only matching children will be
- # counted.
- # * <tt>:content</tt>: the textual content of the node must match the
- # given value. This will not match HTML tags in the body of a
- # tag--only text.
- #
- # Conditions are matched using the following algorithm:
- #
- # * if the condition is a string, it must be a substring of the value.
- # * if the condition is a regexp, it must match the value.
- # * if the condition is a number, the value must match number.to_s.
- # * if the condition is +true+, the value must not be +nil+.
- # * if the condition is +false+ or +nil+, the value must be +nil+.
- #
- # # Assert that there is a "span" tag
- # assert_tag tag: "span"
- #
- # # Assert that there is a "span" tag with id="x"
- # assert_tag tag: "span", attributes: { id: "x" }
- #
- # # Assert that there is a "span" tag using the short-hand
- # assert_tag :span
- #
- # # Assert that there is a "span" tag with id="x" using the short-hand
- # assert_tag :span, attributes: { id: "x" }
- #
- # # Assert that there is a "span" inside of a "div"
- # assert_tag tag: "span", parent: { tag: "div" }
- #
- # # Assert that there is a "span" somewhere inside a table
- # assert_tag tag: "span", ancestor: { tag: "table" }
- #
- # # Assert that there is a "span" with at least one "em" child
- # assert_tag tag: "span", child: { tag: "em" }
- #
- # # Assert that there is a "span" containing a (possibly nested)
- # # "strong" tag.
- # assert_tag tag: "span", descendant: { tag: "strong" }
- #
- # # Assert that there is a "span" containing between 2 and 4 "em" tags
- # # as immediate children
- # assert_tag tag: "span",
- # children: { count: 2..4, only: { tag: "em" } }
- #
- # # Get funky: assert that there is a "div", with an "ul" ancestor
- # # and an "li" parent (with "class" = "enum"), and containing a
- # # "span" descendant that contains text matching /hello world/
- # assert_tag tag: "div",
- # ancestor: { tag: "ul" },
- # parent: { tag: "li",
- # attributes: { class: "enum" } },
- # descendant: { tag: "span",
- # child: /hello world/ }
- #
- # <b>Please note</b>: +assert_tag+ and +assert_no_tag+ only work
- # with well-formed XHTML. They recognize a few tags as implicitly self-closing
- # (like br and hr and such) but will not work correctly with tags
- # that allow optional closing tags (p, li, td). <em>You must explicitly
- # close all of your tags to use these assertions.</em>
- def assert_tag(*opts)
- opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
- tag = find_tag(opts)
- assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}"
- end
-
- # Identical to +assert_tag+, but asserts that a matching tag does _not_
- # exist. (See +assert_tag+ for a full discussion of the syntax.)
- #
- # # Assert that there is not a "div" containing a "p"
- # assert_no_tag tag: "div", descendant: { tag: "p" }
- #
- # # Assert that an unordered list is empty
- # assert_no_tag tag: "ul", descendant: { tag: "li" }
- #
- # # Assert that there is not a "p" tag with between 1 to 3 "img" tags
- # # as immediate children
- # assert_no_tag tag: "p",
- # children: { count: 1..3, only: { tag: "img" } }
- def assert_no_tag(*opts)
- opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
- tag = find_tag(opts)
- assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}"
- end
-
- def find_tag(conditions)
- html_document.find(conditions)
- end
-
- def find_all_tag(conditions)
- html_document.find_all(conditions)
- end
-
- def html_document
- xml = @response.content_type =~ /xml$/
- @html_document ||= HTML::Document.new(@response.body, false, xml)
- end
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index 192ccdb9d5..2d1c3ac5c7 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -495,5 +495,9 @@ module ActionDispatch
reset! unless integration_session
integration_session.url_options
end
+
+ def document_root_element
+ html_document.root
+ end
end
end
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index 4e17d57dad..674fb253f4 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -484,6 +484,9 @@ class ForkingExecutor
method = job[1]
reporter = job[2]
result = Minitest.run_one_method klass, method
+ if result.error?
+ translate_exceptions result
+ end
queue.record reporter, result
end
}
@@ -491,6 +494,20 @@ class ForkingExecutor
@size.times { @queue << nil }
pool.each { |pid| Process.waitpid pid }
end
+
+ private
+ def translate_exceptions(result)
+ result.failures.map! { |e|
+ begin
+ Marshal.dump e
+ e
+ rescue TypeError
+ ex = Exception.new e.message
+ ex.set_backtrace e.backtrace
+ Minitest::UnexpectedError.new ex
+ end
+ }
+ end
end
if ActiveSupport::Testing::Isolation.forking_env? && PROCESS_COUNT > 0
diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb
index b6b5a218cc..2cf48df6c0 100644
--- a/actionpack/test/controller/action_pack_assertions_test.rb
+++ b/actionpack/test/controller/action_pack_assertions_test.rb
@@ -1,5 +1,4 @@
require 'abstract_unit'
-require 'action_view/vendor/html-scanner'
require 'controller/fake_controllers'
class ActionPackAssertionsController < ActionController::Base
@@ -147,11 +146,6 @@ end
class ActionPackAssertionsControllerTest < ActionController::TestCase
- def test_assert_tag_and_url_for
- get :render_url
- assert_tag :content => "/action_pack_assertions/flash_me"
- end
-
def test_render_file_absolute_path
get :render_file_absolute_path
assert_match(/\A= Action Pack/, @response.body)
@@ -488,6 +482,11 @@ class AssertTemplateTest < ActionController::TestCase
assert_raise(ActiveSupport::TestCase::Assertion) do
assert_template :file => 'test/hello_world'
end
+
+ get :render_file_absolute_path
+ assert_raise(ActiveSupport::TestCase::Assertion) do
+ assert_template file: nil
+ end
end
def test_with_nil_passes_when_no_template_rendered
@@ -612,6 +611,24 @@ class AssertTemplateTest < ActionController::TestCase
get :nothing
assert_template nil
+
+ get :partial
+ assert_template partial: 'test/_partial'
+
+ get :nothing
+ assert_template partial: nil
+
+ get :render_with_layout
+ assert_template layout: 'layouts/standard'
+
+ get :nothing
+ assert_template layout: nil
+
+ get :render_file_relative_path
+ assert_template file: 'README.rdoc'
+
+ get :nothing
+ assert_template file: nil
end
end
diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb
deleted file mode 100644
index f07d201563..0000000000
--- a/actionpack/test/controller/assert_select_test.rb
+++ /dev/null
@@ -1,356 +0,0 @@
-# encoding: utf-8
-#--
-# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
-# Under MIT and/or CC By license.
-#++
-
-require 'abstract_unit'
-require 'controller/fake_controllers'
-
-require 'action_mailer'
-require 'action_view'
-
-ActionMailer::Base.send(:include, ActionView::Layouts)
-ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH
-
-class AssertSelectTest < ActionController::TestCase
- Assertion = ActiveSupport::TestCase::Assertion
-
- class AssertSelectMailer < ActionMailer::Base
- def test(html)
- mail :body => html, :content_type => "text/html",
- :subject => "Test e-mail", :from => "test@test.host", :to => "test <test@test.host>"
- end
- end
-
- class AssertMultipartSelectMailer < ActionMailer::Base
- def test(options)
- mail :subject => "Test e-mail", :from => "test@test.host", :to => "test <test@test.host>" do |format|
- format.text { render :text => options[:text] }
- format.html { render :text => options[:html] }
- end
- end
- end
-
- class AssertSelectController < ActionController::Base
- def response_with=(content)
- @content = content
- end
-
- def response_with(&block)
- @update = block
- end
-
- def html()
- render :text=>@content, :layout=>false, :content_type=>Mime::HTML
- @content = nil
- end
-
- def xml()
- render :text=>@content, :layout=>false, :content_type=>Mime::XML
- @content = nil
- end
- end
-
- tests AssertSelectController
-
- def setup
- super
- @old_delivery_method = ActionMailer::Base.delivery_method
- @old_perform_deliveries = ActionMailer::Base.perform_deliveries
- ActionMailer::Base.delivery_method = :test
- ActionMailer::Base.perform_deliveries = true
- end
-
- def teardown
- super
- ActionMailer::Base.delivery_method = @old_delivery_method
- ActionMailer::Base.perform_deliveries = @old_perform_deliveries
- ActionMailer::Base.deliveries.clear
- end
-
- def assert_failure(message, &block)
- e = assert_raise(Assertion, &block)
- assert_match(message, e.message) if Regexp === message
- assert_equal(message, e.message) if String === message
- end
-
- #
- # Test assert select.
- #
-
- def test_assert_select
- render_html %Q{<div id="1"></div><div id="2"></div>}
- assert_select "div", 2
- assert_failure(/\AExpected at least 1 element matching \"p\", found 0\.$/) { assert_select "p" }
- end
-
- def test_equality_integer
- render_html %Q{<div id="1"></div><div id="2"></div>}
- assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) { assert_select "div", 3 }
- assert_failure(/\AExpected exactly 0 elements matching \"div\", found 2\.$/) { assert_select "div", 0 }
- end
-
- def test_equality_true_false
- render_html %Q{<div id="1"></div><div id="2"></div>}
- assert_nothing_raised { assert_select "div" }
- assert_raise(Assertion) { assert_select "p" }
- assert_nothing_raised { assert_select "div", true }
- assert_raise(Assertion) { assert_select "p", true }
- assert_raise(Assertion) { assert_select "div", false }
- assert_nothing_raised { assert_select "p", false }
- end
-
- def test_equality_false_message
- render_html %Q{<div id="1"></div><div id="2"></div>}
- assert_failure(/\AExpected exactly 0 elements matching \"div\", found 2\.$/) { assert_select "div", false }
- end
-
- def test_equality_string_and_regexp
- render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
- assert_nothing_raised { assert_select "div", "foo" }
- assert_raise(Assertion) { assert_select "div", "bar" }
- assert_failure(/\A<bar> expected but was\n<foo>\.$/) { assert_select "div", "bar" }
- assert_nothing_raised { assert_select "div", :text=>"foo" }
- assert_raise(Assertion) { assert_select "div", :text=>"bar" }
- assert_nothing_raised { assert_select "div", /(foo|bar)/ }
- assert_raise(Assertion) { assert_select "div", /foobar/ }
- assert_nothing_raised { assert_select "div", :text=>/(foo|bar)/ }
- assert_raise(Assertion) { assert_select "div", :text=>/foobar/ }
- assert_raise(Assertion) { assert_select "p", :text=>/foobar/ }
- end
-
- def test_equality_of_html
- render_html %Q{<p>\n<em>"This is <strong>not</strong> a big problem,"</em> he said.\n</p>}
- text = "\"This is not a big problem,\" he said."
- html = "<em>\"This is <strong>not</strong> a big problem,\"</em> he said."
- assert_nothing_raised { assert_select "p", text }
- assert_raise(Assertion) { assert_select "p", html }
- assert_nothing_raised { assert_select "p", :html=>html }
- assert_raise(Assertion) { assert_select "p", :html=>text }
- assert_failure(/\A<#{text}> expected but was\n<#{html}>\.$/) { assert_select "p", :html=>text }
- # No stripping for pre.
- render_html %Q{<pre>\n<em>"This is <strong>not</strong> a big problem,"</em> he said.\n</pre>}
- text = "\n\"This is not a big problem,\" he said.\n"
- html = "\n<em>\"This is <strong>not</strong> a big problem,\"</em> he said.\n"
- assert_nothing_raised { assert_select "pre", text }
- assert_raise(Assertion) { assert_select "pre", html }
- assert_nothing_raised { assert_select "pre", :html=>html }
- assert_raise(Assertion) { assert_select "pre", :html=>text }
- end
-
- def test_strip_textarea
- render_html %Q{<textarea>\n\nfoo\n</textarea>}
- assert_select "textarea", "\nfoo\n"
- render_html %Q{<textarea>\nfoo</textarea>}
- assert_select "textarea", "foo"
- end
-
- def test_counts
- render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
- assert_nothing_raised { assert_select "div", 2 }
- assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) do
- assert_select "div", 3
- end
- assert_nothing_raised { assert_select "div", 1..2 }
- assert_failure(/\AExpected between 3 and 4 elements matching \"div\", found 2\.$/) do
- assert_select "div", 3..4
- end
- assert_nothing_raised { assert_select "div", :count=>2 }
- assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) do
- assert_select "div", :count=>3
- end
- assert_nothing_raised { assert_select "div", :minimum=>1 }
- assert_nothing_raised { assert_select "div", :minimum=>2 }
- assert_failure(/\AExpected at least 3 elements matching \"div\", found 2\.$/) do
- assert_select "div", :minimum=>3
- end
- assert_nothing_raised { assert_select "div", :maximum=>2 }
- assert_nothing_raised { assert_select "div", :maximum=>3 }
- assert_failure(/\AExpected at most 1 element matching \"div\", found 2\.$/) do
- assert_select "div", :maximum=>1
- end
- assert_nothing_raised { assert_select "div", :minimum=>1, :maximum=>2 }
- assert_failure(/\AExpected between 3 and 4 elements matching \"div\", found 2\.$/) do
- assert_select "div", :minimum=>3, :maximum=>4
- end
- end
-
- def test_substitution_values
- render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
- assert_select "div#?", /\d+/ do |elements|
- assert_equal 2, elements.size
- end
- assert_select "div" do
- assert_select "div#?", /\d+/ do |elements|
- assert_equal 2, elements.size
- assert_select "#1"
- assert_select "#2"
- end
- end
- end
-
- def test_nested_assert_select
- render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
- assert_select "div" do |elements|
- assert_equal 2, elements.size
- assert_select elements[0], "#1"
- assert_select elements[1], "#2"
- end
- assert_select "div" do
- assert_select "div" do |elements|
- assert_equal 2, elements.size
- # Testing in a group is one thing
- assert_select "#1,#2"
- # Testing individually is another.
- assert_select "#1"
- assert_select "#2"
- assert_select "#3", false
- end
- end
-
- assert_failure(/\AExpected at least 1 element matching \"#4\", found 0\.$/) do
- assert_select "div" do
- assert_select "#4"
- end
- end
- end
-
- def test_assert_select_text_match
- render_html %Q{<div id="1"><span>foo</span></div><div id="2"><span>bar</span></div>}
- assert_select "div" do
- assert_nothing_raised { assert_select "div", "foo" }
- assert_nothing_raised { assert_select "div", "bar" }
- assert_nothing_raised { assert_select "div", /\w*/ }
- assert_nothing_raised { assert_select "div", :text => /\w*/, :count=>2 }
- assert_raise(Assertion) { assert_select "div", :text=>"foo", :count=>2 }
- assert_nothing_raised { assert_select "div", :html=>"<span>bar</span>" }
- assert_nothing_raised { assert_select "div", :html=>"<span>bar</span>" }
- assert_nothing_raised { assert_select "div", :html=>/\w*/ }
- assert_nothing_raised { assert_select "div", :html=>/\w*/, :count=>2 }
- assert_raise(Assertion) { assert_select "div", :html=>"<span>foo</span>", :count=>2 }
- end
- end
-
- def test_elect_with_xml_namespace_attributes
- render_html %Q{<link xlink:href="http://nowhere.com"></link>}
- assert_nothing_raised { assert_select "link[xlink:href=http://nowhere.com]" }
- end
-
- #
- # Test css_select.
- #
-
- def test_css_select
- render_html %Q{<div id="1"></div><div id="2"></div>}
- assert_equal 2, css_select("div").size
- assert_equal 0, css_select("p").size
- end
-
- def test_nested_css_select
- render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
- assert_select "div#?", /\d+/ do |elements|
- assert_equal 1, css_select(elements[0], "div").size
- assert_equal 1, css_select(elements[1], "div").size
- end
- assert_select "div" do
- assert_equal 2, css_select("div").size
- css_select("div").each do |element|
- # Testing as a group is one thing
- assert !css_select("#1,#2").empty?
- # Testing individually is another
- assert !css_select("#1").empty?
- assert !css_select("#2").empty?
- end
- end
- end
-
- def test_feed_item_encoded
- render_xml <<-EOF
-<rss version="2.0">
- <channel>
- <item>
- <description>
- <![CDATA[
- <p>Test 1</p>
- ]]>
- </description>
- </item>
- <item>
- <description>
- <![CDATA[
- <p>Test 2</p>
- ]]>
- </description>
- </item>
- </channel>
-</rss>
-EOF
- assert_select "channel item description" do
- # Test element regardless of wrapper.
- assert_select_encoded do
- assert_select "p", :count=>2, :text=>/Test/
- end
- # Test through encoded wrapper.
- assert_select_encoded do
- assert_select "encoded p", :count=>2, :text=>/Test/
- end
- # Use :root instead (recommended)
- assert_select_encoded do
- assert_select ":root p", :count=>2, :text=>/Test/
- end
- # Test individually.
- assert_select "description" do |elements|
- assert_select_encoded elements[0] do
- assert_select "p", "Test 1"
- end
- assert_select_encoded elements[1] do
- assert_select "p", "Test 2"
- end
- end
- end
-
- # Test that we only un-encode element itself.
- assert_select "channel item" do
- assert_select_encoded do
- assert_select "p", 0
- end
- end
- end
-
- #
- # Test assert_select_email
- #
-
- def test_assert_select_email
- assert_raise(Assertion) { assert_select_email {} }
- AssertSelectMailer.test("<div><p>foo</p><p>bar</p></div>").deliver
- assert_select_email do
- assert_select "div:root" do
- assert_select "p:first-child", "foo"
- assert_select "p:last-child", "bar"
- end
- end
- end
-
- def test_assert_select_email_multipart
- AssertMultipartSelectMailer.test(:html => "<div><p>foo</p><p>bar</p></div>", :text => 'foo bar').deliver
- assert_select_email do
- assert_select "div:root" do
- assert_select "p:first-child", "foo"
- assert_select "p:last-child", "bar"
- end
- end
- end
-
- protected
- def render_html(html)
- @controller.response_with = html
- get :html
- end
-
- def render_xml(xml)
- @controller.response_with = xml
- get :xml
- end
-end
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index 78cce3aa64..99b53e6fd2 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -1,6 +1,5 @@
require 'abstract_unit'
require 'controller/fake_controllers'
-require 'action_view/vendor/html-scanner'
require 'rails/engine'
class SessionTest < ActiveSupport::TestCase
@@ -293,7 +292,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
assert_equal({}, cookies.to_hash)
assert_equal "OK", body
assert_equal "OK", response.body
- assert_kind_of HTML::Document, html_document
+ assert_kind_of Nokogiri::HTML::Document, html_document
assert_equal 1, request_count
end
end
@@ -309,7 +308,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
assert_equal({}, cookies.to_hash)
assert_equal "Created", body
assert_equal "Created", response.body
- assert_kind_of HTML::Document, html_document
+ assert_kind_of Nokogiri::HTML::Document, html_document
assert_equal 1, request_count
end
end
@@ -369,7 +368,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
assert_response :redirect
assert_response :found
assert_equal "<html><body>You are being <a href=\"http://www.example.com/get\">redirected</a>.</body></html>", response.body
- assert_kind_of HTML::Document, html_document
+ assert_kind_of Nokogiri::HTML::Document, html_document
assert_equal 1, request_count
follow_redirect!
diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb
index 0500b7c789..7fd1276e98 100644
--- a/actionpack/test/controller/live_stream_test.rb
+++ b/actionpack/test/controller/live_stream_test.rb
@@ -162,7 +162,7 @@ module ActionController
end
def with_stale
- render :text => 'stale' if stale?(:etag => "123")
+ render text: 'stale' if stale?(etag: "123", template: false)
end
def exception_in_view
diff --git a/actionpack/test/controller/mime/respond_with_test.rb b/actionpack/test/controller/mime/respond_with_test.rb
deleted file mode 100644
index 115f3b2f41..0000000000
--- a/actionpack/test/controller/mime/respond_with_test.rb
+++ /dev/null
@@ -1,737 +0,0 @@
-require 'abstract_unit'
-require 'controller/fake_models'
-
-class RespondWithController < ActionController::Base
- class CustomerWithJson < Customer
- def to_json; super; end
- end
-
- respond_to :html, :json, :touch
- respond_to :xml, :except => :using_resource_with_block
- respond_to :js, :only => [ :using_resource_with_block, :using_resource, 'using_hash_resource' ]
-
- def using_resource
- respond_with(resource)
- end
-
- def using_hash_resource
- respond_with({:result => resource})
- end
-
- def using_resource_with_block
- respond_with(resource) do |format|
- format.csv { render :text => "CSV" }
- end
- end
-
- def using_resource_with_overwrite_block
- respond_with(resource) do |format|
- format.html { render :text => "HTML" }
- end
- end
-
- def using_resource_with_collection
- respond_with([resource, Customer.new("jamis", 9)])
- end
-
- def using_resource_with_parent
- respond_with(Quiz::Store.new("developer?", 11), Customer.new("david", 13))
- end
-
- def using_resource_with_status_and_location
- respond_with(resource, :location => "http://test.host/", :status => :created)
- end
-
- def using_resource_with_json
- respond_with(CustomerWithJson.new("david", request.delete? ? nil : 13))
- end
-
- def using_invalid_resource_with_template
- respond_with(resource)
- end
-
- def using_options_with_template
- @customer = resource
- respond_with(@customer, :status => 123, :location => "http://test.host/")
- end
-
- def using_resource_with_responder
- responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" }
- respond_with(resource, :responder => responder)
- end
-
- def using_resource_with_action
- respond_with(resource, :action => :foo) do |format|
- format.html { raise ActionView::MissingTemplate.new([], "bar", ["foo"], {}, false) }
- end
- end
-
- def using_responder_with_respond
- responder = Class.new(ActionController::Responder) do
- def respond; @controller.render :text => "respond #{format}"; end
- end
- respond_with(resource, :responder => responder)
- end
-
- def respond_with_additional_params
- @params = RespondWithController.params
- respond_with({:result => resource}, @params)
- end
-
-protected
- def self.params
- {
- :foo => 'bar'
- }
- end
-
- def resource
- Customer.new("david", request.delete? ? nil : 13)
- end
-end
-
-class InheritedRespondWithController < RespondWithController
- clear_respond_to
- respond_to :xml, :json
-
- def index
- respond_with(resource) do |format|
- format.json { render :text => "JSON" }
- end
- end
-end
-
-class RenderJsonRespondWithController < RespondWithController
- clear_respond_to
- respond_to :json
-
- def index
- respond_with(resource) do |format|
- format.json { render :json => RenderJsonTestException.new('boom') }
- end
- end
-
- def create
- resource = ValidatedCustomer.new(params[:name], 1)
- respond_with(resource) do |format|
- format.json do
- if resource.errors.empty?
- render :json => { :valid => true }
- else
- render :json => { :valid => false }
- end
- end
- end
- end
-end
-
-class CsvRespondWithController < ActionController::Base
- respond_to :csv
-
- class RespondWithCsv
- def to_csv
- "c,s,v"
- end
- end
-
- def index
- respond_with(RespondWithCsv.new)
- end
-end
-
-class EmptyRespondWithController < ActionController::Base
- def index
- respond_with(Customer.new("david", 13))
- end
-end
-
-class RespondWithControllerTest < ActionController::TestCase
- def setup
- super
- @request.host = "www.example.com"
- Mime::Type.register_alias('text/html', :iphone)
- Mime::Type.register_alias('text/html', :touch)
- Mime::Type.register('text/x-mobile', :mobile)
- end
-
- def teardown
- super
- Mime::Type.unregister(:iphone)
- Mime::Type.unregister(:touch)
- Mime::Type.unregister(:mobile)
- end
-
- def test_respond_with_shouldnt_modify_original_hash
- get :respond_with_additional_params
- assert_equal RespondWithController.params, assigns(:params)
- end
-
- def test_using_resource
- @request.accept = "application/xml"
- get :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal "<name>david</name>", @response.body
-
- @request.accept = "application/json"
- assert_raise ActionView::MissingTemplate do
- get :using_resource
- end
- end
-
- def test_using_resource_with_js_simply_tries_to_render_the_template
- @request.accept = "text/javascript"
- get :using_resource
- assert_equal "text/javascript", @response.content_type
- assert_equal "alert(\"Hi\");", @response.body
- end
-
- def test_using_hash_resource_with_js_raises_an_error_if_template_cant_be_found
- @request.accept = "text/javascript"
- assert_raise ActionView::MissingTemplate do
- get :using_hash_resource
- end
- end
-
- def test_using_hash_resource
- @request.accept = "application/xml"
- get :using_hash_resource
- assert_equal "application/xml", @response.content_type
- assert_equal "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<hash>\n <name>david</name>\n</hash>\n", @response.body
-
- @request.accept = "application/json"
- get :using_hash_resource
- assert_equal "application/json", @response.content_type
- assert @response.body.include?("result")
- assert @response.body.include?('"name":"david"')
- assert @response.body.include?('"id":13')
- end
-
- def test_using_hash_resource_with_post
- @request.accept = "application/json"
- assert_raise ArgumentError, "Nil location provided. Can't build URI." do
- post :using_hash_resource
- end
- end
-
- def test_using_resource_with_block
- @request.accept = "*/*"
- get :using_resource_with_block
- assert_equal "text/html", @response.content_type
- assert_equal 'Hello world!', @response.body
-
- @request.accept = "text/csv"
- get :using_resource_with_block
- assert_equal "text/csv", @response.content_type
- assert_equal "CSV", @response.body
-
- @request.accept = "application/xml"
- get :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal "<name>david</name>", @response.body
- end
-
- def test_using_resource_with_overwrite_block
- get :using_resource_with_overwrite_block
- assert_equal "text/html", @response.content_type
- assert_equal "HTML", @response.body
- end
-
- def test_not_acceptable
- @request.accept = "application/xml"
- assert_raises(ActionController::UnknownFormat) do
- get :using_resource_with_block
- end
-
- @request.accept = "text/javascript"
- assert_raises(ActionController::UnknownFormat) do
- get :using_resource_with_overwrite_block
- end
- end
-
- def test_using_resource_for_post_with_html_redirects_on_success
- with_test_route_set do
- post :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 302, @response.status
- assert_equal "http://www.example.com/customers/13", @response.location
- assert @response.redirect?
- end
- end
-
- def test_using_resource_for_post_with_html_rerender_on_failure
- with_test_route_set do
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- post :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 200, @response.status
- assert_equal "New world!\n", @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_for_post_with_xml_yields_created_on_success
- with_test_route_set do
- @request.accept = "application/xml"
- post :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal 201, @response.status
- assert_equal "<name>david</name>", @response.body
- assert_equal "http://www.example.com/customers/13", @response.location
- end
- end
-
- def test_using_resource_for_post_with_xml_yields_unprocessable_entity_on_failure
- with_test_route_set do
- @request.accept = "application/xml"
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- post :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal 422, @response.status
- assert_equal errors.to_xml, @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_for_post_with_json_yields_unprocessable_entity_on_failure
- with_test_route_set do
- @request.accept = "application/json"
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- post :using_resource
- assert_equal "application/json", @response.content_type
- assert_equal 422, @response.status
- errors = {:errors => errors}
- assert_equal errors.to_json, @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_for_patch_with_html_redirects_on_success
- with_test_route_set do
- patch :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 302, @response.status
- assert_equal "http://www.example.com/customers/13", @response.location
- assert @response.redirect?
- end
- end
-
- def test_using_resource_for_patch_with_html_rerender_on_failure
- with_test_route_set do
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- patch :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 200, @response.status
- assert_equal "Edit world!\n", @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_for_patch_with_html_rerender_on_failure_even_on_method_override
- with_test_route_set do
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- @request.env["rack.methodoverride.original_method"] = "POST"
- patch :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 200, @response.status
- assert_equal "Edit world!\n", @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_for_put_with_html_redirects_on_success
- with_test_route_set do
- put :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 302, @response.status
- assert_equal "http://www.example.com/customers/13", @response.location
- assert @response.redirect?
- end
- end
-
- def test_using_resource_for_put_with_html_rerender_on_failure
- with_test_route_set do
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- put :using_resource
-
- assert_equal "text/html", @response.content_type
- assert_equal 200, @response.status
- assert_equal "Edit world!\n", @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_for_put_with_html_rerender_on_failure_even_on_method_override
- with_test_route_set do
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- @request.env["rack.methodoverride.original_method"] = "POST"
- put :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 200, @response.status
- assert_equal "Edit world!\n", @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_for_put_with_xml_yields_no_content_on_success
- @request.accept = "application/xml"
- put :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal 204, @response.status
- assert_equal "", @response.body
- end
-
- def test_using_resource_for_put_with_json_yields_no_content_on_success
- @request.accept = "application/json"
- put :using_resource_with_json
- assert_equal "application/json", @response.content_type
- assert_equal 204, @response.status
- assert_equal "", @response.body
- end
-
- def test_using_resource_for_put_with_xml_yields_unprocessable_entity_on_failure
- @request.accept = "application/xml"
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- put :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal 422, @response.status
- assert_equal errors.to_xml, @response.body
- assert_nil @response.location
- end
-
- def test_using_resource_for_put_with_json_yields_unprocessable_entity_on_failure
- @request.accept = "application/json"
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- put :using_resource
- assert_equal "application/json", @response.content_type
- assert_equal 422, @response.status
- errors = {:errors => errors}
- assert_equal errors.to_json, @response.body
- assert_nil @response.location
- end
-
- def test_using_resource_for_delete_with_html_redirects_on_success
- with_test_route_set do
- Customer.any_instance.stubs(:destroyed?).returns(true)
- delete :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 302, @response.status
- assert_equal "http://www.example.com/customers", @response.location
- end
- end
-
- def test_using_resource_for_delete_with_xml_yields_no_content_on_success
- Customer.any_instance.stubs(:destroyed?).returns(true)
- @request.accept = "application/xml"
- delete :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal 204, @response.status
- assert_equal "", @response.body
- end
-
- def test_using_resource_for_delete_with_json_yields_no_content_on_success
- Customer.any_instance.stubs(:destroyed?).returns(true)
- @request.accept = "application/json"
- delete :using_resource_with_json
- assert_equal "application/json", @response.content_type
- assert_equal 204, @response.status
- assert_equal "", @response.body
- end
-
- def test_using_resource_for_delete_with_html_redirects_on_failure
- with_test_route_set do
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- Customer.any_instance.stubs(:destroyed?).returns(false)
- delete :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 302, @response.status
- assert_equal "http://www.example.com/customers", @response.location
- end
- end
-
- def test_using_resource_with_parent_for_get
- @request.accept = "application/xml"
- get :using_resource_with_parent
- assert_equal "application/xml", @response.content_type
- assert_equal 200, @response.status
- assert_equal "<name>david</name>", @response.body
- end
-
- def test_using_resource_with_parent_for_post
- with_test_route_set do
- @request.accept = "application/xml"
-
- post :using_resource_with_parent
- assert_equal "application/xml", @response.content_type
- assert_equal 201, @response.status
- assert_equal "<name>david</name>", @response.body
- assert_equal "http://www.example.com/quiz_stores/11/customers/13", @response.location
-
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- post :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal 422, @response.status
- assert_equal errors.to_xml, @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_with_collection
- @request.accept = "application/xml"
- get :using_resource_with_collection
- assert_equal "application/xml", @response.content_type
- assert_equal 200, @response.status
- assert_match(/<name>david<\/name>/, @response.body)
- assert_match(/<name>jamis<\/name>/, @response.body)
- end
-
- def test_using_resource_with_action
- @controller.instance_eval do
- def render(params={})
- self.response_body = "#{params[:action]} - #{formats}"
- end
- end
-
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
-
- post :using_resource_with_action
- assert_equal "foo - #{[:html].to_s}", @controller.response.body
- end
-
- def test_respond_as_responder_entry_point
- @request.accept = "text/html"
- get :using_responder_with_respond
- assert_equal "respond html", @response.body
-
- @request.accept = "application/xml"
- get :using_responder_with_respond
- assert_equal "respond xml", @response.body
- end
-
- def test_clear_respond_to
- @controller = InheritedRespondWithController.new
- @request.accept = "text/html"
- assert_raises(ActionController::UnknownFormat) do
- get :index
- end
- end
-
- def test_first_in_respond_to_has_higher_priority
- @controller = InheritedRespondWithController.new
- @request.accept = "*/*"
- get :index
- assert_equal "application/xml", @response.content_type
- assert_equal "<name>david</name>", @response.body
- end
-
- def test_block_inside_respond_with_is_rendered
- @controller = InheritedRespondWithController.new
- @request.accept = "application/json"
- get :index
- assert_equal "JSON", @response.body
- end
-
- def test_render_json_object_responds_to_str_still_produce_json
- @controller = RenderJsonRespondWithController.new
- @request.accept = "application/json"
- get :index, :format => :json
- assert_match(/"message":"boom"/, @response.body)
- assert_match(/"error":"RenderJsonTestException"/, @response.body)
- end
-
- def test_api_response_with_valid_resource_respect_override_block
- @controller = RenderJsonRespondWithController.new
- post :create, :name => "sikachu", :format => :json
- assert_equal '{"valid":true}', @response.body
- end
-
- def test_api_response_with_invalid_resource_respect_override_block
- @controller = RenderJsonRespondWithController.new
- post :create, :name => "david", :format => :json
- assert_equal '{"valid":false}', @response.body
- end
-
- def test_no_double_render_is_raised
- @request.accept = "text/html"
- assert_raise ActionView::MissingTemplate do
- get :using_resource
- end
- end
-
- def test_using_resource_with_status_and_location
- @request.accept = "text/html"
- post :using_resource_with_status_and_location
- assert @response.redirect?
- assert_equal "http://test.host/", @response.location
-
- @request.accept = "application/xml"
- get :using_resource_with_status_and_location
- assert_equal 201, @response.status
- end
-
- def test_using_resource_with_status_and_location_with_invalid_resource
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
-
- @request.accept = "text/xml"
-
- post :using_resource_with_status_and_location
- assert_equal errors.to_xml, @response.body
- assert_equal 422, @response.status
- assert_equal nil, @response.location
-
- put :using_resource_with_status_and_location
- assert_equal errors.to_xml, @response.body
- assert_equal 422, @response.status
- assert_equal nil, @response.location
- end
-
- def test_using_invalid_resource_with_template
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
-
- @request.accept = "text/xml"
-
- post :using_invalid_resource_with_template
- assert_equal errors.to_xml, @response.body
- assert_equal 422, @response.status
- assert_equal nil, @response.location
-
- put :using_invalid_resource_with_template
- assert_equal errors.to_xml, @response.body
- assert_equal 422, @response.status
- assert_equal nil, @response.location
- end
-
- def test_using_options_with_template
- @request.accept = "text/xml"
-
- post :using_options_with_template
- assert_equal "<customer-name>david</customer-name>", @response.body
- assert_equal 123, @response.status
- assert_equal "http://test.host/", @response.location
-
- put :using_options_with_template
- assert_equal "<customer-name>david</customer-name>", @response.body
- assert_equal 123, @response.status
- assert_equal "http://test.host/", @response.location
- end
-
- def test_using_resource_with_responder
- get :using_resource_with_responder
- assert_equal "Resource name is david", @response.body
- end
-
- def test_using_resource_with_set_responder
- RespondWithController.responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" }
- get :using_resource
- assert_equal "Resource name is david", @response.body
- ensure
- RespondWithController.responder = ActionController::Responder
- end
-
- def test_uses_renderer_if_an_api_behavior
- ActionController::Renderers.add :csv do |obj, options|
- send_data obj.to_csv, type: Mime::CSV
- end
- @controller = CsvRespondWithController.new
- get :index, format: 'csv'
- assert_equal Mime::CSV, @response.content_type
- assert_equal "c,s,v", @response.body
- ensure
- ActionController::Renderers.remove :csv
- end
-
- def test_raises_missing_renderer_if_an_api_behavior_with_no_renderer
- @controller = CsvRespondWithController.new
- assert_raise ActionController::MissingRenderer do
- get :index, format: 'csv'
- end
- end
-
- def test_removing_renderers
- ActionController::Renderers.add :csv do |obj, options|
- send_data obj.to_csv, type: Mime::CSV
- end
- @controller = CsvRespondWithController.new
- @request.accept = "text/csv"
- get :index, format: 'csv'
- assert_equal Mime::CSV, @response.content_type
-
- ActionController::Renderers.remove :csv
- assert_raise ActionController::MissingRenderer do
- get :index, format: 'csv'
- end
- ensure
- ActionController::Renderers.remove :csv
- end
-
- def test_error_is_raised_if_no_respond_to_is_declared_and_respond_with_is_called
- @controller = EmptyRespondWithController.new
- @request.accept = "*/*"
- assert_raise RuntimeError do
- get :index
- end
- end
-
- private
- def with_test_route_set
- with_routing do |set|
- set.draw do
- resources :customers
- resources :quiz_stores do
- resources :customers
- end
- get ":controller/:action"
- end
- yield
- end
- end
-end
-
-class FlashResponder < ActionController::Responder
- def initialize(controller, resources, options={})
- super
- end
-
- def to_html
- controller.flash[:notice] = 'Success'
- super
- end
-end
-
-class FlashResponderController < ActionController::Base
- self.responder = FlashResponder
- respond_to :html
-
- def index
- respond_with Object.new do |format|
- format.html { render :text => 'HTML' }
- end
- end
-end
-
-class FlashResponderControllerTest < ActionController::TestCase
- tests FlashResponderController
-
- def test_respond_with_block_executed
- get :index
- assert_equal 'HTML', @response.body
- end
-
- def test_flash_responder_executed
- get :index
- assert_equal 'Success', flash[:notice]
- end
-end
diff --git a/actionpack/test/controller/mime/responders_test.rb b/actionpack/test/controller/mime/responders_test.rb
new file mode 100644
index 0000000000..032b4c0ab1
--- /dev/null
+++ b/actionpack/test/controller/mime/responders_test.rb
@@ -0,0 +1,32 @@
+require 'abstract_unit'
+require 'controller/fake_models'
+
+class ResponderTest < ActionController::TestCase
+ def test_class_level_respond_to
+ e = assert_raises(NoMethodError) do
+ Class.new(ActionController::Base) do
+ respond_to :json
+ end
+ end
+
+ assert_includes e.message, '`responders` gem'
+ assert_includes e.message, '~> 2.0'
+ end
+
+ def test_respond_with
+ klass = Class.new(ActionController::Base) do
+ def index
+ respond_with Customer.new("david", 13)
+ end
+ end
+
+ @controller = klass.new
+
+ e = assert_raises(NoMethodError) do
+ get :index
+ end
+
+ assert_includes e.message, '`responders` gem'
+ assert_includes e.message, '~> 2.0'
+ end
+end
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index 9926130c02..b036b6c08e 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -10,11 +10,17 @@ class TestControllerWithExtraEtags < ActionController::Base
etag { nil }
def fresh
- render text: "stale" if stale?(etag: '123')
+ render text: "stale" if stale?(etag: '123', template: false)
end
def array
- render text: "stale" if stale?(etag: %w(1 2 3))
+ render text: "stale" if stale?(etag: %w(1 2 3), template: false)
+ end
+
+ def with_template
+ if stale? template: 'test/hello_world'
+ render text: 'stale'
+ end
end
end
@@ -409,6 +415,32 @@ class EtagRenderTest < ActionController::TestCase
assert_response :success
end
+ def test_etag_reflects_template_digest
+ get :with_template
+ assert_response :ok
+ assert_not_nil etag = @response.etag
+
+ request.if_none_match = etag
+ get :with_template
+ assert_response :not_modified
+
+ # Modify the template digest
+ path = File.expand_path('../../fixtures/test/hello_world.erb', __FILE__)
+ old = File.read(path)
+
+ begin
+ File.write path, 'foo'
+ ActionView::Digestor.cache.clear
+
+ request.if_none_match = etag
+ get :with_template
+ assert_response :ok
+ assert_not_equal etag, @response.etag
+ ensure
+ File.write path, old
+ end
+ end
+
def etag(record)
Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record)).inspect
end
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
index 2a5aad9c0e..05ad8b6cdc 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -389,7 +389,8 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController
SecureRandom.stubs(:base64).returns(@token + '<=?')
get :meta
assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token'
- assert_select 'meta[name=?][content=?]', 'csrf-token', 'cf50faa3fe97702ca1ae&lt;=?'
+ assert_select 'meta[name=?]', 'csrf-token'
+ assert_match(/cf50faa3fe97702ca1ae&lt;=\?/, @response.body)
end
end
diff --git a/actionpack/test/controller/selector_test.rb b/actionpack/test/controller/selector_test.rb
deleted file mode 100644
index 1e80c8601c..0000000000
--- a/actionpack/test/controller/selector_test.rb
+++ /dev/null
@@ -1,629 +0,0 @@
-#--
-# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
-# Under MIT and/or CC By license.
-#++
-
-require 'abstract_unit'
-require 'controller/fake_controllers'
-require 'action_view/vendor/html-scanner'
-
-class SelectorTest < ActiveSupport::TestCase
- #
- # Basic selector: element, id, class, attributes.
- #
-
- def test_element
- parse(%Q{<div id="1"></div><p></p><div id="2"></div>})
- # Match element by name.
- select("div")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "2", @matches[1].attributes["id"]
- # Not case sensitive.
- select("DIV")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "2", @matches[1].attributes["id"]
- # Universal match (all elements).
- select("*")
- assert_equal 3, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal nil, @matches[1].attributes["id"]
- assert_equal "2", @matches[2].attributes["id"]
- end
-
-
- def test_identifier
- parse(%Q{<div id="1"></div><p></p><div id="2"></div>})
- # Match element by ID.
- select("div#1")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- # Match element by ID, substitute value.
- select("div#?", 2)
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Element name does not match ID.
- select("p#?", 2)
- assert_equal 0, @matches.size
- # Use regular expression.
- select("#?", /\d/)
- assert_equal 2, @matches.size
- end
-
-
- def test_class_name
- parse(%Q{<div id="1" class=" foo "></div><p id="2" class=" foo bar "></p><div id="3" class="bar"></div>})
- # Match element with specified class.
- select("div.foo")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- # Match any element with specified class.
- select("*.foo")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "2", @matches[1].attributes["id"]
- # Match elements with other class.
- select("*.bar")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- # Match only element with both class names.
- select("*.bar.foo")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- end
-
-
- def test_attribute
- parse(%Q{<div id="1"></div><p id="2" title="" bar="foo"></p><div id="3" title="foo"></div>})
- # Match element with attribute.
- select("div[title]")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- # Match any element with attribute.
- select("*[title]")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- # Match element with attribute value.
- select("*[title=foo]")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- # Match element with attribute and attribute value.
- select("[bar=foo][title]")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Not case sensitive.
- select("[BAR=foo][TiTle]")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- end
-
-
- def test_attribute_quoted
- parse(%Q{<div id="1" title="foo"></div><div id="2" title="bar"></div><div id="3" title=" bar "></div>})
- # Match without quotes.
- select("[title = bar]")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Match with single quotes.
- select("[title = 'bar' ]")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Match with double quotes.
- select("[title = \"bar\" ]")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Match with spaces.
- select("[title = \" bar \" ]")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- end
-
-
- def test_attribute_equality
- parse(%Q{<div id="1" title="foo bar"></div><div id="2" title="barbaz"></div>})
- # Match (fail) complete value.
- select("[title=bar]")
- assert_equal 0, @matches.size
- # Match space-separate word.
- select("[title~=foo]")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- select("[title~=bar]")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- # Match beginning of value.
- select("[title^=ba]")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Match end of value.
- select("[title$=ar]")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- # Match text in value.
- select("[title*=bar]")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "2", @matches[1].attributes["id"]
- # Match first space separated word.
- select("[title|=foo]")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- select("[title|=bar]")
- assert_equal 0, @matches.size
- end
-
-
- #
- # Selector composition: groups, sibling, children
- #
-
-
- def test_selector_group
- parse(%Q{<h1 id="1"></h1><h2 id="2"></h2><h3 id="3"></h3>})
- # Simple group selector.
- select("h1,h3")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- select("h1 , h3")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- # Complex group selector.
- parse(%Q{<h1 id="1"><a href="foo"></a></h1><h2 id="2"><a href="bar"></a></h2><h3 id="2"><a href="baz"></a></h3>})
- select("h1 a, h3 a")
- assert_equal 2, @matches.size
- assert_equal "foo", @matches[0].attributes["href"]
- assert_equal "baz", @matches[1].attributes["href"]
- # And now for the three selector challenge.
- parse(%Q{<h1 id="1"><a href="foo"></a></h1><h2 id="2"><a href="bar"></a></h2><h3 id="2"><a href="baz"></a></h3>})
- select("h1 a, h2 a, h3 a")
- assert_equal 3, @matches.size
- assert_equal "foo", @matches[0].attributes["href"]
- assert_equal "bar", @matches[1].attributes["href"]
- assert_equal "baz", @matches[2].attributes["href"]
- end
-
-
- def test_sibling_selector
- parse(%Q{<h1 id="1"></h1><h2 id="2"></h2><h3 id="3"></h3>})
- # Test next sibling.
- select("h1+*")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- select("h1+h2")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- select("h1+h3")
- assert_equal 0, @matches.size
- select("*+h3")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- # Test any sibling.
- select("h1~*")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- select("h2~*")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- end
-
-
- def test_children_selector
- parse(%Q{<div><p id="1"><span id="2"></span></p></div><div><p id="3"><span id="4" class="foo"></span></p></div>})
- # Test child selector.
- select("div>p")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- select("div>span")
- assert_equal 0, @matches.size
- select("div>p#3")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- select("div>p>span")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- # Test descendant selector.
- select("div p")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- select("div span")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- select("div *#3")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- select("div *#4")
- assert_equal 1, @matches.size
- assert_equal "4", @matches[0].attributes["id"]
- # This is here because it failed before when whitespaces
- # were not properly stripped.
- select("div .foo")
- assert_equal 1, @matches.size
- assert_equal "4", @matches[0].attributes["id"]
- end
-
-
- #
- # Pseudo selectors: root, nth-child, empty, content, etc
- #
-
-
- def test_root_selector
- parse(%Q{<div id="1"><div id="2"></div></div>})
- # Can only find element if it's root.
- select(":root")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- select("#1:root")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- select("#2:root")
- assert_equal 0, @matches.size
- # Opposite for nth-child.
- select("#1:nth-child(1)")
- assert_equal 0, @matches.size
- end
-
-
- def test_nth_child_odd_even
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # Test odd nth children.
- select("tr:nth-child(odd)")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- # Test even nth children.
- select("tr:nth-child(even)")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- end
-
-
- def test_nth_child_a_is_zero
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # Test the third child.
- select("tr:nth-child(0n+3)")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- # Same but an can be omitted when zero.
- select("tr:nth-child(3)")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- # Second element (but not every second element).
- select("tr:nth-child(0n+2)")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Before first and past last returns nothing.:
- assert_raise(ArgumentError) { select("tr:nth-child(-1)") }
- select("tr:nth-child(0)")
- assert_equal 0, @matches.size
- select("tr:nth-child(5)")
- assert_equal 0, @matches.size
- end
-
-
- def test_nth_child_a_is_one
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # a is group of one, pick every element in group.
- select("tr:nth-child(1n+0)")
- assert_equal 4, @matches.size
- # Same but a can be omitted when one.
- select("tr:nth-child(n+0)")
- assert_equal 4, @matches.size
- # Same but b can be omitted when zero.
- select("tr:nth-child(n)")
- assert_equal 4, @matches.size
- end
-
-
- def test_nth_child_b_is_zero
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # If b is zero, pick the n-th element (here each one).
- select("tr:nth-child(n+0)")
- assert_equal 4, @matches.size
- # If b is zero, pick the n-th element (here every second).
- select("tr:nth-child(2n+0)")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- # If a and b are both zero, no element selected.
- select("tr:nth-child(0n+0)")
- assert_equal 0, @matches.size
- select("tr:nth-child(0)")
- assert_equal 0, @matches.size
- end
-
-
- def test_nth_child_a_is_negative
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # Since a is -1, picks the first three elements.
- select("tr:nth-child(-n+3)")
- assert_equal 3, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "2", @matches[1].attributes["id"]
- assert_equal "3", @matches[2].attributes["id"]
- # Since a is -2, picks the first in every second of first four elements.
- select("tr:nth-child(-2n+3)")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- # Since a is -2, picks the first in every second of first three elements.
- select("tr:nth-child(-2n+2)")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- end
-
-
- def test_nth_child_b_is_negative
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # Select last of four.
- select("tr:nth-child(4n-1)")
- assert_equal 1, @matches.size
- assert_equal "4", @matches[0].attributes["id"]
- # Select first of four.
- select("tr:nth-child(4n-4)")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- # Select last of every second.
- select("tr:nth-child(2n-1)")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- # Select nothing since an+b always < 0
- select("tr:nth-child(-1n-1)")
- assert_equal 0, @matches.size
- end
-
-
- def test_nth_child_substitution_values
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # Test with ?n?.
- select("tr:nth-child(?n?)", 2, 1)
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- select("tr:nth-child(?n?)", 2, 2)
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- select("tr:nth-child(?n?)", 4, 2)
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Test with ? (b only).
- select("tr:nth-child(?)", 3)
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- select("tr:nth-child(?)", 5)
- assert_equal 0, @matches.size
- end
-
-
- def test_nth_last_child
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # Last two elements.
- select("tr:nth-last-child(-n+2)")
- assert_equal 2, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- # All old elements counting from last one.
- select("tr:nth-last-child(odd)")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- end
-
-
- def test_nth_of_type
- parse(%Q{<table><thead></thead><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # First two elements.
- select("tr:nth-of-type(-n+2)")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "2", @matches[1].attributes["id"]
- # All old elements counting from last one.
- select("tr:nth-last-of-type(odd)")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- end
-
-
- def test_first_and_last
- parse(%Q{<table><thead></thead><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # First child.
- select("tr:first-child")
- assert_equal 0, @matches.size
- select(":first-child")
- assert_equal 1, @matches.size
- assert_equal "thead", @matches[0].name
- # First of type.
- select("tr:first-of-type")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- select("thead:first-of-type")
- assert_equal 1, @matches.size
- assert_equal "thead", @matches[0].name
- select("div:first-of-type")
- assert_equal 0, @matches.size
- # Last child.
- select("tr:last-child")
- assert_equal 1, @matches.size
- assert_equal "4", @matches[0].attributes["id"]
- # Last of type.
- select("tr:last-of-type")
- assert_equal 1, @matches.size
- assert_equal "4", @matches[0].attributes["id"]
- select("thead:last-of-type")
- assert_equal 1, @matches.size
- assert_equal "thead", @matches[0].name
- select("div:last-of-type")
- assert_equal 0, @matches.size
- end
-
-
- def test_only_child_and_only_type_first_and_last
- # Only child.
- parse(%Q{<table><tr></tr></table>})
- select("table:only-child")
- assert_equal 0, @matches.size
- select("tr:only-child")
- assert_equal 1, @matches.size
- assert_equal "tr", @matches[0].name
- parse(%Q{<table><tr></tr><tr></tr></table>})
- select("tr:only-child")
- assert_equal 0, @matches.size
- # Only of type.
- parse(%Q{<table><thead></thead><tr></tr><tr></tr></table>})
- select("thead:only-of-type")
- assert_equal 1, @matches.size
- assert_equal "thead", @matches[0].name
- select("td:only-of-type")
- assert_equal 0, @matches.size
- end
-
-
- def test_empty
- parse(%Q{<table><tr></tr></table>})
- select("table:empty")
- assert_equal 0, @matches.size
- select("tr:empty")
- assert_equal 1, @matches.size
- parse(%Q{<div> </div>})
- select("div:empty")
- assert_equal 1, @matches.size
- end
-
-
- def test_content
- parse(%Q{<div> </div>})
- select("div:content()")
- assert_equal 1, @matches.size
- parse(%Q{<div>something </div>})
- select("div:content()")
- assert_equal 0, @matches.size
- select("div:content(something)")
- assert_equal 1, @matches.size
- select("div:content( 'something' )")
- assert_equal 1, @matches.size
- select("div:content( \"something\" )")
- assert_equal 1, @matches.size
- select("div:content(?)", "something")
- assert_equal 1, @matches.size
- select("div:content(?)", /something/)
- assert_equal 1, @matches.size
- end
-
-
- #
- # Test negation.
- #
-
-
- def test_element_negation
- parse(%Q{<p></p><div></div>})
- select("*")
- assert_equal 2, @matches.size
- select("*:not(p)")
- assert_equal 1, @matches.size
- assert_equal "div", @matches[0].name
- select("*:not(div)")
- assert_equal 1, @matches.size
- assert_equal "p", @matches[0].name
- select("*:not(span)")
- assert_equal 2, @matches.size
- end
-
-
- def test_id_negation
- parse(%Q{<p id="1"></p><p id="2"></p>})
- select("p")
- assert_equal 2, @matches.size
- select(":not(#1)")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- select(":not(#2)")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- end
-
-
- def test_class_name_negation
- parse(%Q{<p class="foo"></p><p class="bar"></p>})
- select("p")
- assert_equal 2, @matches.size
- select(":not(.foo)")
- assert_equal 1, @matches.size
- assert_equal "bar", @matches[0].attributes["class"]
- select(":not(.bar)")
- assert_equal 1, @matches.size
- assert_equal "foo", @matches[0].attributes["class"]
- end
-
-
- def test_attribute_negation
- parse(%Q{<p title="foo"></p><p title="bar"></p>})
- select("p")
- assert_equal 2, @matches.size
- select(":not([title=foo])")
- assert_equal 1, @matches.size
- assert_equal "bar", @matches[0].attributes["title"]
- select(":not([title=bar])")
- assert_equal 1, @matches.size
- assert_equal "foo", @matches[0].attributes["title"]
- end
-
-
- def test_pseudo_class_negation
- parse(%Q{<div><p id="1"></p><p id="2"></p></div>})
- select("p")
- assert_equal 2, @matches.size
- select("p:not(:first-child)")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- select("p:not(:nth-child(2))")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- end
-
-
- def test_negation_details
- parse(%Q{<p id="1"></p><p id="2"></p><p id="3"></p>})
- assert_raise(ArgumentError) { select(":not(") }
- assert_raise(ArgumentError) { select(":not(:not())") }
- select("p:not(#1):not(#3)")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- end
-
-
- def test_select_from_element
- parse(%Q{<div><p id="1"></p><p id="2"></p></div>})
- select("div")
- @matches = @matches[0].select("p")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "2", @matches[1].attributes["id"]
- end
-
-
-protected
-
- def parse(html)
- @html = HTML::Document.new(html).root
- end
-
- def select(*selector)
- @matches = HTML.selector(*selector).select(@html)
- end
-
-end
diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
index 060c940100..1280e0d9a3 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -353,168 +353,6 @@ XML
assert_equal "bar", assigns[:bar]
end
- def test_assert_tag_tag
- process :test_html_output
-
- # there is a 'form' tag
- assert_tag :tag => 'form'
- # there is not an 'hr' tag
- assert_no_tag :tag => 'hr'
- end
-
- def test_assert_tag_attributes
- process :test_html_output
-
- # there is a tag with an 'id' of 'bar'
- assert_tag :attributes => { :id => "bar" }
- # there is no tag with a 'name' of 'baz'
- assert_no_tag :attributes => { :name => "baz" }
- end
-
- def test_assert_tag_parent
- process :test_html_output
-
- # there is a tag with a parent 'form' tag
- assert_tag :parent => { :tag => "form" }
- # there is no tag with a parent of 'input'
- assert_no_tag :parent => { :tag => "input" }
- end
-
- def test_assert_tag_child
- process :test_html_output
-
- # there is a tag with a child 'input' tag
- assert_tag :child => { :tag => "input" }
- # there is no tag with a child 'strong' tag
- assert_no_tag :child => { :tag => "strong" }
- end
-
- def test_assert_tag_ancestor
- process :test_html_output
-
- # there is a 'li' tag with an ancestor having an id of 'foo'
- assert_tag :ancestor => { :attributes => { :id => "foo" } }, :tag => "li"
- # there is no tag of any kind with an ancestor having an href matching 'foo'
- assert_no_tag :ancestor => { :attributes => { :href => /foo/ } }
- end
-
- def test_assert_tag_descendant
- process :test_html_output
-
- # there is a tag with a descendant 'li' tag
- assert_tag :descendant => { :tag => "li" }
- # there is no tag with a descendant 'html' tag
- assert_no_tag :descendant => { :tag => "html" }
- end
-
- def test_assert_tag_sibling
- process :test_html_output
-
- # there is a tag with a sibling of class 'item'
- assert_tag :sibling => { :attributes => { :class => "item" } }
- # there is no tag with a sibling 'ul' tag
- assert_no_tag :sibling => { :tag => "ul" }
- end
-
- def test_assert_tag_after
- process :test_html_output
-
- # there is a tag following a sibling 'div' tag
- assert_tag :after => { :tag => "div" }
- # there is no tag following a sibling tag with id 'bar'
- assert_no_tag :after => { :attributes => { :id => "bar" } }
- end
-
- def test_assert_tag_before
- process :test_html_output
-
- # there is a tag preceding a tag with id 'bar'
- assert_tag :before => { :attributes => { :id => "bar" } }
- # there is no tag preceding a 'form' tag
- assert_no_tag :before => { :tag => "form" }
- end
-
- def test_assert_tag_children_count
- process :test_html_output
-
- # there is a tag with 2 children
- assert_tag :children => { :count => 2 }
- # in particular, there is a <ul> tag with two children (a nameless pair of <li>s)
- assert_tag :tag => 'ul', :children => { :count => 2 }
- # there is no tag with 4 children
- assert_no_tag :children => { :count => 4 }
- end
-
- def test_assert_tag_children_less_than
- process :test_html_output
-
- # there is a tag with less than 5 children
- assert_tag :children => { :less_than => 5 }
- # there is no 'ul' tag with less than 2 children
- assert_no_tag :children => { :less_than => 2 }, :tag => "ul"
- end
-
- def test_assert_tag_children_greater_than
- process :test_html_output
-
- # there is a 'body' tag with more than 1 children
- assert_tag :children => { :greater_than => 1 }, :tag => "body"
- # there is no tag with more than 10 children
- assert_no_tag :children => { :greater_than => 10 }
- end
-
- def test_assert_tag_children_only
- process :test_html_output
-
- # there is a tag containing only one child with an id of 'foo'
- assert_tag :children => { :count => 1,
- :only => { :attributes => { :id => "foo" } } }
- # there is no tag containing only one 'li' child
- assert_no_tag :children => { :count => 1, :only => { :tag => "li" } }
- end
-
- def test_assert_tag_content
- process :test_html_output
-
- # the output contains the string "Name"
- assert_tag :content => /Name/
- # the output does not contain the string "test"
- assert_no_tag :content => /test/
- end
-
- def test_assert_tag_multiple
- process :test_html_output
-
- # there is a 'div', id='bar', with an immediate child whose 'action'
- # attribute matches the regexp /somewhere/.
- assert_tag :tag => "div", :attributes => { :id => "bar" },
- :child => { :attributes => { :action => /somewhere/ } }
-
- # there is no 'div', id='foo', with a 'ul' child with more than
- # 2 "li" children.
- assert_no_tag :tag => "div", :attributes => { :id => "foo" },
- :child => {
- :tag => "ul",
- :children => { :greater_than => 2,
- :only => { :tag => "li" } } }
- end
-
- def test_assert_tag_children_without_content
- process :test_html_output
-
- # there is a form tag with an 'input' child which is a self closing tag
- assert_tag :tag => "form",
- :children => { :count => 1,
- :only => { :tag => "input" } }
-
- # the body tag has an 'a' child which in turn has an 'img' child
- assert_tag :tag => "body",
- :children => { :count => 1,
- :only => { :tag => "a",
- :children => { :count => 1,
- :only => { :tag => "img" } } } }
- end
-
def test_should_not_impose_childless_html_tags_in_xml
process :test_xml_output
@@ -529,23 +367,6 @@ XML
assert err.empty?
end
- def test_assert_tag_attribute_matching
- @response.body = '<input type="text" name="my_name">'
- assert_tag :tag => 'input',
- :attributes => { :name => /my/, :type => 'text' }
- assert_no_tag :tag => 'input',
- :attributes => { :name => 'my', :type => 'text' }
- assert_no_tag :tag => 'input',
- :attributes => { :name => /^my$/, :type => 'text' }
- end
-
- def test_assert_tag_content_matching
- @response.body = "<p>hello world</p>"
- assert_tag :tag => "p", :content => "hello world"
- assert_tag :tag => "p", :content => /hello/
- assert_no_tag :tag => "p", :content => "hello"
- end
-
def test_assert_generates
assert_generates 'controller/action/5', :controller => 'controller', :action => 'action', :id => '5'
assert_generates 'controller/action/7', {:id => "7"}, {:controller => "controller", :action => "action"}
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index 0f145666d1..9b03c805a0 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -21,6 +21,16 @@ class CookiesTest < ActionController::TestCase
end
end
+ class JSONWrapper
+ def initialize(obj)
+ @obj = obj
+ end
+
+ def as_json(options = nil)
+ "wrapped: #{@obj.as_json(options)}"
+ end
+ end
+
class TestController < ActionController::Base
def authenticate
cookies["user_name"] = "david"
@@ -85,6 +95,11 @@ class CookiesTest < ActionController::TestCase
head :ok
end
+ def set_wrapped_signed_cookie
+ cookies.signed[:user_id] = JSONWrapper.new(45)
+ head :ok
+ end
+
def get_signed_cookie
cookies.signed[:user_id]
head :ok
@@ -95,6 +110,11 @@ class CookiesTest < ActionController::TestCase
head :ok
end
+ def set_wrapped_encrypted_cookie
+ cookies.encrypted[:foo] = JSONWrapper.new('bar')
+ head :ok
+ end
+
def get_encrypted_cookie
cookies.encrypted[:foo]
head :ok
@@ -369,6 +389,35 @@ class CookiesTest < ActionController::TestCase
assert_equal 'Jamie', @controller.send(:cookies).permanent[:user_name]
end
+ def test_signed_cookie_using_default_digest
+ get :set_signed_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal 45, cookies.signed[:user_id]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
+ secret = key_generator.generate_key(signed_cookie_salt)
+
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: 'SHA1')
+ assert_equal verifier.generate(45), cookies[:user_id]
+ end
+
+ def test_signed_cookie_using_custom_digest
+ @request.env["action_dispatch.cookies_digest"] = 'SHA256'
+ get :set_signed_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal 45, cookies.signed[:user_id]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
+ secret = key_generator.generate_key(signed_cookie_salt)
+
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: 'SHA256')
+ assert_equal verifier.generate(45), cookies[:user_id]
+ end
+
def test_signed_cookie_using_default_serializer
get :set_signed_cookie
cookies = @controller.send :cookies
@@ -392,6 +441,14 @@ class CookiesTest < ActionController::TestCase
assert_equal 45, cookies.signed[:user_id]
end
+ def test_wrapped_signed_cookie_using_json_serializer
+ @request.env["action_dispatch.cookies_serializer"] = :json
+ get :set_wrapped_signed_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 'wrapped: 45', cookies[:user_id]
+ assert_equal 'wrapped: 45', cookies.signed[:user_id]
+ end
+
def test_signed_cookie_using_custom_serializer
@request.env["action_dispatch.cookies_serializer"] = CustomSerializer
get :set_signed_cookie
@@ -474,6 +531,17 @@ class CookiesTest < ActionController::TestCase
assert_equal 'bar', cookies.encrypted[:foo]
end
+ def test_wrapped_encrypted_cookie_using_json_serializer
+ @request.env["action_dispatch.cookies_serializer"] = :json
+ 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_equal 'wrapped: bar', cookies.encrypted[:foo]
+ end
+
def test_encrypted_cookie_using_custom_serializer
@request.env["action_dispatch.cookies_serializer"] = CustomSerializer
get :set_encrypted_cookie
@@ -481,6 +549,27 @@ 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
diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb
index 3e554a9cf6..889f9a4736 100644
--- a/actionpack/test/dispatch/mapper_test.rb
+++ b/actionpack/test/dispatch/mapper_test.rb
@@ -38,7 +38,7 @@ module ActionDispatch
def test_mapping_requirements
options = { :controller => 'foo', :action => 'bar', :via => :get }
- m = Mapper::Mapping.build({}, FakeSet.new, '/store/:name(*rest)', options)
+ m = Mapper::Mapping.build({}, FakeSet.new, '/store/:name(*rest)', nil, options)
_, _, requirements, _ = m.to_route
assert_equal(/.+?/, requirements[:rest])
end
diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb
index 6737609567..fe9ee6f73d 100644
--- a/actionpack/test/dispatch/request_test.rb
+++ b/actionpack/test/dispatch/request_test.rb
@@ -640,7 +640,7 @@ end
class RequestMethod < BaseRequestTest
test "method returns environment's request method when it has not been
- overriden by middleware".squish do
+ overridden by middleware".squish do
ActionDispatch::Request::HTTP_METHODS.each do |method|
request = stub_request('REQUEST_METHOD' => method)
diff --git a/actionpack/test/fixtures/respond_with/edit.html.erb b/actionpack/test/fixtures/respond_with/edit.html.erb
deleted file mode 100644
index ae82dfa4fc..0000000000
--- a/actionpack/test/fixtures/respond_with/edit.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-Edit world!
diff --git a/actionpack/test/fixtures/respond_with/new.html.erb b/actionpack/test/fixtures/respond_with/new.html.erb
deleted file mode 100644
index 96c8f1b88b..0000000000
--- a/actionpack/test/fixtures/respond_with/new.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-New world!
diff --git a/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb b/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb
deleted file mode 100644
index bf5869ed22..0000000000
--- a/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb
+++ /dev/null
@@ -1 +0,0 @@
-<content>I should not be displayed</content> \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb b/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb
deleted file mode 100644
index b313017913..0000000000
--- a/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb
+++ /dev/null
@@ -1 +0,0 @@
-<customer-name><%= @customer.name %></customer-name> \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_with/using_resource.js.erb b/actionpack/test/fixtures/respond_with/using_resource.js.erb
deleted file mode 100644
index 4417680bce..0000000000
--- a/actionpack/test/fixtures/respond_with/using_resource.js.erb
+++ /dev/null
@@ -1 +0,0 @@
-alert("Hi"); \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_with/using_resource_with_block.html.erb b/actionpack/test/fixtures/respond_with/using_resource_with_block.html.erb
deleted file mode 100644
index 6769dd60bd..0000000000
--- a/actionpack/test/fixtures/respond_with/using_resource_with_block.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-Hello world! \ No newline at end of file
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 3fc2ab178c..552a902349 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,9 @@
+* Add I18n support for input/textarea placeholder text.
+
+ Placeholder I18n follows the same convention as `label` I18n.
+
+ *Alex Robbin*
+
* Fix that render layout: 'messages/layout' should also be added to the dependency tracker tree.
*DHH*
diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec
index e45dd04225..1ea00cff22 100644
--- a/actionview/actionview.gemspec
+++ b/actionview/actionview.gemspec
@@ -23,6 +23,7 @@ Gem::Specification.new do |s|
s.add_dependency 'builder', '~> 3.1'
s.add_dependency 'erubis', '~> 2.7.0'
+ s.add_dependency 'rails-deprecated_sanitizer'
s.add_development_dependency 'actionpack', version
s.add_development_dependency 'activemodel', version
diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb
index 50712e0830..6a1837c6e2 100644
--- a/actionview/lib/action_view.rb
+++ b/actionview/lib/action_view.rb
@@ -86,7 +86,6 @@ module ActionView
super
ActionView::Helpers.eager_load!
ActionView::Template.eager_load!
- HTML.eager_load!
end
end
diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb
index 049af275b6..153c64d691 100644
--- a/actionview/lib/action_view/helpers/sanitize_helper.rb
+++ b/actionview/lib/action_view/helpers/sanitize_helper.rb
@@ -1,5 +1,6 @@
require 'active_support/core_ext/object/try'
-require 'action_view/vendor/html-scanner'
+require 'active_support/deprecation'
+require 'rails-deprecated_sanitizer'
module ActionView
# = Action View Sanitize Helpers
@@ -27,7 +28,29 @@ module ActionView
#
# <%= sanitize @article.body %>
#
- # Custom Use (only the mentioned tags and attributes are allowed, nothing else)
+ # Custom Use - Custom Scrubber
+ # (supply a Loofah::Scrubber that does the sanitization)
+ #
+ # scrubber can either wrap a block:
+ # scrubber = Loofah::Scrubber.new do |node|
+ # node.text = "dawn of cats"
+ # end
+ #
+ # or be a subclass of Loofah::Scrubber which responds to scrub:
+ # class KittyApocalypse < Loofah::Scrubber
+ # def scrub(node)
+ # node.text = "dawn of cats"
+ # end
+ # end
+ # scrubber = KittyApocalypse.new
+ #
+ # <%= sanitize @article.body, scrubber: scrubber %>
+ #
+ # A custom scrubber takes precedence over custom tags and attributes
+ # Learn more about scrubbers here: https://github.com/flavorjones/loofah
+ #
+ # Custom Use - tags and attributes
+ # (only the mentioned tags and attributes are allowed, nothing else)
#
# <%= sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) %>
#
@@ -65,9 +88,9 @@ module ActionView
self.class.white_list_sanitizer.sanitize_css(style)
end
- # Strips all HTML tags from the +html+, including comments. This uses the
- # html-scanner tokenizer and so its HTML parsing ability is limited by
- # that of html-scanner.
+ # Strips all HTML tags from the +html+, including comments. This uses
+ # Nokogiri for tokenization (via Loofah) and so its HTML parsing ability
+ # is limited by that of Nokogiri.
#
# strip_tags("Strip <i>these</i> tags!")
# # => Strip these tags!
@@ -98,47 +121,42 @@ module ActionView
module ClassMethods #:nodoc:
attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
- def sanitized_protocol_separator
- white_list_sanitizer.protocol_separator
- end
+ [:protocol_separator,
+ :uri_attributes,
+ :bad_tags,
+ :allowed_css_properties,
+ :allowed_css_keywords,
+ :shorthand_css_properties,
+ :allowed_protocols].each do |meth|
+ meth_name = "sanitized_#{meth}"
+ imp = lambda do |name|
+ ActiveSupport::Deprecation.warn("#{name} is deprecated and has no effect.")
+ end
- def sanitized_uri_attributes
- white_list_sanitizer.uri_attributes
+ define_method(meth_name) { imp.(meth_name) }
+ define_method("#{meth_name}=") { |value| imp.("#{meth_name}=") }
end
- def sanitized_bad_tags
- white_list_sanitizer.bad_tags
+ # Vendors the full, link and white list sanitizers.
+ # This uses html-scanner for the HTML sanitization.
+ # In the next Rails version this will use Rails::Html::Sanitizer instead.
+ # To get this new behavior now, in your Gemfile, add:
+ #
+ # gem 'rails-html-sanitizer'
+ #
+ def sanitizer_vendor
+ Rails::DeprecatedSanitizer
end
def sanitized_allowed_tags
- white_list_sanitizer.allowed_tags
+ sanitizer_vendor.white_list_sanitizer.allowed_tags
end
def sanitized_allowed_attributes
- white_list_sanitizer.allowed_attributes
- end
-
- def sanitized_allowed_css_properties
- white_list_sanitizer.allowed_css_properties
- end
-
- def sanitized_allowed_css_keywords
- white_list_sanitizer.allowed_css_keywords
- end
-
- def sanitized_shorthand_css_properties
- white_list_sanitizer.shorthand_css_properties
+ sanitizer_vendor.white_list_sanitizer.allowed_attributes
end
- def sanitized_allowed_protocols
- white_list_sanitizer.allowed_protocols
- end
-
- def sanitized_protocol_separator=(value)
- white_list_sanitizer.protocol_separator = value
- end
-
- # Gets the HTML::FullSanitizer instance used by +strip_tags+. Replace with
+ # Gets the Rails::Html::FullSanitizer instance used by +strip_tags+. Replace with
# any object that responds to +sanitize+.
#
# class Application < Rails::Application
@@ -146,21 +164,21 @@ module ActionView
# end
#
def full_sanitizer
- @full_sanitizer ||= HTML::FullSanitizer.new
+ @full_sanitizer ||= sanitizer_vendor.full_sanitizer.new
end
- # Gets the HTML::LinkSanitizer instance used by +strip_links+. Replace with
- # any object that responds to +sanitize+.
+ # Gets the Rails::Html::LinkSanitizer instance used by +strip_links+.
+ # Replace with any object that responds to +sanitize+.
#
# class Application < Rails::Application
# config.action_view.link_sanitizer = MySpecialSanitizer.new
# end
#
def link_sanitizer
- @link_sanitizer ||= HTML::LinkSanitizer.new
+ @link_sanitizer ||= sanitizer_vendor.link_sanitizer.new
end
- # Gets the HTML::WhiteListSanitizer instance used by sanitize and +sanitize_css+.
+ # Gets the Rails::Html::WhiteListSanitizer instance used by sanitize and +sanitize_css+.
# Replace with any object that responds to +sanitize+.
#
# class Application < Rails::Application
@@ -168,87 +186,27 @@ module ActionView
# end
#
def white_list_sanitizer
- @white_list_sanitizer ||= HTML::WhiteListSanitizer.new
- end
-
- # Adds valid HTML attributes that the +sanitize+ helper checks for URIs.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_uri_attributes = 'lowsrc', 'target'
- # end
- #
- def sanitized_uri_attributes=(attributes)
- HTML::WhiteListSanitizer.uri_attributes.merge(attributes)
+ @white_list_sanitizer ||= sanitizer_vendor.white_list_sanitizer.new
end
- # Adds to the Set of 'bad' tags for the +sanitize+ helper.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_bad_tags = 'embed', 'object'
- # end
- #
- def sanitized_bad_tags=(attributes)
- HTML::WhiteListSanitizer.bad_tags.merge(attributes)
- end
-
- # Adds to the Set of allowed tags for the +sanitize+ helper.
+ # Replaces the allowed tags for the +sanitize+ helper.
#
# class Application < Rails::Application
# config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
# end
#
- def sanitized_allowed_tags=(attributes)
- HTML::WhiteListSanitizer.allowed_tags.merge(attributes)
+ def sanitized_allowed_tags=(tags)
+ sanitizer_vendor.white_list_sanitizer.allowed_tags = tags
end
- # Adds to the Set of allowed HTML attributes for the +sanitize+ helper.
+ # Replaces the allowed HTML attributes for the +sanitize+ helper.
#
# class Application < Rails::Application
# config.action_view.sanitized_allowed_attributes = ['onclick', 'longdesc']
# end
#
def sanitized_allowed_attributes=(attributes)
- HTML::WhiteListSanitizer.allowed_attributes.merge(attributes)
- end
-
- # Adds to the Set of allowed CSS properties for the #sanitize and +sanitize_css+ helpers.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_css_properties = 'expression'
- # end
- #
- def sanitized_allowed_css_properties=(attributes)
- HTML::WhiteListSanitizer.allowed_css_properties.merge(attributes)
- end
-
- # Adds to the Set of allowed CSS keywords for the +sanitize+ and +sanitize_css+ helpers.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_css_keywords = 'expression'
- # end
- #
- def sanitized_allowed_css_keywords=(attributes)
- HTML::WhiteListSanitizer.allowed_css_keywords.merge(attributes)
- end
-
- # Adds to the Set of allowed shorthand CSS properties for the +sanitize+ and +sanitize_css+ helpers.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_shorthand_css_properties = 'expression'
- # end
- #
- def sanitized_shorthand_css_properties=(attributes)
- HTML::WhiteListSanitizer.shorthand_css_properties.merge(attributes)
- end
-
- # Adds to the Set of allowed protocols for the +sanitize+ helper.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_protocols = 'ssh', 'feed'
- # end
- #
- def sanitized_allowed_protocols=(attributes)
- HTML::WhiteListSanitizer.allowed_protocols.merge(attributes)
+ sanitizer_vendor.white_list_sanitizer.allowed_attributes = attributes
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/placeholderable.rb b/actionview/lib/action_view/helpers/tags/placeholderable.rb
new file mode 100644
index 0000000000..313aa725c9
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/placeholderable.rb
@@ -0,0 +1,32 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ module Placeholderable # :nodoc:
+ def initialize(*)
+ super
+
+ if tag_value = @options[:placeholder]
+ object_name = @object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1')
+ method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}"
+
+ if object.respond_to?(:to_model)
+ key = object.class.model_name.i18n_key
+ i18n_default = ["#{key}.#{method_and_value}".to_sym, ""]
+ end
+
+ i18n_default ||= ""
+ placeholder = I18n.t("#{object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.placeholder").presence
+
+ placeholder ||= if object && object.class.respond_to?(:human_attribute_name)
+ object.class.human_attribute_name(method_and_value)
+ end
+
+ placeholder ||= @method_name.humanize
+
+ @options[:placeholder] = placeholder
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/text_area.rb b/actionview/lib/action_view/helpers/tags/text_area.rb
index 9ee83ee7c2..69038c1498 100644
--- a/actionview/lib/action_view/helpers/tags/text_area.rb
+++ b/actionview/lib/action_view/helpers/tags/text_area.rb
@@ -1,7 +1,11 @@
+require 'action_view/helpers/tags/placeholderable'
+
module ActionView
module Helpers
module Tags # :nodoc:
class TextArea < Base # :nodoc:
+ include Placeholderable
+
def render
options = @options.stringify_keys
add_default_name_and_id(options)
diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb
index e0b80d81c2..5c576a20ca 100644
--- a/actionview/lib/action_view/helpers/tags/text_field.rb
+++ b/actionview/lib/action_view/helpers/tags/text_field.rb
@@ -1,7 +1,11 @@
+require 'action_view/helpers/tags/placeholderable'
+
module ActionView
module Helpers
module Tags # :nodoc:
class TextField < Base # :nodoc:
+ include Placeholderable
+
def render
options = @options.stringify_keys
options["size"] = options["maxlength"] unless options.key?("size")
diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb
index 17ec6a40bf..1d50ea2ff5 100644
--- a/actionview/lib/action_view/helpers/translation_helper.rb
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -1,4 +1,5 @@
require 'action_view/helpers/tag_helper'
+require 'active_support/core_ext/string/access'
require 'i18n/exceptions'
module ActionView
diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb
index 881a123572..75febb8652 100644
--- a/actionview/lib/action_view/routing_url_for.rb
+++ b/actionview/lib/action_view/routing_url_for.rb
@@ -82,7 +82,9 @@ module ActionView
when nil
super({:only_path => true})
when Hash
- super({ :only_path => options[:host].nil? }.merge!(options.symbolize_keys))
+ options = options.symbolize_keys
+ options[:only_path] = options[:host].nil? unless options.key?(:only_path)
+ super(options)
when :back
_back_url
when Symbol
diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb
index d0da415c5d..7edfc436a6 100644
--- a/actionview/lib/action_view/test_case.rb
+++ b/actionview/lib/action_view/test_case.rb
@@ -3,6 +3,8 @@ require 'action_controller'
require 'action_controller/test_case'
require 'action_view'
+require 'rails-dom-testing'
+
module ActionView
# = Action View Test Case
class TestCase < ActiveSupport::TestCase
@@ -34,6 +36,7 @@ module ActionView
extend ActiveSupport::Concern
include ActionDispatch::Assertions, ActionDispatch::TestProcess
+ include Rails::Dom::Testing::Assertions
include ActionController::TemplateAssertions
include ActionView::Context
@@ -99,7 +102,9 @@ module ActionView
def setup_with_controller
@controller = ActionView::TestCase::TestController.new
@request = @controller.request
- @output_buffer = ActiveSupport::SafeBuffer.new
+ # empty string ensures buffer has UTF-8 encoding as
+ # new without arguments returns ASCII-8BIT encoded buffer like String#new
+ @output_buffer = ActiveSupport::SafeBuffer.new ''
@rendered = ''
make_test_case_available_to_view!
@@ -151,11 +156,10 @@ module ActionView
private
- # Support the selector assertions
- #
# Need to experiment if this priority is the best one: rendered => output_buffer
- def response_from_page
- HTML::Document.new(@rendered.blank? ? @output_buffer : @rendered).root
+ def document_root_element
+ @html_document ||= Nokogiri::HTML::Document.parse(@rendered.blank? ? @output_buffer : @rendered)
+ @html_document.root
end
def say_no_to_protect_against_forgery!
@@ -236,7 +240,8 @@ module ActionView
:@test_passed,
:@view,
:@view_context_class,
- :@_subscribers
+ :@_subscribers,
+ :@html_document
]
def _user_defined_ivars
diff --git a/actionview/lib/action_view/vendor/html-scanner.rb b/actionview/lib/action_view/vendor/html-scanner.rb
deleted file mode 100644
index 775b827529..0000000000
--- a/actionview/lib/action_view/vendor/html-scanner.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/html-scanner"
-
-module HTML
- extend ActiveSupport::Autoload
-
- eager_autoload do
- autoload :CDATA, 'html/node'
- autoload :Document, 'html/document'
- autoload :FullSanitizer, 'html/sanitizer'
- autoload :LinkSanitizer, 'html/sanitizer'
- autoload :Node, 'html/node'
- autoload :Sanitizer, 'html/sanitizer'
- autoload :Selector, 'html/selector'
- autoload :Tag, 'html/node'
- autoload :Text, 'html/node'
- autoload :Tokenizer, 'html/tokenizer'
- autoload :Version, 'html/version'
- autoload :WhiteListSanitizer, 'html/sanitizer'
- end
-end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/document.rb b/actionview/lib/action_view/vendor/html-scanner/html/document.rb
deleted file mode 100644
index 386820300a..0000000000
--- a/actionview/lib/action_view/vendor/html-scanner/html/document.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-require 'html/tokenizer'
-require 'html/node'
-require 'html/selector'
-require 'html/sanitizer'
-
-module HTML #:nodoc:
- # A top-level HTML document. You give it a body of text, and it will parse that
- # text into a tree of nodes.
- class Document #:nodoc:
-
- # The root of the parsed document.
- attr_reader :root
-
- # Create a new Document from the given text.
- def initialize(text, strict=false, xml=false)
- tokenizer = Tokenizer.new(text)
- @root = Node.new(nil)
- node_stack = [ @root ]
- while token = tokenizer.next
- node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token, strict)
-
- node_stack.last.children << node unless node.tag? && node.closing == :close
- if node.tag?
- if node_stack.length > 1 && node.closing == :close
- if node_stack.last.name == node.name
- if node_stack.last.children.empty?
- node_stack.last.children << Text.new(node_stack.last, node.line, node.position, "")
- end
- node_stack.pop
- else
- open_start = node_stack.last.position - 20
- open_start = 0 if open_start < 0
- close_start = node.position - 20
- close_start = 0 if close_start < 0
- msg = <<EOF.strip
-ignoring attempt to close #{node_stack.last.name} with #{node.name}
- opened at byte #{node_stack.last.position}, line #{node_stack.last.line}
- closed at byte #{node.position}, line #{node.line}
- attributes at open: #{node_stack.last.attributes.inspect}
- text around open: #{text[open_start,40].inspect}
- text around close: #{text[close_start,40].inspect}
-EOF
- strict ? raise(msg) : warn(msg)
- end
- elsif !node.childless?(xml) && node.closing != :close
- node_stack.push node
- end
- end
- end
- end
-
- # Search the tree for (and return) the first node that matches the given
- # conditions. The conditions are interpreted differently for different node
- # types, see HTML::Text#find and HTML::Tag#find.
- def find(conditions)
- @root.find(conditions)
- end
-
- # Search the tree for (and return) all nodes that match the given
- # conditions. The conditions are interpreted differently for different node
- # types, see HTML::Text#find and HTML::Tag#find.
- def find_all(conditions)
- @root.find_all(conditions)
- end
-
- end
-
-end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/node.rb b/actionview/lib/action_view/vendor/html-scanner/html/node.rb
deleted file mode 100644
index 27f0f2f6f8..0000000000
--- a/actionview/lib/action_view/vendor/html-scanner/html/node.rb
+++ /dev/null
@@ -1,532 +0,0 @@
-require 'strscan'
-
-module HTML #:nodoc:
-
- class Conditions < Hash #:nodoc:
- def initialize(hash)
- super()
- hash = { :content => hash } unless Hash === hash
- hash = keys_to_symbols(hash)
- hash.each do |k,v|
- case k
- when :tag, :content then
- # keys are valid, and require no further processing
- when :attributes then
- hash[k] = keys_to_strings(v)
- when :parent, :child, :ancestor, :descendant, :sibling, :before,
- :after
- hash[k] = Conditions.new(v)
- when :children
- hash[k] = v = keys_to_symbols(v)
- v.each do |key,value|
- case key
- when :count, :greater_than, :less_than
- # keys are valid, and require no further processing
- when :only
- v[key] = Conditions.new(value)
- else
- raise "illegal key #{key.inspect} => #{value.inspect}"
- end
- end
- else
- raise "illegal key #{k.inspect} => #{v.inspect}"
- end
- end
- update hash
- end
-
- private
-
- def keys_to_strings(hash)
- Hash[hash.keys.map {|k| [k.to_s, hash[k]]}]
- end
-
- def keys_to_symbols(hash)
- Hash[hash.keys.map do |k|
- raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym)
- [k.to_sym, hash[k]]
- end]
- end
- end
-
- # The base class of all nodes, textual and otherwise, in an HTML document.
- class Node #:nodoc:
- # The array of children of this node. Not all nodes have children.
- attr_reader :children
-
- # The parent node of this node. All nodes have a parent, except for the
- # root node.
- attr_reader :parent
-
- # The line number of the input where this node was begun
- attr_reader :line
-
- # The byte position in the input where this node was begun
- attr_reader :position
-
- # Create a new node as a child of the given parent.
- def initialize(parent, line=0, pos=0)
- @parent = parent
- @children = []
- @line, @position = line, pos
- end
-
- # Returns a textual representation of the node.
- def to_s
- @children.join()
- end
-
- # Returns false (subclasses must override this to provide specific matching
- # behavior.) +conditions+ may be of any type.
- def match(conditions)
- false
- end
-
- # Search the children of this node for the first node for which #find
- # returns non +nil+. Returns the result of the #find call that succeeded.
- def find(conditions)
- conditions = validate_conditions(conditions)
- @children.each do |child|
- node = child.find(conditions)
- return node if node
- end
- nil
- end
-
- # Search for all nodes that match the given conditions, and return them
- # as an array.
- def find_all(conditions)
- conditions = validate_conditions(conditions)
-
- matches = []
- matches << self if match(conditions)
- @children.each do |child|
- matches.concat child.find_all(conditions)
- end
- matches
- end
-
- # Returns +false+. Subclasses may override this if they define a kind of
- # tag.
- def tag?
- false
- end
-
- def validate_conditions(conditions)
- Conditions === conditions ? conditions : Conditions.new(conditions)
- end
-
- def ==(node)
- return false unless self.class == node.class && children.size == node.children.size
-
- equivalent = true
-
- children.size.times do |i|
- equivalent &&= children[i] == node.children[i]
- end
-
- equivalent
- end
-
- class <<self
- def parse(parent, line, pos, content, strict=true)
- if content !~ /^<\S/
- Text.new(parent, line, pos, content)
- else
- scanner = StringScanner.new(content)
-
- unless scanner.skip(/</)
- if strict
- raise "expected <"
- else
- return Text.new(parent, line, pos, content)
- end
- end
-
- if scanner.skip(/!\[CDATA\[/)
- unless scanner.skip_until(/\]\]>/)
- if strict
- raise "expected ]]> (got #{scanner.rest.inspect} for #{content})"
- else
- scanner.skip_until(/\Z/)
- end
- end
-
- return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, ''))
- end
-
- closing = ( scanner.scan(/\//) ? :close : nil )
- return Text.new(parent, line, pos, content) unless name = scanner.scan(/[^\s!>\/]+/)
- name.downcase!
-
- unless closing
- scanner.skip(/\s*/)
- attributes = {}
- while attr = scanner.scan(/[-\w:]+/)
- value = true
- if scanner.scan(/\s*=\s*/)
- if delim = scanner.scan(/['"]/)
- value = ""
- while text = scanner.scan(/[^#{delim}\\]+|./)
- case text
- when "\\" then
- value << text
- break if scanner.eos?
- value << scanner.getch
- when delim
- break
- else value << text
- end
- end
- else
- value = scanner.scan(/[^\s>\/]+/)
- end
- end
- attributes[attr.downcase] = value
- scanner.skip(/\s*/)
- end
-
- closing = ( scanner.scan(/\//) ? :self : nil )
- end
-
- unless scanner.scan(/\s*>/)
- if strict
- raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
- else
- # throw away all text until we find what we're looking for
- scanner.skip_until(/>/) or scanner.terminate
- end
- end
-
- Tag.new(parent, line, pos, name, attributes, closing)
- end
- end
- end
- end
-
- # A node that represents text, rather than markup.
- class Text < Node #:nodoc:
-
- attr_reader :content
-
- # Creates a new text node as a child of the given parent, with the given
- # content.
- def initialize(parent, line, pos, content)
- super(parent, line, pos)
- @content = content
- end
-
- # Returns the content of this node.
- def to_s
- @content
- end
-
- # Returns +self+ if this node meets the given conditions. Text nodes support
- # conditions of the following kinds:
- #
- # * if +conditions+ is a string, it must be a substring of the node's
- # content
- # * if +conditions+ is a regular expression, it must match the node's
- # content
- # * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that
- # is either a string or a regexp, and which is interpreted as described
- # above.
- def find(conditions)
- match(conditions) && self
- end
-
- # Returns non-+nil+ if this node meets the given conditions, or +nil+
- # otherwise. See the discussion of #find for the valid conditions.
- def match(conditions)
- case conditions
- when String
- @content == conditions
- when Regexp
- @content =~ conditions
- when Hash
- conditions = validate_conditions(conditions)
-
- # Text nodes only have :content, :parent, :ancestor
- unless (conditions.keys - [:content, :parent, :ancestor]).empty?
- return false
- end
-
- match(conditions[:content])
- else
- nil
- end
- end
-
- def ==(node)
- return false unless super
- content == node.content
- end
- end
-
- # A CDATA node is simply a text node with a specialized way of displaying
- # itself.
- class CDATA < Text #:nodoc:
- def to_s
- "<![CDATA[#{super}]]>"
- end
- end
-
- # A Tag is any node that represents markup. It may be an opening tag, a
- # closing tag, or a self-closing tag. It has a name, and may have a hash of
- # attributes.
- class Tag < Node #:nodoc:
-
- # Either +nil+, <tt>:close</tt>, or <tt>:self</tt>
- attr_reader :closing
-
- # Either +nil+, or a hash of attributes for this node.
- attr_reader :attributes
-
- # The name of this tag.
- attr_reader :name
-
- # Create a new node as a child of the given parent, using the given content
- # to describe the node. It will be parsed and the node name, attributes and
- # closing status extracted.
- def initialize(parent, line, pos, name, attributes, closing)
- super(parent, line, pos)
- @name = name
- @attributes = attributes
- @closing = closing
- end
-
- # A convenience for obtaining an attribute of the node. Returns +nil+ if
- # the node has no attributes.
- def [](attr)
- @attributes ? @attributes[attr] : nil
- end
-
- # Returns non-+nil+ if this tag can contain child nodes.
- def childless?(xml = false)
- return false if xml && @closing.nil?
- !@closing.nil? ||
- @name =~ /^(img|br|hr|link|meta|area|base|basefont|
- col|frame|input|isindex|param)$/ox
- end
-
- # Returns a textual representation of the node
- def to_s
- if @closing == :close
- "</#{@name}>"
- else
- s = "<#{@name}"
- @attributes.each do |k,v|
- s << " #{k}"
- s << "=\"#{v}\"" if String === v
- end
- s << " /" if @closing == :self
- s << ">"
- @children.each { |child| s << child.to_s }
- s << "</#{@name}>" if @closing != :self && !@children.empty?
- s
- end
- end
-
- # If either the node or any of its children meet the given conditions, the
- # matching node is returned. Otherwise, +nil+ is returned. (See the
- # description of the valid conditions in the +match+ method.)
- def find(conditions)
- match(conditions) && self || super
- end
-
- # Returns +true+, indicating that this node represents an HTML tag.
- def tag?
- true
- end
-
- # Returns +true+ if the node meets any of the given conditions. The
- # +conditions+ parameter must be a hash of any of the following keys
- # (all are optional):
- #
- # * <tt>:tag</tt>: the node name must match the corresponding value
- # * <tt>:attributes</tt>: a hash. The node's values must match the
- # corresponding values in the hash.
- # * <tt>:parent</tt>: a hash. The node's parent must match the
- # corresponding hash.
- # * <tt>:child</tt>: a hash. At least one of the node's immediate children
- # must meet the criteria described by the hash.
- # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
- # meet the criteria described by the hash.
- # * <tt>:descendant</tt>: a hash. At least one of the node's descendants
- # must meet the criteria described by the hash.
- # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
- # meet the criteria described by the hash.
- # * <tt>:after</tt>: a hash. The node must be after any sibling meeting
- # the criteria described by the hash, and at least one sibling must match.
- # * <tt>:before</tt>: a hash. The node must be before any sibling meeting
- # the criteria described by the hash, and at least one sibling must match.
- # * <tt>:children</tt>: a hash, for counting children of a node. Accepts the
- # keys:
- # ** <tt>:count</tt>: either a number or a range which must equal (or
- # include) the number of children that match.
- # ** <tt>:less_than</tt>: the number of matching children must be less than
- # this number.
- # ** <tt>:greater_than</tt>: the number of matching children must be
- # greater than this number.
- # ** <tt>:only</tt>: another hash consisting of the keys to use
- # to match on the children, and only matching children will be
- # counted.
- #
- # Conditions are matched using the following algorithm:
- #
- # * if the condition is a string, it must be a substring of the value.
- # * if the condition is a regexp, it must match the value.
- # * if the condition is a number, the value must match number.to_s.
- # * if the condition is +true+, the value must not be +nil+.
- # * if the condition is +false+ or +nil+, the value must be +nil+.
- #
- # Usage:
- #
- # # test if the node is a "span" tag
- # node.match tag: "span"
- #
- # # test if the node's parent is a "div"
- # node.match parent: { tag: "div" }
- #
- # # test if any of the node's ancestors are "table" tags
- # node.match ancestor: { tag: "table" }
- #
- # # test if any of the node's immediate children are "em" tags
- # node.match child: { tag: "em" }
- #
- # # test if any of the node's descendants are "strong" tags
- # node.match descendant: { tag: "strong" }
- #
- # # test if the node has between 2 and 4 span tags as immediate children
- # node.match children: { count: 2..4, only: { tag: "span" } }
- #
- # # get funky: test to see if the node is a "div", has a "ul" ancestor
- # # and an "li" parent (with "class" = "enum"), and whether or not it has
- # # a "span" descendant that contains # text matching /hello world/:
- # node.match tag: "div",
- # ancestor: { tag: "ul" },
- # parent: { tag: "li",
- # attributes: { class: "enum" } },
- # descendant: { tag: "span",
- # child: /hello world/ }
- def match(conditions)
- conditions = validate_conditions(conditions)
- # check content of child nodes
- if conditions[:content]
- if children.empty?
- return false unless match_condition("", conditions[:content])
- else
- return false unless children.find { |child| child.match(conditions[:content]) }
- end
- end
-
- # test the name
- return false unless match_condition(@name, conditions[:tag]) if conditions[:tag]
-
- # test attributes
- (conditions[:attributes] || {}).each do |key, value|
- return false unless match_condition(self[key], value)
- end
-
- # test parent
- return false unless parent.match(conditions[:parent]) if conditions[:parent]
-
- # test children
- return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child]
-
- # test ancestors
- if conditions[:ancestor]
- return false unless catch :found do
- p = self
- throw :found, true if p.match(conditions[:ancestor]) while p = p.parent
- end
- end
-
- # test descendants
- if conditions[:descendant]
- return false unless children.find do |child|
- # test the child
- child.match(conditions[:descendant]) ||
- # test the child's descendants
- child.match(:descendant => conditions[:descendant])
- end
- end
-
- # count children
- if opts = conditions[:children]
- matches = children.select do |c|
- (c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?))
- end
-
- matches = matches.select { |c| c.match(opts[:only]) } if opts[:only]
- opts.each do |key, value|
- next if key == :only
- case key
- when :count
- if Integer === value
- return false if matches.length != value
- else
- return false unless value.include?(matches.length)
- end
- when :less_than
- return false unless matches.length < value
- when :greater_than
- return false unless matches.length > value
- else raise "unknown count condition #{key}"
- end
- end
- end
-
- # test siblings
- if conditions[:sibling] || conditions[:before] || conditions[:after]
- siblings = parent ? parent.children : []
- self_index = siblings.index(self)
-
- if conditions[:sibling]
- return false unless siblings.detect do |s|
- s != self && s.match(conditions[:sibling])
- end
- end
-
- if conditions[:before]
- return false unless siblings[self_index+1..-1].detect do |s|
- s != self && s.match(conditions[:before])
- end
- end
-
- if conditions[:after]
- return false unless siblings[0,self_index].detect do |s|
- s != self && s.match(conditions[:after])
- end
- end
- end
-
- true
- end
-
- def ==(node)
- return false unless super
- return false unless closing == node.closing && self.name == node.name
- attributes == node.attributes
- end
-
- private
- # Match the given value to the given condition.
- def match_condition(value, condition)
- case condition
- when String
- value && value == condition
- when Regexp
- value && value.match(condition)
- when Numeric
- value == condition.to_s
- when true
- !value.nil?
- when false, nil
- value.nil?
- else
- false
- end
- end
- end
-end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb
deleted file mode 100644
index ed34eecf55..0000000000
--- a/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb
+++ /dev/null
@@ -1,188 +0,0 @@
-require 'set'
-require 'cgi'
-require 'active_support/core_ext/module/attribute_accessors'
-
-module HTML
- class Sanitizer
- def sanitize(text, options = {})
- validate_options(options)
- return text unless sanitizeable?(text)
- tokenize(text, options).join
- end
-
- def sanitizeable?(text)
- !(text.nil? || text.empty? || !text.index("<"))
- end
-
- protected
- def tokenize(text, options)
- tokenizer = HTML::Tokenizer.new(text)
- result = []
- while token = tokenizer.next
- node = Node.parse(nil, 0, 0, token, false)
- process_node node, result, options
- end
- result
- end
-
- def process_node(node, result, options)
- result << node.to_s
- end
-
- def validate_options(options)
- if options[:tags] && !options[:tags].is_a?(Enumerable)
- raise ArgumentError, "You should pass :tags as an Enumerable"
- end
-
- if options[:attributes] && !options[:attributes].is_a?(Enumerable)
- raise ArgumentError, "You should pass :attributes as an Enumerable"
- end
- end
- end
-
- class FullSanitizer < Sanitizer
- def sanitize(text, options = {})
- result = super
- # strip any comments, and if they have a newline at the end (ie. line with
- # only a comment) strip that too
- result = result.gsub(/<!--(.*?)-->[\n]?/m, "") if (result && result =~ /<!--(.*?)-->[\n]?/m)
- # Recurse - handle all dirty nested tags
- result == text ? result : sanitize(result, options)
- end
-
- def process_node(node, result, options)
- result << node.to_s if node.class == HTML::Text
- end
- end
-
- class LinkSanitizer < FullSanitizer
- cattr_accessor :included_tags, :instance_writer => false
- self.included_tags = Set.new(%w(a href))
-
- def sanitizeable?(text)
- !(text.nil? || text.empty? || !((text.index("<a") || text.index("<href")) && text.index(">")))
- end
-
- protected
- def process_node(node, result, options)
- result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name)
- end
- end
-
- class WhiteListSanitizer < Sanitizer
- [:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags,
- :allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr|
- class_attribute attr, :instance_writer => false
- end
-
- # A regular expression of the valid characters used to separate protocols like
- # the ':' in 'http://foo.com'
- self.protocol_separator = /:|(&#0*58)|(&#x70)|(&#x0*3a)|(%|&#37;)3A/i
-
- # Specifies a Set of HTML attributes that can have URIs.
- self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc))
-
- # Specifies a Set of 'bad' tags that the #sanitize helper will remove completely, as opposed
- # to just escaping harmless tags like &lt;font&gt;
- self.bad_tags = Set.new(%w(script))
-
- # Specifies the default Set of tags that the #sanitize helper will allow unscathed.
- self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub
- sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr
- acronym a img blockquote del ins))
-
- # Specifies the default Set of html attributes that the #sanitize helper will leave
- # in the allowed tag.
- self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr))
-
- # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept.
- self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto
- feed svn urn aim rsync tag ssh sftp rtsp afs))
-
- # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept.
- self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse
- border-color border-left-color border-right-color border-top-color clear color cursor direction display
- elevation float font font-family font-size font-style font-variant font-weight height letter-spacing line-height
- overflow pause pause-after pause-before pitch pitch-range richness speak speak-header speak-numeral speak-punctuation
- speech-rate stress text-align text-decoration text-indent unicode-bidi vertical-align voice-family volume white-space
- width))
-
- # Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept.
- self.allowed_css_keywords = Set.new(%w(auto aqua black block blue bold both bottom brown center
- collapse dashed dotted fuchsia gray green !important italic left lime maroon medium none navy normal
- nowrap olive pointer purple red right solid silver teal top transparent underline white yellow))
-
- # Specifies the default Set of allowed shorthand css properties for the #sanitize and #sanitize_css helpers.
- self.shorthand_css_properties = Set.new(%w(background border margin padding))
-
- # Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute
- def sanitize_css(style)
- # disallow urls
- style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ')
-
- # gauntlet
- if style !~ /\A([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*\z/ ||
- style !~ /\A(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*\z/
- return ''
- end
-
- clean = []
- style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val|
- if allowed_css_properties.include?(prop.downcase)
- clean << prop + ': ' + val + ';'
- elsif shorthand_css_properties.include?(prop.split('-')[0].downcase)
- unless val.split().any? do |keyword|
- !allowed_css_keywords.include?(keyword) &&
- keyword !~ /\A(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)\z/
- end
- clean << prop + ': ' + val + ';'
- end
- end
- end
- clean.join(' ')
- end
-
- protected
- def tokenize(text, options)
- options[:parent] = []
- options[:attributes] ||= allowed_attributes
- options[:tags] ||= allowed_tags
- super
- end
-
- def process_node(node, result, options)
- result << case node
- when HTML::Tag
- if node.closing == :close
- options[:parent].shift
- else
- options[:parent].unshift node.name
- end
-
- process_attributes_for node, options
-
- options[:tags].include?(node.name) ? node : nil
- else
- bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "&lt;")
- end
- end
-
- def process_attributes_for(node, options)
- return unless node.attributes
- node.attributes.keys.each do |attr_name|
- value = node.attributes[attr_name].to_s
-
- if !options[:attributes].include?(attr_name) || contains_bad_protocols?(attr_name, value)
- node.attributes.delete(attr_name)
- else
- node.attributes[attr_name] = attr_name == 'style' ? sanitize_css(value) : CGI::escapeHTML(CGI::unescapeHTML(value))
- end
- end
- end
-
- def contains_bad_protocols?(attr_name, value)
- uri_attributes.include?(attr_name) &&
- (value =~ /(^[^\/:]*):|(&#0*58)|(&#x70)|(&#x0*3a)|(%|&#37;)3A/i && !allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip))
- end
- end
-end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb
deleted file mode 100644
index dfdd724b9b..0000000000
--- a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb
+++ /dev/null
@@ -1,830 +0,0 @@
-#--
-# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
-# Under MIT and/or CC By license.
-#++
-
-module HTML
-
- # Selects HTML elements using CSS 2 selectors.
- #
- # The +Selector+ class uses CSS selector expressions to match and select
- # HTML elements.
- #
- # For example:
- # selector = HTML::Selector.new "form.login[action=/login]"
- # creates a new selector that matches any +form+ element with the class
- # +login+ and an attribute +action+ with the value <tt>/login</tt>.
- #
- # === Matching Elements
- #
- # Use the #match method to determine if an element matches the selector.
- #
- # For simple selectors, the method returns an array with that element,
- # or +nil+ if the element does not match. For complex selectors (see below)
- # the method returns an array with all matched elements, of +nil+ if no
- # match found.
- #
- # For example:
- # if selector.match(element)
- # puts "Element is a login form"
- # end
- #
- # === Selecting Elements
- #
- # Use the #select method to select all matching elements starting with
- # one element and going through all children in depth-first order.
- #
- # This method returns an array of all matching elements, an empty array
- # if no match is found
- #
- # For example:
- # selector = HTML::Selector.new "input[type=text]"
- # matches = selector.select(element)
- # matches.each do |match|
- # puts "Found text field with name #{match.attributes['name']}"
- # end
- #
- # === Expressions
- #
- # Selectors can match elements using any of the following criteria:
- # * <tt>name</tt> -- Match an element based on its name (tag name).
- # For example, <tt>p</tt> to match a paragraph. You can use <tt>*</tt>
- # to match any element.
- # * <tt>#</tt><tt>id</tt> -- Match an element based on its identifier (the
- # <tt>id</tt> attribute). For example, <tt>#</tt><tt>page</tt>.
- # * <tt>.class</tt> -- Match an element based on its class name, all
- # class names if more than one specified.
- # * <tt>[attr]</tt> -- Match an element that has the specified attribute.
- # * <tt>[attr=value]</tt> -- Match an element that has the specified
- # attribute and value. (More operators are supported see below)
- # * <tt>:pseudo-class</tt> -- Match an element based on a pseudo class,
- # such as <tt>:nth-child</tt> and <tt>:empty</tt>.
- # * <tt>:not(expr)</tt> -- Match an element that does not match the
- # negation expression.
- #
- # When using a combination of the above, the element name comes first
- # followed by identifier, class names, attributes, pseudo classes and
- # negation in any order. Do not separate these parts with spaces!
- # Space separation is used for descendant selectors.
- #
- # For example:
- # selector = HTML::Selector.new "form.login[action=/login]"
- # The matched element must be of type +form+ and have the class +login+.
- # It may have other classes, but the class +login+ is required to match.
- # It must also have an attribute called +action+ with the value
- # <tt>/login</tt>.
- #
- # This selector will match the following element:
- # <form class="login form" method="post" action="/login">
- # but will not match the element:
- # <form method="post" action="/logout">
- #
- # === Attribute Values
- #
- # Several operators are supported for matching attributes:
- # * <tt>name</tt> -- The element must have an attribute with that name.
- # * <tt>name=value</tt> -- The element must have an attribute with that
- # name and value.
- # * <tt>name^=value</tt> -- The attribute value must start with the
- # specified value.
- # * <tt>name$=value</tt> -- The attribute value must end with the
- # specified value.
- # * <tt>name*=value</tt> -- The attribute value must contain the
- # specified value.
- # * <tt>name~=word</tt> -- The attribute value must contain the specified
- # word (space separated).
- # * <tt>name|=word</tt> -- The attribute value must start with specified
- # word.
- #
- # For example, the following two selectors match the same element:
- # #my_id
- # [id=my_id]
- # and so do the following two selectors:
- # .my_class
- # [class~=my_class]
- #
- # === Alternatives, siblings, children
- #
- # Complex selectors use a combination of expressions to match elements:
- # * <tt>expr1 expr2</tt> -- Match any element against the second expression
- # if it has some parent element that matches the first expression.
- # * <tt>expr1 > expr2</tt> -- Match any element against the second expression
- # if it is the child of an element that matches the first expression.
- # * <tt>expr1 + expr2</tt> -- Match any element against the second expression
- # if it immediately follows an element that matches the first expression.
- # * <tt>expr1 ~ expr2</tt> -- Match any element against the second expression
- # that comes after an element that matches the first expression.
- # * <tt>expr1, expr2</tt> -- Match any element against the first expression,
- # or against the second expression.
- #
- # Since children and sibling selectors may match more than one element given
- # the first element, the #match method may return more than one match.
- #
- # === Pseudo classes
- #
- # Pseudo classes were introduced in CSS 3. They are most often used to select
- # elements in a given position:
- # * <tt>:root</tt> -- Match the element only if it is the root element
- # (no parent element).
- # * <tt>:empty</tt> -- Match the element only if it has no child elements,
- # and no text content.
- # * <tt>:content(string)</tt> -- Match the element only if it has <tt>string</tt>
- # as its text content (ignoring leading and trailing whitespace).
- # * <tt>:only-child</tt> -- Match the element if it is the only child (element)
- # of its parent element.
- # * <tt>:only-of-type</tt> -- Match the element if it is the only child (element)
- # of its parent element and its type.
- # * <tt>:first-child</tt> -- Match the element if it is the first child (element)
- # of its parent element.
- # * <tt>:first-of-type</tt> -- Match the element if it is the first child (element)
- # of its parent element of its type.
- # * <tt>:last-child</tt> -- Match the element if it is the last child (element)
- # of its parent element.
- # * <tt>:last-of-type</tt> -- Match the element if it is the last child (element)
- # of its parent element of its type.
- # * <tt>:nth-child(b)</tt> -- Match the element if it is the b-th child (element)
- # of its parent element. The value <tt>b</tt> specifies its index, starting with 1.
- # * <tt>:nth-child(an+b)</tt> -- Match the element if it is the b-th child (element)
- # in each group of <tt>a</tt> child elements of its parent element.
- # * <tt>:nth-child(-an+b)</tt> -- Match the element if it is the first child (element)
- # in each group of <tt>a</tt> child elements, up to the first <tt>b</tt> child
- # elements of its parent element.
- # * <tt>:nth-child(odd)</tt> -- Match element in the odd position (i.e. first, third).
- # Same as <tt>:nth-child(2n+1)</tt>.
- # * <tt>:nth-child(even)</tt> -- Match element in the even position (i.e. second,
- # fourth). Same as <tt>:nth-child(2n+2)</tt>.
- # * <tt>:nth-of-type(..)</tt> -- As above, but only counts elements of its type.
- # * <tt>:nth-last-child(..)</tt> -- As above, but counts from the last child.
- # * <tt>:nth-last-of-type(..)</tt> -- As above, but counts from the last child and
- # only elements of its type.
- # * <tt>:not(selector)</tt> -- Match the element only if the element does not
- # match the simple selector.
- #
- # As you can see, <tt>:nth-child</tt> pseudo class and its variant can get quite
- # tricky and the CSS specification doesn't do a much better job explaining it.
- # But after reading the examples and trying a few combinations, it's easy to
- # figure out.
- #
- # For example:
- # table tr:nth-child(odd)
- # Selects every second row in the table starting with the first one.
- #
- # div p:nth-child(4)
- # Selects the fourth paragraph in the +div+, but not if the +div+ contains
- # other elements, since those are also counted.
- #
- # div p:nth-of-type(4)
- # Selects the fourth paragraph in the +div+, counting only paragraphs, and
- # ignoring all other elements.
- #
- # div p:nth-of-type(-n+4)
- # Selects the first four paragraphs, ignoring all others.
- #
- # And you can always select an element that matches one set of rules but
- # not another using <tt>:not</tt>. For example:
- # p:not(.post)
- # Matches all paragraphs that do not have the class <tt>.post</tt>.
- #
- # === Substitution Values
- #
- # You can use substitution with identifiers, class names and element values.
- # A substitution takes the form of a question mark (<tt>?</tt>) and uses the
- # next value in the argument list following the CSS expression.
- #
- # The substitution value may be a string or a regular expression. All other
- # values are converted to strings.
- #
- # For example:
- # selector = HTML::Selector.new "#?", /^\d+$/
- # matches any element whose identifier consists of one or more digits.
- #
- # See http://www.w3.org/TR/css3-selectors/
- class Selector
-
-
- # An invalid selector.
- class InvalidSelectorError < StandardError #:nodoc:
- end
-
-
- class << self
-
- # :call-seq:
- # Selector.for_class(cls) => selector
- #
- # Creates a new selector for the given class name.
- def for_class(cls)
- self.new([".?", cls])
- end
-
-
- # :call-seq:
- # Selector.for_id(id) => selector
- #
- # Creates a new selector for the given id.
- def for_id(id)
- self.new(["#?", id])
- end
-
- end
-
-
- # :call-seq:
- # Selector.new(string, [values ...]) => selector
- #
- # Creates a new selector from a CSS 2 selector expression.
- #
- # The first argument is the selector expression. All other arguments
- # are used for value substitution.
- #
- # Throws InvalidSelectorError is the selector expression is invalid.
- def initialize(selector, *values)
- raise ArgumentError, "CSS expression cannot be empty" if selector.empty?
- @source = ""
- values = values[0] if values.size == 1 && values[0].is_a?(Array)
-
- # We need a copy to determine if we failed to parse, and also
- # preserve the original pass by-ref statement.
- statement = selector.strip.dup
-
- # Create a simple selector, along with negation.
- simple_selector(statement, values).each { |name, value| instance_variable_set("@#{name}", value) }
-
- @alternates = []
- @depends = nil
-
- # Alternative selector.
- if statement.sub!(/^\s*,\s*/, "")
- second = Selector.new(statement, values)
- @alternates << second
- # If there are alternate selectors, we group them in the top selector.
- if alternates = second.instance_variable_get(:@alternates)
- second.instance_variable_set(:@alternates, [])
- @alternates.concat alternates
- end
- @source << " , " << second.to_s
- # Sibling selector: create a dependency into second selector that will
- # match element immediately following this one.
- elsif statement.sub!(/^\s*\+\s*/, "")
- second = next_selector(statement, values)
- @depends = lambda do |element, first|
- if element = next_element(element)
- second.match(element, first)
- end
- end
- @source << " + " << second.to_s
- # Adjacent selector: create a dependency into second selector that will
- # match all elements following this one.
- elsif statement.sub!(/^\s*~\s*/, "")
- second = next_selector(statement, values)
- @depends = lambda do |element, first|
- matches = []
- while element = next_element(element)
- if subset = second.match(element, first)
- if first && !subset.empty?
- matches << subset.first
- break
- else
- matches.concat subset
- end
- end
- end
- matches.empty? ? nil : matches
- end
- @source << " ~ " << second.to_s
- # Child selector: create a dependency into second selector that will
- # match a child element of this one.
- elsif statement.sub!(/^\s*>\s*/, "")
- second = next_selector(statement, values)
- @depends = lambda do |element, first|
- matches = []
- element.children.each do |child|
- if child.tag? && subset = second.match(child, first)
- if first && !subset.empty?
- matches << subset.first
- break
- else
- matches.concat subset
- end
- end
- end
- matches.empty? ? nil : matches
- end
- @source << " > " << second.to_s
- # Descendant selector: create a dependency into second selector that
- # will match all descendant elements of this one. Note,
- elsif statement =~ /^\s+\S+/ && statement != selector
- second = next_selector(statement, values)
- @depends = lambda do |element, first|
- matches = []
- stack = element.children.reverse
- while node = stack.pop
- next unless node.tag?
- if subset = second.match(node, first)
- if first && !subset.empty?
- matches << subset.first
- break
- else
- matches.concat subset
- end
- elsif children = node.children
- stack.concat children.reverse
- end
- end
- matches.empty? ? nil : matches
- end
- @source << " " << second.to_s
- else
- # The last selector is where we check that we parsed
- # all the parts.
- unless statement.empty? || statement.strip.empty?
- raise ArgumentError, "Invalid selector: #{statement}"
- end
- end
- end
-
-
- # :call-seq:
- # match(element, first?) => array or nil
- #
- # Matches an element against the selector.
- #
- # For a simple selector this method returns an array with the
- # element if the element matches, nil otherwise.
- #
- # For a complex selector (sibling and descendant) this method
- # returns an array with all matching elements, nil if no match is
- # found.
- #
- # Use +first_only=true+ if you are only interested in the first element.
- #
- # For example:
- # if selector.match(element)
- # puts "Element is a login form"
- # end
- def match(element, first_only = false)
- # Match element if no element name or element name same as element name
- if matched = (!@tag_name || @tag_name == element.name)
- # No match if one of the attribute matches failed
- for attr in @attributes
- if element.attributes[attr[0]] !~ attr[1]
- matched = false
- break
- end
- end
- end
-
- # Pseudo class matches (nth-child, empty, etc).
- if matched
- for pseudo in @pseudo
- unless pseudo.call(element)
- matched = false
- break
- end
- end
- end
-
- # Negation. Same rules as above, but we fail if a match is made.
- if matched && @negation
- for negation in @negation
- if negation[:tag_name] == element.name
- matched = false
- else
- for attr in negation[:attributes]
- if element.attributes[attr[0]] =~ attr[1]
- matched = false
- break
- end
- end
- end
- if matched
- for pseudo in negation[:pseudo]
- if pseudo.call(element)
- matched = false
- break
- end
- end
- end
- break unless matched
- end
- end
-
- # If element matched but depends on another element (child,
- # sibling, etc), apply the dependent matches instead.
- if matched && @depends
- matches = @depends.call(element, first_only)
- else
- matches = matched ? [element] : nil
- end
-
- # If this selector is part of the group, try all the alternative
- # selectors (unless first_only).
- if !first_only || !matches
- @alternates.each do |alternate|
- break if matches && first_only
- if subset = alternate.match(element, first_only)
- if matches
- matches.concat subset
- else
- matches = subset
- end
- end
- end
- end
-
- matches
- end
-
-
- # :call-seq:
- # select(root) => array
- #
- # Selects and returns an array with all matching elements, beginning
- # with one node and traversing through all children depth-first.
- # Returns an empty array if no match is found.
- #
- # The root node may be any element in the document, or the document
- # itself.
- #
- # For example:
- # selector = HTML::Selector.new "input[type=text]"
- # matches = selector.select(element)
- # matches.each do |match|
- # puts "Found text field with name #{match.attributes['name']}"
- # end
- def select(root)
- matches = []
- stack = [root]
- while node = stack.pop
- if node.tag? && subset = match(node, false)
- subset.each do |match|
- matches << match unless matches.any? { |item| item.equal?(match) }
- end
- elsif children = node.children
- stack.concat children.reverse
- end
- end
- matches
- end
-
-
- # Similar to #select but returns the first matching element. Returns +nil+
- # if no element matches the selector.
- def select_first(root)
- stack = [root]
- while node = stack.pop
- if node.tag? && subset = match(node, true)
- return subset.first if !subset.empty?
- elsif children = node.children
- stack.concat children.reverse
- end
- end
- nil
- end
-
-
- def to_s #:nodoc:
- @source
- end
-
-
- # Returns the next element after this one. Skips sibling text nodes.
- #
- # With the +name+ argument, returns the next element with that name,
- # skipping other sibling elements.
- def next_element(element, name = nil)
- if siblings = element.parent.children
- found = false
- siblings.each do |node|
- if node.equal?(element)
- found = true
- elsif found && node.tag?
- return node if (name.nil? || node.name == name)
- end
- end
- end
- nil
- end
-
-
- protected
-
-
- # Creates a simple selector given the statement and array of
- # substitution values.
- #
- # Returns a hash with the values +tag_name+, +attributes+,
- # +pseudo+ (classes) and +negation+.
- #
- # Called the first time with +can_negate+ true to allow
- # negation. Called a second time with false since negation
- # cannot be negated.
- def simple_selector(statement, values, can_negate = true)
- tag_name = nil
- attributes = []
- pseudo = []
- negation = []
-
- # Element name. (Note that in negation, this can come at
- # any order, but for simplicity we allow if only first).
- statement.sub!(/^(\*|[[:alpha:]][\w\-]*)/) do |match|
- match.strip!
- tag_name = match.downcase unless match == "*"
- @source << match
- "" # Remove
- end
-
- # Get identifier, class, attribute name, pseudo or negation.
- while true
- # Element identifier.
- next if statement.sub!(/^#(\?|[\w\-]+)/) do
- id = $1
- if id == "?"
- id = values.shift
- end
- @source << "##{id}"
- id = Regexp.new("^#{Regexp.escape(id.to_s)}$") unless id.is_a?(Regexp)
- attributes << ["id", id]
- "" # Remove
- end
-
- # Class name.
- next if statement.sub!(/^\.([\w\-]+)/) do
- class_name = $1
- @source << ".#{class_name}"
- class_name = Regexp.new("(^|\s)#{Regexp.escape(class_name)}($|\s)") unless class_name.is_a?(Regexp)
- attributes << ["class", class_name]
- "" # Remove
- end
-
- # Attribute value.
- next if statement.sub!(/^\[\s*([[:alpha:]][\w\-:]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do
- name, equality, value = $1, $2, $3
- if value == "?"
- value = values.shift
- else
- # Handle single and double quotes.
- value.strip!
- if (value[0] == ?" || value[0] == ?') && value[0] == value[-1]
- value = value[1..-2]
- end
- end
- @source << "[#{name}#{equality}'#{value}']"
- attributes << [name.downcase.strip, attribute_match(equality, value)]
- "" # Remove
- end
-
- # Root element only.
- next if statement.sub!(/^:root/) do
- pseudo << lambda do |element|
- element.parent.nil? || !element.parent.tag?
- end
- @source << ":root"
- "" # Remove
- end
-
- # Nth-child including last and of-type.
- next if statement.sub!(/^:nth-(last-)?(child|of-type)\((odd|even|(\d+|\?)|(-?\d*|\?)?n([+\-]\d+|\?)?)\)/) do |match|
- reverse = $1 == "last-"
- of_type = $2 == "of-type"
- @source << ":nth-#{$1}#{$2}("
- case $3
- when "odd"
- pseudo << nth_child(2, 1, of_type, reverse)
- @source << "odd)"
- when "even"
- pseudo << nth_child(2, 2, of_type, reverse)
- @source << "even)"
- when /^(\d+|\?)$/ # b only
- b = ($1 == "?" ? values.shift : $1).to_i
- pseudo << nth_child(0, b, of_type, reverse)
- @source << "#{b})"
- when /^(-?\d*|\?)?n([+\-]\d+|\?)?$/
- a = ($1 == "?" ? values.shift :
- $1 == "" ? 1 : $1 == "-" ? -1 : $1).to_i
- b = ($2 == "?" ? values.shift : $2).to_i
- pseudo << nth_child(a, b, of_type, reverse)
- @source << (b >= 0 ? "#{a}n+#{b})" : "#{a}n#{b})")
- else
- raise ArgumentError, "Invalid nth-child #{match}"
- end
- "" # Remove
- end
- # First/last child (of type).
- next if statement.sub!(/^:(first|last)-(child|of-type)/) do
- reverse = $1 == "last"
- of_type = $2 == "of-type"
- pseudo << nth_child(0, 1, of_type, reverse)
- @source << ":#{$1}-#{$2}"
- "" # Remove
- end
- # Only child (of type).
- next if statement.sub!(/^:only-(child|of-type)/) do
- of_type = $1 == "of-type"
- pseudo << only_child(of_type)
- @source << ":only-#{$1}"
- "" # Remove
- end
-
- # Empty: no child elements or meaningful content (whitespaces
- # are ignored).
- next if statement.sub!(/^:empty/) do
- pseudo << lambda do |element|
- empty = true
- for child in element.children
- if child.tag? || !child.content.strip.empty?
- empty = false
- break
- end
- end
- empty
- end
- @source << ":empty"
- "" # Remove
- end
- # Content: match the text content of the element, stripping
- # leading and trailing spaces.
- next if statement.sub!(/^:content\(\s*(\?|'[^']*'|"[^"]*"|[^)]*)\s*\)/) do
- content = $1
- if content == "?"
- content = values.shift
- elsif (content[0] == ?" || content[0] == ?') && content[0] == content[-1]
- content = content[1..-2]
- end
- @source << ":content('#{content}')"
- content = Regexp.new("^#{Regexp.escape(content.to_s)}$") unless content.is_a?(Regexp)
- pseudo << lambda do |element|
- text = ""
- for child in element.children
- unless child.tag?
- text << child.content
- end
- end
- text.strip =~ content
- end
- "" # Remove
- end
-
- # Negation. Create another simple selector to handle it.
- if statement.sub!(/^:not\(\s*/, "")
- raise ArgumentError, "Double negatives are not missing feature" unless can_negate
- @source << ":not("
- negation << simple_selector(statement, values, false)
- raise ArgumentError, "Negation not closed" unless statement.sub!(/^\s*\)/, "")
- @source << ")"
- next
- end
-
- # No match: moving on.
- break
- end
-
- # Return hash. The keys are mapped to instance variables.
- {:tag_name=>tag_name, :attributes=>attributes, :pseudo=>pseudo, :negation=>negation}
- end
-
-
- # Create a regular expression to match an attribute value based
- # on the equality operator (=, ^=, |=, etc).
- def attribute_match(equality, value)
- regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
- case equality
- when "=" then
- # Match the attribute value in full
- Regexp.new("^#{regexp}$")
- when "~=" then
- # Match a space-separated word within the attribute value
- Regexp.new("(^|\s)#{regexp}($|\s)")
- when "^="
- # Match the beginning of the attribute value
- Regexp.new("^#{regexp}")
- when "$="
- # Match the end of the attribute value
- Regexp.new("#{regexp}$")
- when "*="
- # Match substring of the attribute value
- regexp.is_a?(Regexp) ? regexp : Regexp.new(regexp)
- when "|=" then
- # Match the first space-separated item of the attribute value
- Regexp.new("^#{regexp}($|\s)")
- else
- raise InvalidSelectorError, "Invalid operation/value" unless value.empty?
- # Match all attributes values (existence check)
- //
- end
- end
-
-
- # Returns a lambda that can match an element against the nth-child
- # pseudo class, given the following arguments:
- # * +a+ -- Value of a part.
- # * +b+ -- Value of b part.
- # * +of_type+ -- True to test only elements of this type (of-type).
- # * +reverse+ -- True to count in reverse order (last-).
- def nth_child(a, b, of_type, reverse)
- # a = 0 means select at index b, if b = 0 nothing selected
- return lambda { |element| false } if a == 0 && b == 0
- # a < 0 and b < 0 will never match against an index
- return lambda { |element| false } if a < 0 && b < 0
- b = a + b + 1 if b < 0 # b < 0 just picks last element from each group
- b -= 1 unless b == 0 # b == 0 is same as b == 1, otherwise zero based
- lambda do |element|
- # Element must be inside parent element.
- return false unless element.parent && element.parent.tag?
- index = 0
- # Get siblings, reverse if counting from last.
- siblings = element.parent.children
- siblings = siblings.reverse if reverse
- # Match element name if of-type, otherwise ignore name.
- name = of_type ? element.name : nil
- found = false
- for child in siblings
- # Skip text nodes/comments.
- if child.tag? && (name == nil || child.name == name)
- if a == 0
- # Shortcut when a == 0 no need to go past count
- if index == b
- found = child.equal?(element)
- break
- end
- elsif a < 0
- # Only look for first b elements
- break if index > b
- if child.equal?(element)
- found = (index % a) == 0
- break
- end
- else
- # Otherwise, break if child found and count == an+b
- if child.equal?(element)
- found = (index % a) == b
- break
- end
- end
- index += 1
- end
- end
- found
- end
- end
-
-
- # Creates a only child lambda. Pass +of-type+ to only look at
- # elements of its type.
- def only_child(of_type)
- lambda do |element|
- # Element must be inside parent element.
- return false unless element.parent && element.parent.tag?
- name = of_type ? element.name : nil
- other = false
- for child in element.parent.children
- # Skip text nodes/comments.
- if child.tag? && (name == nil || child.name == name)
- unless child.equal?(element)
- other = true
- break
- end
- end
- end
- !other
- end
- end
-
-
- # Called to create a dependent selector (sibling, descendant, etc).
- # Passes the remainder of the statement that will be reduced to zero
- # eventually, and array of substitution values.
- #
- # This method is called from four places, so it helps to put it here
- # for reuse. The only logic deals with the need to detect comma
- # separators (alternate) and apply them to the selector group of the
- # top selector.
- def next_selector(statement, values)
- second = Selector.new(statement, values)
- # If there are alternate selectors, we group them in the top selector.
- if alternates = second.instance_variable_get(:@alternates)
- second.instance_variable_set(:@alternates, [])
- @alternates.concat alternates
- end
- second
- end
-
- end
-
-
- # See HTML::Selector.new
- def self.selector(statement, *values)
- Selector.new(statement, *values)
- end
-
-
- class Tag
-
- def select(selector, *values)
- selector = HTML::Selector.new(selector, values)
- selector.select(self)
- end
-
- end
-
-end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb
deleted file mode 100644
index adf4e45930..0000000000
--- a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-require 'strscan'
-
-module HTML #:nodoc:
-
- # A simple HTML tokenizer. It simply breaks a stream of text into tokens, where each
- # token is a string. Each string represents either "text", or an HTML element.
- #
- # This currently assumes valid XHTML, which means no free < or > characters.
- #
- # Usage:
- #
- # tokenizer = HTML::Tokenizer.new(text)
- # while token = tokenizer.next
- # p token
- # end
- class Tokenizer #:nodoc:
-
- # The current (byte) position in the text
- attr_reader :position
-
- # The current line number
- attr_reader :line
-
- # Create a new Tokenizer for the given text.
- def initialize(text)
- text.encode!
- @scanner = StringScanner.new(text)
- @position = 0
- @line = 0
- @current_line = 1
- end
-
- # Returns the next token in the sequence, or +nil+ if there are no more tokens in
- # the stream.
- def next
- return nil if @scanner.eos?
- @position = @scanner.pos
- @line = @current_line
- if @scanner.check(/<\S/)
- update_current_line(scan_tag)
- else
- update_current_line(scan_text)
- end
- end
-
- private
-
- # Treat the text at the current position as a tag, and scan it. Supports
- # comments, doctype tags, and regular tags, and ignores less-than and
- # greater-than characters within quoted strings.
- def scan_tag
- tag = @scanner.getch
- if @scanner.scan(/!--/) # comment
- tag << @scanner.matched
- tag << (@scanner.scan_until(/--\s*>/) || @scanner.scan_until(/\Z/))
- elsif @scanner.scan(/!\[CDATA\[/)
- tag << @scanner.matched
- tag << (@scanner.scan_until(/\]\]>/) || @scanner.scan_until(/\Z/))
- elsif @scanner.scan(/!/) # doctype
- tag << @scanner.matched
- tag << consume_quoted_regions
- else
- tag << consume_quoted_regions
- end
- tag
- end
-
- # Scan all text up to the next < character and return it.
- def scan_text
- "#{@scanner.getch}#{@scanner.scan(/[^<]*/)}"
- end
-
- # Counts the number of newlines in the text and updates the current line
- # accordingly.
- def update_current_line(text)
- text.scan(/\r?\n/) { @current_line += 1 }
- end
-
- # Skips over quoted strings, so that less-than and greater-than characters
- # within the strings are ignored.
- def consume_quoted_regions
- text = ""
- loop do
- match = @scanner.scan_until(/['"<>]/) or break
-
- delim = @scanner.matched
- if delim == "<"
- match = match.chop
- @scanner.pos -= 1
- end
-
- text << match
- break if delim == "<" || delim == ">"
-
- # consume the quoted region
- while match = @scanner.scan_until(/[\\#{delim}]/)
- text << match
- break if @scanner.matched == delim
- break if @scanner.eos?
- text << @scanner.getch # skip the escaped character
- end
- end
- text
- end
- end
-
-end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/version.rb b/actionview/lib/action_view/vendor/html-scanner/html/version.rb
deleted file mode 100644
index 6d645c3e14..0000000000
--- a/actionview/lib/action_view/vendor/html-scanner/html/version.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module HTML #:nodoc:
- module Version #:nodoc:
-
- MAJOR = 0
- MINOR = 5
- TINY = 3
-
- STRING = [ MAJOR, MINOR, TINY ].join(".")
-
- end
-end
diff --git a/actionview/test/activerecord/controller_runtime_test.rb b/actionview/test/activerecord/controller_runtime_test.rb
index 368bec1c70..b5bfcf12a3 100644
--- a/actionview/test/activerecord/controller_runtime_test.rb
+++ b/actionview/test/activerecord/controller_runtime_test.rb
@@ -8,8 +8,6 @@ ActionController::Base.send :include, ActiveRecord::Railties::ControllerRuntime
class ControllerRuntimeLogSubscriberTest < ActionController::TestCase
class LogSubscriberController < ActionController::Base
- respond_to :html
-
def show
render :inline => "<%= Project.all %>"
end
@@ -21,7 +19,7 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase
def create
ActiveRecord::LogSubscriber.runtime += 100
project = Project.last
- respond_with(project, location: url_for(action: :show))
+ redirect_to "/"
end
def redirect
diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb
index 63b5ac0fab..68b44c4f0d 100644
--- a/actionview/test/template/atom_feed_helper_test.rb
+++ b/actionview/test/template/atom_feed_helper_test.rb
@@ -254,7 +254,7 @@ class AtomFeedTest < ActionController::TestCase
def test_self_url_should_default_to_current_request_url
with_restful_routing(:scrolls) do
get :index, :id => "defaults"
- assert_select "link[rel=self][href=http://www.nextangle.com/scrolls?id=defaults]"
+ assert_select "link[rel=self][href=\"http://www.nextangle.com/scrolls?id=defaults\"]"
end
end
@@ -318,22 +318,22 @@ class AtomFeedTest < ActionController::TestCase
with_restful_routing(:scrolls) do
get :index, :id => "feed_with_xhtml_content"
assert_match %r{xmlns="http://www.w3.org/1999/xhtml"}, @response.body
- assert_select "summary div p", :text => "Something Boring"
- assert_select "summary div p", :text => "after 2"
+ assert_select "summary", :text => /Something Boring/
+ assert_select "summary", :text => /after 2/
end
end
def test_feed_entry_type_option_default_to_text_html
with_restful_routing(:scrolls) do
get :index, :id => 'defaults'
- assert_select "entry link[rel=alternate][type=text/html]"
+ assert_select "entry link[rel=alternate][type=\"text/html\"]"
end
end
def test_feed_entry_type_option_specified
with_restful_routing(:scrolls) do
get :index, :id => 'entry_type_options'
- assert_select "entry link[rel=alternate][type=text/xml]"
+ assert_select "entry link[rel=alternate][type=\"text/xml\"]"
end
end
diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb
index b86ae910c4..0cdb130710 100644
--- a/actionview/test/template/date_helper_test.rb
+++ b/actionview/test/template/date_helper_test.rb
@@ -1652,9 +1652,9 @@ class DateHelperTest < ActionView::TestCase
concat f.date_select(:written_on)
end
- expected = "<select id='post_written_on_1i' name='post[written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
- expected << "<select id='post_written_on_2i' name='post[written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
- expected << "<select id='post_written_on_3i' name='post[written_on(3i)]'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
+ expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
assert_dom_equal(expected, output_buffer)
end
@@ -1668,9 +1668,9 @@ class DateHelperTest < ActionView::TestCase
concat f.date_select(:written_on)
end
- expected = "<select id='post_#{id}_written_on_1i' name='post[#{id}][written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
- expected << "<select id='post_#{id}_written_on_2i' name='post[#{id}][written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
- expected << "<select id='post_#{id}_written_on_3i' name='post[#{id}][written_on(3i)]'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
+ expected = %{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
+ expected << %{<select id="post_#{id}_written_on_2i" name="post[#{id}][written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
+ expected << %{<select id="post_#{id}_written_on_3i" name="post[#{id}][written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
assert_dom_equal(expected, output_buffer)
end
@@ -1684,9 +1684,10 @@ class DateHelperTest < ActionView::TestCase
concat f.date_select(:written_on)
end
- expected = "<select id='post_#{id}_written_on_1i' name='post[#{id}][written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
- expected << "<select id='post_#{id}_written_on_2i' name='post[#{id}][written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
- expected << "<select id='post_#{id}_written_on_3i' name='post[#{id}][written_on(3i)]'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
+
+ expected = %{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
+ expected << %{<select id="post_#{id}_written_on_2i" name="post[#{id}][written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
+ expected << %{<select id="post_#{id}_written_on_3i" name="post[#{id}][written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
assert_dom_equal(expected, output_buffer)
end
@@ -2374,11 +2375,11 @@ class DateHelperTest < ActionView::TestCase
concat f.datetime_select(:updated_at, {}, :class => 'selector')
end
- expected = "<select id='post_updated_at_1i' name='post[updated_at(1i)]' class='selector'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
- expected << "<select id='post_updated_at_2i' name='post[updated_at(2i)]' class='selector'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
- expected << "<select id='post_updated_at_3i' name='post[updated_at(3i)]' class='selector'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
- expected << " &mdash; <select id='post_updated_at_4i' name='post[updated_at(4i)]' class='selector'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option selected='selected' value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n</select>\n"
- expected << " : <select id='post_updated_at_5i' name='post[updated_at(5i)]' class='selector'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n<option value='32'>32</option>\n<option value='33'>33</option>\n<option value='34'>34</option>\n<option selected='selected' value='35'>35</option>\n<option value='36'>36</option>\n<option value='37'>37</option>\n<option value='38'>38</option>\n<option value='39'>39</option>\n<option value='40'>40</option>\n<option value='41'>41</option>\n<option value='42'>42</option>\n<option value='43'>43</option>\n<option value='44'>44</option>\n<option value='45'>45</option>\n<option value='46'>46</option>\n<option value='47'>47</option>\n<option value='48'>48</option>\n<option value='49'>49</option>\n<option value='50'>50</option>\n<option value='51'>51</option>\n<option value='52'>52</option>\n<option value='53'>53</option>\n<option value='54'>54</option>\n<option value='55'>55</option>\n<option value='56'>56</option>\n<option value='57'>57</option>\n<option value='58'>58</option>\n<option value='59'>59</option>\n</select>\n"
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]" class="selector">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]" class="selector">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
+ expected << %{ &mdash; <select id="post_updated_at_4i" name="post[updated_at(4i)]" class="selector">\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option selected="selected" value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n</select>\n}
+ expected << %{ : <select id="post_updated_at_5i" name="post[updated_at(5i)]" class="selector">\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option selected="selected" value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n</select>\n}
assert_dom_equal expected, output_buffer
end
diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb
index 5e991d87ad..b193d387c3 100644
--- a/actionview/test/template/form_collections_helper_test.rb
+++ b/actionview/test/template/form_collections_helper_test.rb
@@ -185,8 +185,8 @@ class FormCollectionsHelperTest < ActionView::TestCase
p.collection_radio_buttons :category_id, collection, :id, :name
end
- assert_select 'input#post_category_id_1[type=radio][value=1]'
- assert_select 'input#post_category_id_2[type=radio][value=2]'
+ assert_select 'input#post_category_id_1[type=radio][value="1"]'
+ assert_select 'input#post_category_id_2[type=radio][value="2"]'
assert_select 'label[for=post_category_id_1]', 'Category 1'
assert_select 'label[for=post_category_id_2]', 'Category 2'
@@ -203,36 +203,36 @@ class FormCollectionsHelperTest < ActionView::TestCase
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
with_collection_check_boxes :user, :category_ids, collection, :id, :name
- assert_select 'input#user_category_ids_1[type=checkbox][value=1]'
- assert_select 'input#user_category_ids_2[type=checkbox][value=2]'
+ assert_select 'input#user_category_ids_1[type=checkbox][value="1"]'
+ assert_select 'input#user_category_ids_2[type=checkbox][value="2"]'
end
test 'collection check boxes generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
with_collection_check_boxes :user, :category_ids, collection, :id, :name
- assert_select "input[type=hidden][name='user[category_ids][]'][value=]", :count => 1
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 1
end
test 'collection check boxes generates a hidden field using the given :name in :html_options' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
with_collection_check_boxes :user, :category_ids, collection, :id, :name, {}, {name: "user[other_category_ids][]"}
- assert_select "input[type=hidden][name='user[other_category_ids][]'][value=]", :count => 1
+ assert_select "input[type=hidden][name='user[other_category_ids][]'][value='']", :count => 1
end
test 'collection check boxes generates a hidden field with index if it was provided' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
with_collection_check_boxes :user, :category_ids, collection, :id, :name, { index: 322 }
- assert_select "input[type=hidden][name='user[322][category_ids][]'][value=]", count: 1
+ assert_select "input[type=hidden][name='user[322][category_ids][]'][value='']", count: 1
end
test 'collection check boxes does not generate a hidden field if include_hidden option is false' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
with_collection_check_boxes :user, :category_ids, collection, :id, :name, include_hidden: false
- assert_select "input[type=hidden][name='user[category_ids][]'][value=]", :count => 0
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 0
end
test 'collection check boxes accepts a collection and generate a series of checkboxes with labels for label method' do
@@ -260,8 +260,8 @@ class FormCollectionsHelperTest < ActionView::TestCase
collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]]
with_collection_check_boxes :user, :active, collection, :first, :second
- assert_select 'input[type=checkbox][value=1].foo'
- assert_select 'input[type=checkbox][value=2].bar'
+ assert_select 'input[type=checkbox][value="1"].foo'
+ assert_select 'input[type=checkbox][value="2"].bar'
end
test 'collection check boxes sets the label class defined inside the block' do
@@ -286,27 +286,27 @@ class FormCollectionsHelperTest < ActionView::TestCase
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => [1, 3]
- assert_select 'input[type=checkbox][value=1][checked=checked]'
- assert_select 'input[type=checkbox][value=3][checked=checked]'
- assert_no_select 'input[type=checkbox][value=2][checked=checked]'
+ assert_select 'input[type=checkbox][value="1"][checked=checked]'
+ assert_select 'input[type=checkbox][value="3"][checked=checked]'
+ assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
end
test 'collection check boxes accepts selected string values as :checked option' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => ['1', '3']
- assert_select 'input[type=checkbox][value=1][checked=checked]'
- assert_select 'input[type=checkbox][value=3][checked=checked]'
- assert_no_select 'input[type=checkbox][value=2][checked=checked]'
+ assert_select 'input[type=checkbox][value="1"][checked=checked]'
+ assert_select 'input[type=checkbox][value="3"][checked=checked]'
+ assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
end
test 'collection check boxes accepts a single checked value' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => 3
- assert_select 'input[type=checkbox][value=3][checked=checked]'
- assert_no_select 'input[type=checkbox][value=1][checked=checked]'
- assert_no_select 'input[type=checkbox][value=2][checked=checked]'
+ assert_select 'input[type=checkbox][value="3"][checked=checked]'
+ assert_no_select 'input[type=checkbox][value="1"][checked=checked]'
+ assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
end
test 'collection check boxes accepts selected values as :checked option and override the model values' do
@@ -317,71 +317,71 @@ class FormCollectionsHelperTest < ActionView::TestCase
p.collection_check_boxes :category_ids, collection, :first, :last, :checked => [1, 3]
end
- assert_select 'input[type=checkbox][value=1][checked=checked]'
- assert_select 'input[type=checkbox][value=3][checked=checked]'
- assert_no_select 'input[type=checkbox][value=2][checked=checked]'
+ assert_select 'input[type=checkbox][value="1"][checked=checked]'
+ assert_select 'input[type=checkbox][value="3"][checked=checked]'
+ assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
end
test 'collection check boxes accepts multiple disabled items' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => [1, 3]
- assert_select 'input[type=checkbox][value=1][disabled=disabled]'
- assert_select 'input[type=checkbox][value=3][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
+ assert_select 'input[type=checkbox][value="1"][disabled=disabled]'
+ assert_select 'input[type=checkbox][value="3"][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]'
end
test 'collection check boxes accepts single disabled item' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => 1
- assert_select 'input[type=checkbox][value=1][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=3][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
+ assert_select 'input[type=checkbox][value="1"][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value="3"][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]'
end
test 'collection check boxes accepts a proc to disabled items' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => proc { |i| i.first == 1 }
- assert_select 'input[type=checkbox][value=1][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=3][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
+ assert_select 'input[type=checkbox][value="1"][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value="3"][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]'
end
test 'collection check boxes accepts multiple readonly items' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => [1, 3]
- assert_select 'input[type=checkbox][value=1][readonly=readonly]'
- assert_select 'input[type=checkbox][value=3][readonly=readonly]'
- assert_no_select 'input[type=checkbox][value=2][readonly=readonly]'
+ assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
+ assert_select 'input[type=checkbox][value="3"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
end
test 'collection check boxes accepts single readonly item' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => 1
- assert_select 'input[type=checkbox][value=1][readonly=readonly]'
- assert_no_select 'input[type=checkbox][value=3][readonly=readonly]'
- assert_no_select 'input[type=checkbox][value=2][readonly=readonly]'
+ assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="3"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
end
test 'collection check boxes accepts a proc to readonly items' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => proc { |i| i.first == 1 }
- assert_select 'input[type=checkbox][value=1][readonly=readonly]'
- assert_no_select 'input[type=checkbox][value=3][readonly=readonly]'
- assert_no_select 'input[type=checkbox][value=2][readonly=readonly]'
+ assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="3"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
end
test 'collection check boxes accepts html options' do
collection = [[1, 'Category 1'], [2, 'Category 2']]
with_collection_check_boxes :user, :category_ids, collection, :first, :last, {}, :class => 'check'
- assert_select 'input.check[type=checkbox][value=1]'
- assert_select 'input.check[type=checkbox][value=2]'
+ assert_select 'input.check[type=checkbox][value="1"]'
+ assert_select 'input.check[type=checkbox][value="2"]'
end
test 'collection check boxes with fields for' do
@@ -390,8 +390,8 @@ class FormCollectionsHelperTest < ActionView::TestCase
p.collection_check_boxes :category_ids, collection, :id, :name
end
- assert_select 'input#post_category_ids_1[type=checkbox][value=1]'
- assert_select 'input#post_category_ids_2[type=checkbox][value=2]'
+ assert_select 'input#post_category_ids_1[type=checkbox][value="1"]'
+ assert_select 'input#post_category_ids_2[type=checkbox][value="2"]'
assert_select 'label[for=post_category_ids_1]', 'Category 1'
assert_select 'label[for=post_category_ids_2]', 'Category 2'
diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb
index a9f137aec6..d944214961 100644
--- a/actionview/test/template/form_helper_test.rb
+++ b/actionview/test/template/form_helper_test.rb
@@ -59,6 +59,35 @@ class FormHelperTest < ActionView::TestCase
}
}
+ I18n.backend.store_translations 'placeholder', {
+ activemodel: {
+ attributes: {
+ post: {
+ cost: "Total cost"
+ },
+ :"post/cost" => {
+ uk: "Pounds"
+ }
+ }
+ },
+ helpers: {
+ placeholder: {
+ post: {
+ title: "What is this about?",
+ written_on: {
+ spanish: "Escrito en"
+ },
+ comments: {
+ body: "Write body here"
+ }
+ },
+ tag: {
+ value: "Tag"
+ }
+ }
+ }
+ }
+
@post = Post.new
@comment = Comment.new
def @post.errors()
@@ -297,6 +326,68 @@ class FormHelperTest < ActionView::TestCase
)
end
+ def test_text_field_placeholder_without_locales
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_body" name="post[body]" placeholder="Body" type="text" value="Back to the hill and over it again!" />', text_field(:post, :body, placeholder: true))
+ end
+ end
+
+ def test_text_field_placeholder_with_locales
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_title" name="post[title]" placeholder="What is this about?" type="text" value="Hello World" />', text_field(:post, :title, placeholder: true))
+ end
+ end
+
+ def test_text_field_placeholder_with_human_attribute_name
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Total cost" type="text" />', text_field(:post, :cost, placeholder: true))
+ end
+ end
+
+ def test_text_field_placeholder_with_human_attribute_name_and_value
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Pounds" type="text" />', text_field(:post, :cost, placeholder: "uk"))
+ end
+ end
+
+ def test_text_field_placeholder_with_locales_and_value
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_written_on" name="post[written_on]" placeholder="Escrito en" type="text" value="2004-06-15" />', text_field(:post, :written_on, placeholder: "spanish"))
+ end
+ end
+
+ def test_text_field_placeholder_with_locales_and_nested_attributes
+ with_locale :placeholder do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:comments) do |cf|
+ concat cf.text_field(:body, placeholder: true)
+ end
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ '<input id="post_comments_attributes_0_body" name="post[comments_attributes][0][body]" placeholder="Write body here" type="text" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_text_field_placeholder_with_locales_fallback_and_nested_attributes
+ with_locale :placeholder do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:tags) do |cf|
+ concat cf.text_field(:value, placeholder: true)
+ end
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" placeholder="Tag" type="text" value="new tag" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
def test_text_field
assert_dom_equal(
'<input id="post_title" name="post[title]" type="text" value="Hello World" />',
@@ -665,6 +756,83 @@ class FormHelperTest < ActionView::TestCase
)
end
+ def test_text_area_placeholder_without_locales
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_body" name="post[body]" placeholder="Body">\nBack to the hill and over it again!</textarea>},
+ text_area(:post, :body, placeholder: true)
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_locales
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_title" name="post[title]" placeholder="What is this about?">\nHello World</textarea>},
+ text_area(:post, :title, placeholder: true)
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_human_attribute_name
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_cost" name="post[cost]" placeholder="Total cost">\n</textarea>},
+ text_area(:post, :cost, placeholder: true)
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_human_attribute_name_and_value
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_cost" name="post[cost]" placeholder="Pounds">\n</textarea>},
+ text_area(:post, :cost, placeholder: "uk")
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_locales_and_value
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_written_on" name="post[written_on]" placeholder="Escrito en">\n2004-06-15</textarea>},
+ text_area(:post, :written_on, placeholder: "spanish")
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_locales_and_nested_attributes
+ with_locale :placeholder do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:comments) do |cf|
+ concat cf.text_area(:body, placeholder: true)
+ end
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ %{<textarea id="post_comments_attributes_0_body" name="post[comments_attributes][0][body]" placeholder="Write body here">\n</textarea>}
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_text_area_placeholder_with_locales_fallback_and_nested_attributes
+ with_locale :placeholder do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:tags) do |cf|
+ concat cf.text_area(:value, placeholder: true)
+ end
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ %{<textarea id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" placeholder="Tag">\nnew tag</textarea>}
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
def test_text_area
assert_dom_equal(
%{<textarea id="post_body" name="post[body]">\nBack to the hill and over it again!</textarea>},
diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb
index 18c739674a..771e3fefc3 100644
--- a/actionview/test/template/form_tag_helper_test.rb
+++ b/actionview/test/template/form_tag_helper_test.rb
@@ -632,6 +632,6 @@ class FormTagHelperTest < ActionView::TestCase
private
def root_elem(rendered_content)
- HTML::Document.new(rendered_content).root.children[0]
+ Nokogiri::HTML::DocumentFragment.parse(rendered_content).children.first # extract from nodeset
end
end
diff --git a/actionview/test/template/html-scanner/cdata_node_test.rb b/actionview/test/template/html-scanner/cdata_node_test.rb
deleted file mode 100644
index 9b58174641..0000000000
--- a/actionview/test/template/html-scanner/cdata_node_test.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require 'abstract_unit'
-
-class CDATANodeTest < ActiveSupport::TestCase
- def setup
- @node = HTML::CDATA.new(nil, 0, 0, "<p>howdy</p>")
- end
-
- def test_to_s
- assert_equal "<![CDATA[<p>howdy</p>]]>", @node.to_s
- end
-
- def test_content
- assert_equal "<p>howdy</p>", @node.content
- end
-end
diff --git a/actionview/test/template/html-scanner/document_test.rb b/actionview/test/template/html-scanner/document_test.rb
deleted file mode 100644
index 17f045d549..0000000000
--- a/actionview/test/template/html-scanner/document_test.rb
+++ /dev/null
@@ -1,148 +0,0 @@
-require 'abstract_unit'
-
-class DocumentTest < ActiveSupport::TestCase
- def test_handle_doctype
- doc = nil
- assert_nothing_raised do
- doc = HTML::Document.new <<-HTML.strip
- <!DOCTYPE "blah" "blah" "blah">
- <html>
- </html>
- HTML
- end
- assert_equal 3, doc.root.children.length
- assert_equal %{<!DOCTYPE "blah" "blah" "blah">}, doc.root.children[0].content
- assert_match %r{\s+}m, doc.root.children[1].content
- assert_equal "html", doc.root.children[2].name
- end
-
- def test_find_img
- doc = HTML::Document.new <<-HTML.strip
- <html>
- <body>
- <p><img src="hello.gif"></p>
- </body>
- </html>
- HTML
- assert doc.find(:tag=>"img", :attributes=>{"src"=>"hello.gif"})
- end
-
- def test_find_all
- doc = HTML::Document.new <<-HTML.strip
- <html>
- <body>
- <p class="test"><img src="hello.gif"></p>
- <div class="foo">
- <p class="test">something</p>
- <p>here is <em class="test">more</em></p>
- </div>
- </body>
- </html>
- HTML
- all = doc.find_all :attributes => { :class => "test" }
- assert_equal 3, all.length
- assert_equal [ "p", "p", "em" ], all.map { |n| n.name }
- end
-
- def test_find_with_text
- doc = HTML::Document.new <<-HTML.strip
- <html>
- <body>
- <p>Some text</p>
- </body>
- </html>
- HTML
- assert doc.find(:content => "Some text")
- assert doc.find(:tag => "p", :child => { :content => "Some text" })
- assert doc.find(:tag => "p", :child => "Some text")
- assert doc.find(:tag => "p", :content => "Some text")
- end
-
- def test_parse_xml
- assert_nothing_raised { HTML::Document.new("<tags><tag/></tags>", true, true) }
- assert_nothing_raised { HTML::Document.new("<outer><link>something</link></outer>", true, true) }
- end
-
- def test_parse_document
- doc = HTML::Document.new(<<-HTML)
- <div>
- <h2>blah</h2>
- <table>
- </table>
- </div>
- HTML
- assert_not_nil doc.find(:tag => "div", :children => { :count => 1, :only => { :tag => "table" } })
- end
-
- def test_tag_nesting_nothing_to_s
- doc = HTML::Document.new("<tag></tag>")
- assert_equal "<tag></tag>", doc.root.to_s
- end
-
- def test_tag_nesting_space_to_s
- doc = HTML::Document.new("<tag> </tag>")
- assert_equal "<tag> </tag>", doc.root.to_s
- end
-
- def test_tag_nesting_text_to_s
- doc = HTML::Document.new("<tag>text</tag>")
- assert_equal "<tag>text</tag>", doc.root.to_s
- end
-
- def test_tag_nesting_tag_to_s
- doc = HTML::Document.new("<tag><nested /></tag>")
- assert_equal "<tag><nested /></tag>", doc.root.to_s
- end
-
- def test_parse_cdata
- doc = HTML::Document.new(<<-HTML)
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
- <head>
- <title><![CDATA[<br>]]></title>
- </head>
- <body>
- <p>this document has &lt;br&gt; for a title</p>
- </body>
-</html>
-HTML
-
- assert_nil doc.find(:tag => "title", :descendant => { :tag => "br" })
- assert doc.find(:tag => "title", :child => "<br>")
- end
-
- def test_find_empty_tag
- doc = HTML::Document.new("<div id='map'></div>")
- assert_nil doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /./)
- assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /\A\Z/)
- assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /^$/)
- assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => "")
- assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => nil)
- end
-
- def test_parse_invalid_document
- assert_nothing_raised do
- HTML::Document.new("<html>
- <table>
- <tr>
- <td style=\"color: #FFFFFF; height: 17px; onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" style=\"cursor:pointer; height: 17px;\"; nowrap onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" onmouseout=\"this.bgColor='#0066cc'; this.style.color='#FFFFFF'\" onmouseover=\"this.bgColor='#ffffff'; this.style.color='#0033cc'\">About Us</td>
- </tr>
- </table>
- </html>")
- end
- end
-
- def test_invalid_document_raises_exception_when_strict
- assert_raise RuntimeError do
- HTML::Document.new("<html>
- <table>
- <tr>
- <td style=\"color: #FFFFFF; height: 17px; onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" style=\"cursor:pointer; height: 17px;\"; nowrap onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" onmouseout=\"this.bgColor='#0066cc'; this.style.color='#FFFFFF'\" onmouseover=\"this.bgColor='#ffffff'; this.style.color='#0033cc'\">About Us</td>
- </tr>
- </table>
- </html>", true)
- end
- end
-
-end
diff --git a/actionview/test/template/html-scanner/node_test.rb b/actionview/test/template/html-scanner/node_test.rb
deleted file mode 100644
index 5b5d092036..0000000000
--- a/actionview/test/template/html-scanner/node_test.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-require 'abstract_unit'
-
-class NodeTest < ActiveSupport::TestCase
-
- class MockNode
- def initialize(matched, value)
- @matched = matched
- @value = value
- end
-
- def find(conditions)
- @matched && self
- end
-
- def to_s
- @value.to_s
- end
- end
-
- def setup
- @node = HTML::Node.new("parent")
- @node.children.concat [MockNode.new(false,1), MockNode.new(true,"two"), MockNode.new(false,:three)]
- end
-
- def test_match
- assert !@node.match("foo")
- end
-
- def test_tag
- assert !@node.tag?
- end
-
- def test_to_s
- assert_equal "1twothree", @node.to_s
- end
-
- def test_find
- assert_equal "two", @node.find('blah').to_s
- end
-
- def test_parse_strict
- s = "<b foo='hello'' bar='baz'>"
- assert_raise(RuntimeError) { HTML::Node.parse(nil,0,0,s) }
- end
-
- def test_parse_relaxed
- s = "<b foo='hello'' bar='baz'>"
- node = nil
- assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
- assert node.attributes.has_key?("foo")
- assert !node.attributes.has_key?("bar")
- end
-
- def test_to_s_with_boolean_attrs
- s = "<b foo bar>"
- node = HTML::Node.parse(nil,0,0,s)
- assert node.attributes.has_key?("foo")
- assert node.attributes.has_key?("bar")
- assert "<b foo bar>", node.to_s
- end
-
- def test_parse_with_unclosed_tag
- s = "<span onmouseover='bang'"
- node = nil
- assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
- assert node.attributes.has_key?("onmouseover")
- end
-
- def test_parse_with_valid_cdata_section
- s = "<![CDATA[<span>contents</span>]]>"
- node = nil
- assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
- assert_kind_of HTML::CDATA, node
- assert_equal '<span>contents</span>', node.content
- end
-
- def test_parse_strict_with_unterminated_cdata_section
- s = "<![CDATA[neverending..."
- assert_raise(RuntimeError) { HTML::Node.parse(nil,0,0,s) }
- end
-
- def test_parse_relaxed_with_unterminated_cdata_section
- s = "<![CDATA[neverending..."
- node = nil
- assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
- assert_kind_of HTML::CDATA, node
- assert_equal 'neverending...', node.content
- end
-end
diff --git a/actionview/test/template/html-scanner/sanitizer_test.rb b/actionview/test/template/html-scanner/sanitizer_test.rb
deleted file mode 100644
index b1c1b83807..0000000000
--- a/actionview/test/template/html-scanner/sanitizer_test.rb
+++ /dev/null
@@ -1,330 +0,0 @@
-require 'abstract_unit'
-
-class SanitizerTest < ActionController::TestCase
- def setup
- @sanitizer = nil # used by assert_sanitizer
- end
-
- def test_strip_tags_with_quote
- sanitizer = HTML::FullSanitizer.new
- string = '<" <img src="trollface.gif" onload="alert(1)"> hi'
-
- assert_equal ' hi', sanitizer.sanitize(string)
- end
-
- def test_strip_tags
- sanitizer = HTML::FullSanitizer.new
- assert_equal("<<<bad html", sanitizer.sanitize("<<<bad html"))
- assert_equal("<<", sanitizer.sanitize("<<<bad html>"))
- assert_equal("Dont touch me", sanitizer.sanitize("Dont touch me"))
- assert_equal("This is a test.", sanitizer.sanitize("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>"))
- assert_equal("Weirdos", sanitizer.sanitize("Wei<<a>a onclick='alert(document.cookie);'</a>/>rdos"))
- assert_equal("This is a test.", sanitizer.sanitize("This is a test."))
- assert_equal(
- %{This is a test.\n\n\nIt no longer contains any HTML.\n}, sanitizer.sanitize(
- %{<title>This is <b>a <a href="" target="_blank">test</a></b>.</title>\n\n<!-- it has a comment -->\n\n<p>It no <b>longer <strong>contains <em>any <strike>HTML</strike></em>.</strong></b></p>\n}))
- assert_equal "This has a here.", sanitizer.sanitize("This has a <!-- comment --> here.")
- assert_equal "This has a here.", sanitizer.sanitize("This has a <![CDATA[<section>]]> here.")
- assert_equal "This has an unclosed ", sanitizer.sanitize("This has an unclosed <![CDATA[<section>]] here...")
- [nil, '', ' '].each { |blank| assert_equal blank, sanitizer.sanitize(blank) }
- assert_nothing_raised { sanitizer.sanitize("This is a frozen string with no tags".freeze) }
- end
-
- def test_strip_links
- sanitizer = HTML::LinkSanitizer.new
- assert_equal "Dont touch me", sanitizer.sanitize("Dont touch me")
- assert_equal "on my mind\nall day long", sanitizer.sanitize("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>")
- assert_equal "0wn3d", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>")
- assert_equal "Magic", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
- assert_equal "FrrFox", sanitizer.sanitize("<href onlclick='steal()'>FrrFox</a></href>")
- assert_equal "My mind\nall <b>day</b> long", sanitizer.sanitize("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>")
- assert_equal "all <b>day</b> long", sanitizer.sanitize("<<a>a href='hello'>all <b>day</b> long<</A>/a>")
-
- assert_equal "<a<a", sanitizer.sanitize("<a<a")
- end
-
- def test_sanitize_form
- assert_sanitized "<form action=\"/foo/bar\" method=\"post\"><input></form>", ''
- end
-
- def test_sanitize_plaintext
- raw = "<plaintext><span>foo</span></plaintext>"
- assert_sanitized raw, "<span>foo</span>"
- end
-
- def test_sanitize_script
- assert_sanitized "a b c<script language=\"Javascript\">blah blah blah</script>d e f", "a b cd e f"
- end
-
- def test_sanitize_js_handlers
- raw = %{onthis="do that" <a href="#" onclick="hello" name="foo" onbogus="remove me">hello</a>}
- assert_sanitized raw, %{onthis="do that" <a name="foo" href="#">hello</a>}
- end
-
- def test_sanitize_javascript_href
- raw = %{href="javascript:bang" <a href="javascript:bang" name="hello">foo</a>, <span href="javascript:bang">bar</span>}
- assert_sanitized raw, %{href="javascript:bang" <a name="hello">foo</a>, <span>bar</span>}
- end
-
- def test_sanitize_image_src
- raw = %{src="javascript:bang" <img src="javascript:bang" width="5">foo</img>, <span src="javascript:bang">bar</span>}
- assert_sanitized raw, %{src="javascript:bang" <img width="5">foo</img>, <span>bar</span>}
- end
-
- HTML::WhiteListSanitizer.allowed_tags.each do |tag_name|
- define_method "test_should_allow_#{tag_name}_tag" do
- assert_sanitized "start <#{tag_name} title=\"1\" onclick=\"foo\">foo <bad>bar</bad> baz</#{tag_name}> end", %(start <#{tag_name} title="1">foo bar baz</#{tag_name}> end)
- end
- end
-
- def test_should_allow_anchors
- assert_sanitized %(<a href="foo" onclick="bar"><script>baz</script></a>), %(<a href="foo"></a>)
- end
-
- # RFC 3986, sec 4.2
- def test_allow_colons_in_path_component
- assert_sanitized("<a href=\"./this:that\">foo</a>")
- end
-
- %w(src width height alt).each do |img_attr|
- define_method "test_should_allow_image_#{img_attr}_attribute" do
- assert_sanitized %(<img #{img_attr}="foo" onclick="bar" />), %(<img #{img_attr}="foo" />)
- end
- end
-
- def test_should_handle_non_html
- assert_sanitized 'abc'
- end
-
- def test_should_handle_blank_text
- assert_sanitized nil
- assert_sanitized ''
- end
-
- def test_should_allow_custom_tags
- text = "<u>foo</u>"
- sanitizer = HTML::WhiteListSanitizer.new
- assert_equal(text, sanitizer.sanitize(text, :tags => %w(u)))
- end
-
- def test_should_allow_only_custom_tags
- text = "<u>foo</u> with <i>bar</i>"
- sanitizer = HTML::WhiteListSanitizer.new
- assert_equal("<u>foo</u> with bar", sanitizer.sanitize(text, :tags => %w(u)))
- end
-
- def test_should_allow_custom_tags_with_attributes
- text = %(<blockquote cite="http://example.com/">foo</blockquote>)
- sanitizer = HTML::WhiteListSanitizer.new
- assert_equal(text, sanitizer.sanitize(text))
- end
-
- def test_should_allow_custom_tags_with_custom_attributes
- text = %(<blockquote foo="bar">Lorem ipsum</blockquote>)
- sanitizer = HTML::WhiteListSanitizer.new
- assert_equal(text, sanitizer.sanitize(text, :attributes => ['foo']))
- end
-
- def test_should_raise_argument_error_if_tags_is_not_enumerable
- sanitizer = HTML::WhiteListSanitizer.new
- e = assert_raise(ArgumentError) do
- sanitizer.sanitize('', :tags => 'foo')
- end
-
- assert_equal "You should pass :tags as an Enumerable", e.message
- end
-
- def test_should_raise_argument_error_if_attributes_is_not_enumerable
- sanitizer = HTML::WhiteListSanitizer.new
- e = assert_raise(ArgumentError) do
- sanitizer.sanitize('', :attributes => 'foo')
- end
-
- assert_equal "You should pass :attributes as an Enumerable", e.message
- end
-
- [%w(img src), %w(a href)].each do |(tag, attr)|
- define_method "test_should_strip_#{attr}_attribute_in_#{tag}_with_bad_protocols" do
- assert_sanitized %(<#{tag} #{attr}="javascript:bang" title="1">boo</#{tag}>), %(<#{tag} title="1">boo</#{tag}>)
- end
- end
-
- def test_should_flag_bad_protocols
- sanitizer = HTML::WhiteListSanitizer.new
- %w(about chrome data disk hcp help javascript livescript lynxcgi lynxexec ms-help ms-its mhtml mocha opera res resource shell vbscript view-source vnd.ms.radio wysiwyg).each do |proto|
- assert sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://bad")
- end
- end
-
- def test_should_accept_good_protocols_ignoring_case
- sanitizer = HTML::WhiteListSanitizer.new
- HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
- assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto.capitalize}://good")
- end
- end
-
- def test_should_accept_good_protocols_ignoring_space
- sanitizer = HTML::WhiteListSanitizer.new
- HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
- assert !sanitizer.send(:contains_bad_protocols?, 'src', " #{proto}://good")
- end
- end
-
- def test_should_accept_good_protocols
- sanitizer = HTML::WhiteListSanitizer.new
- HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
- assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://good")
- end
- end
-
- def test_should_reject_hex_codes_in_protocol
- assert_sanitized %(<a href="&#37;6A&#37;61&#37;76&#37;61&#37;73&#37;63&#37;72&#37;69&#37;70&#37;74&#37;3A&#37;61&#37;6C&#37;65&#37;72&#37;74&#37;28&#37;22&#37;58&#37;53&#37;53&#37;22&#37;29">1</a>), "<a>1</a>"
- assert @sanitizer.send(:contains_bad_protocols?, 'src', "%6A%61%76%61%73%63%72%69%70%74%3A%61%6C%65%72%74%28%22%58%53%53%22%29")
- end
-
- def test_should_block_script_tag
- assert_sanitized %(<SCRIPT\nSRC=http://ha.ckers.org/xss.js></SCRIPT>), ""
- end
-
- [%(<IMG SRC="javascript:alert('XSS');">),
- %(<IMG SRC=javascript:alert('XSS')>),
- %(<IMG SRC=JaVaScRiPt:alert('XSS')>),
- %(<IMG """><SCRIPT>alert("XSS")</SCRIPT>">),
- %(<IMG SRC=javascript:alert(&quot;XSS&quot;)>),
- %(<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>),
- %(<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>),
- %(<IMG SRC=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>),
- %(<IMG SRC=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>),
- %(<IMG SRC="jav\tascript:alert('XSS');">),
- %(<IMG SRC="jav&#x09;ascript:alert('XSS');">),
- %(<IMG SRC="jav&#x0A;ascript:alert('XSS');">),
- %(<IMG SRC="jav&#x0D;ascript:alert('XSS');">),
- %(<IMG SRC=" &#14; javascript:alert('XSS');">),
- %(<IMG SRC="javascript&#x3a;alert('XSS');">),
- %(<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>)].each_with_index do |img_hack, i|
- define_method "test_should_not_fall_for_xss_image_hack_#{i+1}" do
- assert_sanitized img_hack, "<img>"
- end
- end
-
- def test_should_sanitize_tag_broken_up_by_null
- assert_sanitized %(<SCR\0IPT>alert(\"XSS\")</SCR\0IPT>), "alert(\"XSS\")"
- end
-
- def test_should_sanitize_invalid_script_tag
- assert_sanitized %(<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT>), ""
- end
-
- def test_should_sanitize_script_tag_with_multiple_open_brackets
- assert_sanitized %(<<SCRIPT>alert("XSS");//<</SCRIPT>), "&lt;"
- assert_sanitized %(<iframe src=http://ha.ckers.org/scriptlet.html\n<a), %(&lt;a)
- end
-
- def test_should_sanitize_unclosed_script
- assert_sanitized %(<SCRIPT SRC=http://ha.ckers.org/xss.js?<B>), "<b>"
- end
-
- def test_should_sanitize_half_open_scripts
- assert_sanitized %(<IMG SRC="javascript:alert('XSS')"), "<img>"
- end
-
- def test_should_not_fall_for_ridiculous_hack
- img_hack = %(<IMG\nSRC\n=\n"\nj\na\nv\na\ns\nc\nr\ni\np\nt\n:\na\nl\ne\nr\nt\n(\n'\nX\nS\nS\n'\n)\n"\n>)
- assert_sanitized img_hack, "<img>"
- end
-
- def test_should_sanitize_attributes
- assert_sanitized %(<SPAN title="'><script>alert()</script>">blah</SPAN>), %(<span title="#{CGI.escapeHTML "'><script>alert()</script>"}">blah</span>)
- end
-
- def test_should_sanitize_illegal_style_properties
- raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;)
- expected = %(display: block; width: 100%; height: 100%; background-color: black; background-image: ; background-x: center; background-y: center;)
- assert_equal expected, sanitize_css(raw)
- end
-
- def test_should_sanitize_with_trailing_space
- raw = "display:block; "
- expected = "display: block;"
- assert_equal expected, sanitize_css(raw)
- end
-
- def test_should_sanitize_xul_style_attributes
- raw = %(-moz-binding:url('http://ha.ckers.org/xssmoz.xml#xss'))
- assert_equal '', sanitize_css(raw)
- end
-
- def test_should_sanitize_invalid_tag_names
- assert_sanitized(%(a b c<script/XSS src="http://ha.ckers.org/xss.js"></script>d e f), "a b cd e f")
- end
-
- def test_should_sanitize_non_alpha_and_non_digit_characters_in_tags
- assert_sanitized('<a onclick!#$%&()*~+-_.,:;?@[/|\]^`=alert("XSS")>foo</a>', "<a>foo</a>")
- end
-
- def test_should_sanitize_invalid_tag_names_in_single_tags
- assert_sanitized('<img/src="http://ha.ckers.org/xss.js"/>', "<img />")
- end
-
- def test_should_sanitize_img_dynsrc_lowsrc
- assert_sanitized(%(<img lowsrc="javascript:alert('XSS')" />), "<img />")
- end
-
- def test_should_sanitize_div_background_image_unicode_encoded
- raw = %(background-image:\0075\0072\006C\0028'\006a\0061\0076\0061\0073\0063\0072\0069\0070\0074\003a\0061\006c\0065\0072\0074\0028.1027\0058.1053\0053\0027\0029'\0029)
- assert_equal '', sanitize_css(raw)
- end
-
- def test_should_sanitize_div_style_expression
- raw = %(width: expression(alert('XSS'));)
- assert_equal '', sanitize_css(raw)
- end
-
- def test_should_sanitize_across_newlines
- raw = %(\nwidth:\nexpression(alert('XSS'));\n)
- assert_equal '', sanitize_css(raw)
- end
-
- def test_should_sanitize_img_vbscript
- assert_sanitized %(<img src='vbscript:msgbox("XSS")' />), '<img />'
- end
-
- def test_should_sanitize_cdata_section
- assert_sanitized "<![CDATA[<span>section</span>]]>", "&lt;![CDATA[&lt;span>section&lt;/span>]]>"
- end
-
- def test_should_sanitize_unterminated_cdata_section
- assert_sanitized "<![CDATA[<span>neverending...", "&lt;![CDATA[&lt;span>neverending...]]>"
- end
-
- def test_should_not_mangle_urls_with_ampersand
- assert_sanitized %{<a href=\"http://www.domain.com?var1=1&amp;var2=2\">my link</a>}
- end
-
- def test_should_sanitize_neverending_attribute
- assert_sanitized "<span class=\"\\", "<span class=\"\\\">"
- end
-
- def test_x03a
- assert_sanitized %(<a href="javascript&#x3a;alert('XSS');">), "<a>"
- assert_sanitized %(<a href="javascript&#x003a;alert('XSS');">), "<a>"
- assert_sanitized %(<a href="http&#x3a;//legit">), %(<a href="http://legit">)
- assert_sanitized %(<a href="javascript&#x3A;alert('XSS');">), "<a>"
- assert_sanitized %(<a href="javascript&#x003A;alert('XSS');">), "<a>"
- assert_sanitized %(<a href="http&#x3A;//legit">), %(<a href="http://legit">)
- end
-
-protected
- def assert_sanitized(input, expected = nil)
- @sanitizer ||= HTML::WhiteListSanitizer.new
- if input
- assert_dom_equal expected || input, @sanitizer.sanitize(input)
- else
- assert_nil @sanitizer.sanitize(input)
- end
- end
-
- def sanitize_css(input)
- (@sanitizer ||= HTML::WhiteListSanitizer.new).sanitize_css(input)
- end
-end
diff --git a/actionview/test/template/html-scanner/tag_node_test.rb b/actionview/test/template/html-scanner/tag_node_test.rb
deleted file mode 100644
index a29d2d43d7..0000000000
--- a/actionview/test/template/html-scanner/tag_node_test.rb
+++ /dev/null
@@ -1,243 +0,0 @@
-require 'abstract_unit'
-
-class TagNodeTest < ActiveSupport::TestCase
- def test_open_without_attributes
- node = tag("<tag>")
- assert_equal "tag", node.name
- assert_equal Hash.new, node.attributes
- assert_nil node.closing
- end
-
- def test_open_with_attributes
- node = tag("<TAG1 foo=hey_ho x:bar=\"blah blah\" BAZ='blah blah blah' >")
- assert_equal "tag1", node.name
- assert_equal "hey_ho", node["foo"]
- assert_equal "blah blah", node["x:bar"]
- assert_equal "blah blah blah", node["baz"]
- end
-
- def test_self_closing_without_attributes
- node = tag("<tag/>")
- assert_equal "tag", node.name
- assert_equal Hash.new, node.attributes
- assert_equal :self, node.closing
- end
-
- def test_self_closing_with_attributes
- node = tag("<tag a=b/>")
- assert_equal "tag", node.name
- assert_equal( { "a" => "b" }, node.attributes )
- assert_equal :self, node.closing
- end
-
- def test_closing_without_attributes
- node = tag("</tag>")
- assert_equal "tag", node.name
- assert_nil node.attributes
- assert_equal :close, node.closing
- end
-
- def test_bracket_op_when_no_attributes
- node = tag("</tag>")
- assert_nil node["foo"]
- end
-
- def test_bracket_op_when_attributes
- node = tag("<tag a=b/>")
- assert_equal "b", node["a"]
- end
-
- def test_attributes_with_escaped_quotes
- node = tag("<tag a='b\\'c' b=\"bob \\\"float\\\"\">")
- assert_equal "b\\'c", node["a"]
- assert_equal "bob \\\"float\\\"", node["b"]
- end
-
- def test_to_s
- node = tag("<a b=c d='f' g=\"h 'i'\" />")
- node = node.to_s
- assert node.include?('a')
- assert node.include?('b="c"')
- assert node.include?('d="f"')
- assert node.include?('g="h')
- assert node.include?('i')
- end
-
- def test_tag
- assert tag("<tag>").tag?
- end
-
- def test_match_tag_as_string
- assert tag("<tag>").match(:tag => "tag")
- assert !tag("<tag>").match(:tag => "b")
- end
-
- def test_match_tag_as_regexp
- assert tag("<tag>").match(:tag => /t.g/)
- assert !tag("<tag>").match(:tag => /t[bqs]g/)
- end
-
- def test_match_attributes_as_string
- t = tag("<tag a=something b=else />")
- assert t.match(:attributes => {"a" => "something"})
- assert t.match(:attributes => {"b" => "else"})
- end
-
- def test_match_attributes_as_regexp
- t = tag("<tag a=something b=else />")
- assert t.match(:attributes => {"a" => /^something$/})
- assert t.match(:attributes => {"b" => /e.*e/})
- assert t.match(:attributes => {"a" => /me..i/, "b" => /.ls.$/})
- end
-
- def test_match_attributes_as_number
- t = tag("<tag a=15 b=3.1415 />")
- assert t.match(:attributes => {"a" => 15})
- assert t.match(:attributes => {"b" => 3.1415})
- assert t.match(:attributes => {"a" => 15, "b" => 3.1415})
- end
-
- def test_match_attributes_exist
- t = tag("<tag a=15 b=3.1415 />")
- assert t.match(:attributes => {"a" => true})
- assert t.match(:attributes => {"b" => true})
- assert t.match(:attributes => {"a" => true, "b" => true})
- end
-
- def test_match_attributes_not_exist
- t = tag("<tag a=15 b=3.1415 />")
- assert t.match(:attributes => {"c" => false})
- assert t.match(:attributes => {"c" => nil})
- assert t.match(:attributes => {"a" => true, "c" => false})
- end
-
- def test_match_parent_success
- t = tag("<tag a=15 b='hello'>", tag("<foo k='value'>"))
- assert t.match(:parent => {:tag => "foo", :attributes => {"k" => /v.l/, "j" => false}})
- end
-
- def test_match_parent_fail
- t = tag("<tag a=15 b='hello'>", tag("<foo k='value'>"))
- assert !t.match(:parent => {:tag => /kafka/})
- end
-
- def test_match_child_success
- t = tag("<tag x:k='something'>")
- tag("<child v=john a=kelly>", t)
- tag("<sib m=vaughn v=james>", t)
- assert t.match(:child => { :tag => "sib", :attributes => {"v" => /j/}})
- assert t.match(:child => { :attributes => {"a" => "kelly"}})
- end
-
- def test_match_child_fail
- t = tag("<tag x:k='something'>")
- tag("<child v=john a=kelly>", t)
- tag("<sib m=vaughn v=james>", t)
- assert !t.match(:child => { :tag => "sib", :attributes => {"v" => /r/}})
- assert !t.match(:child => { :attributes => {"v" => false}})
- end
-
- def test_match_ancestor_success
- t = tag("<tag x:k='something'>", tag("<parent v=john a=kelly>", tag("<grandparent m=vaughn v=james>")))
- assert t.match(:ancestor => {:tag => "parent", :attributes => {"a" => /ll/}})
- assert t.match(:ancestor => {:attributes => {"m" => "vaughn"}})
- end
-
- def test_match_ancestor_fail
- t = tag("<tag x:k='something'>", tag("<parent v=john a=kelly>", tag("<grandparent m=vaughn v=james>")))
- assert !t.match(:ancestor => {:tag => /^parent/, :attributes => {"v" => /m/}})
- assert !t.match(:ancestor => {:attributes => {"v" => false}})
- end
-
- def test_match_descendant_success
- tag("<grandchild m=vaughn v=james>", tag("<child v=john a=kelly>", t = tag("<tag x:k='something'>")))
- assert t.match(:descendant => {:tag => "child", :attributes => {"a" => /ll/}})
- assert t.match(:descendant => {:attributes => {"m" => "vaughn"}})
- end
-
- def test_match_descendant_fail
- tag("<grandchild m=vaughn v=james>", tag("<child v=john a=kelly>", t = tag("<tag x:k='something'>")))
- assert !t.match(:descendant => {:tag => /^child/, :attributes => {"v" => /m/}})
- assert !t.match(:descendant => {:attributes => {"v" => false}})
- end
-
- def test_match_child_count
- t = tag("<tag x:k='something'>")
- tag("hello", t)
- tag("<child v=john a=kelly>", t)
- tag("<sib m=vaughn v=james>", t)
- assert t.match(:children => { :count => 2 })
- assert t.match(:children => { :count => 2..4 })
- assert t.match(:children => { :less_than => 4 })
- assert t.match(:children => { :greater_than => 1 })
- assert !t.match(:children => { :count => 3 })
- end
-
- def test_conditions_as_strings
- t = tag("<tag x:k='something'>")
- assert t.match("tag" => "tag")
- assert t.match("attributes" => { "x:k" => "something" })
- assert !t.match("tag" => "gat")
- assert !t.match("attributes" => { "x:j" => "something" })
- end
-
- def test_attributes_as_symbols
- t = tag("<child v=john a=kelly>")
- assert t.match(:attributes => { :v => /oh/ })
- assert t.match(:attributes => { :a => /ll/ })
- end
-
- def test_match_sibling
- t = tag("<tag x:k='something'>")
- tag("hello", t)
- tag("<span a=b>", t)
- tag("world", t)
- m = tag("<span k=r>", t)
- tag("<span m=l>", t)
-
- assert m.match(:sibling => {:tag => "span", :attributes => {:a => true}})
- assert m.match(:sibling => {:tag => "span", :attributes => {:m => true}})
- assert !m.match(:sibling => {:tag => "span", :attributes => {:k => true}})
- end
-
- def test_match_sibling_before
- t = tag("<tag x:k='something'>")
- tag("hello", t)
- tag("<span a=b>", t)
- tag("world", t)
- m = tag("<span k=r>", t)
- tag("<span m=l>", t)
-
- assert m.match(:before => {:tag => "span", :attributes => {:m => true}})
- assert !m.match(:before => {:tag => "span", :attributes => {:a => true}})
- assert !m.match(:before => {:tag => "span", :attributes => {:k => true}})
- end
-
- def test_match_sibling_after
- t = tag("<tag x:k='something'>")
- tag("hello", t)
- tag("<span a=b>", t)
- tag("world", t)
- m = tag("<span k=r>", t)
- tag("<span m=l>", t)
-
- assert m.match(:after => {:tag => "span", :attributes => {:a => true}})
- assert !m.match(:after => {:tag => "span", :attributes => {:m => true}})
- assert !m.match(:after => {:tag => "span", :attributes => {:k => true}})
- end
-
- def test_tag_to_s
- t = tag("<b x='foo'>")
- tag("hello", t)
- tag("<hr />", t)
- assert_equal %(<b x="foo">hello<hr /></b>), t.to_s
- end
-
- private
-
- def tag(content, parent=nil)
- node = HTML::Node.parse(parent,0,0,content)
- parent.children << node if parent
- node
- end
-end
diff --git a/actionview/test/template/html-scanner/text_node_test.rb b/actionview/test/template/html-scanner/text_node_test.rb
deleted file mode 100644
index cbcb9e78f0..0000000000
--- a/actionview/test/template/html-scanner/text_node_test.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'abstract_unit'
-
-class TextNodeTest < ActiveSupport::TestCase
- def setup
- @node = HTML::Text.new(nil, 0, 0, "hello, howdy, aloha, annyeong")
- end
-
- def test_to_s
- assert_equal "hello, howdy, aloha, annyeong", @node.to_s
- end
-
- def test_find_string
- assert_equal @node, @node.find("hello, howdy, aloha, annyeong")
- assert_equal false, @node.find("bogus")
- end
-
- def test_find_regexp
- assert_equal @node, @node.find(/an+y/)
- assert_nil @node.find(/b/)
- end
-
- def test_find_hash
- assert_equal @node, @node.find(:content => /howdy/)
- assert_nil @node.find(:content => /^howdy$/)
- assert_equal false, @node.find(:content => "howdy")
- end
-
- def test_find_other
- assert_nil @node.find(:hello)
- end
-
- def test_match_string
- assert @node.match("hello, howdy, aloha, annyeong")
- assert_equal false, @node.match("bogus")
- end
-
- def test_match_regexp
- assert_not_nil @node, @node.match(/an+y/)
- assert_nil @node.match(/b/)
- end
-
- def test_match_hash
- assert_not_nil @node, @node.match(:content => "howdy")
- assert_nil @node.match(:content => /^howdy$/)
- end
-
- def test_match_other
- assert_nil @node.match(:hello)
- end
-end
diff --git a/actionview/test/template/html-scanner/tokenizer_test.rb b/actionview/test/template/html-scanner/tokenizer_test.rb
deleted file mode 100644
index 1d59de23b6..0000000000
--- a/actionview/test/template/html-scanner/tokenizer_test.rb
+++ /dev/null
@@ -1,131 +0,0 @@
-require 'abstract_unit'
-
-class TokenizerTest < ActiveSupport::TestCase
-
- def test_blank
- tokenize ""
- assert_end
- end
-
- def test_space
- tokenize " "
- assert_next " "
- assert_end
- end
-
- def test_tag_simple_open
- tokenize "<tag>"
- assert_next "<tag>"
- assert_end
- end
-
- def test_tag_simple_self_closing
- tokenize "<tag />"
- assert_next "<tag />"
- assert_end
- end
-
- def test_tag_simple_closing
- tokenize "</tag>"
- assert_next "</tag>"
- end
-
- def test_tag_with_single_quoted_attribute
- tokenize %{<tag a='hello'>x}
- assert_next %{<tag a='hello'>}
- end
-
- def test_tag_with_single_quoted_attribute_with_escape
- tokenize %{<tag a='hello\\''>x}
- assert_next %{<tag a='hello\\''>}
- end
-
- def test_tag_with_double_quoted_attribute
- tokenize %{<tag a="hello">x}
- assert_next %{<tag a="hello">}
- end
-
- def test_tag_with_double_quoted_attribute_with_escape
- tokenize %{<tag a="hello\\"">x}
- assert_next %{<tag a="hello\\"">}
- end
-
- def test_tag_with_unquoted_attribute
- tokenize %{<tag a=hello>x}
- assert_next %{<tag a=hello>}
- end
-
- def test_tag_with_lt_char_in_attribute
- tokenize %{<tag a="x < y">x}
- assert_next %{<tag a="x < y">}
- end
-
- def test_tag_with_gt_char_in_attribute
- tokenize %{<tag a="x > y">x}
- assert_next %{<tag a="x > y">}
- end
-
- def test_doctype_tag
- tokenize %{<!DOCTYPE "blah" "blah" "blah">\n <html>}
- assert_next %{<!DOCTYPE "blah" "blah" "blah">}
- assert_next %{\n }
- assert_next %{<html>}
- end
-
- def test_cdata_tag
- tokenize %{<![CDATA[<br>]]>}
- assert_next %{<![CDATA[<br>]]>}
- assert_end
- end
-
- def test_unterminated_cdata_tag
- tokenize %{<content:encoded><![CDATA[ neverending...}
- assert_next %{<content:encoded>}
- assert_next %{<![CDATA[ neverending...}
- assert_end
- end
-
- def test_less_than_with_space
- tokenize %{original < hello > world}
- assert_next %{original }
- assert_next %{< hello > world}
- end
-
- def test_less_than_without_matching_greater_than
- tokenize %{hello <span onmouseover="gotcha"\n<b>foo</b>\nbar</span>}
- assert_next %{hello }
- assert_next %{<span onmouseover="gotcha"\n}
- assert_next %{<b>}
- assert_next %{foo}
- assert_next %{</b>}
- assert_next %{\nbar}
- assert_next %{</span>}
- assert_end
- end
-
- def test_unterminated_comment
- tokenize %{hello <!-- neverending...}
- assert_next %{hello }
- assert_next %{<!-- neverending...}
- assert_end
- end
-
- private
-
- def tokenize(text)
- @tokenizer = HTML::Tokenizer.new(text)
- end
-
- def assert_next(expected, message=nil)
- token = @tokenizer.next
- assert_equal expected, token, message
- end
-
- def assert_sequence(*expected)
- assert_next expected.shift until expected.empty?
- end
-
- def assert_end(message=nil)
- assert_nil @tokenizer.next, message
- end
-end
diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb
index f7c8f36b78..e4be21be2c 100644
--- a/actionview/test/template/sanitize_helper_test.rb
+++ b/actionview/test/template/sanitize_helper_test.rb
@@ -1,19 +1,15 @@
require 'abstract_unit'
-# The exhaustive tests are in test/template/html-scanner/sanitizer_test.rb
-# This tests the that the helpers hook up correctly to the sanitizer classes.
+# The exhaustive tests are in test/controller/html/sanitizer_test.rb.
+# This tests that the helpers hook up correctly to the sanitizer classes.
class SanitizeHelperTest < ActionView::TestCase
tests ActionView::Helpers::SanitizeHelper
def test_strip_links
assert_equal "Dont touch me", strip_links("Dont touch me")
- assert_equal "<a<a", strip_links("<a<a")
assert_equal "on my mind\nall day long", strip_links("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>")
- assert_equal "0wn3d", strip_links("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>")
assert_equal "Magic", strip_links("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
- assert_equal "FrrFox", strip_links("<href onlclick='steal()'>FrrFox</a></href>")
assert_equal "My mind\nall <b>day</b> long", strip_links("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>")
- assert_equal "all <b>day</b> long", strip_links("<<a>a href='hello'>all <b>day</b> long<</A>/a>")
end
def test_sanitize_form
@@ -22,27 +18,15 @@ class SanitizeHelperTest < ActionView::TestCase
def test_should_sanitize_illegal_style_properties
raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;)
- expected = %(display: block; width: 100%; height: 100%; background-color: black; background-image: ; background-x: center; background-y: center;)
+ expected = %(display: block; width: 100%; height: 100%; background-color: black; background-x: center; background-y: center;)
assert_equal expected, sanitize_css(raw)
end
def test_strip_tags
- assert_equal("<<<bad html", strip_tags("<<<bad html"))
- assert_equal("<<", strip_tags("<<<bad html>"))
assert_equal("Dont touch me", strip_tags("Dont touch me"))
assert_equal("This is a test.", strip_tags("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>"))
- assert_equal("Weirdos", strip_tags("Wei<<a>a onclick='alert(document.cookie);'</a>/>rdos"))
- assert_equal("This is a test.", strip_tags("This is a test."))
- assert_equal(
- %{This is a test.\n\n\nIt no longer contains any HTML.\n}, strip_tags(
- %{<title>This is <b>a <a href="" target="_blank">test</a></b>.</title>\n\n<!-- it has a comment -->\n\n<p>It no <b>longer <strong>contains <em>any <strike>HTML</strike></em>.</strong></b></p>\n}))
assert_equal "This has a here.", strip_tags("This has a <!-- comment --> here.")
- [nil, '', ' '].each do |blank|
- stripped = strip_tags(blank)
- assert_equal blank, stripped
- end
assert_equal "", strip_tags("<script>")
- assert_equal "something &lt;img onerror=alert(1337)", ERB::Util.html_escape(strip_tags("something <img onerror=alert(1337)"))
end
def test_sanitize_is_marked_safe
diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb
index db416a8de4..667f9002da 100644
--- a/actionview/test/template/text_helper_test.rb
+++ b/actionview/test/template/text_helper_test.rb
@@ -187,7 +187,9 @@ class TextHelperTest < ActionView::TestCase
"This text is not changed because we supplied an empty phrase",
highlight("This text is not changed because we supplied an empty phrase", nil)
)
+ end
+ def test_highlight_pending
assert_equal ' ', highlight(' ', 'blank text is returned verbatim')
end
diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb
index 35279a4558..e0678ae1f7 100644
--- a/actionview/test/template/url_helper_test.rb
+++ b/actionview/test/template/url_helper_test.rb
@@ -25,7 +25,7 @@ class UrlHelperTest < ActiveSupport::TestCase
include routes.url_helpers
include ActionView::Helpers::JavaScriptHelper
- include ActionDispatch::Assertions::DomAssertions
+ include Rails::Dom::Testing::Assertions::DomAssertions
include ActionView::Context
include RenderERBUtils
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md
new file mode 100644
index 0000000000..b04883413e
--- /dev/null
+++ b/activejob/CHANGELOG.md
@@ -0,0 +1 @@
+* Started project. \ No newline at end of file
diff --git a/activejob/MIT-LICENSE b/activejob/MIT-LICENSE
new file mode 100644
index 0000000000..8b1e97b776
--- /dev/null
+++ b/activejob/MIT-LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2014 David Heinemeier Hansson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/activejob/README.md b/activejob/README.md
new file mode 100644
index 0000000000..e48070bcfc
--- /dev/null
+++ b/activejob/README.md
@@ -0,0 +1,141 @@
+# Active Job -- Make work happen later
+
+Active Job is a framework for declaring jobs and making them run on a variety
+of queueing backends. These jobs can be everything from regularly scheduled
+clean-ups, billing charges, or mailings. Anything that can be chopped up into
+small units of work and run in parallel, really.
+
+It also serves as the backend for ActionMailer's #deliver_later functionality
+that makes it easy to turn any mailing into a job for running later. That's
+one of the most common jobs in a modern web application: Sending emails outside
+of the request-response cycle, so the user doesn't have to wait on it.
+
+The main point is to ensure that all Rails apps will have a job infrastructure
+in place, even if it's in the form of an "immediate runner". We can then have
+framework features and other gems build on top of that, without having to worry
+about API differences between Delayed Job and Resque. Picking your queuing
+backend becomes more of an operational concern, then. And you'll be able to
+switch between them without having to rewrite your jobs.
+
+
+## Usage
+
+Set the queue adapter for Active Job:
+
+``` ruby
+ActiveJob::Base.queue_adapter = :inline # default queue adapter
+# Adapters currently supported: :backburner, :delayed_job, :qu, :que, :queue_classic,
+# :resque, :sidekiq, :sneakers, :sucker_punch
+```
+
+Declare a job like so:
+
+```ruby
+class MyJob < ActiveJob::Base
+ queue_as :my_jobs
+
+ def perform(record)
+ record.do_work
+ end
+end
+```
+
+Enqueue a job like so:
+
+```ruby
+MyJob.enqueue record # Enqueue a job to be performed as soon the queueing system is free.
+```
+
+```ruby
+MyJob.enqueue_at Date.tomorrow.noon, record # Enqueue a job to be performed tomorrow at noon.
+```
+
+```ruby
+MyJob.enqueue_in 1.week, record # Enqueue a job to be performed 1 week from now.
+```
+
+That's it!
+
+
+## GlobalID support
+
+Active Job supports [GlobalID serialization](https://github.com/rails/globalid/) for parameters. This makes it possible
+to pass live Active Record objects to your job instead of class/id pairs, which
+you then have to manually deserialize. Before, jobs would look like this:
+
+```ruby
+class TrashableCleanupJob
+ def perform(trashable_class, trashable_id, depth)
+ trashable = trashable_class.constantize.find(trashable_id)
+ trashable.cleanup(depth)
+ end
+end
+```
+
+Now you can simply do:
+
+```ruby
+class TrashableCleanupJob
+ def perform(trashable, depth)
+ trashable.cleanup(depth)
+ end
+end
+```
+
+This works with any class that mixes in GlobalID::Identification, which
+by default has been mixed into Active Record classes.
+
+
+## Supported queueing systems
+
+We currently have adapters for:
+
+* [Backburner](https://github.com/nesquena/backburner)
+* [Delayed Job](https://github.com/collectiveidea/delayed_job)
+* [Qu](https://github.com/bkeepers/qu)
+* [Que](https://github.com/chanks/que)
+* [QueueClassic](https://github.com/ryandotsmith/queue_classic)
+* [Resque 1.x](https://github.com/resque/resque)
+* [Sidekiq](https://github.com/mperham/sidekiq)
+* [Sneakers](https://github.com/jondot/sneakers)
+* [Sucker Punch](https://github.com/brandonhilkert/sucker_punch)
+
+
+## Auxiliary gems
+
+* [activejob-stats](https://github.com/seuros/activejob-stats)
+
+## Download and installation
+
+The latest version of Active Job can be installed with RubyGems:
+
+```
+ % [sudo] gem install activejob
+```
+
+Source code can be downloaded as part of the Rails project on GitHub
+
+* https://github.com/rails/rails/tree/master/activejob
+
+## License
+
+ActiveJob is released under the MIT license:
+
+* http://www.opensource.org/licenses/MIT
+
+
+## Support
+
+API documentation is at
+
+* http://api.rubyonrails.org
+
+Bug reports can be filed for the Ruby on Rails project here:
+
+* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
+
diff --git a/activejob/Rakefile b/activejob/Rakefile
new file mode 100644
index 0000000000..e661622165
--- /dev/null
+++ b/activejob/Rakefile
@@ -0,0 +1,84 @@
+require 'rake/testtask'
+require 'rubygems/package_task'
+
+dir = File.dirname(__FILE__)
+
+def run_without_aborting(*tasks)
+ errors = []
+
+ tasks.each do |task|
+ begin
+ Rake::Task[task].invoke
+ rescue Exception
+ errors << task
+ end
+ end
+
+ abort "Errors running #{errors.join(', ')}" if errors.any?
+end
+
+task default: :test
+
+ACTIVEJOB_ADAPTERS = %w(inline delayed_job qu que queue_classic resque sidekiq sneakers sucker_punch backburner)
+
+desc 'Run all adapter tests'
+task :test do
+ tasks = ACTIVEJOB_ADAPTERS.map{|a| "test_#{a}" }
+ run_without_aborting(*tasks)
+end
+
+namespace :test do
+ desc 'Run all adapter tests in isolation'
+ task :isolated do
+ tasks = ACTIVEJOB_ADAPTERS.map{|a| "isolated_test_#{a}" }
+ run_without_aborting(*tasks)
+ end
+end
+
+
+ACTIVEJOB_ADAPTERS.each do |adapter|
+ namespace :test do
+ Rake::TestTask.new(adapter => "#{adapter}:env") do |t|
+ t.description = ""
+ t.libs << 'test'
+ t.test_files = FileList['test/cases/**/*_test.rb']
+ t.verbose = true
+ end
+
+ namespace :isolated do
+ task adapter => "#{adapter}:env" do
+ Dir.glob("#{dir}/test/cases/**/*_test.rb").all? do |file|
+ sh(Gem.ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file)
+ end or raise 'Failures'
+ end
+ end
+ end
+
+ namespace adapter do
+ task test: "test_#{adapter}"
+ task isolated_test: "isolated_test_#{adapter}"
+
+ task(:env) { ENV['AJADAPTER'] = adapter }
+ end
+
+
+ desc "Run #{adapter} tests"
+ task "test_#{adapter}" => ["#{adapter}:env", "test:#{adapter}"]
+
+ desc "Run #{adapter} tests in isolation"
+ task "isolated_test_#{adapter}" => ["#{adapter}:env", "test:isolated:#{adapter}"]
+end
+
+
+spec = eval(File.read('activejob.gemspec'))
+
+Gem::PackageTask.new(spec) do |p|
+ p.gem_spec = spec
+end
+
+desc 'Release to rubygems'
+task release: :package do
+ require 'rake/gemcutter'
+ Rake::Gemcutter::Tasks.new(spec).define
+ Rake::Task['gem:push'].invoke
+end
diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec
new file mode 100644
index 0000000000..d609bb8fce
--- /dev/null
+++ b/activejob/activejob.gemspec
@@ -0,0 +1,22 @@
+version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
+
+Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = 'activejob'
+ s.version = version
+ s.summary = 'Job framework with pluggable queues.'
+ s.description = 'Declare job classes that can be run by a variety of queueing backends.'
+
+ s.required_ruby_version = '>= 1.9.3'
+
+ s.license = 'MIT'
+
+ s.author = 'David Heinemeier Hansson'
+ s.email = 'david@loudthinking.com'
+ s.homepage = 'http://www.rubyonrails.org'
+
+ s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.md', 'lib/**/*']
+ s.require_path = 'lib'
+
+ s.add_dependency 'globalid'
+end
diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb
new file mode 100644
index 0000000000..ef92406725
--- /dev/null
+++ b/activejob/lib/active_job.rb
@@ -0,0 +1,33 @@
+#--
+# Copyright (c) 2014 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#++
+
+require 'active_support'
+require 'active_support/rails'
+require 'active_job/version'
+require 'global_id'
+
+module ActiveJob
+ extend ActiveSupport::Autoload
+
+ autoload :Base
+end \ No newline at end of file
diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb
new file mode 100644
index 0000000000..369e716912
--- /dev/null
+++ b/activejob/lib/active_job/arguments.rb
@@ -0,0 +1,62 @@
+module ActiveJob
+ class DeserializationError < StandardError
+ attr_reader :original_exception
+
+ def initialize(e)
+ super ("Error while trying to deserialize arguments: #{e.message}")
+ @original_exception = e
+ set_backtrace e.backtrace
+ end
+ end
+
+ module Arguments
+ extend self
+ TYPE_WHITELIST = [ NilClass, Fixnum, Float, String, TrueClass, FalseClass, Bignum ]
+
+ def serialize(arguments)
+ arguments.map { |argument| serialize_argument(argument) }
+ end
+
+ def deserialize(arguments)
+ arguments.map { |argument| deserialize_argument(argument) }
+ end
+
+ private
+ def serialize_argument(argument)
+ case argument
+ when GlobalID::Identification
+ argument.global_id.to_s
+ when *TYPE_WHITELIST
+ argument
+ when Array
+ serialize(argument)
+ when Hash
+ Hash[ argument.map { |key, value| [ serialize_hash_key(key), serialize_argument(value) ] } ]
+ else
+ raise "Unsupported argument type: #{argument.class.name}"
+ end
+ end
+
+ def deserialize_argument(argument)
+ case argument
+ when Array
+ deserialize(argument)
+ when Hash
+ Hash[ argument.map { |key, value| [ key, deserialize_argument(value) ] } ].with_indifferent_access
+ else
+ GlobalID::Locator.locate(argument) || argument
+ end
+ rescue => e
+ raise DeserializationError.new(e)
+ end
+
+ def serialize_hash_key(key)
+ case key
+ when String, Symbol
+ key.to_s
+ else
+ raise "Unsupported hash key type: #{key.class.name}"
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb
new file mode 100644
index 0000000000..d5ba253016
--- /dev/null
+++ b/activejob/lib/active_job/base.rb
@@ -0,0 +1,22 @@
+require 'active_job/queue_adapter'
+require 'active_job/queue_name'
+require 'active_job/enqueuing'
+require 'active_job/execution'
+require 'active_job/callbacks'
+require 'active_job/identifier'
+require 'active_job/logging'
+
+module ActiveJob
+ class Base
+ extend QueueAdapter
+
+ include QueueName
+ include Enqueuing
+ include Execution
+ include Callbacks
+ include Identifier
+ include Logging
+
+ ActiveSupport.run_load_hooks(:active_job, self)
+ end
+end
diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb
new file mode 100644
index 0000000000..af92031bc9
--- /dev/null
+++ b/activejob/lib/active_job/callbacks.rb
@@ -0,0 +1,144 @@
+require 'active_support/callbacks'
+
+module ActiveJob
+ # = Active Job Callbacks
+ #
+ # Active Job provides hooks during the lifecycle of a job. Callbacks allows you to trigger
+ # logic during the lifecycle of a job. Available callbacks:
+ #
+ # * <tt>before_enqueue</tt>
+ # * <tt>around_enqueue</tt>
+ # * <tt>after_enqueue</tt>
+ # * <tt>before_perform</tt>
+ # * <tt>around_perform</tt>
+ # * <tt>after_perform</tt>
+ #
+ module Callbacks
+ extend ActiveSupport::Concern
+ include ActiveSupport::Callbacks
+
+ included do
+ define_callbacks :perform
+ define_callbacks :enqueue
+ end
+
+ module ClassMethods
+ # Defines a callback that will get called right before the
+ # job's perform method is executed.
+ #
+ # class VideoProcessJob < ActiveJob::Base
+ # queue_as :default
+ #
+ # before_perform do |job|
+ # UserMailer.notify_video_started_processing(job.arguments.first)
+ # end
+ #
+ # def perform(video_id)
+ # Video.find(video_id).process
+ # end
+ # end
+ #
+ def before_perform(*filters, &blk)
+ set_callback(:perform, :before, *filters, &blk)
+ end
+
+ # Defines a callback that will get called right after the
+ # job's perform method has finished.
+ #
+ # class VideoProcessJob < ActiveJob::Base
+ # queue_as :default
+ #
+ # after_perform do |job|
+ # UserMailer.notify_video_processed(job.arguments.first)
+ # end
+ #
+ # def perform(video_id)
+ # Video.find(video_id).process
+ # end
+ # end
+ #
+ def after_perform(*filters, &blk)
+ set_callback(:perform, :after, *filters, &blk)
+ end
+
+ # Defines a callback that will get called around the job's perform method.
+ #
+ # class VideoProcessJob < ActiveJob::Base
+ # queue_as :default
+ #
+ # around_perform do |job, block|
+ # UserMailer.notify_video_started_processing(job.arguments.first)
+ # block.call
+ # UserMailer.notify_video_processed(job.arguments.first)
+ # end
+ #
+ # def perform(video_id)
+ # Video.find(video_id).process
+ # end
+ # end
+ #
+ def around_perform(*filters, &blk)
+ set_callback(:perform, :around, *filters, &blk)
+ end
+
+ # Defines a callback that will get called right before the
+ # job is enqueued.
+ #
+ # class VideoProcessJob < ActiveJob::Base
+ # queue_as :default
+ #
+ # before_enqueue do |job|
+ # $statsd.increment "enqueue-video-job.try"
+ # end
+ #
+ # def perform(video_id)
+ # Video.find(video_id).process
+ # end
+ # end
+ #
+ def before_enqueue(*filters, &blk)
+ set_callback(:enqueue, :before, *filters, &blk)
+ end
+
+ # Defines a callback that will get called right after the
+ # job is enqueued.
+ #
+ # class VideoProcessJob < ActiveJob::Base
+ # queue_as :default
+ #
+ # after_enqueue do |job|
+ # $statsd.increment "enqueue-video-job.success"
+ # end
+ #
+ # def perform(video_id)
+ # Video.find(video_id).process
+ # end
+ # end
+ #
+ def after_enqueue(*filters, &blk)
+ set_callback(:enqueue, :after, *filters, &blk)
+ end
+
+ # Defines a callback that will get called before and after the
+ # job is enqueued.
+ #
+ # class VideoProcessJob < ActiveJob::Base
+ # queue_as :default
+ #
+ # around_enqueue do |job, block|
+ # $statsd.time "video-job.process" do
+ # block.call
+ # end
+ # end
+ #
+ # def perform(video_id)
+ # Video.find(video_id).process
+ # end
+ # end
+ #
+ def around_enqueue(*filters, &blk)
+ set_callback(:enqueue, :around, *filters, &blk)
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb
new file mode 100644
index 0000000000..3d00d51867
--- /dev/null
+++ b/activejob/lib/active_job/enqueuing.rb
@@ -0,0 +1,71 @@
+require 'active_job/arguments'
+
+module ActiveJob
+ module Enqueuing
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Push a job onto the queue. The arguments must be legal JSON types
+ # (string, int, float, nil, true, false, hash or array) or
+ # GlobalID::Identification instances. Arbitrary Ruby objects
+ # are not supported.
+ #
+ # Returns an instance of the job class queued with args available in
+ # Job#arguments.
+ def enqueue(*args)
+ new(args).tap do |job|
+ job.run_callbacks :enqueue do
+ queue_adapter.enqueue self, job.job_id, *Arguments.serialize(args)
+ end
+ end
+ end
+
+ # Enqueue a job to be performed at +interval+ from now.
+ #
+ # enqueue_in(1.week, "mike")
+ #
+ # Returns an instance of the job class queued with args available in
+ # Job#arguments and the timestamp in Job#enqueue_at.
+ def enqueue_in(interval, *args)
+ enqueue_at interval.seconds.from_now, *args
+ end
+
+ # Enqueue a job to be performed at an explicit point in time.
+ #
+ # enqueue_at(Date.tomorrow.midnight, "mike")
+ #
+ # Returns an instance of the job class queued with args available in
+ # Job#arguments and the timestamp in Job#enqueue_at.
+ def enqueue_at(timestamp, *args)
+ new(args).tap do |job|
+ job.enqueued_at = timestamp
+
+ job.run_callbacks :enqueue do
+ queue_adapter.enqueue_at self, timestamp.to_f, job.job_id, *Arguments.serialize(args)
+ end
+ end
+ end
+ end
+
+ included do
+ attr_accessor :arguments
+ attr_accessor :enqueued_at
+ end
+
+ def initialize(arguments = nil)
+ @arguments = arguments
+ end
+
+ def retry_now
+ self.class.enqueue(*arguments)
+ end
+
+ def retry_in(interval)
+ self.class.enqueue_in interval, *arguments
+ end
+
+ def retry_at(timestamp)
+ self.class.enqueue_at timestamp, *arguments
+ end
+ end
+end
diff --git a/activejob/lib/active_job/execution.rb b/activejob/lib/active_job/execution.rb
new file mode 100644
index 0000000000..0e7b5bdd72
--- /dev/null
+++ b/activejob/lib/active_job/execution.rb
@@ -0,0 +1,33 @@
+require 'active_support/rescuable'
+require 'active_job/arguments'
+
+module ActiveJob
+ module Execution
+ extend ActiveSupport::Concern
+
+ included do
+ include ActiveSupport::Rescuable
+ end
+
+ def execute(job_id, *serialized_args)
+ self.job_id = job_id
+ self.arguments = deserialize_arguments(serialized_args)
+
+ run_callbacks :perform do
+ perform(*arguments)
+ end
+ rescue => exception
+ rescue_with_handler(exception) || raise(exception)
+ end
+
+ def perform(*)
+ fail NotImplementedError
+ end
+
+ private
+ def deserialize_arguments(serialized_args)
+ Arguments.deserialize(serialized_args)
+ end
+
+ end
+end
diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb
new file mode 100644
index 0000000000..c166020b28
--- /dev/null
+++ b/activejob/lib/active_job/gem_version.rb
@@ -0,0 +1,15 @@
+module ActiveJob
+ # Returns the version of the currently loaded ActiveJob as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 4
+ MINOR = 2
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/activejob/lib/active_job/identifier.rb b/activejob/lib/active_job/identifier.rb
new file mode 100644
index 0000000000..c7f522087d
--- /dev/null
+++ b/activejob/lib/active_job/identifier.rb
@@ -0,0 +1,15 @@
+require 'active_job/arguments'
+
+module ActiveJob
+ module Identifier
+ extend ActiveSupport::Concern
+
+ included do
+ attr_writer :job_id
+ end
+
+ def job_id
+ @job_id ||= SecureRandom.uuid
+ end
+ end
+end
diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb
new file mode 100644
index 0000000000..d9e544acf5
--- /dev/null
+++ b/activejob/lib/active_job/logging.rb
@@ -0,0 +1,90 @@
+require 'active_support/core_ext/string/filters'
+require 'active_support/tagged_logging'
+require 'active_support/logger'
+
+module ActiveJob
+ module Logging
+ extend ActiveSupport::Concern
+
+ included do
+ cattr_accessor(:logger) { ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT)) }
+
+ around_enqueue do |_, block, _|
+ tag_logger do
+ block.call
+ end
+ end
+
+ around_perform do |job, block, _|
+ tag_logger(job.class.name, job.job_id) do
+ payload = {adapter: job.class.queue_adapter, job: job.class, args: job.arguments}
+ ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup)
+ ActiveSupport::Notifications.instrument("perform.active_job", payload) do
+ block.call
+ end
+ end
+ end
+
+ before_enqueue do |job|
+ if job.enqueued_at
+ ActiveSupport::Notifications.instrument "enqueue_at.active_job",
+ adapter: job.class.queue_adapter, job: job.class, job_id: job.job_id, args: job.arguments, timestamp: job.enqueued_at
+ else
+ ActiveSupport::Notifications.instrument "enqueue.active_job",
+ adapter: job.class.queue_adapter, job: job.class, job_id: job.job_id, args: job.arguments
+ end
+ end
+ end
+
+ private
+ def tag_logger(*tags)
+ if logger.respond_to?(:tagged)
+ tags.unshift "ActiveJob" unless logger_tagged_by_active_job?
+ ActiveJob::Base.logger.tagged(*tags){ yield }
+ else
+ yield
+ end
+ end
+
+ def logger_tagged_by_active_job?
+ logger.formatter.current_tags.include?("ActiveJob")
+ end
+
+ class LogSubscriber < ActiveSupport::LogSubscriber
+ def enqueue(event)
+ info "Enqueued #{event.payload[:job].name} (Job ID: #{event.payload[:job_id]}) to #{queue_name(event)}" + args_info(event)
+ end
+
+ def enqueue_at(event)
+ info "Enqueued #{event.payload[:job].name} (Job ID: #{event.payload[:job_id]}) to #{queue_name(event)} at #{enqueued_at(event)}" + args_info(event)
+ end
+
+ def perform_start(event)
+ info "Performing #{event.payload[:job].name} from #{queue_name(event)}" + args_info(event)
+ end
+
+ def perform(event)
+ info "Performed #{event.payload[:job].name} from #{queue_name(event)} in #{event.duration.round(2).to_s}ms"
+ end
+
+ private
+ def queue_name(event)
+ event.payload[:adapter].name.demodulize.remove('Adapter') + "(#{event.payload[:job].queue_name})"
+ end
+
+ def args_info(event)
+ event.payload[:args].any? ? " with arguments: #{event.payload[:args].map(&:inspect).join(", ")}" : ""
+ end
+
+ def enqueued_at(event)
+ Time.at(event.payload[:timestamp]).utc
+ end
+
+ def logger
+ ActiveJob::Base.logger
+ end
+ end
+ end
+end
+
+ActiveJob::Logging::LogSubscriber.attach_to :active_job
diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb
new file mode 100644
index 0000000000..8f2f8b86ea
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapter.rb
@@ -0,0 +1,24 @@
+require 'active_job/queue_adapters/inline_adapter'
+require 'active_support/core_ext/string/inflections'
+
+module ActiveJob
+ module QueueAdapter
+ mattr_reader(:queue_adapter) { ActiveJob::QueueAdapters::InlineAdapter }
+
+ def queue_adapter=(name_or_adapter)
+ @@queue_adapter = \
+ case name_or_adapter
+ when Symbol, String
+ load_adapter(name_or_adapter)
+ when Class
+ name_or_adapter
+ end
+ end
+
+ private
+ def load_adapter(name)
+ require "active_job/queue_adapters/#{name}_adapter"
+ "ActiveJob::QueueAdapters::#{name.to_s.camelize}Adapter".constantize
+ end
+ end
+end \ No newline at end of file
diff --git a/activejob/lib/active_job/queue_adapters/backburner_adapter.rb b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb
new file mode 100644
index 0000000000..7a6032e56b
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb
@@ -0,0 +1,25 @@
+require 'backburner'
+
+module ActiveJob
+ module QueueAdapters
+ class BackburnerAdapter
+ class << self
+ def enqueue(job, *args)
+ Backburner::Worker.enqueue JobWrapper, [ job.name, *args ], queue: job.queue_name
+ end
+
+ def enqueue_at(job, timestamp, *args)
+ raise NotImplementedError
+ end
+ end
+
+ class JobWrapper
+ class << self
+ def perform(job_name, *args)
+ job_name.constantize.new.execute *args
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
new file mode 100644
index 0000000000..bfeaa836d2
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
@@ -0,0 +1,23 @@
+require 'delayed_job'
+
+module ActiveJob
+ module QueueAdapters
+ class DelayedJobAdapter
+ class << self
+ def enqueue(job, *args)
+ JobWrapper.new.delay(queue: job.queue_name).perform(job, *args)
+ end
+
+ def enqueue_at(job, timestamp, *args)
+ JobWrapper.new.delay(queue: job.queue_name, run_at: Time.at(timestamp)).perform(job, *args)
+ end
+ end
+
+ class JobWrapper
+ def perform(job, *args)
+ job.new.execute *args
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/inline_adapter.rb b/activejob/lib/active_job/queue_adapters/inline_adapter.rb
new file mode 100644
index 0000000000..50d14a321d
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/inline_adapter.rb
@@ -0,0 +1,15 @@
+module ActiveJob
+ module QueueAdapters
+ class InlineAdapter
+ class << self
+ def enqueue(job, *args)
+ job.new.execute *args
+ end
+
+ def enqueue_at(*)
+ raise NotImplementedError.new("Use a queueing backend to enqueue jobs in the future. Read more at https://github.com/rails/activejob")
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/qu_adapter.rb b/activejob/lib/active_job/queue_adapters/qu_adapter.rb
new file mode 100644
index 0000000000..cdf4ae4ce9
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/qu_adapter.rb
@@ -0,0 +1,30 @@
+require 'qu'
+
+module ActiveJob
+ module QueueAdapters
+ class QuAdapter
+ class << self
+ def enqueue(job, *args)
+ Qu::Payload.new(klass: JobWrapper, args: [job.name, *args]).tap do |payload|
+ payload.instance_variable_set(:@queue, job.queue_name)
+ end.push
+ end
+
+ def enqueue_at(job, timestamp, *args)
+ raise NotImplementedError
+ end
+ end
+
+ class JobWrapper < Qu::Job
+ def initialize(job_name, *args)
+ @job = job_name.constantize
+ @args = args
+ end
+
+ def perform
+ @job.new.execute *@args
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/que_adapter.rb b/activejob/lib/active_job/queue_adapters/que_adapter.rb
new file mode 100644
index 0000000000..15a607bcb6
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/que_adapter.rb
@@ -0,0 +1,23 @@
+require 'que'
+
+module ActiveJob
+ module QueueAdapters
+ class QueAdapter
+ class << self
+ def enqueue(job, *args)
+ JobWrapper.enqueue job.name, *args, queue: job.queue_name
+ end
+
+ def enqueue_at(job, timestamp, *args)
+ raise NotImplementedError
+ end
+ end
+
+ class JobWrapper < Que::Job
+ def run(job_name, *args)
+ job_name.constantize.new.execute *args
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb
new file mode 100644
index 0000000000..c61e0e30db
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb
@@ -0,0 +1,23 @@
+require 'queue_classic'
+
+module ActiveJob
+ module QueueAdapters
+ class QueueClassicAdapter
+ class << self
+ def enqueue(job, *args)
+ QC::Queue.new(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.name, *args)
+ end
+
+ def enqueue_at(job, timestamp, *args)
+ raise NotImplementedError
+ end
+ end
+
+ class JobWrapper
+ def self.perform(job_name, *args)
+ job_name.constantize.new.execute *args
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/resque_adapter.rb b/activejob/lib/active_job/queue_adapters/resque_adapter.rb
new file mode 100644
index 0000000000..384aa0c4cc
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/resque_adapter.rb
@@ -0,0 +1,41 @@
+require 'resque'
+require 'active_support/core_ext/enumerable'
+require 'active_support/core_ext/array/access'
+
+begin
+ require 'resque-scheduler'
+rescue LoadError
+ begin
+ require 'resque_scheduler'
+ rescue LoadError
+ false
+ end
+end
+
+module ActiveJob
+ module QueueAdapters
+ class ResqueAdapter
+ class << self
+ def enqueue(job, *args)
+ Resque.enqueue_to job.queue_name, JobWrapper, job.name, *args
+ end
+
+ def enqueue_at(job, timestamp, *args)
+ unless Resque.respond_to?(:enqueue_at_with_queue)
+ raise NotImplementedError, "To be able to schedule jobs with Resque you need the " \
+ "resque-scheduler gem. Please add it to your Gemfile and run bundle install"
+ end
+ Resque.enqueue_at_with_queue job.queue_name, timestamp, JobWrapper, job.name, *args
+ end
+ end
+
+ class JobWrapper
+ class << self
+ def perform(job_name, *args)
+ job_name.constantize.new.execute *args
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb
new file mode 100644
index 0000000000..f738a7d91c
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb
@@ -0,0 +1,35 @@
+require 'sidekiq'
+
+module ActiveJob
+ module QueueAdapters
+ class SidekiqAdapter
+ class << self
+ def enqueue(job, *args)
+ #Sidekiq::Client does not support symbols as keys
+ Sidekiq::Client.push \
+ 'class' => JobWrapper,
+ 'queue' => job.queue_name,
+ 'args' => [ job, *args ],
+ 'retry' => true
+ end
+
+ def enqueue_at(job, timestamp, *args)
+ Sidekiq::Client.push \
+ 'class' => JobWrapper,
+ 'queue' => job.queue_name,
+ 'args' => [ job, *args ],
+ 'retry' => true,
+ 'at' => timestamp
+ end
+ end
+
+ class JobWrapper
+ include Sidekiq::Worker
+
+ def perform(job_name, *args)
+ job_name.constantize.new.execute *args
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb
new file mode 100644
index 0000000000..051a8c3bd7
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb
@@ -0,0 +1,34 @@
+require 'sneakers'
+require 'thread'
+
+module ActiveJob
+ module QueueAdapters
+ class SneakersAdapter
+ @monitor = Monitor.new
+
+ class << self
+ def enqueue(job, *args)
+ @monitor.synchronize do
+ JobWrapper.from_queue job.queue_name
+ JobWrapper.enqueue ActiveSupport::JSON.encode([ job.name, *args ])
+ end
+ end
+
+ def enqueue_at(job, timestamp, *args)
+ raise NotImplementedError
+ end
+ end
+
+ class JobWrapper
+ include Sneakers::Worker
+ from_queue 'active_jobs_default'
+
+ def work(msg)
+ job_name, *args = ActiveSupport::JSON.decode(msg)
+ job_name.constantize.new.execute *args
+ ack!
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb
new file mode 100644
index 0000000000..64b9c3ca15
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb
@@ -0,0 +1,25 @@
+require 'sucker_punch'
+
+module ActiveJob
+ module QueueAdapters
+ class SuckerPunchAdapter
+ class << self
+ def enqueue(job, *args)
+ JobWrapper.new.async.perform job, *args
+ end
+
+ def enqueue_at(job, timestamp, *args)
+ raise NotImplementedError
+ end
+ end
+
+ class JobWrapper
+ include SuckerPunch::Job
+
+ def perform(job, *args)
+ job.new.execute *args
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_name.rb b/activejob/lib/active_job/queue_name.rb
new file mode 100644
index 0000000000..c2186d9fe9
--- /dev/null
+++ b/activejob/lib/active_job/queue_name.rb
@@ -0,0 +1,18 @@
+module ActiveJob
+ module QueueName
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ mattr_accessor(:default_queue_name) { "default" }
+
+ def queue_as(part_name)
+ self.queue_name = part_name.to_s.presence || default_queue_name
+ end
+ end
+
+ included do
+ class_attribute :queue_name
+ self.queue_name = default_queue_name
+ end
+ end
+end
diff --git a/activejob/lib/active_job/railtie.rb b/activejob/lib/active_job/railtie.rb
new file mode 100644
index 0000000000..6538ac1b30
--- /dev/null
+++ b/activejob/lib/active_job/railtie.rb
@@ -0,0 +1,23 @@
+require 'global_id/railtie'
+require 'active_job'
+
+module ActiveJob
+ # = Active Job Railtie
+ class Railtie < Rails::Railtie # :nodoc:
+ config.active_job = ActiveSupport::OrderedOptions.new
+
+ initializer 'active_job.logger' do
+ ActiveSupport.on_load(:active_job) { self.logger = ::Rails.logger }
+ end
+
+ initializer "active_job.set_configs" do |app|
+ options = app.config.active_job
+ options.queue_adapter ||= :inline
+
+ ActiveSupport.on_load(:active_job) do
+ options.each { |k,v| send("#{k}=", v) }
+ end
+ end
+
+ end
+end
diff --git a/activejob/lib/active_job/version.rb b/activejob/lib/active_job/version.rb
new file mode 100644
index 0000000000..7e646fa3c4
--- /dev/null
+++ b/activejob/lib/active_job/version.rb
@@ -0,0 +1,8 @@
+require_relative 'gem_version'
+
+module ActiveJob
+ # Returns the version of the currently loaded ActiveJob as a <tt>Gem::Version</tt>
+ def self.version
+ gem_version
+ end
+end
diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb
new file mode 100644
index 0000000000..78a9c27606
--- /dev/null
+++ b/activejob/lib/rails/generators/job/job_generator.rb
@@ -0,0 +1,22 @@
+require 'rails/generators/named_base'
+
+module Rails
+ module Generators # :nodoc:
+ class JobGenerator < Rails::Generators::NamedBase # :nodoc:
+ desc 'This generator creates an active job file at app/jobs'
+
+ class_option :queue, type: :string, default: 'default', desc: 'The queue name for the generated job'
+
+ def self.default_generator_root
+ File.dirname(__FILE__)
+ end
+
+ check_class_collision suffix: 'Job'
+
+ def create_job_file
+ template 'job.rb', File.join('app/jobs', class_path, "#{file_name}_job.rb")
+ end
+
+ end
+ end
+end
diff --git a/activejob/lib/rails/generators/job/templates/job.rb b/activejob/lib/rails/generators/job/templates/job.rb
new file mode 100644
index 0000000000..462c71d917
--- /dev/null
+++ b/activejob/lib/rails/generators/job/templates/job.rb
@@ -0,0 +1,9 @@
+<% module_namespacing do -%>
+class <%= class_name %>Job < ActiveJob::Base
+ queue_as :<%= options[:queue] %>
+
+ def perform(*args)
+ # Do something later
+ end
+end
+<% end -%>
diff --git a/activejob/test/adapters/backburner.rb b/activejob/test/adapters/backburner.rb
new file mode 100644
index 0000000000..65d05f850b
--- /dev/null
+++ b/activejob/test/adapters/backburner.rb
@@ -0,0 +1,3 @@
+require 'support/backburner/inline'
+
+ActiveJob::Base.queue_adapter = :backburner \ No newline at end of file
diff --git a/activejob/test/adapters/delayed_job.rb b/activejob/test/adapters/delayed_job.rb
new file mode 100644
index 0000000000..afd9c9deb7
--- /dev/null
+++ b/activejob/test/adapters/delayed_job.rb
@@ -0,0 +1,7 @@
+ActiveJob::Base.queue_adapter = :delayed_job
+
+$LOAD_PATH << File.dirname(__FILE__) + "/../support/delayed_job"
+
+Delayed::Worker.delay_jobs = false
+Delayed::Worker.backend = :test
+
diff --git a/activejob/test/adapters/inline.rb b/activejob/test/adapters/inline.rb
new file mode 100644
index 0000000000..e0092552c4
--- /dev/null
+++ b/activejob/test/adapters/inline.rb
@@ -0,0 +1 @@
+ActiveJob::Base.queue_adapter = :inline \ No newline at end of file
diff --git a/activejob/test/adapters/qu.rb b/activejob/test/adapters/qu.rb
new file mode 100644
index 0000000000..7728c843b4
--- /dev/null
+++ b/activejob/test/adapters/qu.rb
@@ -0,0 +1,3 @@
+require 'qu-immediate'
+
+ActiveJob::Base.queue_adapter = :qu
diff --git a/activejob/test/adapters/que.rb b/activejob/test/adapters/que.rb
new file mode 100644
index 0000000000..640061bf54
--- /dev/null
+++ b/activejob/test/adapters/que.rb
@@ -0,0 +1,2 @@
+ActiveJob::Base.queue_adapter = :que
+Que.mode = :sync
diff --git a/activejob/test/adapters/queue_classic.rb b/activejob/test/adapters/queue_classic.rb
new file mode 100644
index 0000000000..ad5ced3cc2
--- /dev/null
+++ b/activejob/test/adapters/queue_classic.rb
@@ -0,0 +1,2 @@
+require 'support/queue_classic/inline'
+ActiveJob::Base.queue_adapter = :queue_classic
diff --git a/activejob/test/adapters/resque.rb b/activejob/test/adapters/resque.rb
new file mode 100644
index 0000000000..af7080358d
--- /dev/null
+++ b/activejob/test/adapters/resque.rb
@@ -0,0 +1,2 @@
+ActiveJob::Base.queue_adapter = :resque
+Resque.inline = true
diff --git a/activejob/test/adapters/sidekiq.rb b/activejob/test/adapters/sidekiq.rb
new file mode 100644
index 0000000000..cd9d2034de
--- /dev/null
+++ b/activejob/test/adapters/sidekiq.rb
@@ -0,0 +1,2 @@
+require 'sidekiq/testing/inline'
+ActiveJob::Base.queue_adapter = :sidekiq
diff --git a/activejob/test/adapters/sneakers.rb b/activejob/test/adapters/sneakers.rb
new file mode 100644
index 0000000000..204166a700
--- /dev/null
+++ b/activejob/test/adapters/sneakers.rb
@@ -0,0 +1,2 @@
+require 'support/sneakers/inline'
+ActiveJob::Base.queue_adapter = :sneakers
diff --git a/activejob/test/adapters/sucker_punch.rb b/activejob/test/adapters/sucker_punch.rb
new file mode 100644
index 0000000000..d2d1712946
--- /dev/null
+++ b/activejob/test/adapters/sucker_punch.rb
@@ -0,0 +1,2 @@
+require 'sucker_punch/testing/inline'
+ActiveJob::Base.queue_adapter = :sucker_punch
diff --git a/activejob/test/cases/adapter_test.rb b/activejob/test/cases/adapter_test.rb
new file mode 100644
index 0000000000..4fc235ae40
--- /dev/null
+++ b/activejob/test/cases/adapter_test.rb
@@ -0,0 +1,8 @@
+require 'helper'
+
+class AdapterTest < ActiveSupport::TestCase
+ test "should load #{ENV['AJADAPTER']} adapter" do
+ ActiveJob::Base.queue_adapter = ENV['AJADAPTER'].to_sym
+ assert_equal ActiveJob::Base.queue_adapter, "active_job/queue_adapters/#{ENV['AJADAPTER']}_adapter".classify.constantize
+ end
+end
diff --git a/activejob/test/cases/callbacks_test.rb b/activejob/test/cases/callbacks_test.rb
new file mode 100644
index 0000000000..9a0657ee89
--- /dev/null
+++ b/activejob/test/cases/callbacks_test.rb
@@ -0,0 +1,22 @@
+require 'helper'
+require 'jobs/callback_job'
+
+require 'active_support/core_ext/object/inclusion'
+
+class CallbacksTest < ActiveSupport::TestCase
+ test 'perform callbacks' do
+ performed_callback_job = CallbackJob.new.tap { |j| j.execute("A-JOB-ID") }
+ assert "CallbackJob ran before_perform".in? performed_callback_job.history
+ assert "CallbackJob ran after_perform".in? performed_callback_job.history
+ assert "CallbackJob ran around_perform_start".in? performed_callback_job.history
+ assert "CallbackJob ran around_perform_stop".in? performed_callback_job.history
+ end
+
+ test 'enqueue callbacks' do
+ enqueued_callback_job = CallbackJob.enqueue
+ assert "CallbackJob ran before_enqueue".in? enqueued_callback_job.history
+ assert "CallbackJob ran after_enqueue".in? enqueued_callback_job.history
+ assert "CallbackJob ran around_enqueue_start".in? enqueued_callback_job.history
+ assert "CallbackJob ran around_enqueue_stop".in? enqueued_callback_job.history
+ end
+end
diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb
new file mode 100644
index 0000000000..fc1b77744c
--- /dev/null
+++ b/activejob/test/cases/job_serialization_test.rb
@@ -0,0 +1,15 @@
+require 'helper'
+require 'jobs/gid_job'
+require 'models/person'
+
+class JobSerializationTest < ActiveSupport::TestCase
+ setup do
+ JobBuffer.clear
+ @person = Person.find(5)
+ end
+
+ test 'serialize job with gid' do
+ GidJob.enqueue @person
+ assert_equal "Person with ID: 5", JobBuffer.last_value
+ end
+end
diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb
new file mode 100644
index 0000000000..888c183a0b
--- /dev/null
+++ b/activejob/test/cases/logging_test.rb
@@ -0,0 +1,96 @@
+require 'helper'
+require "active_support/log_subscriber/test_helper"
+require 'active_support/core_ext/numeric/time'
+require 'jobs/hello_job'
+require 'jobs/logging_job'
+require 'jobs/nested_job'
+
+class AdapterTest < ActiveSupport::TestCase
+ include ActiveSupport::LogSubscriber::TestHelper
+ include ActiveSupport::Logger::Severity
+
+ class TestLogger < ActiveSupport::Logger
+ def initialize
+ @file = StringIO.new
+ super(@file)
+ end
+
+ def messages
+ @file.rewind
+ @file.read
+ end
+ end
+
+ def setup
+ super
+ JobBuffer.clear
+ @old_logger = ActiveJob::Base.logger
+ @logger = ActiveSupport::TaggedLogging.new(TestLogger.new)
+ set_logger @logger
+ ActiveJob::Logging::LogSubscriber.attach_to :active_job
+ end
+
+ def teardown
+ super
+ ActiveJob::Logging::LogSubscriber.log_subscribers.pop
+ ActiveJob::Base.logger = @old_logger
+ end
+
+ def set_logger(logger)
+ ActiveJob::Base.logger = logger
+ end
+
+
+ def test_uses_active_job_as_tag
+ HelloJob.enqueue "Cristian"
+ assert_match(/\[ActiveJob\]/, @logger.messages)
+ end
+
+ def test_enqueue_job_logging
+ HelloJob.enqueue "Cristian"
+ assert_match(/Enqueued HelloJob \(Job ID: .*?\) to .*?:.*Cristian/, @logger.messages)
+ end
+
+ def test_perform_job_logging
+ LoggingJob.enqueue "Dummy"
+ assert_match(/Performing LoggingJob from .*? with arguments:.*Dummy/, @logger.messages)
+ assert_match(/Dummy, here is it: Dummy/, @logger.messages)
+ assert_match(/Performed LoggingJob from .*? in .*ms/, @logger.messages)
+ end
+
+ def test_perform_uses_job_name_job_logging
+ LoggingJob.enqueue "Dummy"
+ assert_match(/\[LoggingJob\]/, @logger.messages)
+ end
+
+ def test_perform_uses_job_id_job_logging
+ LoggingJob.enqueue "Dummy"
+ assert_match(/\[LOGGING-JOB-ID\]/, @logger.messages)
+ end
+
+ def test_perform_nested_jobs_logging
+ NestedJob.enqueue
+ assert_match(/\[LoggingJob\] \[.*?\]/, @logger.messages)
+ assert_match(/\[ActiveJob\] Enqueued NestedJob \(Job ID: .*\) to/, @logger.messages)
+ assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob from/, @logger.messages)
+ assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Enqueued LoggingJob \(Job ID: .*?\) to .* with arguments: "NestedJob"/, @logger.messages)
+ assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob from .* with arguments: "NestedJob"/, @logger.messages)
+ assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Dummy, here is it: NestedJob/, @logger.messages)
+ assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob from .* in/, @logger.messages)
+ assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob from .* in/, @logger.messages)
+ end
+
+ def test_enqueue_at_job_logging
+ HelloJob.enqueue_at 1, "Cristian"
+ assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages)
+ rescue NotImplementedError
+ skip
+ end
+
+ def test_enqueue_in_job_logging
+ HelloJob.enqueue_in 2, "Cristian"
+ assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages)
+ rescue NotImplementedError
+ skip
+ end
+end
diff --git a/activejob/test/cases/parameters_test.rb b/activejob/test/cases/parameters_test.rb
new file mode 100644
index 0000000000..76e8a8059a
--- /dev/null
+++ b/activejob/test/cases/parameters_test.rb
@@ -0,0 +1,77 @@
+require 'helper'
+require 'active_job/arguments'
+require 'models/person'
+require 'active_support/core_ext/hash/indifferent_access'
+
+class ParameterSerializationTest < ActiveSupport::TestCase
+ test 'should make no change to regular values' do
+ assert_equal [ 1, "something" ], ActiveJob::Arguments.serialize([ 1, "something" ])
+ end
+
+ test 'should not allow complex objects' do
+ assert_equal [ nil ], ActiveJob::Arguments.serialize([ nil ])
+ assert_equal [ 1 ], ActiveJob::Arguments.serialize([ 1 ])
+ assert_equal [ 1.0 ], ActiveJob::Arguments.serialize([ 1.0 ])
+ assert_equal [ 'a' ], ActiveJob::Arguments.serialize([ 'a' ])
+ assert_equal [ true ], ActiveJob::Arguments.serialize([ true ])
+ assert_equal [ false ], ActiveJob::Arguments.serialize([ false ])
+ assert_equal [ { "a" => 1, "b" => 2 } ], ActiveJob::Arguments.serialize([ { a: 1, "b" => 2 } ])
+ assert_equal [ [ 1 ] ], ActiveJob::Arguments.serialize([ [ 1 ] ])
+ assert_equal [ 1_000_000_000_000_000_000_000 ], ActiveJob::Arguments.serialize([ 1_000_000_000_000_000_000_000 ])
+
+ err = assert_raises RuntimeError do
+ ActiveJob::Arguments.serialize([ 1, self ])
+ end
+ assert_equal "Unsupported argument type: #{self.class.name}", err.message
+ end
+
+ test 'should dive deep into arrays or hashes' do
+ assert_equal [ { "a" => Person.find(5).gid.to_s }.with_indifferent_access ], ActiveJob::Arguments.serialize([ { a: Person.find(5) } ])
+ assert_equal [ [ Person.find(5).gid.to_s ] ], ActiveJob::Arguments.serialize([ [ Person.find(5) ] ])
+ end
+
+ test 'should dive deep into arrays or hashes and raise exception on complex objects' do
+ err = assert_raises RuntimeError do
+ ActiveJob::Arguments.serialize([ 1, [self] ])
+ end
+ assert_equal "Unsupported argument type: #{self.class.name}", err.message
+ end
+
+ test 'shoud dive deep into hashes and allow raise exception on not string/symbol keys' do
+ err = assert_raises RuntimeError do
+ ActiveJob::Arguments.serialize([ [ { 1 => 2 } ] ])
+ end
+ assert_equal "Unsupported hash key type: Fixnum", err.message
+ end
+
+ test 'should serialize records with global id' do
+ assert_equal [ Person.find(5).gid.to_s ], ActiveJob::Arguments.serialize([ Person.find(5) ])
+ end
+
+ test 'should serialize values and records together' do
+ assert_equal [ 3, Person.find(5).gid.to_s ], ActiveJob::Arguments.serialize([ 3, Person.find(5) ])
+ end
+end
+
+class ParameterDeserializationTest < ActiveSupport::TestCase
+ test 'should make no change to regular values' do
+ assert_equal [ 1, "something" ], ActiveJob::Arguments.deserialize([ 1, "something" ])
+ end
+
+ test 'should deserialize records with global id' do
+ assert_equal [ Person.find(5) ], ActiveJob::Arguments.deserialize([ Person.find(5).gid ])
+ end
+
+ test 'should serialize values and records together' do
+ assert_equal [ 3, Person.find(5) ], ActiveJob::Arguments.deserialize([ 3, Person.find(5).gid ])
+ end
+
+ test 'should dive deep when deserialising arrays' do
+ assert_equal [ [ 3, Person.find(5) ] ], ActiveJob::Arguments.deserialize([ [ 3, Person.find(5).gid ] ])
+ end
+
+ test 'should dive deep when deserialising hashes' do
+ assert_equal [ { "5" => Person.find(5) } ], ActiveJob::Arguments.deserialize([ { "5" => Person.find(5).gid } ])
+ end
+
+end
diff --git a/activejob/test/cases/queue_naming_test.rb b/activejob/test/cases/queue_naming_test.rb
new file mode 100644
index 0000000000..426af608f0
--- /dev/null
+++ b/activejob/test/cases/queue_naming_test.rb
@@ -0,0 +1,23 @@
+require 'helper'
+require 'jobs/hello_job'
+require 'jobs/logging_job'
+require 'jobs/nested_job'
+
+class QueueNamingTest < ActiveSupport::TestCase
+ test 'name derived from base' do
+ assert_equal "default", HelloJob.queue_name
+ end
+
+ test 'name appended in job' do
+ begin
+ HelloJob.queue_as :greetings
+ LoggingJob.queue_as :bookkeeping
+
+ assert_equal "default", NestedJob.queue_name
+ assert_equal "greetings", HelloJob.queue_name
+ assert_equal "bookkeeping", LoggingJob.queue_name
+ ensure
+ HelloJob.queue_name = LoggingJob.queue_name = ActiveJob::Base.default_queue_name
+ end
+ end
+end
diff --git a/activejob/test/cases/queuing_test.rb b/activejob/test/cases/queuing_test.rb
new file mode 100644
index 0000000000..f020316d7e
--- /dev/null
+++ b/activejob/test/cases/queuing_test.rb
@@ -0,0 +1,44 @@
+require 'helper'
+require 'jobs/hello_job'
+require 'active_support/core_ext/numeric/time'
+
+
+class QueuingTest < ActiveSupport::TestCase
+ setup do
+ JobBuffer.clear
+ end
+
+ test 'run queued job' do
+ HelloJob.enqueue
+ assert_equal "David says hello", JobBuffer.last_value
+ end
+
+ test 'run queued job with arguments' do
+ HelloJob.enqueue "Jamie"
+ assert_equal "Jamie says hello", JobBuffer.last_value
+ end
+
+ test 'run queued job later' do
+ begin
+ result = HelloJob.enqueue_at 1.second.ago, "Jamie"
+ assert result
+ rescue NotImplementedError
+ skip
+ end
+ end
+
+ test 'job returned by enqueue has the arguments available' do
+ job = HelloJob.enqueue "Jamie"
+ assert_equal [ "Jamie" ], job.arguments
+ end
+
+
+ test 'job returned by enqueue_at has the timestamp available' do
+ begin
+ job = HelloJob.enqueue_at Time.utc(2014, 1, 1)
+ assert_equal Time.utc(2014, 1, 1), job.enqueued_at
+ rescue NotImplementedError
+ skip
+ end
+ end
+end
diff --git a/activejob/test/cases/rescue_test.rb b/activejob/test/cases/rescue_test.rb
new file mode 100644
index 0000000000..c9473314f2
--- /dev/null
+++ b/activejob/test/cases/rescue_test.rb
@@ -0,0 +1,30 @@
+require 'helper'
+require 'jobs/rescue_job'
+
+require 'active_support/core_ext/object/inclusion'
+
+class RescueTest < ActiveSupport::TestCase
+ setup do
+ JobBuffer.clear
+ end
+
+ test 'rescue perform exception with retry' do
+ job = RescueJob.new
+ job.execute(SecureRandom.uuid, "david")
+ assert_equal [ "rescued from ArgumentError", "performed beautifully" ], JobBuffer.values
+ end
+
+ test 'let through unhandled perform exception' do
+ job = RescueJob.new
+ assert_raises(RescueJob::OtherError) do
+ job.execute(SecureRandom.uuid, "other")
+ end
+ end
+
+ test 'rescue from deserialization errors' do
+ RescueJob.enqueue Person.new(404)
+ assert_includes JobBuffer.values, 'rescued from DeserializationError'
+ assert_includes JobBuffer.values, 'DeserializationError original exception was Person::RecordNotFound'
+ assert_not_includes JobBuffer.values, 'performed beautifully'
+ end
+end
diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb
new file mode 100644
index 0000000000..ca67700273
--- /dev/null
+++ b/activejob/test/helper.rb
@@ -0,0 +1,50 @@
+require File.expand_path('../../../load_paths', __FILE__)
+
+require 'active_job'
+
+GlobalID.app = 'aj'
+
+@adapter = ENV['AJADAPTER'] || 'inline'
+
+def sidekiq?
+ @adapter == 'sidekiq'
+end
+
+def rubinius?
+ RUBY_ENGINE == 'rbx'
+end
+
+def ruby_193?
+ RUBY_VERSION == '1.9.3' && RUBY_ENGINE != 'java'
+end
+
+#Sidekiq don't work with MRI 1.9.3
+#Travis uses rbx 2.6 which don't support unicode characters in methods.
+#Remove the check when Travis change to rbx 2.7+
+exit if sidekiq? && (ruby_193? || rubinius?)
+
+require "adapters/#{@adapter}"
+
+require 'active_support/testing/autorun'
+
+ActiveJob::Base.logger.level = Logger::DEBUG
+
+module JobBuffer
+ class << self
+ def clear
+ @buffer = []
+ end
+
+ def add(value)
+ @buffer << value
+ end
+
+ def values
+ @buffer
+ end
+
+ def last_value
+ @buffer.last
+ end
+ end
+end
diff --git a/activejob/test/jobs/callback_job.rb b/activejob/test/jobs/callback_job.rb
new file mode 100644
index 0000000000..056dd073e8
--- /dev/null
+++ b/activejob/test/jobs/callback_job.rb
@@ -0,0 +1,32 @@
+class CallbackJob < ActiveJob::Base
+ before_perform ->(job) { job.history << "CallbackJob ran before_perform" }
+ after_perform ->(job) { job.history << "CallbackJob ran after_perform" }
+
+ before_enqueue ->(job) { job.history << "CallbackJob ran before_enqueue" }
+ after_enqueue ->(job) { job.history << "CallbackJob ran after_enqueue" }
+
+ around_perform :around_perform
+ around_enqueue :around_enqueue
+
+
+ def perform(person = "david")
+ # NOTHING!
+ end
+
+ def history
+ @history ||= []
+ end
+
+ # FIXME: Not sure why these can't be declared inline like before/after
+ def around_perform
+ history << "CallbackJob ran around_perform_start"
+ yield
+ history << "CallbackJob ran around_perform_stop"
+ end
+
+ def around_enqueue
+ history << "CallbackJob ran around_enqueue_start"
+ yield
+ history << "CallbackJob ran around_enqueue_stop"
+ end
+end
diff --git a/activejob/test/jobs/gid_job.rb b/activejob/test/jobs/gid_job.rb
new file mode 100644
index 0000000000..35c2366ec4
--- /dev/null
+++ b/activejob/test/jobs/gid_job.rb
@@ -0,0 +1,6 @@
+class GidJob < ActiveJob::Base
+ def perform(person)
+ JobBuffer.add("Person with ID: #{person.id}")
+ end
+end
+
diff --git a/activejob/test/jobs/hello_job.rb b/activejob/test/jobs/hello_job.rb
new file mode 100644
index 0000000000..4c6256af0d
--- /dev/null
+++ b/activejob/test/jobs/hello_job.rb
@@ -0,0 +1,5 @@
+class HelloJob < ActiveJob::Base
+ def perform(greeter = "David")
+ JobBuffer.add("#{greeter} says hello")
+ end
+end
diff --git a/activejob/test/jobs/logging_job.rb b/activejob/test/jobs/logging_job.rb
new file mode 100644
index 0000000000..d84ed8589b
--- /dev/null
+++ b/activejob/test/jobs/logging_job.rb
@@ -0,0 +1,10 @@
+class LoggingJob < ActiveJob::Base
+ def perform(dummy)
+ logger.info "Dummy, here is it: #{dummy}"
+ end
+
+ def job_id
+ "LOGGING-JOB-ID"
+ end
+end
+
diff --git a/activejob/test/jobs/nested_job.rb b/activejob/test/jobs/nested_job.rb
new file mode 100644
index 0000000000..fd66f68991
--- /dev/null
+++ b/activejob/test/jobs/nested_job.rb
@@ -0,0 +1,10 @@
+class NestedJob < ActiveJob::Base
+ def perform
+ LoggingJob.enqueue "NestedJob"
+ end
+
+ def job_id
+ "NESTED-JOB-ID"
+ end
+end
+
diff --git a/activejob/test/jobs/rescue_job.rb b/activejob/test/jobs/rescue_job.rb
new file mode 100644
index 0000000000..e9cb37d1c4
--- /dev/null
+++ b/activejob/test/jobs/rescue_job.rb
@@ -0,0 +1,25 @@
+class RescueJob < ActiveJob::Base
+ class OtherError < StandardError; end
+
+ rescue_from(ArgumentError) do
+ JobBuffer.add('rescued from ArgumentError')
+ arguments[0] = "DIFFERENT!"
+ retry_now
+ end
+
+ rescue_from(ActiveJob::DeserializationError) do |e|
+ JobBuffer.add('rescued from DeserializationError')
+ JobBuffer.add("DeserializationError original exception was #{e.original_exception.class.name}")
+ end
+
+ def perform(person = "david")
+ case person
+ when "david"
+ raise ArgumentError, "Hair too good"
+ when "other"
+ raise OtherError
+ else
+ JobBuffer.add('performed beautifully')
+ end
+ end
+end
diff --git a/activejob/test/models/person.rb b/activejob/test/models/person.rb
new file mode 100644
index 0000000000..76a8f40616
--- /dev/null
+++ b/activejob/test/models/person.rb
@@ -0,0 +1,20 @@
+class Person
+ class RecordNotFound < StandardError; end
+
+ include GlobalID::Identification
+
+ attr_reader :id
+
+ def self.find(id)
+ raise RecordNotFound.new("Cannot find person with ID=404") if id.to_i==404
+ new(id)
+ end
+
+ def initialize(id)
+ @id = id
+ end
+
+ def ==(other_person)
+ other_person.is_a?(Person) && id.to_s == other_person.id.to_s
+ end
+end
diff --git a/activejob/test/support/backburner/inline.rb b/activejob/test/support/backburner/inline.rb
new file mode 100644
index 0000000000..f761b53e27
--- /dev/null
+++ b/activejob/test/support/backburner/inline.rb
@@ -0,0 +1,8 @@
+require 'backburner'
+
+Backburner::Worker.class_eval do
+ class << self; alias_method :original_enqueue, :enqueue; end
+ def self.enqueue(job_class, args=[], opts={})
+ job_class.perform(*args)
+ end
+end \ No newline at end of file
diff --git a/activejob/test/support/delayed_job/delayed/backend/test.rb b/activejob/test/support/delayed_job/delayed/backend/test.rb
new file mode 100644
index 0000000000..b50ed36fc2
--- /dev/null
+++ b/activejob/test/support/delayed_job/delayed/backend/test.rb
@@ -0,0 +1,113 @@
+#copied from https://github.com/collectiveidea/delayed_job/blob/master/spec/delayed/backend/test.rb
+require 'ostruct'
+
+# An in-memory backend suitable only for testing. Tries to behave as if it were an ORM.
+module Delayed
+ module Backend
+ module Test
+ class Job
+ attr_accessor :id
+ attr_accessor :priority
+ attr_accessor :attempts
+ attr_accessor :handler
+ attr_accessor :last_error
+ attr_accessor :run_at
+ attr_accessor :locked_at
+ attr_accessor :locked_by
+ attr_accessor :failed_at
+ attr_accessor :queue
+
+ include Delayed::Backend::Base
+
+ cattr_accessor :id
+ self.id = 0
+
+ def initialize(hash = {})
+ self.attempts = 0
+ self.priority = 0
+ self.id = (self.class.id += 1)
+ hash.each{|k,v| send(:"#{k}=", v)}
+ end
+
+ @jobs = []
+ def self.all
+ @jobs
+ end
+
+ def self.count
+ all.size
+ end
+
+ def self.delete_all
+ all.clear
+ end
+
+ def self.create(attrs = {})
+ new(attrs).tap do |o|
+ o.save
+ end
+ end
+
+ def self.create!(*args); create(*args); end
+
+ def self.clear_locks!(worker_name)
+ all.select{|j| j.locked_by == worker_name}.each {|j| j.locked_by = nil; j.locked_at = nil}
+ end
+
+ # Find a few candidate jobs to run (in case some immediately get locked by others).
+ def self.find_available(worker_name, limit = 5, max_run_time = Worker.max_run_time)
+ jobs = all.select do |j|
+ j.run_at <= db_time_now &&
+ (j.locked_at.nil? || j.locked_at < db_time_now - max_run_time || j.locked_by == worker_name) &&
+ !j.failed?
+ end
+
+ jobs = jobs.select{|j| Worker.queues.include?(j.queue)} if Worker.queues.any?
+ jobs = jobs.select{|j| j.priority >= Worker.min_priority} if Worker.min_priority
+ jobs = jobs.select{|j| j.priority <= Worker.max_priority} if Worker.max_priority
+ jobs.sort_by{|j| [j.priority, j.run_at]}[0..limit-1]
+ end
+
+ # Lock this job for this worker.
+ # Returns true if we have the lock, false otherwise.
+ def lock_exclusively!(max_run_time, worker)
+ now = self.class.db_time_now
+ if locked_by != worker
+ # We don't own this job so we will update the locked_by name and the locked_at
+ self.locked_at = now
+ self.locked_by = worker
+ end
+
+ return true
+ end
+
+ def self.db_time_now
+ Time.current
+ end
+
+ def update_attributes(attrs = {})
+ attrs.each{|k,v| send(:"#{k}=", v)}
+ save
+ end
+
+ def destroy
+ self.class.all.delete(self)
+ end
+
+ def save
+ self.run_at ||= Time.current
+
+ self.class.all << self unless self.class.all.include?(self)
+ true
+ end
+
+ def save!; save; end
+
+ def reload
+ reset
+ self
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb b/activejob/test/support/delayed_job/delayed/serialization/test.rb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb
+++ b/activejob/test/support/delayed_job/delayed/serialization/test.rb
diff --git a/activejob/test/support/queue_classic/inline.rb b/activejob/test/support/queue_classic/inline.rb
new file mode 100644
index 0000000000..5e9c295e01
--- /dev/null
+++ b/activejob/test/support/queue_classic/inline.rb
@@ -0,0 +1,11 @@
+require 'queue_classic'
+
+module QC
+ class Queue
+ def enqueue(method, *args)
+ receiver_str, _, message = method.rpartition('.')
+ receiver = eval(receiver_str)
+ receiver.send(message, *args)
+ end
+ end
+end
diff --git a/activejob/test/support/sneakers/inline.rb b/activejob/test/support/sneakers/inline.rb
new file mode 100644
index 0000000000..16d9b830fa
--- /dev/null
+++ b/activejob/test/support/sneakers/inline.rb
@@ -0,0 +1,12 @@
+require 'sneakers'
+
+module Sneakers
+ module Worker
+ module ClassMethods
+ def enqueue(msg)
+ worker = self.new(nil, nil, {})
+ worker.work(*msg)
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index d11243c4c0..ca04f48c1c 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -219,7 +219,7 @@ module ActiveModel
rescue TypeError, NoMethodError
end
- changed_attributes[attr] = value
+ set_attribute_was(attr, value)
end
# Handle <tt>reset_*!</tt> for +method_missing+.
@@ -233,8 +233,22 @@ module ActiveModel
def restore_attribute!(attr)
if attribute_changed?(attr)
__send__("#{attr}=", changed_attributes[attr])
- changed_attributes.delete(attr)
+ clear_attribute_changes([attr])
end
end
+
+ # This is necessary because `changed_attributes` might be overridden in
+ # other implemntations (e.g. in `ActiveRecord`)
+ alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
+
+ # Force an attribute to have a particular "before" value
+ def set_attribute_was(attr, old_value)
+ attributes_changed_by_setter[attr] = old_value
+ end
+
+ # Remove changes information for the provided attributes.
+ def clear_attribute_changes(attributes)
+ attributes_changed_by_setter.except!(*attributes)
+ end
end
end
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 4b70d883fe..b09d9e336b 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,50 @@
+* Fixed an issue where custom accessor methods (such as those generated by
+ `enum`) with the same name as a global method are incorrectly overridden
+ when subclassing.
+
+ Fixes #16288.
+
+ *Godfrey Chan*
+
+* `*_was` and `changes` now work correctly for in-place attribute changes as
+ well.
+
+ *Sean Griffin*
+
+* Fix regression on after_commit that didnt fire when having nested transactions.
+
+ Fixes #16425
+
+ *arthurnn*
+
+* Do not try to write timestamps when a table has no timestamps columns.
+
+ Fixes #8813.
+
+ *Sergey Potapov*
+
+* `index_exists?` with `:name` option does verify specified columns.
+
+ Example:
+
+ add_index :articles, :title, name: "idx_title"
+
+ # Before:
+ index_exists? :articles, :title, name: "idx_title" # => `true`
+ index_exists? :articles, :body, name: "idx_title" # => `true`
+
+ # After:
+ index_exists? :articles, :title, name: "idx_title" # => `true`
+ index_exists? :articles, :body, name: "idx_title" # => `false`
+
+ *Yves Senn*, *Matthew Draper*
+
+* When calling `update_columns` on a record that is not persisted, the error
+ message now reflects whether that object is a new record or has been
+ destroyed.
+
+ *Lachlan Sylvester*
+
* Define `id_was` to get the previous value of the primary key.
Currently when we call id_was and we have a custom primary key name
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 7769966a22..b1069e5dcc 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -146,27 +146,9 @@ task :drop_postgresql_databases => 'db:postgresql:drop'
task :rebuild_postgresql_databases => 'db:postgresql:rebuild'
task :lines do
- lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
-
- FileList["lib/active_record/**/*.rb"].each do |file_name|
- next if file_name =~ /vendor/
- File.open(file_name, 'r') do |f|
- while line = f.gets
- lines += 1
- next if line =~ /^\s*$/
- next if line =~ /^\s*#/
- codelines += 1
- end
- end
- puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
-
- total_lines += lines
- total_codelines += codelines
-
- lines, codelines = 0, 0
- end
-
- puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
+ load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics'
+ files = FileList["lib/active_record/**/*.rb"]
+ CodeTools::LineStatistics.new(files).print_loc
end
spec = eval(File.read('activerecord.gemspec'))
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index 8075008574..6231851be5 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -24,5 +24,5 @@ Gem::Specification.new do |s|
s.add_dependency 'activesupport', version
s.add_dependency 'activemodel', version
- s.add_dependency 'arel', '~> 6.0.0'
+ s.add_dependency 'arel', '>= 6.0.0.beta1', '< 6.1'
end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index c39183ab31..d3b9b8251a 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1131,7 +1131,7 @@ module ActiveRecord
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
# * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>)
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
#
# === Options
# [:class_name]
@@ -1209,7 +1209,7 @@ module ActiveRecord
# Option examples:
# has_many :comments, -> { order "posted_on" }
# has_many :comments, -> { includes :author }
- # has_many :people, -> { where("deleted = 0").order("name") }, class_name: "Person"
+ # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
# has_many :tracks, -> { order "position" }, dependent: :destroy
# has_many :comments, dependent: :nullify
# has_many :tags, as: :taggable
@@ -1259,7 +1259,7 @@ module ActiveRecord
#
# === Options
#
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
#
# Options are:
# [:class_name]
@@ -1364,7 +1364,7 @@ module ActiveRecord
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
# * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>)
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
#
# === Options
#
@@ -1435,7 +1435,7 @@ module ActiveRecord
# belongs_to :firm, foreign_key: "client_of"
# belongs_to :person, primary_key: "name", foreign_key: "person_name"
# belongs_to :author, class_name: "Person", foreign_key: "author_id"
- # belongs_to :valid_coupon, ->(o) { where "discounts > #{o.payments_count}" },
+ # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count },
# class_name: "Coupon", foreign_key: "coupon_id"
# belongs_to :attachable, polymorphic: true
# belongs_to :project, readonly: true
@@ -1541,7 +1541,7 @@ module ActiveRecord
# * <tt>Developer#projects.exists?(...)</tt>
# * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>)
# * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>)
- # The declaration may include an options hash to specialize the behavior of the association.
+ # The declaration may include an +options+ hash to specialize the behavior of the association.
#
# === Options
#
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 79c3d2b0f5..1413efaf7f 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -103,7 +103,7 @@ module ActiveRecord
if has_cached_counter?(reflection)
counter = cached_counter_attribute_name(reflection)
owner[counter] += difference
- owner.changed_attributes.delete(counter) # eww
+ owner.send(:clear_attribute_changes, counter) # eww
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 1fed7f74e7..d57da366bd 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -63,7 +63,7 @@ module ActiveRecord
should_reset = (through_scope != through_reflection.klass.unscoped) ||
(reflection.options[:source_type] && through_reflection.collection?)
- # Dont cache the association - we would only be caching a subset
+ # Don't cache the association - we would only be caching a subset
if should_reset
owners.each { |owner|
owner.association(association_name).reset
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
index 6d38224830..8cc1904575 100644
--- a/activerecord/lib/active_record/attribute.rb
+++ b/activerecord/lib/active_record/attribute.rb
@@ -30,10 +30,14 @@ module ActiveRecord
def value
# `defined?` is cheaper than `||=` when we get back falsy values
- @value = type_cast(value_before_type_cast) unless defined?(@value)
+ @value = original_value unless defined?(@value)
@value
end
+ def original_value
+ type_cast(value_before_type_cast)
+ end
+
def value_for_database
type.type_cast_for_database(value)
end
@@ -54,7 +58,7 @@ module ActiveRecord
self.class.from_database(name, value, type)
end
- def type_cast
+ def type_cast(*)
raise NotImplementedError
end
@@ -62,6 +66,13 @@ module ActiveRecord
true
end
+ def ==(other)
+ self.class == other.class &&
+ name == other.name &&
+ value_before_type_cast == other.value_before_type_cast &&
+ type == other.type
+ end
+
protected
def initialize_dup(other)
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index a2bb78dfcc..1ff28ceccc 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -57,6 +57,8 @@ module ActiveRecord
end
end
+ class GeneratedAttributeMethods < Module; end # :nodoc:
+
module ClassMethods
def inherited(child_class) #:nodoc:
child_class.initialize_generated_modules
@@ -64,7 +66,7 @@ module ActiveRecord
end
def initialize_generated_modules # :nodoc:
- @generated_attribute_methods = Module.new { extend Mutex_m }
+ @generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m }
@attribute_methods_generated = false
include @generated_attribute_methods
end
@@ -113,10 +115,11 @@ module ActiveRecord
if superclass == Base
super
else
- # If B < A and A defines its own attribute method, then we don't want to overwrite that.
- defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods)
- base_defined = Base.method_defined?(method_name) || Base.private_method_defined?(method_name)
- defined && !base_defined || super
+ # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
+ # defines its own attribute method, then we don't want to overwrite that.
+ defined = method_defined_within?(method_name, superclass, Base) &&
+ ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
+ defined || super
end
end
@@ -279,9 +282,9 @@ module ActiveRecord
end
# Returns an <tt>#inspect</tt>-like string for the value of the
- # attribute +attr_name+. String attributes are truncated upto 50
+ # attribute +attr_name+. String attributes are truncated up to 50
# characters, Date and Time attributes are returned in the
- # <tt>:db</tt> format, Array attributes are truncated upto 10 values.
+ # <tt>:db</tt> format, Array attributes are truncated up to 10 values.
# Other attributes return the value of <tt>#inspect</tt> without
# modification.
#
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index b58295a106..d3f4e51c33 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -51,14 +51,6 @@ module ActiveRecord
super | changed_in_place
end
- def attribute_changed?(attr_name, options = {})
- result = super
- # We can't change "from" something in place. Only setters can define
- # "from" and "to"
- result ||= changed_in_place?(attr_name) unless options.key?(:from)
- result
- end
-
def changes_applied
super
store_original_raw_attributes
@@ -69,12 +61,16 @@ module ActiveRecord
original_raw_attributes.clear
end
+ def changed_attributes
+ super.reverse_merge(attributes_changed_in_place).freeze
+ end
+
private
def calculate_changes_from_defaults
@changed_attributes = nil
self.class.column_defaults.each do |attr, orig_value|
- changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value)
+ set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value)
end
end
@@ -100,9 +96,9 @@ module ActiveRecord
def save_changed_attribute(attr, old_value)
if attribute_changed?(attr)
- changed_attributes.delete(attr) unless _field_changed?(attr, old_value)
+ clear_attribute_changes(attr) unless _field_changed?(attr, old_value)
else
- changed_attributes[attr] = old_value if _field_changed?(attr, old_value)
+ set_attribute_was(attr, old_value) if _field_changed?(attr, old_value)
end
end
@@ -132,6 +128,13 @@ module ActiveRecord
@attributes[attr].changed_from?(old_value)
end
+ def attributes_changed_in_place
+ changed_in_place.each_with_object({}) do |attr_name, h|
+ orig = @attributes[attr_name].original_value
+ h[attr_name] = orig
+ end
+ end
+
def changed_in_place
self.class.attribute_names.select do |attr_name|
changed_in_place?(attr_name)
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 e44ccb7d81..9e07e9a5c4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -56,6 +56,19 @@ module ActiveRecord
end
end
+ module TimestampDefaultDeprecation # :nodoc:
+ def emit_warning_if_null_unspecified(options)
+ return if options.key?(:null)
+
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
+ `timestamp` was called without specifying an option for `null`. In Rails
+ 5.0, this behavior will change to `null: false`. You should manually
+ specify `null: true` to prevent the behavior of your existing migrations
+ from changing.
+ MESSAGE
+ end
+ end
+
# Represents the schema of an SQL table in an abstract way. This class
# provides methods for manipulating the schema representation.
#
@@ -77,6 +90,8 @@ module ActiveRecord
# The table definitions
# The Columns are stored as a ColumnDefinition in the +columns+ attribute.
class TableDefinition
+ include TimestampDefaultDeprecation
+
# An array of ColumnDefinition objects, representing the column changes
# that have been defined.
attr_accessor :indexes
@@ -276,6 +291,7 @@ module ActiveRecord
# <tt>:updated_at</tt> to the table.
def timestamps(*args)
options = args.extract_options!
+ emit_warning_if_null_unspecified(options)
column(:created_at, :datetime, options)
column(:updated_at, :datetime, options)
end
@@ -405,6 +421,8 @@ module ActiveRecord
# end
#
class Table
+ include TimestampDefaultDeprecation
+
def initialize(table_name, base)
@table_name = table_name
@base = base
@@ -452,8 +470,9 @@ module ActiveRecord
# Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps
#
# t.timestamps
- def timestamps
- @base.add_timestamps(@table_name)
+ def timestamps(options = {})
+ emit_warning_if_null_unspecified(options)
+ @base.add_timestamps(@table_name, options)
end
# Changes the column's definition according to the new options.
@@ -559,6 +578,5 @@ module ActiveRecord
@base.native_database_types
end
end
-
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 10753defc2..7105df1ee4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -43,13 +43,14 @@ module ActiveRecord
# index_exists?(:suppliers, :company_id, name: "idx_company_id")
#
def index_exists?(table_name, column_name, options = {})
- column_names = Array(column_name)
- index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names)
- if options[:unique]
- indexes(table_name).any?{ |i| i.unique && i.name == index_name }
- else
- indexes(table_name).any?{ |i| i.name == index_name }
- end
+ column_names = Array(column_name).map(&:to_s)
+ index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names)
+ checks = []
+ checks << lambda { |i| i.name == index_name }
+ checks << lambda { |i| i.columns == column_names }
+ checks << lambda { |i| i.unique } if options[:unique]
+
+ indexes(table_name).any? { |i| checks.all? { |check| check[i] } }
end
# Returns an array of Column objects for the table specified by +table_name+.
@@ -838,9 +839,9 @@ module ActiveRecord
#
# add_timestamps(:suppliers)
#
- def add_timestamps(table_name)
- add_column table_name, :created_at, :datetime
- add_column table_name, :updated_at, :datetime
+ def add_timestamps(table_name, options = {})
+ add_column table_name, :created_at, :datetime, options
+ add_column table_name, :updated_at, :datetime, options
end
# Removes the timestamp columns (+created_at+ and +updated_at+) from the table definition.
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 4a7f2aaca8..8f06cf3a1f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -111,6 +111,8 @@ module ActiveRecord
def commit
super
connection.release_savepoint(savepoint_name)
+ parent = connection.transaction_manager.current_transaction
+ records.each { |r| parent.add_record(r) }
end
def full_rollback?; false; end
@@ -169,16 +171,14 @@ module ActiveRecord
transaction = begin_transaction options
yield
rescue Exception => error
- transaction.rollback if transaction
+ rollback_transaction if transaction
raise
ensure
begin
- transaction.commit unless error
+ commit_transaction unless error
rescue Exception
transaction.rollback
raise
- ensure
- @stack.pop if transaction
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 e5417a9556..a1c370b05d 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -764,8 +764,8 @@ module ActiveRecord
"DROP INDEX #{index_name}"
end
- def add_timestamps_sql(table_name)
- [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]
+ def add_timestamps_sql(table_name, options = {})
+ [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)]
end
def remove_timestamps_sql(table_name)
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 34ed32ad35..380c50fc14 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
@@ -12,7 +12,7 @@ module ActiveRecord
# roundtripping 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
- # consitent with our encoder's output.
+ # consistent with our encoder's output.
raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value))
super(raw_old_value, new_value)
end
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index f0ee433d0b..5958373e88 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -145,11 +145,11 @@ module ActiveRecord
value = read_attribute(attr_name)
if attribute_changed?(attr_name)
if mapping[old] == value
- changed_attributes.delete(attr_name)
+ clear_attribute_changes([attr_name])
end
else
if old != value
- changed_attributes[attr_name] = mapping.key old
+ set_attribute_was(attr_name, mapping.key(old))
end
end
else
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index b5799d6bff..755ff2b2f1 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -287,7 +287,8 @@ module ActiveRecord
# This method raises an +ActiveRecord::ActiveRecordError+ when called on new
# objects, or when at least one of the attributes is marked as readonly.
def update_columns(attributes)
- raise ActiveRecordError, "cannot update on a new record object" unless persisted?
+ raise ActiveRecordError, "cannot update a new record" if new_record?
+ raise ActiveRecordError, "cannot update a destroyed record" if destroyed?
attributes.each_key do |key|
verify_readonly_attribute(key.to_s)
@@ -465,7 +466,7 @@ module ActiveRecord
changes[self.class.locking_column] = increment_lock if locking_enabled?
- changed_attributes.except!(*changes.keys)
+ clear_attribute_changes(changes.keys)
primary_key = self.class.primary_key
self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1
else
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 1262b2c291..c8f382ad78 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -952,9 +952,7 @@ WARNING
self.bind_values += bind_values
attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts)
- attributes.values.grep(ActiveRecord::Relation) do |rel|
- self.bind_values += rel.bind_values
- end
+ add_relations_to_bind_values(attributes)
PredicateBuilder.build_from_hash(klass, attributes, table)
else
@@ -1137,5 +1135,19 @@ WARNING
raise ArgumentError, "The method .#{method_name}() must contain arguments."
end
end
+
+ # This function is recursive just for better readablity.
+ # #where argument doesn't support more than one level nested hash in real world.
+ def add_relations_to_bind_values(attributes)
+ if attributes.is_a?(Hash)
+ attributes.each_value do |value|
+ if value.is_a?(ActiveRecord::Relation)
+ self.bind_values += value.bind_values
+ else
+ add_relations_to_bind_values(value)
+ end
+ end
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index 3d02ee07d0..ce1de4b76e 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -54,7 +54,7 @@ module ActiveRecord
command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}"
raise 'Error dumping database' unless Kernel.system(command)
- File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" }
+ File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" }
end
def structure_load(filename)
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index ddf3e1804c..936a18d99a 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -47,8 +47,9 @@ module ActiveRecord
current_time = current_time_from_proper_timezone
all_timestamp_attributes.each do |column|
- if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil?
- write_attribute(column.to_s, current_time)
+ column = column.to_s
+ if has_attribute?(column) && !attribute_present?(column)
+ write_attribute(column, current_time)
end
end
end
@@ -113,7 +114,7 @@ module ActiveRecord
def clear_timestamp_attributes
all_timestamp_attributes_in_model.each do |attribute_name|
self[attribute_name] = nil
- changed_attributes.delete(attribute_name)
+ clear_attribute_changes([attribute_name])
end
end
end
diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb
index e0a783fb45..9456a4a56c 100644
--- a/activerecord/lib/active_record/type/value.rb
+++ b/activerecord/lib/active_record/type/value.rb
@@ -69,13 +69,20 @@ module ActiveRecord
end
# Determines whether the mutable value has been modified since it was
- # read. Returns +false+ by default. This method should not need to be
- # overriden directly. Types which return a mutable value should include
+ # read. Returns +false+ by default. This method should not be overridden
+ # directly. Types which return a mutable value should include
# +Type::Mutable+, which will define this method.
def changed_in_place?(*)
false
end
+ def ==(other)
+ self.class == other.class &&
+ precision == other.precision &&
+ scale == other.scale &&
+ limit == other.limit
+ end
+
private
def type_cast(value)
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb
index fd94a2d038..f7bf6987c4 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb
@@ -9,7 +9,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration
<% end -%>
<% end -%>
<% if options[:timestamps] %>
- t.timestamps
+ t.timestamps null: false
<% end -%>
end
<% attributes_with_index.each do |attribute| -%>
diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
index 7c0f11b033..a84673e452 100644
--- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
@@ -105,7 +105,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
with_real_execute do
begin
ActiveRecord::Base.connection.create_table :delete_me do |t|
- t.timestamps
+ t.timestamps null: true
end
ActiveRecord::Base.connection.remove_timestamps :delete_me
assert !column_present?('delete_me', 'updated_at', 'datetime')
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
index b0759dffde..a7b0addc1b 100644
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql/connection_test.rb
@@ -150,7 +150,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
- def test_mysql_sql_mode_variable_overides_strict_mode
+ def test_mysql_sql_mode_variable_overrides_strict_mode
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode'
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index cefc3e3c7e..49cfafd7a5 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -105,7 +105,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
with_real_execute do
begin
ActiveRecord::Base.connection.create_table :delete_me do |t|
- t.timestamps
+ t.timestamps null: true
end
ActiveRecord::Base.connection.remove_timestamps :delete_me
assert !column_present?('delete_me', 'updated_at', 'datetime')
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 3b35e69e0d..beedb4f3a1 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -76,7 +76,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
- def test_mysql_sql_mode_variable_overides_strict_mode
+ def test_mysql_sql_mode_variable_overrides_strict_mode
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode'
diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
index 3614b29190..eb32c4d2c2 100644
--- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
@@ -87,7 +87,7 @@ class TimestampTest < ActiveRecord::TestCase
def test_timestamps_helper_with_custom_precision
ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :precision => 4
+ t.timestamps :null => true, :precision => 4
end
assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision')
assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision')
@@ -95,7 +95,7 @@ class TimestampTest < ActiveRecord::TestCase
def test_passing_precision_to_timestamp_does_not_set_limit
ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :precision => 4
+ t.timestamps :null => true, :precision => 4
end
assert_nil activerecord_column_option("foos", "created_at", "limit")
assert_nil activerecord_column_option("foos", "updated_at", "limit")
@@ -104,14 +104,14 @@ class TimestampTest < ActiveRecord::TestCase
def test_invalid_timestamp_precision_raises_error
assert_raises ActiveRecord::ActiveRecordError do
ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :precision => 7
+ t.timestamps :null => true, :precision => 7
end
end
end
def test_postgres_agrees_with_activerecord_about_precision
ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :precision => 4
+ t.timestamps :null => true, :precision => 4
end
assert_equal '4', pg_datetime_precision('foos', 'created_at')
assert_equal '4', pg_datetime_precision('foos', 'updated_at')
diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
index 8700b20dee..3f5858714a 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -14,6 +14,7 @@ if ActiveRecord::Base.connection.supports_migrations?
@connection.drop_table :fruits rescue nil
@connection.drop_table :nep_fruits rescue nil
@connection.drop_table :nep_schema_migrations rescue nil
+ @connection.drop_table :has_timestamps rescue nil
ActiveRecord::SchemaMigration.delete_all rescue nil
end
@@ -88,5 +89,61 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017")
assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947")
end
+
+ def test_timestamps_without_null_is_deprecated_on_create_table
+ assert_deprecated do
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps do |t|
+ t.timestamps
+ end
+ end
+ end
+ end
+
+ def test_timestamps_without_null_is_deprecated_on_change_table
+ assert_deprecated do
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps do |t|
+ t.timestamps
+ end
+ end
+ end
+ end
+
+ def test_no_deprecation_warning_from_timestamps_on_create_table
+ assert_not_deprecated do
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps do |t|
+ t.timestamps null: true
+ end
+
+ drop_table :has_timestamps
+
+ create_table :has_timestamps do |t|
+ t.timestamps null: false
+ end
+ end
+ end
+ end
+
+ def test_no_deprecation_warning_from_timestamps_on_change_table
+ assert_not_deprecated do
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+ change_table :has_timestamps do |t|
+ t.timestamps null: true
+ end
+
+ drop_table :has_timestamps
+
+ create_table :has_timestamps
+ change_table :has_timestamps do |t|
+ t.timestamps null: false, default: Time.now
+ end
+ end
+ end
+ end
end
end
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 cc58a4a1a2..859310575e 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
@@ -254,7 +254,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_build
devel = Developer.find(1)
- proj = assert_no_queries { devel.projects.build("name" => "Projekt") }
+ proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") }
assert !devel.projects.loaded?
assert_equal devel.projects.last, proj
@@ -269,7 +269,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_new_aliased_to_build
devel = Developer.find(1)
- proj = assert_no_queries { devel.projects.new("name" => "Projekt") }
+ proj = assert_no_queries(ignore_none: false) { devel.projects.new("name" => "Projekt") }
assert !devel.projects.loaded?
assert_equal devel.projects.last, proj
@@ -503,7 +503,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer = project.developers.first
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert project.developers.loaded?
assert project.developers.include?(developer)
end
@@ -824,7 +824,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations
projects = Developer.new.projects
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal [], projects
assert_equal [], projects.where(title: 'omg')
assert_equal [], projects.pluck(:title)
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index ab67cf4085..b4917e727a 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -263,7 +263,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
assert_equal klass.column_names, klass.new.attributes.keys
- assert_not klass.new.attributes.key?('id')
+ assert_not klass.new.has_attribute?('id')
end
def test_hashes_not_mangled
@@ -810,6 +810,24 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "lol", topic.author_name
end
+ def test_inherited_custom_accessors_with_reserved_names
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'computers'
+ self.abstract_class = true
+ def system; "omg"; end
+ def system=(val); self.developer = val; end
+ end
+
+ subklass = Class.new(klass)
+ [klass, subklass].each(&:define_attribute_methods)
+
+ computer = subklass.find(1)
+ assert_equal "omg", computer.system
+
+ computer.developer = 99
+ assert_equal 99, computer.developer
+ end
+
def test_on_the_fly_super_invokable_generated_attribute_methods_via_method_missing
klass = new_topic_like_ar_class do
def title
diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb
index 91f6aee931..7b325abf1d 100644
--- a/activerecord/test/cases/attribute_test.rb
+++ b/activerecord/test/cases/attribute_test.rb
@@ -138,5 +138,35 @@ module ActiveRecord
test "uninitialized attributes have no value" do
assert_nil Attribute.uninitialized(:foo, nil).value
end
+
+ test "attributes equal other attributes with the same constructor arguments" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:foo, 1, Type::Integer.new)
+ assert_equal first, second
+ end
+
+ test "attributes do not equal attributes with different names" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:bar, 1, Type::Integer.new)
+ assert_not_equal first, second
+ end
+
+ test "attributes do not equal attributes with different types" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:foo, 1, Type::Float.new)
+ assert_not_equal first, second
+ end
+
+ test "attributes do not equal attributes with different values" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:foo, 2, Type::Integer.new)
+ assert_not_equal first, second
+ end
+
+ test "attributes do not equal attributes of other classes" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_user(:foo, 1, Type::Integer.new)
+ assert_not_equal first, second
+ end
end
end
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 69a7f25213..0c77eedb52 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -661,6 +661,27 @@ class DirtyTest < ActiveRecord::TestCase
assert_not model.foo_changed?
end
+ test "in place mutation detection" do
+ pirate = Pirate.create!(catchphrase: "arrrr")
+ pirate.catchphrase << " matey!"
+
+ assert pirate.catchphrase_changed?
+ expected_changes = {
+ "catchphrase" => ["arrrr", "arrrr matey!"]
+ }
+ assert_equal(expected_changes, pirate.changes)
+ assert_equal("arrrr", pirate.catchphrase_was)
+ assert pirate.catchphrase_changed?(from: "arrrr")
+ assert_not pirate.catchphrase_changed?(from: "anything else")
+ assert pirate.changed_attributes.include?(:catchphrase)
+
+ pirate.save!
+ pirate.reload
+
+ assert_equal "arrrr matey!", pirate.catchphrase
+ assert_not pirate.changed?
+ end
+
private
def with_partial_writes(klass, on = true)
old = klass.partial_writes?
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 40e51a0cdc..95d006279d 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -51,7 +51,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_symbols_table_ref
- Post.first # warm up
+ Post.where("author_id" => nil) # warm up
x = Symbol.all_symbols.count
Post.where("title" => {"xxxqqqq" => "bar"})
assert_equal x, Symbol.all_symbols.count
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index c66eaf1ee1..bd3dd29f4d 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -176,8 +176,11 @@ module ActiveRecord
end
def test_create_table_with_timestamps_should_create_datetime_columns
- connection.create_table table_name do |t|
- t.timestamps
+ # FIXME: Remove the silence when we change the default `null` behavior
+ ActiveSupport::Deprecation.silence do
+ connection.create_table table_name do |t|
+ t.timestamps
+ end
end
created_columns = connection.columns(table_name)
diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb
index 3e9d957ed3..777a48ad14 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -88,8 +88,8 @@ module ActiveRecord
def test_timestamps_creates_updated_at_and_created_at
with_change_table do |t|
- @connection.expect :add_timestamps, nil, [:delete_me]
- t.timestamps
+ @connection.expect :add_timestamps, nil, [:delete_me, null: true]
+ t.timestamps null: true
end
end
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index 4e6d7963aa..e6aa901814 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -66,6 +66,9 @@ module ActiveRecord
def test_mysql_rename_column_preserves_auto_increment
rename_column "test_models", "id", "id_test"
assert_equal "auto_increment", connection.columns("test_models").find { |c| c.name == "id_test" }.extra
+ TestModel.reset_column_information
+ ensure
+ rename_column "test_models", "id_test", "id"
end
end
diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb
index e28feedcf9..4dad77e8fd 100644
--- a/activerecord/test/cases/migration/helper.rb
+++ b/activerecord/test/cases/migration/helper.rb
@@ -22,7 +22,7 @@ module ActiveRecord
super
@connection = ActiveRecord::Base.connection
connection.create_table :test_models do |t|
- t.timestamps
+ t.timestamps null: true
end
TestModel.reset_column_information
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
index 93c3bfae7a..ac932378fd 100644
--- a/activerecord/test/cases/migration/index_test.rb
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -95,6 +95,12 @@ module ActiveRecord
assert connection.index_exists?(:testings, [:foo, :bar])
end
+ def test_index_exists_with_custom_name_checks_columns
+ connection.add_index :testings, [:foo, :bar], name: "my_index"
+ assert connection.index_exists?(:testings, [:foo, :bar], name: "my_index")
+ assert_not connection.index_exists?(:testings, [:foo], name: "my_index")
+ end
+
def test_valid_index_options
assert_raise ArgumentError do
connection.add_index :testings, :foo, unqiue: true
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index ef3f073472..11338e1fb6 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -561,7 +561,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
t.string :qualification, :experience
t.integer :age, :default => 0
t.date :birthdate
- t.timestamps
+ t.timestamps null: true
end
end
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index a6a36a6fd9..b4804aa9d7 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -61,6 +61,15 @@ module ActiveRecord
assert_equal expected.to_sql, actual.to_sql
end
+ def test_belongs_to_nested_where_with_relation
+ author = authors(:david)
+
+ expected = Author.where(id: author ).joins(:posts)
+ actual = Author.where(posts: { author_id: Author.where(id: author.id) }).joins(:posts)
+
+ assert_equal expected.to_a, actual.to_a
+ end
+
def test_polymorphic_shallow_where
treasure = Treasure.new
treasure.id = 1
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 4e71d04bc0..066e6b6468 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -2,6 +2,8 @@ require "cases/helper"
require 'support/schema_dumping_helper'
class SchemaDumperTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
setup do
ActiveRecord::SchemaMigration.create_table
end
@@ -21,6 +23,8 @@ class SchemaDumperTest < ActiveRecord::TestCase
schema_info = ActiveRecord::Base.connection.dump_schema_information
assert_match(/20100201010101.*20100301010101/m, schema_info)
+ ensure
+ ActiveRecord::SchemaMigration.delete_all
end
def test_magic_comment
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index d8a467ec4d..8e512e118a 100644
--- a/activerecord/test/cases/scoping/relation_scoping_test.rb
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -11,6 +11,10 @@ require 'models/reference'
class RelationScopingTest < ActiveRecord::TestCase
fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects
+ setup do
+ developers(:david)
+ end
+
def test_reverse_order
assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order
end
@@ -260,7 +264,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase
end
end
-class HasManyScopingTest< ActiveRecord::TestCase
+class HasManyScopingTest < ActiveRecord::TestCase
fixtures :comments, :posts, :people, :references
def setup
@@ -306,7 +310,7 @@ class HasManyScopingTest< ActiveRecord::TestCase
end
end
-class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase
+class HasAndBelongsToManyScopingTest < ActiveRecord::TestCase
fixtures :posts, :categories, :categories_posts
def setup
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index 23a170388e..4070216733 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -93,7 +93,7 @@ module ActiveRecord
# ignored SQL, or better yet, use a different notification for the queries
# instead examining the SQL content.
oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
- mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i]
+ mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /]
postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im]
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index 0472246f71..abf6becc17 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -1,4 +1,5 @@
require 'cases/helper'
+require 'support/ddl_helper'
require 'models/developer'
require 'models/owner'
require 'models/pet'
@@ -424,3 +425,21 @@ class TimestampTest < ActiveRecord::TestCase
assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model)
end
end
+
+class TimestampsWithoutTransactionTest < ActiveRecord::TestCase
+ include DdlHelper
+ self.use_transactional_fixtures = false
+
+ class TimestampAttributePost < ActiveRecord::Base
+ attr_accessor :created_at, :updated_at
+ end
+
+ def test_do_not_write_timestamps_on_save_if_they_are_not_attributes
+ with_example_table ActiveRecord::Base.connection, "timestamp_attribute_posts", "id integer primary key" do
+ post = TimestampAttributePost.new(id: 1)
+ post.save! # should not try to assign and persist created_at, updated_at
+ assert_nil post.created_at
+ assert_nil post.updated_at
+ end
+ end
+end
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index 3d64ecb464..a3f39804b7 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -129,6 +129,19 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert_equal [:commit_on_update], @first.history
end
+ def test_only_call_after_commit_on_top_level_transactions
+ @first.after_commit_block{|r| r.history << :after_commit}
+ assert @first.history.empty?
+
+ @first.transaction do
+ @first.transaction(requires_new: true) do
+ @first.touch
+ end
+ assert @first.history.empty?
+ end
+ assert_equal [:after_commit], @first.history
+ end
+
def test_call_after_rollback_after_transaction_rollsback
@first.after_commit_block{|r| r.history << :after_commit}
@first.after_rollback_block{|r| r.history << :after_rollback}
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index b4849222b8..9cfe041de2 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -80,6 +80,30 @@ class TransactionTest < ActiveRecord::TestCase
end
end
+ def test_number_of_transactions_in_commit
+ num = nil
+
+ Topic.connection.class_eval do
+ alias :real_commit_db_transaction :commit_db_transaction
+ define_method(:commit_db_transaction) do
+ num = transaction_manager.open_transactions
+ real_commit_db_transaction
+ end
+ end
+
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ end
+
+ assert_equal 0, num
+ ensure
+ Topic.connection.class_eval do
+ remove_method :commit_db_transaction
+ alias :commit_db_transaction :real_commit_db_transaction rescue nil
+ end
+ end
+
def test_successful_with_instance_method
@first.transaction do
@first.approved = true
diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb
index 5c54812f30..db4f78d354 100644
--- a/activerecord/test/cases/types_test.rb
+++ b/activerecord/test/cases/types_test.rb
@@ -149,6 +149,12 @@ module ActiveRecord
end
end
+ def test_type_equality
+ assert_equal Type::Value.new, Type::Value.new
+ assert_not_equal Type::Value.new, Type::Integer.new
+ assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2)
+ end
+
if current_adapter?(:SQLite3Adapter)
def test_binary_encoding
type = SQLite3Binary.new
diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb
index 3db742c15b..268d7914b5 100644
--- a/activerecord/test/cases/validations/i18n_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_validation_test.rb
@@ -6,6 +6,7 @@ class I18nValidationTest < ActiveRecord::TestCase
repair_validations(Topic, Reply)
def setup
+ repair_validations(Topic, Reply)
Reply.validates_presence_of(:title)
@topic = Topic.new
@old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb
index c02b3241cd..2bbf0f23b3 100644
--- a/activerecord/test/cases/validations_repair_helper.rb
+++ b/activerecord/test/cases/validations_repair_helper.rb
@@ -13,7 +13,7 @@ module ActiveRecord
end
def repair_validations(*model_classes)
- yield
+ yield if block_given?
ensure
model_classes.each do |k|
k.clear_validators!
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index a8b21904ac..98f2492ef8 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -138,7 +138,7 @@ ActiveRecord::Schema.define do
t.integer :engines_count
t.integer :wheels_count
t.column :lock_version, :integer, null: false, default: 0
- t.timestamps
+ t.timestamps null: false
end
create_table :categories, force: true do |t|
@@ -537,7 +537,7 @@ ActiveRecord::Schema.define do
t.references :best_friend_of
t.integer :insures, null: false, default: 0
t.timestamp :born_at
- t.timestamps
+ t.timestamps null: false
end
create_table :peoples_treasures, id: false, force: true do |t|
@@ -548,7 +548,7 @@ ActiveRecord::Schema.define do
create_table :pets, primary_key: :pet_id, force: true do |t|
t.string :name
t.integer :owner_id, :integer
- t.timestamps
+ t.timestamps null: false
end
create_table :pirates, force: true do |t|
@@ -726,13 +726,13 @@ ActiveRecord::Schema.define do
t.string :parent_title
t.string :type
t.string :group
- t.timestamps
+ t.timestamps null: true
end
create_table :toys, primary_key: :toy_id, force: true do |t|
t.string :name
t.integer :pet_id, :integer
- t.timestamps
+ t.timestamps null: false
end
create_table :traffic_lights, force: true do |t|
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 03e8d73ea7..96bce53999 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,14 @@
+* Fix rounding errors with #travel_to by resetting the usec on any passed time to zero, so we only travel
+ with per-second precision, not anything deeper than that.
+
+ *DHH*
+
+* Fix ActiveSupport::TestCase not to order users' test cases by default.
+ If this change breaks your tests because your tests are order dependent, you need to explicitly call
+ ActiveSupport::TestCase.my_tests_are_order_dependent! at the top of your tests.
+
+ *Akira Matsuda*
+
* Fix DateTime comparison with DateTime::Infinity object.
*Rafael Mendonça França*
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index a3f672d4cc..ff67a6828c 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -237,7 +237,7 @@ module ActiveSupport
# seconds. Because of extended life of the previous cache, other processes
# will continue to use slightly stale data for a just a bit longer. In the
# meantime that first process will go ahead and will write into cache the
- # new value. After that all the processes will start getting new value.
+ # new value. After that all the processes will start getting the new value.
# The key is to keep <tt>:race_condition_ttl</tt> small.
#
# If the process regenerating the entry errors out, the entry will be
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index 18ba79a8f9..53022de549 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -303,8 +303,8 @@ module ActiveSupport
def safe_constantize(camel_cased_word)
constantize(camel_cased_word)
rescue NameError => e
- raise unless e.message =~ /(uninitialized constant|wrong constant name) #{const_regexp(camel_cased_word)}$/ ||
- e.name.to_s == camel_cased_word.to_s
+ raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
+ e.name.to_s == camel_cased_word.to_s)
rescue ArgumentError => e
raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/
end
diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb
index b019ad0dec..92ab6fe648 100644
--- a/activesupport/lib/active_support/message_encryptor.rb
+++ b/activesupport/lib/active_support/message_encryptor.rb
@@ -40,6 +40,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>:digest</tt> - String of digest to use for signing. Default is +SHA1+.
# * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
def initialize(secret, *signature_key_or_options)
options = signature_key_or_options.extract_options!
@@ -47,7 +48,7 @@ module ActiveSupport
@secret = secret
@sign_secret = sign_secret
@cipher = options[:cipher] || 'aes-256-cbc'
- @verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer)
+ @verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer)
@serializer = options[:serializer] || Marshal
end
diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb
index 62caff77a3..89009d1f55 100644
--- a/activesupport/lib/active_support/multibyte/unicode.rb
+++ b/activesupport/lib/active_support/multibyte/unicode.rb
@@ -336,7 +336,7 @@ module ActiveSupport
begin
@codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, 'rb') { |f| Marshal.load f.read }
rescue => e
- raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable")
+ raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable")
end
# Redefine the === method so we can write shorter rules for grapheme cluster breaks
@@ -368,6 +368,7 @@ module ActiveSupport
private
def apply_mapping(string, mapping) #:nodoc:
+ database.codepoints
string.each_codepoint.map do |codepoint|
cp = database.codepoints[codepoint]
if cp and (ncp = cp.send(mapping)) and ncp > 0
@@ -385,7 +386,6 @@ module ActiveSupport
def database
@database ||= UnicodeDatabase.new
end
-
end
end
end
diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb
index a6a878140c..0df599b692 100644
--- a/activesupport/lib/active_support/test_case.rb
+++ b/activesupport/lib/active_support/test_case.rb
@@ -15,16 +15,11 @@ module ActiveSupport
class TestCase < ::Minitest::Test
Assertion = Minitest::Assertion
- alias_method :method_name, :name
-
- $tags = {}
- def self.for_tag(tag)
- yield if $tags[tag]
+ class << self
+ alias :my_tests_are_order_dependent! :i_suck_and_my_tests_are_order_dependent!
end
- # FIXME: we have tests that depend on run order, we should fix that and
- # remove this method call.
- self.i_suck_and_my_tests_are_order_dependent!
+ alias_method :method_name, :name
include ActiveSupport::Testing::TaggedLogging
include ActiveSupport::Testing::SetupAndTeardown
diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb
index eefa84262e..1112c6e0b1 100644
--- a/activesupport/lib/active_support/testing/time_helpers.rb
+++ b/activesupport/lib/active_support/testing/time_helpers.rb
@@ -78,6 +78,10 @@ module ActiveSupport
# or <tt>Date.today</tt>, in order to honor the application time zone
# please always use <tt>Time.current</tt> and <tt>Date.current</tt>.)
#
+ # Note that the usec for the time passed will be set to 0 to prevent rounding
+ # errors with external services, like MySQL (which will round instead of floor,
+ # leading to off-by-one-second errors).
+ #
# This method also accepts a block, which will return the current time back to its original
# state at the end of the block:
#
@@ -90,7 +94,7 @@ module ActiveSupport
if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime)
now = date_or_time.midnight.to_time
else
- now = date_or_time.to_time
+ now = date_or_time.to_time.change(usec: 0)
end
simple_stubs.stub_object(Time, :now, now)
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index a013aadd67..5fc3de651a 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -47,18 +47,22 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_tracking_loaded_files
- require_dependency 'dependencies/service_one'
- require_dependency 'dependencies/service_two'
- assert_equal 2, ActiveSupport::Dependencies.loaded.size
+ with_loading do
+ require_dependency 'dependencies/service_one'
+ require_dependency 'dependencies/service_two'
+ assert_equal 2, ActiveSupport::Dependencies.loaded.size
+ end
ensure
Object.send(:remove_const, :ServiceOne) if Object.const_defined?(:ServiceOne)
Object.send(:remove_const, :ServiceTwo) if Object.const_defined?(:ServiceTwo)
end
def test_tracking_identical_loaded_files
- require_dependency 'dependencies/service_one'
- require_dependency 'dependencies/service_one'
- assert_equal 1, ActiveSupport::Dependencies.loaded.size
+ with_loading do
+ require_dependency 'dependencies/service_one'
+ require_dependency 'dependencies/service_one'
+ assert_equal 1, ActiveSupport::Dependencies.loaded.size
+ end
ensure
Object.send(:remove_const, :ServiceOne) if Object.const_defined?(:ServiceOne)
end
@@ -990,11 +994,4 @@ class DependenciesTest < ActiveSupport::TestCase
ensure
ActiveSupport::Dependencies.hook!
end
-
-private
- def remove_constants(*constants)
- constants.each do |constant|
- Object.send(:remove_const, constant) if Object.const_defined?(constant)
- end
- end
end
diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb
index 659fceb852..73f640e8e5 100644
--- a/activesupport/test/multibyte_chars_test.rb
+++ b/activesupport/test/multibyte_chars_test.rb
@@ -3,19 +3,12 @@ require 'abstract_unit'
require 'multibyte_test_helpers'
require 'active_support/core_ext/string/multibyte'
-class String
- def __method_for_multibyte_testing_with_integer_result; 1; end
- def __method_for_multibyte_testing; 'result'; end
- def __method_for_multibyte_testing!; 'result'; end
- def __method_for_multibyte_testing_that_returns_nil!; end
-end
-
class MultibyteCharsTest < ActiveSupport::TestCase
include MultibyteTestHelpers
def setup
@proxy_class = ActiveSupport::Multibyte::Chars
- @chars = @proxy_class.new UNICODE_STRING
+ @chars = @proxy_class.new UNICODE_STRING.dup
end
def test_wraps_the_original_string
@@ -24,6 +17,8 @@ class MultibyteCharsTest < ActiveSupport::TestCase
end
def test_should_allow_method_calls_to_string
+ @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing; 'result'; end }
+
assert_nothing_raised do
@chars.__method_for_multibyte_testing
end
@@ -33,28 +28,36 @@ class MultibyteCharsTest < ActiveSupport::TestCase
end
def test_forwarded_method_calls_should_return_new_chars_instance
+ @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing; 'result'; end }
+
assert_kind_of @proxy_class, @chars.__method_for_multibyte_testing
assert_not_equal @chars.object_id, @chars.__method_for_multibyte_testing.object_id
end
def test_forwarded_bang_method_calls_should_return_the_original_chars_instance_when_result_is_not_nil
+ @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing!; 'result'; end }
+
assert_kind_of @proxy_class, @chars.__method_for_multibyte_testing!
assert_equal @chars.object_id, @chars.__method_for_multibyte_testing!.object_id
end
def test_forwarded_bang_method_calls_should_return_nil_when_result_is_nil
+ @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing_that_returns_nil!; end }
+
assert_nil @chars.__method_for_multibyte_testing_that_returns_nil!
end
def test_methods_are_forwarded_to_wrapped_string_for_byte_strings
original_encoding = BYTE_STRING.encoding
assert_equal BYTE_STRING.length, BYTE_STRING.mb_chars.length
- ensure
- BYTE_STRING.force_encoding(original_encoding)
end
def test_forwarded_method_with_non_string_result_should_be_returned_vertabim
- assert_equal ''.__method_for_multibyte_testing_with_integer_result, @chars.__method_for_multibyte_testing_with_integer_result
+ str = ''
+ str.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end }
+ @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end }
+
+ assert_equal str.__method_for_multibyte_testing_with_integer_result, @chars.__method_for_multibyte_testing_with_integer_result
end
def test_should_concatenate
@@ -103,7 +106,6 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
@chars = UNICODE_STRING.dup.mb_chars
# Ruby 1.9 only supports basic whitespace
@whitespace = "\n\t "
- @byte_order_mark = [65279].pack('U')
end
def test_split_should_return_an_array_of_chars_instances
diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb
index fdbe2f4350..90af2dadd4 100644
--- a/activesupport/test/multibyte_test_helpers.rb
+++ b/activesupport/test/multibyte_test_helpers.rb
@@ -1,9 +1,9 @@
# encoding: utf-8
module MultibyteTestHelpers
- UNICODE_STRING = 'こにちわ'
- ASCII_STRING = 'ohayo'
- BYTE_STRING = "\270\236\010\210\245".force_encoding("ASCII-8BIT")
+ UNICODE_STRING = 'こにちわ'.freeze
+ ASCII_STRING = 'ohayo'.freeze
+ BYTE_STRING = "\270\236\010\210\245".force_encoding("ASCII-8BIT").freeze
def chars(str)
ActiveSupport::Multibyte::Chars.new(str)
diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_test.rb
index 6f63a8a725..b74bc9bf20 100644
--- a/activesupport/test/test_test.rb
+++ b/activesupport/test/test_test.rb
@@ -188,7 +188,7 @@ class TimeHelperTest < ActiveSupport::TestCase
expected_time = Time.now + 1.day
travel 1.day
- assert_equal expected_time, Time.now
+ assert_equal expected_time.to_s(:db), Time.now.to_s(:db)
assert_equal expected_time.to_date, Date.today
end
@@ -196,11 +196,11 @@ class TimeHelperTest < ActiveSupport::TestCase
expected_time = Time.now + 1.day
travel 1.day do
- assert_equal expected_time, Time.now
+ assert_equal expected_time.to_s(:db), Time.now.to_s(:db)
assert_equal expected_time.to_date, Date.today
end
- assert_not_equal expected_time, Time.now
+ assert_not_equal expected_time.to_s(:db), Time.now.to_s(:db)
assert_not_equal expected_time.to_date, Date.today
end
@@ -235,4 +235,11 @@ class TimeHelperTest < ActiveSupport::TestCase
assert_not_equal expected_time, Time.now
assert_not_equal Date.new(2004, 11, 24), Date.today
end
+
+ def test_travel_to_will_reset_the_usec_to_avoid_mysql_rouding
+ travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do
+ assert_equal 50, Time.now.sec
+ assert_equal 0, Time.now.usec
+ end
+ end
end
diff --git a/ci/travis.rb b/ci/travis.rb
index 956a01dbee..db6e41ecfa 100755
--- a/ci/travis.rb
+++ b/ci/travis.rb
@@ -21,7 +21,8 @@ class Build
'amo' => 'activemodel',
'as' => 'activesupport',
'ar' => 'activerecord',
- 'av' => 'actionview'
+ 'av' => 'actionview',
+ 'aj' => 'activejob'
}
attr_reader :component, :options
diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md
index a39dd9ace0..e8d1dc361a 100644
--- a/guides/source/4_2_release_notes.md
+++ b/guides/source/4_2_release_notes.md
@@ -94,6 +94,12 @@ Action Pack
Please refer to the [Changelog][action-pack] for detailed changes.
+### Removals
+
+* Removed deprecated `AbstractController::Helpers::ClassMethods::MissingHelperError`
+ in favor of `AbstractController::Helpers::MissingHelperError`.
+ ([Commit](https://github.com/rails/rails/commit/a1ddde15ae0d612ff2973de9cf768ed701b594e8))
+
### Deprecations
* Deprecated support for setting the `:to` option of a router to a symbol or a
diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md
new file mode 100644
index 0000000000..ae5d21d546
--- /dev/null
+++ b/guides/source/active_job_basics.md
@@ -0,0 +1,253 @@
+Active Job Basics
+=================
+
+This guide provides you with all you need to get started in creating,
+enqueueing and executing background jobs.
+
+After reading this guide, you will know:
+
+* How to create jobs.
+* How to enqueue jobs.
+* How to run jobs in the background.
+* How to send emails from your application async.
+
+--------------------------------------------------------------------------------
+
+Introduction
+------------
+
+Active Job is a framework for declaring jobs and making them run on a variety
+of queueing backends. These jobs can be everything from regularly scheduled
+clean-ups, billing charges, or mailings. Anything that can be chopped up
+into small units of work and run in parallel, really.
+
+
+The Purpose of the Active Job
+-----------------------------
+The main point is to ensure that all Rails apps will have a job infrastructure
+in place, even if it's in the form of an "immediate runner". We can then have
+framework features and other gems build on top of that, without having to
+worry about API differences between various job runners such as Delayed Job
+and Resque. Picking your queuing backend becomes more of an operational concern,
+then. And you'll be able to switch between them without having to rewrite your jobs.
+
+
+Creating a Job
+--------------
+
+This section will provide a step-by-step guide to creating a job and enqueue it.
+
+### Create the Job
+
+Active Job provides a Rails generator to create jobs. The following will create a
+job in app/jobs:
+
+```bash
+$ bin/rails generate job guests_cleanup
+create app/jobs/guests_cleanup_job.rb
+```
+
+You can also create a job that will run on a specific queue:
+
+```bash
+$ bin/rails generate job guests_cleanup --queue urgent
+create app/jobs/guests_cleanup_job.rb
+```
+
+As you can see, you can generate jobs just like you use other generators with
+Rails.
+
+If you don't want to use a generator, you could create your own file inside of
+app/jobs, just make sure that it inherits from `ActiveJob::Base`.
+
+Here's how a job looks like:
+
+```ruby
+class GuestsCleanupJob < ActiveJob::Base
+ queue_as :default
+
+ def perform
+ # Do something later
+ end
+end
+```
+
+### Enqueue the Job
+
+Enqueue a job like so:
+
+```ruby
+MyJob.enqueue record # Enqueue a job to be performed as soon the queueing system is free.
+```
+
+```ruby
+MyJob.enqueue_at Date.tomorrow.noon, record # Enqueue a job to be performed tomorrow at noon.
+```
+
+```ruby
+MyJob.enqueue_in 1.week, record # Enqueue a job to be performed 1 week from now.
+```
+
+That's it!
+
+
+Job Execution
+-------------
+
+If not adapter is set, the job is immediately executed.
+
+### Backends
+
+Active Job has adapters for the following queueing backends:
+
+* [Backburner](https://github.com/nesquena/backburner)
+* [Delayed Job](https://github.com/collectiveidea/delayed_job)
+* [Qu](https://github.com/bkeepers/qu)
+* [Que](https://github.com/chanks/que)
+* [QueueClassic](https://github.com/ryandotsmith/queue_classic)
+* [Resque 1.x](https://github.com/resque/resque)
+* [Sidekiq](https://github.com/mperham/sidekiq)
+* [Sneakers](https://github.com/jondot/sneakers)
+* [Sucker Punch](https://github.com/brandonhilkert/sucker_punch)
+
+#### Backends Features
+
+| | Async | Queues | Delayed | Priorities | Timeout | Retries |
+|-----------------------|-------|---------|---------|-------------|---------|---------|
+| **Backburner** | Yes | Yes | Yes | Yes | Job | Global |
+| **Delayed Job** | Yes | Yes | Yes | Job | Global | Global |
+| **Que** | Yes | Yes | Yes | Job | No | Job |
+| **Queue Classic** | Yes | Yes | Gem | No | No | No |
+| **Resque** | Yes | Yes | Gem | Queue | Global | ? |
+| **Sidekiq** | Yes | Yes | Yes | Queue | No | Job |
+| **Sneakers** | Yes | Yes | No | Queue | Queue | No |
+| **Sucker Punch** | Yes | Yes | Yes | No | No | No |
+| **Active Job** | Yes | Yes | WIP | No | No | No |
+| **Active Job Inline** | No | Yes | N/A | N/A | N/A | N/A |
+
+### Change Backends
+
+You can easy change your adapter:
+
+```ruby
+# be sure to have the adapter gem in your Gemfile and follow the adapter specific
+# installation and deployment instructions
+YourApp::Application.config.active_job.queue_adapter = :sidekiq
+```
+
+Queues
+------
+
+Most of the adapters supports multiple queues. With Active Job you can schedule the job
+to run on a specific queue:
+
+```ruby
+class GuestsCleanupJob < ActiveJob::Base
+ queue_as :low_priority
+ #....
+end
+```
+
+NOTE: Make sure your queueing backend "listens" on your queue name. For some backends
+you need to specify the queues to listen to.
+
+
+Callbacks
+---------
+
+Active Job provides hooks during the lifecycle of a job. Callbacks allows you to trigger
+logic during the lifecycle of a job.
+
+### Available callbacks
+
+* before_enqueue
+* around_enqueue
+* after_enqueue
+* before_perform
+* around_perform
+* after_perform
+
+### Usage
+
+```ruby
+class GuestsCleanupJob < ActiveJob::Base
+ queue_as :default
+
+ before_enqueue do |job|
+ # do somthing with the job instance
+ end
+
+ around_perform do |job, block|
+ # do something before perform
+ block.call
+ # do something after perform
+ end
+
+ def perform
+ # Do something later
+ end
+end
+```
+
+ActionMailer
+------------
+One of the most common jobs in a modern web application is sending emails outside
+of the request-response cycle, so the user doesn't have to wait on it. Active Job
+is integrated with Action Mailer so you can easily send emails async:
+
+```ruby
+# Instead of the classic
+UserMailer.welcome(@user).deliver
+
+# use #deliver later to send the email async
+UserMailer.welcome(@user).deliver_later
+```
+
+GlobalID
+--------
+Active Job supports GlobalID for parameters. This makes it possible
+to pass live Active Record objects to your job instead of class/id pairs, which
+you then have to manually deserialize. Before, jobs would look like this:
+
+```ruby
+class TrashableCleanupJob
+ def perform(trashable_class, trashable_id, depth)
+ trashable = trashable_class.constantize.find(trashable_id)
+ trashable.cleanup(depth)
+ end
+end
+```
+
+Now you can simply do:
+
+```ruby
+class TrashableCleanupJob
+ def perform(trashable, depth)
+ trashable.cleanup(depth)
+ end
+end
+```
+
+This works with any class that mixes in ActiveModel::GlobalIdentification, which
+by default has been mixed into Active Model classes.
+
+
+Exceptions
+----------
+Active Job provides a way to catch exceptions raised during the execution of the
+job:
+
+```ruby
+
+class GuestsCleanupJob < ActiveJob::Base
+ queue_as :default
+
+ rescue_from(ActiveRecord:NotFound) do |exception|
+ # do something with the exception
+ end
+
+ def perform
+ # Do something later
+ end
+end
+```
diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md
index 0902e347e2..d0f3a596fe 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -353,7 +353,12 @@ Instead of an options hash, you can also simply pass in a model, Rails will use
class ProductsController < ApplicationController
def show
@product = Product.find(params[:id])
- respond_with(@product) if stale?(@product)
+
+ if stale?(@product)
+ respond_to do |wants|
+ # ... normal response processing
+ end
+ end
end
end
```
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index 0b05725623..8bc4b10591 100644
--- a/guides/source/contributing_to_ruby_on_rails.md
+++ b/guides/source/contributing_to_ruby_on_rails.md
@@ -397,7 +397,7 @@ inside, just indent it with 4 spaces:
class ArticlesController
def index
- respond_with Article.limit(10)
+ render json: Article.limit(10)
end
end
diff --git a/guides/source/engines.md b/guides/source/engines.md
index e9cce3f159..24548a5b01 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -136,7 +136,7 @@ following to the dummy application's routes file at
`test/dummy/config/routes.rb`:
```ruby
-mount Blorgh::Engine, at: "blorgh"
+mount Blorgh::Engine => "/blorgh"
```
### Inside an Engine
@@ -173,7 +173,7 @@ Within `lib/blorgh/engine.rb` is the base class for the engine:
```ruby
module Blorgh
- class Engine < Rails::Engine
+ class Engine < ::Rails::Engine
isolate_namespace Blorgh
end
end
diff --git a/guides/source/generators.md b/guides/source/generators.md
index 5e88fa0c70..2349908979 100644
--- a/guides/source/generators.md
+++ b/guides/source/generators.md
@@ -8,6 +8,7 @@ After reading this guide, you will know:
* How to see which generators are available in your application.
* How to create a generator using templates.
* How Rails searches for generators before invoking them.
+* How Rails internally generates Rails code from the templates.
* How to customize your scaffold by creating new generators.
* How to customize your scaffold by changing generator templates.
* How to use fallbacks to avoid overwriting a huge set of generators.
@@ -340,6 +341,20 @@ end
If you generate another resource, you can see that we get exactly the same result! This is useful if you want to customize your scaffold templates and/or layout by just creating `edit.html.erb`, `index.html.erb` and so on inside `lib/templates/erb/scaffold`.
+Many scaffold templates in Rails are written in ERB tags which needs to be escaped, so that the output is a valid ERB code, that can be used correctly in Rails app.
+
+The following code in one of the generator file,
+
+```ruby
+<%%= stylesheet_include_tag :application %>
+```
+
+when passed through the generator, would generate the following output.
+
+```ruby
+<%= stylesheet_include_tag :application %>
+```
+
Adding Generators Fallbacks
---------------------------
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index f00f7bca1b..8b37b92139 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -903,7 +903,7 @@ You can also specify multiple videos to play by passing an array of videos to th
This will produce:
```erb
-<video><source src="trailer.ogg" /><source src="movie.ogg" /></video>
+<video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
```
#### Linking to Audio Files with the `audio_tag`
diff --git a/guides/source/plugins.md b/guides/source/plugins.md
index 489bd227cd..f10699fbeb 100644
--- a/guides/source/plugins.md
+++ b/guides/source/plugins.md
@@ -440,5 +440,5 @@ $ bin/rake rdoc
* [Developing a RubyGem using Bundler](https://github.com/radar/guides/blob/master/gem-development.md)
* [Using .gemspecs as Intended](http://yehudakatz.com/2010/04/02/using-gemspecs-as-intended/)
-* [Gemspec Reference](http://docs.rubygems.org/read/chapter/20)
+* [Gemspec Reference](http://guides.rubygems.org/specification-reference/)
* [GemPlugins: A Brief Introduction to the Future of Rails Plugins](http://www.intridea.com/blog/2008/6/11/gemplugins-a-brief-introduction-to-the-future-of-rails-plugins)
diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md
index f0230b428b..6206b3c715 100644
--- a/guides/source/ruby_on_rails_guides_guidelines.md
+++ b/guides/source/ruby_on_rails_guides_guidelines.md
@@ -13,17 +13,17 @@ After reading this guide, you will know:
Markdown
-------
-Guides are written in [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown). There is comprehensive [documentation for Markdown](http://daringfireball.net/projects/markdown/syntax), a [cheatsheet](http://daringfireball.net/projects/markdown/basics).
+Guides are written in [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown). There is comprehensive [documentation for Markdown](http://daringfireball.net/projects/markdown/syntax), as well as a [cheatsheet](http://daringfireball.net/projects/markdown/basics).
Prologue
--------
-Each guide should start with motivational text at the top (that's the little introduction in the blue area). The prologue should tell the reader what the guide is about, and what they will learn. See for example the [Routing Guide](routing.html).
+Each guide should start with motivational text at the top (that's the little introduction in the blue area). The prologue should tell the reader what the guide is about, and what they will learn. As an example, see the [Routing Guide](routing.html).
-Titles
+Headings
------
-The title of every guide uses `h1`; guide sections use `h2`; subsections `h3`; etc. However, the generated HTML output will have the heading tag starting from `<h2>`.
+The title of every guide uses an `h1` heading; guide sections use `h2` headings; subsections use `h3` headings; etc. Note that the generated HTML output will use heading tags starting with `<h2>`.
```
Guide Title
@@ -35,14 +35,14 @@ Section
### Sub Section
```
-Capitalize all words except for internal articles, prepositions, conjunctions, and forms of the verb to be:
+When writing headings, capitalize all words except for prepositions, conjunctions, internal articles, and forms of the verb "to be":
```
#### Middleware Stack is an Array
#### When are Objects Saved?
```
-Use the same typography as in regular text:
+Use the same inline formatting as regular text:
```
##### The `:content_type` Option
@@ -51,25 +51,23 @@ Use the same typography as in regular text:
API Documentation Guidelines
----------------------------
-The guides and the API should be coherent and consistent where appropriate. Please have a look at these particular sections of the [API Documentation Guidelines](api_documentation_guidelines.html):
+The guides and the API should be coherent and consistent where appropriate. In particular, these sections of the [API Documentation Guidelines](api_documentation_guidelines.html) also apply to the guides:
* [Wording](api_documentation_guidelines.html#wording)
* [Example Code](api_documentation_guidelines.html#example-code)
-* [Filenames](api_documentation_guidelines.html#filenames)
+* [Filenames](api_documentation_guidelines.html#file-names)
* [Fonts](api_documentation_guidelines.html#fonts)
-Those guidelines apply also to guides.
-
HTML Guides
-----------
Before generating the guides, make sure that you have the latest version of Bundler installed on your system. As of this writing, you must install Bundler 1.3.5 on your device.
-To install the latest version of Bundler, simply run the `gem install bundler` command
+To install the latest version of Bundler, run `gem install bundler`.
### Generation
-To generate all the guides, just `cd` into the `guides` directory, run `bundle install` and execute:
+To generate all the guides, just `cd` into the `guides` directory, run `bundle install`, and execute:
```
bundle exec rake guides:generate
diff --git a/guides/source/security.md b/guides/source/security.md
index ebfcc5bdd0..125dd82666 100644
--- a/guides/source/security.md
+++ b/guides/source/security.md
@@ -118,9 +118,9 @@ It works like this:
* A user receives credits, the amount is stored in a session (which is a bad idea anyway, but we'll do this for demonstration purposes).
* The user buys something.
-* Their new, lower credit will be stored in the session.
-* The dark side of the user forces them to take the cookie from the first step (which they copied) and replace the current cookie in the browser.
-* The user has their credit back.
+* The new adjusted credit value is stored in the session.
+* The user takes the cookie from the first step (which they previously copied) and replaces the current cookie in the browser.
+* The user has their original credit back.
Including a nonce (a random value) in the session solves replay attacks. A nonce is valid only once, and the server has to keep track of all the valid nonces. It gets even more complicated if you have several application servers (mongrels). Storing nonces in a database table would defeat the entire purpose of CookieStore (avoiding accessing the database).
@@ -847,7 +847,7 @@ It is recommended to _use RedCloth in combination with a whitelist input filter_
NOTE: _The same security precautions have to be taken for Ajax actions as for "normal" ones. There is at least one exception, however: The output has to be escaped in the controller already, if the action doesn't render a view._
-If you use the [in_place_editor plugin](http://dev.rubyonrails.org/browser/plugins/in_place_editing), or actions that return a string, rather than rendering a view, _you have to escape the return value in the action_. Otherwise, if the return value contains a XSS string, the malicious code will be executed upon return to the browser. Escape any input value using the h() method.
+If you use the [in_place_editor plugin](https://rubygems.org/gems/in_place_editing), or actions that return a string, rather than rendering a view, _you have to escape the return value in the action_. Otherwise, if the return value contains a XSS string, the malicious code will be executed upon return to the browser. Escape any input value using the h() method.
### Command Line Injection
diff --git a/guides/source/testing.md b/guides/source/testing.md
index 3e12afc000..2ecd560a87 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -605,13 +605,13 @@ end
Testing the response to your request by asserting the presence of key HTML elements and their content is a useful way to test the views of your application. The `assert_select` assertion allows you to do this by using a simple yet powerful syntax.
-NOTE: You may find references to `assert_tag` in other documentation, but this is now deprecated in favor of `assert_select`.
+NOTE: You may find references to `assert_tag` in other documentation. This has been removed in 4.2. Use `assert_select` instead.
There are two forms of `assert_select`:
-`assert_select(selector, [equality], [message])` ensures that the equality condition is met on the selected elements through the selector. The selector may be a CSS selector expression (String), an expression with substitution values, or an `HTML::Selector` object.
+`assert_select(selector, [equality], [message])` ensures that the equality condition is met on the selected elements through the selector. The selector may be a CSS selector expression (String) or an expression with substitution values.
-`assert_select(element, selector, [equality], [message])` ensures that the equality condition is met on all the selected elements through the selector starting from the _element_ (instance of `HTML::Node`) and its descendants.
+`assert_select(element, selector, [equality], [message])` ensures that the equality condition is met on all the selected elements through the selector starting from the _element_ (instance of `Nokogiri::XML::Node` or `Nokogiri::XML::NodeSet`) and its descendants.
For example, you could verify the contents on the title element in your response with:
@@ -641,7 +641,7 @@ assert_select "ol" do
end
```
-The `assert_select` assertion is quite powerful. For more advanced usage, refer to its [documentation](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/SelectorAssertions.html).
+The `assert_select` assertion is quite powerful. For more advanced usage, refer to its [documentation](https://github.com/rails/rails-dom-testing/blob/master/lib/rails/dom/testing/assertions/selector_assertions.rb).
#### Additional View-Based Assertions
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index 62e4071935..799d5f3bc9 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -54,7 +54,8 @@ NOTE: This section is a work in progress.
### Serialized attributes
-When assigning `nil` to a serialized attribute, it will be saved to the database
+When using a custom coder (e.g. `serialize :metadata, JSON`),
+assigning `nil` to a serialized attribute will save it to the database
as `NULL` instead of passing the `nil` value through the coder (e.g. `"null"`
when using the `JSON` coder).
@@ -90,6 +91,38 @@ after_bundle do
end
```
+### Rails Html Sanitizer
+
+There's a new choice for sanitizing HTML fragments in your applications. The
+venerable html-scanner approach is now officially being deprecated in favor of
+[`Rails Html Sanitizer`](https://github.com/rails/rails-html-sanitizer).
+
+This means the methods `sanitize`, `sanitize_css`, `strip_tags` and
+`strip_links` are backed by a new implementation.
+
+In the next major Rails version `Rails Html Sanitizer` will be the default
+sanitizer. It already is for new applications.
+
+Include this in your Gemfile to try it out today:
+
+```ruby
+gem 'rails-html-sanitizer'
+```
+
+This new sanitizer uses [Loofah](https://github.com/flavorjones/loofah) internally. Loofah in turn uses Nokogiri, which
+wraps XML parsers written in both C and Java, so sanitization should be faster
+no matter which Ruby version you run.
+
+The new version updates `sanitize`, so it can take a `Loofah::Scrubber` for
+powerful scrubbing.
+[See some examples of scrubbers here](https://github.com/flavorjones/loofah#loofahscrubber).
+
+Two new scrubbers have also been added: `PermitScrubber` and `TargetScrubber`.
+Read the [gem's readme](https://github.com/rails/rails-html-sanitizer) for more information.
+
+The documentation for `PermitScrubber` and `TargetScrubber` explains how you
+can gain complete control over when and how elements should be stripped.
+
Upgrading from Rails 4.0 to Rails 4.1
-------------------------------------
@@ -624,6 +657,9 @@ Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must rep
* Rails 4.0 has changed `serialized_attributes` and `attr_readonly` to class methods only. You shouldn't use instance methods since it's now deprecated. You should change them to use class methods, e.g. `self.serialized_attributes` to `self.class.serialized_attributes`.
+* When using the default coder, assigning `nil` to a serialized attribute will save it
+to the database as `NULL` instead of passing the `nil` value through YAML (`"--- \n...\n"`).
+
* Rails 4.0 has removed `attr_accessible` and `attr_protected` feature in favor of Strong Parameters. You can use the [Protected Attributes gem](https://github.com/rails/protected_attributes) for a smooth upgrade path.
* If you are not using Protected Attributes, you can remove any options related to
diff --git a/install.rb b/install.rb
index bff8fee934..96e4153165 100644
--- a/install.rb
+++ b/install.rb
@@ -5,7 +5,7 @@ if version.nil?
exit(64)
end
-%w( activesupport activemodel activerecord actionpack actionview actionmailer railties ).each do |framework|
+%w( activesupport activemodel activerecord actionpack actionview actionmailer railties activejob ).each do |framework|
puts "Installing #{framework}..."
`cd #{framework} && gem build #{framework}.gemspec && gem install #{framework}-#{version}.gem --no-ri --no-rdoc && rm #{framework}-#{version}.gem`
end
diff --git a/rails.gemspec b/rails.gemspec
index 4800df0df4..b1a7d16722 100644
--- a/rails.gemspec
+++ b/rails.gemspec
@@ -24,6 +24,7 @@ Gem::Specification.new do |s|
s.add_dependency 'activemodel', version
s.add_dependency 'activerecord', version
s.add_dependency 'actionmailer', version
+ s.add_dependency 'activejob', version
s.add_dependency 'railties', version
s.add_dependency 'bundler', '>= 1.3.0', '< 2.0'
diff --git a/railties/lib/rails/all.rb b/railties/lib/rails/all.rb
index 2e83c0fe14..45361fca83 100644
--- a/railties/lib/rails/all.rb
+++ b/railties/lib/rails/all.rb
@@ -5,6 +5,7 @@ require "rails"
action_controller
action_view
action_mailer
+ active_job
rails/test_unit
sprockets
).each do |framework|
diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb
index 3e32576040..4d49244807 100644
--- a/railties/lib/rails/api/task.rb
+++ b/railties/lib/rails/api/task.rb
@@ -50,6 +50,13 @@ module Rails
)
},
+ 'activejob' => {
+ :include => %w(
+ README.md
+ lib/active_job/**/*.rb
+ )
+ },
+
'railties' => {
:include => %w(
README.rdoc
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index 61639be7c6..6a4660bad0 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -256,7 +256,8 @@ 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.cookies_serializer" => config.action_dispatch.cookies_serializer
+ "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer,
+ "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest
})
end
end
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index aa4f94ef1b..dc3da1eb41 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -509,7 +509,7 @@ module Rails
def call(env)
env.merge!(env_config)
if env['SCRIPT_NAME']
- env.merge! "ROUTES_#{routes.object_id}_SCRIPT_NAME" => env['SCRIPT_NAME'].dup
+ env["ROUTES_#{routes.object_id}_SCRIPT_NAME"] = env['SCRIPT_NAME'].dup
end
app.call(env)
end
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile
index d97be95e27..337f4dd6c1 100644
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile
@@ -15,6 +15,11 @@ source 'https://rubygems.org'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'
+# Use Rails Html Sanitizer for HTML sanitization
+gem 'rails-html-snaitizer', github: 'rails/rails', branch: 'master'
+#temporary gem until a new version of loofah is released
+gem 'loofah', github: 'kaspth/loofah', branch: 'single-scrub'
+
# Use Unicorn as the app server
# gem 'unicorn'
@@ -29,9 +34,9 @@ group :development, :test do
<%- else -%>
gem 'byebug'
<%- end -%>
-
+
# Access an IRB console on exceptions page and /console in development
- gem 'web-console'
+ gem 'web-console', '2.0.0.beta1'
<%- if spring_install? %>
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'spring'
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 277fe01e89..bec7716a9b 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
@@ -70,9 +70,6 @@ Rails.application.configure do
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
- # Disable automatic flushing of the log to improve performance.
- # config.autoflush_log = false
-
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
<%- unless options.skip_active_record? -%>
diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb
index fbecab1823..a48cc13ed7 100644
--- a/railties/lib/rails/generators/rails/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb
@@ -2,7 +2,7 @@ module Rails
module Generators
class ControllerGenerator < NamedBase # :nodoc:
argument :actions, type: :array, default: [], banner: "action action"
- class_option :skip_routes, type: :boolean, desc: "Dont' add routes to config/routes.rb."
+ class_option :skip_routes, type: :boolean, desc: "Don't add routes to config/routes.rb."
check_class_collision suffix: "Controller"
diff --git a/railties/lib/rails/generators/rails/plugin/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile
index 796587f316..35ad9fbf9e 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/Gemfile
+++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile
@@ -31,7 +31,7 @@ end
<% end -%>
<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<%= %(, '#{gem.version}') if gem.version -%>
<% if gem.options.any? -%>
-,<%= gem.padding(max_width) %><%= gem.options.map { |k,v|
+, <%= gem.options.map { |k,v|
"#{k}: #{v.inspect}" }.join(', ') %>
<% end -%>
<% end -%>
diff --git a/railties/lib/rails/rubyprof_ext.rb b/railties/lib/rails/rubyprof_ext.rb
deleted file mode 100644
index 017eba3a76..0000000000
--- a/railties/lib/rails/rubyprof_ext.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'prof'
-
-module Prof #:nodoc:
- # Adapted from Shugo Maeda's unprof.rb
- def self.print_profile(results, io = $stderr)
- total = results.detect { |i|
- i.method_class.nil? && i.method_id == :"#toplevel"
- }.total_time
- total = 0.001 if total < 0.001
-
- io.puts " %% cumulative self self total"
- io.puts " time seconds seconds calls ms/call ms/call name"
-
- sum = 0.0
- results.each do |r|
- sum += r.self_time
-
- name = if r.method_class.nil?
- r.method_id.to_s
- elsif r.method_class.is_a?(Class)
- "#{r.method_class}##{r.method_id}"
- else
- "#{r.method_class}.#{r.method_id}"
- end
- io.printf "%6.2f %8.3f %8.3f %8d %8.2f %8.2f %s\n",
- r.self_time / total * 100,
- sum,
- r.self_time,
- r.count,
- r.self_time * 1000 / r.count,
- r.total_time * 1000 / r.count,
- name
- end
- end
-end
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index 3f31f89473..70c439672f 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -299,7 +299,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
if defined?(JRUBY_VERSION)
assert_gem "therubyrhino"
else
- assert_file "Gemfile", /# gem\s+["']therubyracer["']+, \s+platforms: :ruby$/
+ assert_file "Gemfile", /# gem 'therubyracer', platforms: :ruby/
end
end
@@ -340,7 +340,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_inclusion_of_jbuilder
run_generator
- assert_file "Gemfile", /gem 'jbuilder'/
+ assert_gem 'jbuilder'
end
def test_inclusion_of_a_debugger
@@ -351,9 +351,9 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_no_match(/debugger/, content)
end
elsif RUBY_VERSION < '2.0.0'
- assert_file "Gemfile", /# gem 'debugger'/
+ assert_gem 'debugger'
else
- assert_file "Gemfile", /# gem 'byebug'/
+ assert_gem 'byebug'
end
end
@@ -419,9 +419,14 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "foo bar/config/initializers/session_store.rb", /key: '_foo_bar/
end
+ def test_web_console
+ run_generator
+ assert_gem 'web-console'
+ end
+
def test_spring
run_generator
- assert_file "Gemfile", /gem 'spring', \s+group: :development/
+ assert_gem 'spring'
end
def test_spring_binstubs
@@ -523,6 +528,6 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def assert_gem(gem)
- assert_file "Gemfile", /^gem\s+["']#{gem}["']$/
+ assert_file "Gemfile", /^\s*gem\s+["']#{gem}["']$*/
end
end
diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb
index 8e5b556a7d..c78597c81b 100644
--- a/railties/test/generators/model_generator_test.rb
+++ b/railties/test/generators/model_generator_test.rb
@@ -223,7 +223,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 null: false/
end
def test_migration_timestamps_are_skipped
diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb
index b5765c391e..127e059bf1 100644
--- a/railties/test/generators_test.rb
+++ b/railties/test/generators_test.rb
@@ -152,6 +152,8 @@ class GeneratorsTest < Rails::Generators::TestCase
klass = Rails::Generators.find_by_namespace(:plugin, :remarkable)
assert klass
assert_equal "test_unit:plugin", klass.namespace
+ ensure
+ Rails::Generators.fallbacks.delete(:remarkable)
end
def test_fallbacks_for_generators_on_find_by_namespace_with_context
@@ -159,18 +161,26 @@ class GeneratorsTest < Rails::Generators::TestCase
klass = Rails::Generators.find_by_namespace(:remarkable, :rails, :plugin)
assert klass
assert_equal "test_unit:plugin", klass.namespace
+ ensure
+ Rails::Generators.fallbacks.delete(:remarkable)
end
def test_fallbacks_for_generators_on_invoke
Rails::Generators.fallbacks[:shoulda] = :test_unit
TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {})
Rails::Generators.invoke "shoulda:model", ["Account"]
+ ensure
+ Rails::Generators.fallbacks.delete(:shoulda)
end
def test_nested_fallbacks_for_generators
+ Rails::Generators.fallbacks[:shoulda] = :test_unit
Rails::Generators.fallbacks[:super_shoulda] = :shoulda
TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {})
Rails::Generators.invoke "super_shoulda:model", ["Account"]
+ ensure
+ Rails::Generators.fallbacks.delete(:shoulda)
+ Rails::Generators.fallbacks.delete(:super_shoulda)
end
def test_developer_options_are_overwritten_by_user_options
diff --git a/railties/test/path_generation_test.rb b/railties/test/path_generation_test.rb
new file mode 100644
index 0000000000..13bf29d3c3
--- /dev/null
+++ b/railties/test/path_generation_test.rb
@@ -0,0 +1,88 @@
+# encoding: utf-8
+require 'abstract_unit'
+require 'active_support/core_ext/object/with_options'
+require 'active_support/core_ext/object/json'
+require 'rails'
+require 'rails/application'
+
+ROUTING = ActionDispatch::Routing
+
+class PathGenerationTest < ActiveSupport::TestCase
+ attr_reader :app
+
+ class TestSet < ROUTING::RouteSet
+ def initialize(block)
+ @block = block
+ super()
+ end
+
+ class Dispatcher < ROUTING::RouteSet::Dispatcher
+ def initialize(defaults, set, block)
+ super(defaults)
+ @block = block
+ @set = set
+ end
+
+ def controller_reference(controller_param)
+ block = @block
+ set = @set
+ Class.new(ActionController::Base) {
+ include set.url_helpers
+ define_method(:process) { |name| block.call(self) }
+ def to_a; [200, {}, []]; end
+ }
+ end
+ end
+
+ def dispatcher defaults
+ TestSet::Dispatcher.new defaults, self, @block
+ end
+ end
+
+ def send_request(uri_or_host, method, path, script_name = nil)
+ host = uri_or_host.host unless path
+ path ||= uri_or_host.path
+
+ params = {'PATH_INFO' => path,
+ 'REQUEST_METHOD' => method,
+ 'HTTP_HOST' => host }
+
+ params['SCRIPT_NAME'] = script_name if script_name
+
+ status, headers, body = app.call(params)
+ new_body = []
+ body.each { |part| new_body << part }
+ body.close if body.respond_to? :close
+ [status, headers, new_body]
+ end
+
+ def test_original_script_name
+ original_logger = Rails.logger
+ Rails.logger = Logger.new nil
+
+ app = Class.new(Rails::Application) {
+ attr_accessor :controller
+ def initialize
+ super
+ app = self
+ @routes = TestSet.new ->(c) { app.controller = c }
+ secrets.secret_key_base = "foo"
+ secrets.secret_token = "foo"
+ end
+ def app; routes; end
+ }
+
+ @app = app
+ app.routes.draw { resource :blogs }
+
+ url = URI("http://example.org/blogs")
+
+ send_request(url, 'GET', nil, '/FOO')
+ assert_equal '/FOO/blogs', app.instance.controller.blogs_path
+
+ send_request(url, 'GET', nil)
+ assert_equal '/blogs', app.instance.controller.blogs_path
+ ensure
+ Rails.logger = original_logger
+ end
+end
diff --git a/tasks/release.rb b/tasks/release.rb
index 767feaf236..de05dfad99 100644
--- a/tasks/release.rb
+++ b/tasks/release.rb
@@ -1,4 +1,4 @@
-FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack actionmailer railties )
+FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack actionmailer railties activejob )
root = File.expand_path('../../', __FILE__)
version = File.read("#{root}/RAILS_VERSION").strip
diff --git a/tools/line_statistics b/tools/line_statistics
new file mode 100755
index 0000000000..5eb5cfdc3d
--- /dev/null
+++ b/tools/line_statistics
@@ -0,0 +1,41 @@
+#!/usr/bin/env ruby
+# Example:
+# files = FileList["lib/active_record/**/*.rb"]
+# CodeTools::LineStatistics.new(files).print_loc
+module CodeTools
+ class LineStatistics
+
+ # @param files [Array, FileList, Enumerable]
+ # e.g. FileList["lib/active_record/**/*.rb"]
+ def initialize(files)
+ @files = Array(files).compact
+ end
+
+ # Calculates LOC for each file
+ # Outputs each file and a total LOC
+ def print_loc
+ lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
+
+ @files.each do |file_name|
+ next if file_name =~ /vendor/
+ File.open(file_name, 'r') do |f|
+ while line = f.gets
+ lines += 1
+ next if line =~ /^\s*$/
+ next if line =~ /^\s*#/
+ codelines += 1
+ end
+ end
+ puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
+
+ total_lines += lines
+ total_codelines += codelines
+
+ lines, codelines = 0, 0
+ end
+
+ puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
+ end
+
+ end
+end
diff --git a/tools/profile b/tools/profile
index fbea67492b..a35dd18b77 100755
--- a/tools/profile
+++ b/tools/profile
@@ -1,71 +1,133 @@
#!/usr/bin/env ruby
# Example:
-# tools/profile activesupport/lib/active_support.rb
+# tools/profile activesupport/lib/active_support.rb [ruby-prof mode] [ruby-prof printer]
ENV['NO_RELOAD'] ||= '1'
ENV['RAILS_ENV'] ||= 'development'
-require 'benchmark'
+module CodeTools
+ class Profiler
+ Error = Class.new(StandardError)
-module RequireProfiler
- private
- def require(file, *args) RequireProfiler.profile(file) { super } end
- def load(file, *args) RequireProfiler.profile(file) { super } end
+ attr_reader :path, :mode
+ def initialize(path, mode=nil)
+ assert_ruby_file_exists(path)
+ @path, @mode = path, mode
+ require 'benchmark'
+ end
+
+ def profile_requires
+ GC.start
+ before_rss = `ps -o rss= -p #{Process.pid}`.to_i
+
+ if mode
+ require 'ruby-prof'
+ RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
+ RubyProf.start
+ else
+ Object.instance_eval { include RequireProfiler }
+ end
+
+ elapsed = Benchmark.realtime { require path }
+ results = RubyProf.stop if mode
- @depth, @stats = 0, []
- class << self
- attr_accessor :depth
- attr_accessor :stats
+ GC.start
+ after_rss = `ps -o rss= -p #{Process.pid}`.to_i
- def profile(file)
- stats << [file, depth]
- self.depth += 1
- result = nil
- elapsed = Benchmark.realtime { result = yield }
- self.depth -= 1
- stats.pop if stats.last.first == file
- stats << [file, depth, elapsed] if result
- result
+ if mode
+ if printer = ARGV.shift
+ puts "RubyProf outputting to stderr with printer #{printer}"
+ RubyProf.const_get("#{printer.to_s.classify}Printer").new(results).print($stdout)
+ elsif RubyProf.const_defined?(:CallStackPrinter)
+ filename = "#{File.basename(path, '.rb')}.#{mode}.html"
+ puts "RubyProf outputting to #{filename}"
+ File.open(filename, 'w') do |out|
+ RubyProf::CallStackPrinter.new(results).print(out)
+ end
+ else
+ filename = "#{File.basename(path, '.rb')}.#{mode}.callgrind"
+ puts "RubyProf outputting to #{filename}"
+ File.open(filename, 'w') do |out|
+ RubyProf::CallTreePrinter.new(results).print(out)
+ end
+ end
+ end
+
+ RequireProfiler.stats.each do |file, depth, sec|
+ if sec
+ puts "%8.1f ms %s%s" % [sec * 1000, ' ' * depth, file]
+ else
+ puts "#{' ' * (13 + depth)}#{file}"
+ end
+ end
+ puts "%8.1f ms %d KB RSS" % [elapsed * 1000, after_rss - before_rss]
end
- end
-end
-GC.start
-before_rss = `ps -o rss= -p #{Process.pid}`.to_i
+ private
-path = ARGV.shift
-if mode = ARGV.shift
- require 'ruby-prof'
- RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
- RubyProf.start
-else
- Object.instance_eval { include RequireProfiler }
-end
+ def assert_ruby_file_exists(path)
+ fail Error.new("No such file") unless File.exists?(path)
+ fail Error.new("#{path} is a directory") if File.directory?(path)
+ ruby_extension = File.extname(path) == '.rb'
+ ruby_executable = File.open(path, 'rb') {|f| f.readline } =~ [/\A#!.*ruby/]
+ fail Error.new("Not a ruby file") unless ruby_extension or ruby_executable
+ end
-elapsed = Benchmark.realtime { require path }
-results = RubyProf.stop if mode
+ module RequireProfiler
+ private
+ def require(file, *args) RequireProfiler.profile(file) { super } end
+ def load(file, *args) RequireProfiler.profile(file) { super } end
-GC.start
-after_rss = `ps -o rss= -p #{Process.pid}`.to_i
+ @depth, @stats = 0, []
+ class << self
+ attr_accessor :depth
+ attr_accessor :stats
-if mode
- if printer = ARGV.shift
- RubyProf.const_get("#{printer.to_s.classify}Printer").new(results).print($stdout)
- elsif RubyProf.const_defined?(:CallStackPrinter)
- File.open("#{File.basename(path, '.rb')}.#{mode}.html", 'w') do |out|
- RubyProf::CallStackPrinter.new(results).print(out)
+ def profile(file)
+ stats << [file, depth]
+ self.depth += 1
+ result = nil
+ elapsed = Benchmark.realtime { result = yield }
+ self.depth -= 1
+ stats.pop if stats.last.first == file
+ stats << [file, depth, elapsed] if result
+ result
+ end
+ end
end
- else
- File.open("#{File.basename(path, '.rb')}.#{mode}.callgrind", 'w') do |out|
- RubyProf::CallTreePrinter.new(results).print(out)
+ end
+end
+# ruby-prof printer name causes the third arg to be sent :classify
+# which is probably overkill if you already know the name of the ruby-prof
+# printer you want to use, e.g. Graph
+begin
+ require 'active_support/inflector'
+ require 'active_support/core_ext/string/inflections'
+rescue LoadError
+ STDERR.puts $!.message
+ class String
+ # File activesupport/lib/active_support/inflector/methods.rb, line 150
+ def classify
+ # strip out any leading schema name
+ camelize(self.sub(/.*\./, ''))
+ end
+ # File activesupport/lib/active_support/inflector/methods.rb, line 68
+ def camelize(uppercase_first_letter = true)
+ string = self
+ if uppercase_first_letter
+ string = string.sub(/^[a-z\d]*/) { $&.capitalize }
+ else
+ string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { $&.downcase }
+ end
+ string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub('/', '::')
end
end
end
-
-RequireProfiler.stats.each do |file, depth, sec|
- if sec
- puts "%8.1f ms %s%s" % [sec * 1000, ' ' * depth, file]
+if $0 == __FILE__
+ if (filename = ARGV.shift)
+ path = File.expand_path(filename)
+ mode = ARGV.shift
+ CodeTools::Profiler.new(path, mode).profile_requires
else
- puts "#{' ' * (13 + depth)}#{file}"
+ STDERR.puts "No file path entered. Usage is tools/profile path/to/file.rb [ruby-prof mode] [ruby-prof printer]"
end
end
-puts "%8.1f ms %d KB RSS" % [elapsed * 1000, after_rss - before_rss]