diff options
Diffstat (limited to 'railties')
468 files changed, 18497 insertions, 7177 deletions
diff --git a/railties/.gitignore b/railties/.gitignore index 80dd262d2f..c08562e016 100644 --- a/railties/.gitignore +++ b/railties/.gitignore @@ -1 +1,5 @@ -log/ +/log/ +/test/500.html +/test/fixtures/tmp/ +/test/initializer/root/log/ +/tmp/ diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 573b1f8f69..448fd48f10 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,212 +1,192 @@ -* Fix `NoMethodError` when generating a scaffold inside a full engine. +* Remove `app/assets` and `app/javascript` from `eager_load_paths` and `autoload_paths`. - *Yuji Yaginuma* - -* Adding support for passing a block to the `add_source` action of a custom generator - - *Mike Dalton*, *Hirofumi Wakasugi* + *Gannon McGibbon* -* `assert_file` understands paths with special characters - (eg. `v0.1.4~alpha+nightly`). +* Add JSON support to rails properties route (`/rails/info/properties`). - *Diego Carrion* + Now, `Rails::Info` properties may be accessed in JSON format at `/rails/info/properties.json`. -* Remove ContentLength middleware from the defaults. If you want it, just - add it as a middleware in your config. + *Yoshiyuki Hirano* - *Egg McMuffin* +* Use Ids instead of memory addresses when displaying references in scaffold views. -* Make it possible to customize the executable inside rerun snippets. + Fixes #29200. - *Yves Senn* + *Rasesh Patel* -* Add support for API only apps. - Middleware stack was slimmed down and it has only the needed - middleware for API apps & generators generates the right files, - folders and configurations. +* Adds support for multiple databases to `rails db:migrate:status`. + Subtasks are also added to get the status of individual databases (eg. `rails db:migrate:status:animals`). - *Santiago Pastorino & Jorge Bejar* - -* Make generated scaffold functional tests work inside engines. - - *Yuji Yaginuma* + *Gannon McGibbon* -* Generator a `.keep` file in the `tmp` folder by default as many scripts - assume the existence of this folder and most would fail if it is absent. +* Use Webpacker by default to manage app-level JavaScript through the new app/javascript directory. + Sprockets is now solely in charge, by default, of compiling CSS and other static assets. + Action Cable channel generators will create ES6 stubs rather than use CoffeeScript. + Active Storage, Action Cable, Turbolinks, and Rails-UJS are loaded by a new application.js pack. + Generators no longer generate JavaScript stubs. - See #20299. + *DHH*, *Lachlan Sylvester* - *Yoong Kang Lim*, *Sunny Juneja* +* Add `database` (aliased as `db`) option to model generator to allow + setting the database. This is useful for applications that use + multiple databases and put migrations per database in their own directories. -* `config.static_index` configures directory `index.html` filename + ``` + bin/rails g model Room capacity:integer --database=kingston + invoke active_record + create db/kingston_migrate/20180830151055_create_rooms.rb + ``` - Set `config.static_index` to serve a static directory index file not named - `index`. E.g. to serve `main.html` instead of `index.html` for directory - requests, set `config.static_index` to `"main"`. + Because rails scaffolding uses the model generator, you can + also specify a database with the scaffold generator. - *Eliot Sykes* + *Gannon McGibbon* -* `bin/setup` uses built-in rake tasks (`log:clear`, `tmp:clear`). +* Raise an error when "recyclable cache keys" are being used by a cache store + that does not explicitly support it. Custom cache keys that do support this feature + can bypass this error by implementing the `supports_cache_versioning?` method on their + class and returning a truthy value. - *Mohnish Thallavajhula* + *Richard Schneeman* -* Fix mailer previews with attachments by using the mail gem's own API to - locate the first part of the correct mime type. +* Support environment specific credentials file. - Fixes #14435. + For `production` environment look first for `config/credentials/production.yml.enc` file that can be decrypted by + `ENV["RAILS_MASTER_KEY"]` or `config/credentials/production.key` master key. + Edit given environment credentials file by command `rails credentials:edit --environment production`. + Default paths can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`. - *Andrew White* + *Wojciech Wnętrzak* -* Remove sqlite support from `rails dbconsole`. +* Make `ActiveSupport::Cache::NullStore` the default cache store in the test environment. - *Andrew White* + *Michael C. Nelson* -* Rename `railties/bin` to `railties/exe` to match the new Bundler executables - convention. +* Emit warning for unknown inflection rule when generating model. - *Islam Wazery* + *Yoshiyuki Kinjo* -* Print `bundle install` output in `rails new` as soon as it's available. +* Add `database` (aliased as `db`) option to migration generator. - Running `rails new` will now print the output of `bundle install` as - it is available, instead of waiting until all gems finish installing. + If you're using multiple databases and have a folder for each database + for migrations (ex db/migrate and db/new_db_migrate) you can now pass the + `--database` option to the generator to make sure the the migration + is inserted into the correct folder. - *Max Holder* + ``` + rails g migration CreateHouses --database=kingston + invoke active_record + create db/kingston_migrate/20180830151055_create_houses.rb + ``` -* Respect `pluralize_table_names` when generating fixture file. + *Eileen M. Uchitelle* - Fixes #19519. +* Deprecate `rake routes` in favor of `rails routes`. *Yuji Yaginuma* -* Add a new-line to the end of route method generated code. - - We need to add a `\n`, because we cannot have two routes - in the same line. - - *arthurnn* - -* Add `rake initializers`. - - This task prints out all defined initializers in the order they are invoked - by Rails. This is helpful for debugging issues related to the initialization - process. - - *Naoto Kaneko* +* Deprecate `rake initializers` in favor of `rails initializers`. -* Created rake restart task. Restarts your Rails app by touching the - `tmp/restart.txt`. + *Annie-Claude Côté* - Fixes #18876. +* Deprecate `rake dev:cache` in favor of `rails dev:cache`. - *Hyonjee Joo* + *Annie-Claude Côté* -* Add `config/initializers/active_record_belongs_to_required_by_default.rb`. +* Deprecate `rails notes` subcommands in favor of passing an `annotations` argument to `rails notes`. - Newly generated Rails apps have a new initializer called - `active_record_belongs_to_required_by_default.rb` which sets the value of - the configuration option `config.active_record.belongs_to_required_by_default` - to `true` when ActiveRecord is not skipped. + The following subcommands are replaced by passing `--annotations` or `-a` to `rails notes`: + - `rails notes:custom ANNOTATION=custom` is deprecated in favor of using `rails notes -a custom`. + - `rails notes:optimize` is deprecated in favor of using `rails notes -a OPTIMIZE`. + - `rails notes:todo` is deprecated in favor of using`rails notes -a TODO`. + - `rails notes:fixme` is deprecated in favor of using `rails notes -a FIXME`. - As a result, new Rails apps require `belongs_to` association on model - to be valid. + *Annie-Claude Côté* - This initializer is *not* added when running `rake rails:update`, so - old apps ported to Rails 5 will work without any change. +* Deprecate `SOURCE_ANNOTATION_DIRECTORIES` environment variable used by `rails notes` + through `Rails::SourceAnnotationExtractor::Annotation` in favor of using `config.annotations.register_directories`. - *Josef Šimánek* + *Annie-Claude Côté* -* `delete` operations in configurations are run last in order to eliminate - 'No such middleware' errors when `insert_before` or `insert_after` are added - after the `delete` operation for the middleware being deleted. +* Deprecate `rake notes` in favor of `rails notes`. - Fixes #16433. + *Annie-Claude Côté* - *Guo Xiang Tan* +* Don't generate unused files in `app:update` task. -* Newly generated applications get a `README.md` in Markdown. + Skip the assets' initializer when sprockets isn't loaded. - *Xavier Noria* + Skip `config/spring.rb` when spring isn't loaded. -* Remove the documentation tasks `doc:app`, `doc:rails`, and `doc:guides`. + Skip yarn's contents when yarn integration isn't used. - *Xavier Noria* + *Tsukuru Tanimichi* -* Force generated routes to be inserted into `config/routes.rb`. +* Make the master.key file read-only for the owner upon generation on + POSIX-compliant systems. - *Andrew White* + Previously: -* Don't remove all line endings from `config/routes.rb` when revoking scaffold. + $ ls -l config/master.key + -rw-r--r-- 1 owner group 32 Jan 1 00:00 master.key - Fixes #15913. + Now: - *Andrew White* + $ ls -l config/master.key + -rw------- 1 owner group 32 Jan 1 00:00 master.key -* Rename `--skip-test-unit` option to `--skip-test` in app generator + Fixes #32604. - *Melanie Gilman* + *Jose Luis Duran* -* Add the `method_source` gem to the default Gemfile for apps. +* Deprecate support for using the `HOST` environment to specify the server IP. - *Sean Griffin* + The `BINDING` environment should be used instead. -* Drop old test locations from `rake stats`: + Fixes #29516. - - test/functional - - test/unit - - *Ravil Bayramgalin* - -* Update `rake stats` to correctly count declarative tests - as methods in `_test.rb` files. - - *Ravil Bayramgalin* - -* Remove deprecated `test:all` and `test:all:db` tasks. - - *Rafael Mendonça França* - -* Remove deprecated `Rails::Rack::LogTailer`. - - *Rafael Mendonça França* - -* Remove deprecated `RAILS_CACHE` constant. - - *Rafael Mendonça França* - -* Remove deprecated `serve_static_assets` configuration. + *Yuji Yaginuma* - *Rafael Mendonça França* +* Deprecate passing Rack server name as a regular argument to `rails server`. -* Use local variables in `_form.html.erb` partial generated by scaffold. + Previously: - *Andrew Kozlov* + $ bin/rails server thin -* Add `config/initializers/callback_terminator.rb`. + There wasn't an explicit option for the Rack server to use, now we have the + `--using` option with the `-u` short switch. - Newly generated Rails apps have a new initializer called - `callback_terminator.rb` which sets the value of the configuration option - `config.active_support.halt_callback_chains_on_return_false` to `false`. + Now: - As a result, new Rails apps do not halt callback chains when a callback - returns `false`; only when they are explicitly halted with `throw(:abort)`. + $ bin/rails server -u thin - The terminator is *not* added when running `rake rails:update`, so returning - `false` will still work on old apps ported to Rails 5, displaying a - deprecation warning to prompt users to update their code to the new syntax. + This change also improves the error message if a missing or mistyped rack + server is given. - *claudiob* + *Genadi Samokovarov* -* Generated fixtures won't use the id when generated with references attributes. +* Add "rails routes --expanded" option to output routes in expanded mode like + "psql --expanded". Result looks like: - *Pablo Olmos de Aguilera Corradini* + ``` + $ rails routes --expanded + --[ Route 1 ]------------------------------------------------------------ + Prefix | high_scores + Verb | GET + URI | /high_scores(.:format) + Controller#Action | high_scores#index + --[ Route 2 ]------------------------------------------------------------ + Prefix | new_high_score + Verb | GET + URI | /high_scores/new(.:format) + Controller#Action | high_scores#new + ``` -* Add `--skip-action-mailer` option to the app generator. + *Benoit Tigeot* - *claudiob* +* Rails 6 requires Ruby 2.4.1 or newer. -* Autoload any second level directories called `app/*/concerns`. + *Jeremy Daer* - *Alex Robbin* -Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/railties/CHANGELOG.md) for previous changes. +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/railties/CHANGELOG.md) for previous changes. diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE index 7c2197229d..cce00cbc3a 100644 --- a/railties/MIT-LICENSE +++ b/railties/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2018 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -17,4 +17,4 @@ 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.
\ No newline at end of file +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc index 8d847eaa1c..89fc6bcbce 100644 --- a/railties/RDOC_MAIN.rdoc +++ b/railties/RDOC_MAIN.rdoc @@ -1,73 +1,98 @@ -== Welcome to \Rails - -\Rails is a web-application framework that includes everything needed to create -database-backed web applications according to the {Model-View-Controller (MVC)}[http://en.wikipedia.org/wiki/Model-view-controller] pattern. - -Understanding the MVC pattern is key to understanding \Rails. MVC divides your application -into three layers, each with a specific responsibility. - -The View layer is composed of "templates" that are responsible for providing -appropriate representations of your application's resources. Templates -can come in a variety of formats, but most view templates are \HTML with embedded Ruby -code (.erb files). - -The Model layer represents your domain model (such as Account, Product, Person, Post) -and encapsulates the business logic that is specific to your application. In \Rails, -database-backed model classes are derived from ActiveRecord::Base. Active Record allows -you to present the data from database rows as objects and embellish these data objects -with business logic methods. Although most \Rails models are backed by a database, models -can also be ordinary Ruby classes, or Ruby classes that implement a set of interfaces as -provided by the ActiveModel module. You can read more about Active Record in its -{README}[link:files/activerecord/README_rdoc.html]. - -The Controller layer is responsible for handling incoming HTTP requests and providing a -suitable response. Usually this means returning \HTML, but \Rails controllers can also -generate XML, JSON, PDFs, mobile-specific views, and more. Controllers manipulate models -and render view templates in order to generate the appropriate HTTP response. - -In \Rails, the Controller and View layers are handled together by Action Pack. -These two layers are bundled in a single package due to their heavy interdependence. -This is unlike the relationship between Active Record and Action Pack, which are -independent. Each of these packages can be used independently outside of \Rails. You -can read more about Action Pack in its {README}[link:files/actionpack/README_rdoc.html]. += Welcome to \Rails + +== What's \Rails + +\Rails is a web-application framework that includes everything needed to +create database-backed web applications according to the +{Model-View-Controller (MVC)}[http://en.wikipedia.org/wiki/Model-view-controller] +pattern. + +Understanding the MVC pattern is key to understanding \Rails. MVC divides your +application into three layers: Model, View, and Controller, each with a specific responsibility. + +== Model layer + +The <em><b>Model layer</b></em> represents the domain model (such as Account, Product, +Person, Post, etc.) and encapsulates the business logic specific to +your application. In \Rails, database-backed model classes are derived from +<tt>ActiveRecord::Base</tt>. {Active Record}[link:files/activerecord/README_rdoc.html] allows you to present the data from +database rows as objects and embellish these data objects with business logic +methods. Although most \Rails models are backed by a database, models can also be ordinary +Ruby classes, or Ruby classes that implement a set of interfaces as provided by +the {Active Model}[link:files/activemodel/README_rdoc.html] module. + +== Controller layer + +The <em><b>Controller layer</b></em> is responsible for handling incoming HTTP requests and +providing a suitable response. Usually this means returning \HTML, but \Rails controllers +can also generate XML, JSON, PDFs, mobile-specific views, and more. Controllers load and +manipulate models, and render view templates in order to generate the appropriate HTTP response. +In \Rails, incoming requests are routed by Action Dispatch to an appropriate controller, and +controller classes are derived from <tt>ActionController::Base</tt>. Action Dispatch and Action Controller +are bundled together in {Action Pack}[link:files/actionpack/README_rdoc.html]. + +== View layer + +The <em><b>View layer</b></em> is composed of "templates" that are responsible for providing +appropriate representations of your application's resources. Templates can +come in a variety of formats, but most view templates are \HTML with embedded +Ruby code (ERB files). Views are typically rendered to generate a controller response, +or to generate the body of an email. In \Rails, View generation is handled by {Action View}[link:files/actionview/README_rdoc.html]. + +== Frameworks and libraries + +{Active Record}[link:files/activerecord/README_rdoc.html], {Active Model}[link:files/activemodel/README_rdoc.html], +{Action Pack}[link:files/actionpack/README_rdoc.html], and {Action View}[link:files/actionview/README_rdoc.html] can each be used independently outside \Rails. +In addition to that, \Rails also comes with {Action Mailer}[link:files/actionmailer/README_rdoc.html], a library +to generate and send emails; {Active Job}[link:files/activejob/README_md.html], a +framework for declaring jobs and making them run on a variety of queueing +backends; {Action Cable}[link:files/actioncable/README_md.html], a framework to +integrate WebSockets with a \Rails application; {Active Storage}[link:files/activestorage/README_md.html], +a library to attach cloud and local files to \Rails applications; +and {Active Support}[link:files/activesupport/README_rdoc.html], a collection +of utility classes and standard library extensions that are useful for \Rails, +and may also be used independently outside \Rails. == Getting Started 1. Install \Rails at the command prompt if you haven't yet: - gem install rails + $ gem install rails 2. At the command prompt, create a new \Rails application: - rails new myapp + $ rails new myapp where "myapp" is the application name. 3. Change directory to +myapp+ and start the web server: - cd myapp; rails server + $ cd myapp + $ rails server Run with <tt>--help</tt> or <tt>-h</tt> for options. -4. Go to http://localhost:3000 and you'll see: - - "Welcome aboard: You're riding Ruby on Rails!" +4. Go to <tt>http://localhost:3000</tt> and you'll see: "Yay! You’re on \Rails!" 5. Follow the guidelines to start developing your application. You may find the following resources handy: -* The \README file created within your application. -* {Getting Started with \Rails}[http://guides.rubyonrails.org/getting_started.html]. -* {Ruby on \Rails Tutorial}[http://ruby.railstutorial.org/ruby-on-rails-tutorial-book]. -* {Ruby on \Rails Guides}[http://guides.rubyonrails.org]. -* {The API Documentation}[http://api.rubyonrails.org]. + * The \README file created within your application. + * {Getting Started with \Rails}[https://guides.rubyonrails.org/getting_started.html]. + * {Ruby on \Rails Guides}[https://guides.rubyonrails.org]. + * {The API Documentation}[http://api.rubyonrails.org]. + * {Ruby on \Rails Tutorial}[https://www.railstutorial.org/book]. == Contributing -We encourage you to contribute to Ruby on \Rails! Please check out the {Contributing to Rails -guide}[http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html] for guidelines about how -to proceed. {Join us}[http://contributors.rubyonrails.org]! +We encourage you to contribute to Ruby on \Rails! Please check out the +{Contributing to Ruby on \Rails guide}[https://guides.rubyonrails.org/contributing_to_ruby_on_rails.html] for guidelines about how to proceed. {Join us!}[http://contributors.rubyonrails.org] + +Trying to report a possible security vulnerability in \Rails? Please +check out our {security policy}[http://rubyonrails.org/security/] for +guidelines about how to proceed. +Everyone interacting in \Rails and its sub-projects' codebases, issue trackers, chat rooms, and mailing lists is expected to follow the \Rails {code of conduct}[http://rubyonrails.org/conduct/]. == License -Ruby on \Rails is released under the {MIT License}[http://www.opensource.org/licenses/MIT]. +Ruby on \Rails is released under the {MIT License}[https://opensource.org/licenses/MIT]. diff --git a/railties/README.rdoc b/railties/README.rdoc index a25658668c..2715ccac3f 100644 --- a/railties/README.rdoc +++ b/railties/README.rdoc @@ -23,7 +23,7 @@ Source code can be downloaded as part of the Rails project on GitHub Railties is released under the MIT license: -* http://www.opensource.org/licenses/MIT +* https://opensource.org/licenses/MIT == Support @@ -38,4 +38,3 @@ Bug reports can be filed for the Ruby on Rails project here: Feature requests should be discussed on the rails-core mailing list here: * https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core - diff --git a/railties/Rakefile b/railties/Rakefile index 4393f45790..445f6217b3 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -1,31 +1,83 @@ -require 'rake/testtask' +# frozen_string_literal: true -task :default => :test +require "rake/testtask" + +task default: :test + +task :package desc "Run all unit tests" -task :test => 'test:isolated' +task test: "test:isolated" namespace :test do task :isolated do + dash_i = [ + "test", + "lib", + "../activesupport/lib", + "../actionpack/lib", + "../actionview/lib", + "../activemodel/lib" + ].map { |dir| File.expand_path(dir, __dir__) } + + dash_i.reverse_each do |x| + $:.unshift(x) unless $:.include?(x) + end + $-w = true + + require "bundler/setup" unless defined?(Bundler) + require "active_support" + + # Only generate the template app once. + require_relative "test/isolation/abstract_unit" + + failing_files = [] + dirs = (ENV["TEST_DIR"] || ENV["TEST_DIRS"] || "**").split(",") + test_options = ENV["TESTOPTS"].to_s.split(/[\s]+/) + test_files = dirs.map { |dir| "test/#{dir}/*_test.rb" } Dir[*test_files].each do |file| - next true if file.include?("fixtures") - dash_i = [ - 'test', - 'lib', - "#{File.dirname(__FILE__)}/../activesupport/lib", - "#{File.dirname(__FILE__)}/../actionpack/lib", - "#{File.dirname(__FILE__)}/../activemodel/lib" - ] - ruby "-w", "-I#{dash_i.join ':'}", file + next true if file.start_with?("test/fixtures/") + + fake_command = Shellwords.join([ + FileUtils::RUBY, + "-w", + *dash_i.map { |dir| "-I#{Pathname.new(dir).relative_path_from(Pathname.pwd)}" }, + file, + ]) + puts fake_command + + # We could run these in parallel, but pretty much all of the + # railties tests already run in parallel, so ¯\_(⊙︿⊙)_/¯ + Process.waitpid fork { + ARGV.clear.concat test_options + Rake.application = nil + + load file + } + + unless $?.success? + failing_files << file + end + end + + unless failing_files.empty? + puts + puts "Failed in:" + failing_files.each do |file| + puts " #{file}" + end + puts + + raise "Failure in isolated test runner" end end end -Rake::TestTask.new('test:regular') do |t| - t.libs << 'test' << "#{File.dirname(__FILE__)}/../activesupport/lib" - t.pattern = 'test/**/*_test.rb' +Rake::TestTask.new("test:regular") do |t| + t.libs << "test" << "#{__dir__}/../activesupport/lib" + t.pattern = "test/**/*_test.rb" t.warning = true t.verbose = true t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) diff --git a/railties/bin/test b/railties/bin/test new file mode 100755 index 0000000000..c53377cc97 --- /dev/null +++ b/railties/bin/test @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +COMPONENT_ROOT = File.expand_path("..", __dir__) +require_relative "../../tools/test" diff --git a/railties/exe/rails b/railties/exe/rails index 82c17cabce..79af4db6b6 100755 --- a/railties/exe/rails +++ b/railties/exe/rails @@ -1,9 +1,10 @@ #!/usr/bin/env ruby +# frozen_string_literal: true -git_path = File.expand_path('../../../.git', __FILE__) +git_path = File.expand_path("../../.git", __dir__) if File.exist?(git_path) - railties_path = File.expand_path('../../lib', __FILE__) + railties_path = File.expand_path("../lib", __dir__) $:.unshift(railties_path) end require "rails/cli" diff --git a/railties/lib/minitest/rails_plugin.rb b/railties/lib/minitest/rails_plugin.rb new file mode 100644 index 0000000000..6486fa1798 --- /dev/null +++ b/railties/lib/minitest/rails_plugin.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require "active_support/core_ext/module/attribute_accessors" +require "rails/test_unit/reporter" +require "rails/test_unit/runner" + +module Minitest + class SuppressedSummaryReporter < SummaryReporter + # Disable extra failure output after a run if output is inline. + def aggregated_results(*) + super unless options[:output_inline] + end + end + + def self.plugin_rails_options(opts, options) + ::Rails::TestUnit::Runner.attach_before_load_options(opts) + + opts.on("-b", "--backtrace", "Show the complete backtrace") do + options[:full_backtrace] = true + end + + opts.on("-d", "--defer-output", "Output test failures and errors after the test run") do + options[:output_inline] = false + end + + opts.on("-f", "--fail-fast", "Abort test run on first failure or error") do + options[:fail_fast] = true + end + + opts.on("-c", "--[no-]color", "Enable color in the output") do |value| + options[:color] = value + end + + options[:color] = true + options[:output_inline] = true + end + + # Owes great inspiration to test runner trailblazers like RSpec, + # minitest-reporters, maxitest and others. + def self.plugin_rails_init(options) + unless options[:full_backtrace] || ENV["BACKTRACE"] + # Plugin can run without Rails loaded, check before filtering. + Minitest.backtrace_filter = ::Rails.backtrace_cleaner if ::Rails.respond_to?(:backtrace_cleaner) + end + + # Suppress summary reports when outputting inline rerun snippets. + if reporter.reporters.reject! { |reporter| reporter.kind_of?(SummaryReporter) } + reporter << SuppressedSummaryReporter.new(options[:io], options) + end + + # Replace progress reporter for colors. + if reporter.reporters.reject! { |reporter| reporter.kind_of?(ProgressReporter) } + reporter << ::Rails::TestUnitReporter.new(options[:io], options) + end + end + + # Backwardscompatibility with Rails 5.0 generated plugin test scripts + mattr_reader :run_via, default: {} +end diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index fe789f3c2a..092105d502 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -1,18 +1,21 @@ -require 'rails/ruby_version_check' +# frozen_string_literal: true -require 'pathname' +require "rails/ruby_version_check" -require 'active_support' -require 'active_support/dependencies/autoload' -require 'active_support/core_ext/kernel/reporting' -require 'active_support/core_ext/module/delegation' -require 'active_support/core_ext/array/extract_options' +require "pathname" -require 'rails/application' -require 'rails/version' +require "active_support" +require "active_support/dependencies/autoload" +require "active_support/core_ext/kernel/reporting" +require "active_support/core_ext/module/delegation" +require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/object/blank" -require 'active_support/railtie' -require 'action_dispatch/railtie' +require "rails/application" +require "rails/version" + +require "active_support/railtie" +require "action_dispatch/railtie" # UTF-8 is the default internal and external encoding. silence_warnings do @@ -46,14 +49,14 @@ module Rails def backtrace_cleaner @backtrace_cleaner ||= begin - # Relies on Active Support, so we have to lazy load to postpone definition until AS has been loaded - require 'rails/backtrace_cleaner' + # Relies on Active Support, so we have to lazy load to postpone definition until Active Support has been loaded + require "rails/backtrace_cleaner" Rails::BacktraceCleaner.new end end - # Returns a Pathname object of the current rails project, - # otherwise it returns nil if there is no project: + # Returns a Pathname object of the current Rails project, + # otherwise it returns +nil+ if there is no project: # # Rails.root # # => #<Pathname:/Users/someuser/some/path/project> @@ -67,7 +70,7 @@ module Rails # Rails.env.development? # => true # Rails.env.production? # => false def env - @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development") + @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development") end # Sets the Rails environment. @@ -77,7 +80,7 @@ module Rails @_env = ActiveSupport::StringInquirer.new(environment) end - # Returns all rails groups for loading based on: + # Returns all Rails groups for loading based on: # # * The Rails environment; # * The environment variable RAILS_GROUPS; @@ -86,8 +89,8 @@ module Rails # groups assets: [:development, :test] # # # Returns - # # => [:default, :development, :assets] for Rails.env == "development" - # # => [:default, :production] for Rails.env == "production" + # # => [:default, "development", :assets] for Rails.env == "development" + # # => [:default, "production"] for Rails.env == "production" def groups(*groups) hash = groups.extract_options! env = Rails.env @@ -100,7 +103,7 @@ module Rails end # Returns a Pathname object of the public folder of the current - # rails project, otherwise it returns nil if there is no project: + # Rails project, otherwise it returns +nil+ if there is no project: # # Rails.public_path # # => #<Pathname:/Users/someuser/some/path/project/public> diff --git a/railties/lib/rails/all.rb b/railties/lib/rails/all.rb index 45361fca83..f5dccd2381 100644 --- a/railties/lib/rails/all.rb +++ b/railties/lib/rails/all.rb @@ -1,16 +1,20 @@ +# frozen_string_literal: true + require "rails" %w( - active_record - action_controller - action_view - action_mailer - active_job - rails/test_unit - sprockets -).each do |framework| + active_record/railtie + active_storage/engine + action_controller/railtie + action_view/railtie + action_mailer/railtie + active_job/railtie + action_cable/engine + rails/test_unit/railtie + sprockets/railtie +).each do |railtie| begin - require "#{framework}/railtie" + require railtie rescue LoadError end end diff --git a/railties/lib/rails/api/generator.rb b/railties/lib/rails/api/generator.rb new file mode 100644 index 0000000000..126d4d0438 --- /dev/null +++ b/railties/lib/rails/api/generator.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "sdoc" +require "active_support/core_ext/array/extract" + +class RDoc::Generator::API < RDoc::Generator::SDoc # :nodoc: + RDoc::RDoc.add_generator self + + def generate_class_tree_level(classes, visited = {}) + # Only process core extensions on the first visit and remove + # Active Storage duplicated classes that are at the top level + # since they aren't nested under a definition of the `ActiveStorage` module. + if visited.empty? + classes = classes.reject { |klass| active_storage?(klass) } + core_exts = classes.extract! { |klass| core_extension?(klass) } + + super.unshift([ "Core extensions", "", "", build_core_ext_subtree(core_exts, visited) ]) + else + super + end + end + + private + def build_core_ext_subtree(classes, visited) + classes.map do |klass| + [ klass.name, klass.document_self_or_methods ? klass.path : "", "", + generate_class_tree_level(klass.classes_and_modules, visited) ] + end + end + + def core_extension?(klass) + klass.name != "ActiveSupport" && klass.in_files.any? { |file| file.absolute_name.include?("core_ext") } + end + + def active_storage?(klass) + klass.name != "ActiveStorage" && klass.in_files.all? { |file| file.absolute_name.include?("active_storage") } + end +end diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb index a082932632..e7f0557584 100644 --- a/railties/lib/rails/api/task.rb +++ b/railties/lib/rails/api/task.rb @@ -1,33 +1,35 @@ -require 'rdoc/task' +# frozen_string_literal: true + +require "rdoc/task" +require "rails/api/generator" module Rails module API class Task < RDoc::Task RDOC_FILES = { - 'activesupport' => { - :include => %w( + "activesupport" => { + include: %w( README.rdoc lib/active_support/**/*.rb - ), - :exclude => 'lib/active_support/vendor/*' + ) }, - 'activerecord' => { - :include => %w( + "activerecord" => { + include: %w( README.rdoc lib/active_record/**/*.rb ) }, - 'activemodel' => { - :include => %w( + "activemodel" => { + include: %w( README.rdoc lib/active_model/**/*.rb ) }, - 'actionpack' => { - :include => %w( + "actionpack" => { + include: %w( README.rdoc lib/abstract_controller/**/*.rb lib/action_controller/**/*.rb @@ -35,34 +37,53 @@ module Rails ) }, - 'actionview' => { - :include => %w( + "actionview" => { + include: %w( README.rdoc lib/action_view/**/*.rb ), - :exclude => 'lib/action_view/vendor/*' + exclude: "lib/action_view/vendor/*" }, - 'actionmailer' => { - :include => %w( + "actionmailer" => { + include: %w( README.rdoc lib/action_mailer/**/*.rb ) }, - 'activejob' => { - :include => %w( + "activejob" => { + include: %w( README.md lib/active_job/**/*.rb ) }, - 'railties' => { - :include => %w( + "actioncable" => { + include: %w( + README.md + lib/action_cable/**/*.rb + ) + }, + + "activestorage" => { + include: %w( + README.md + app/**/active_storage/**/*.rb + lib/active_storage/**/*.rb + ) + }, + + "railties" => { + include: %w( README.rdoc lib/**/*.rb ), - :exclude => 'lib/rails/generators/rails/**/templates/**/*.rb' + exclude: %w( + lib/rails/generators/**/templates/**/*.rb + lib/rails/test_unit/* + lib/rails/api/generator.rb + ) } } @@ -73,7 +94,7 @@ module Rails # Be lazy computing stuff to have as light impact as possible to # the rest of tasks. before_running_rdoc do - load_and_configure_sdoc + configure_sdoc configure_rdoc_files setup_horo_variables end @@ -84,20 +105,15 @@ module Rails # no-op end - def load_and_configure_sdoc - require 'sdoc' - - self.title = 'Ruby on Rails API' + def configure_sdoc + self.title = "Ruby on Rails API" self.rdoc_dir = api_dir - options << '-m' << api_main - options << '-e' << 'UTF-8' + options << "-m" << api_main + options << "-e" << "UTF-8" - options << '-f' << 'sdoc' - options << '-T' << 'rails' - rescue LoadError - $stderr.puts %(Unable to load SDoc, please add\n\n gem 'sdoc', require: false\n\nto the Gemfile.) - exit 1 + options << "-f" << "api" + options << "-T" << "rails" end def configure_rdoc_files @@ -114,22 +130,35 @@ module Rails rdoc_files.exclude("#{cdr}/#{pattern}") end end + + # Only generate documentation for files that have been + # changed since the API was generated. + if Dir.exist?("doc/rdoc") && !ENV["ALL"] + last_generation = DateTime.rfc2822(File.open("doc/rdoc/created.rid", &:readline)) + + rdoc_files.keep_if do |file| + File.mtime(file).to_datetime > last_generation + end + + # Nothing to do + exit(0) if rdoc_files.empty? + end end def setup_horo_variables - ENV['HORO_PROJECT_NAME'] = 'Ruby on Rails' - ENV['HORO_PROJECT_VERSION'] = rails_version + ENV["HORO_PROJECT_NAME"] = "Ruby on Rails" + ENV["HORO_PROJECT_VERSION"] = rails_version end def api_main - component_root_dir('railties') + '/RDOC_MAIN.rdoc' + component_root_dir("railties") + "/RDOC_MAIN.rdoc" end end class RepoTask < Task - def load_and_configure_sdoc + def configure_sdoc super - options << '-g' # link to GitHub, SDoc flag + options << "-g" # link to GitHub, SDoc flag end def component_root_dir(component) @@ -137,7 +166,7 @@ module Rails end def api_dir - 'doc/rdoc' + "doc/rdoc" end end @@ -149,7 +178,7 @@ module Rails class StableTask < RepoTask def rails_version - File.read('RAILS_VERSION').strip + File.read("RAILS_VERSION").strip end end end diff --git a/railties/lib/rails/app_loader.rb b/railties/lib/rails/app_loader.rb index a9fe21824e..aabcc5970c 100644 --- a/railties/lib/rails/app_loader.rb +++ b/railties/lib/rails/app_loader.rb @@ -1,22 +1,35 @@ -require 'pathname' -require 'rails/version' +# frozen_string_literal: true + +require "pathname" +require "rails/version" module Rails module AppLoader # :nodoc: extend self RUBY = Gem.ruby - EXECUTABLES = ['bin/rails', 'script/rails'] + EXECUTABLES = ["bin/rails", "script/rails"] BUNDLER_WARNING = <<EOS -Looks like your app's ./bin/rails is a stub that was generated by Bundler. +Beginning in Rails 4, Rails ships with a `rails` binstub at ./bin/rails that +should be used instead of the Bundler-generated `rails` binstub. + +If you are seeing this message, your binstub at ./bin/rails was generated by +Bundler instead of Rails. + +You might need to regenerate your `rails` binstub locally and add it to source +control: + + rails app:update:bin # Bear in mind this generates other binstubs + # too that you may or may not want (like yarn) -In Rails #{Rails::VERSION::MAJOR}, your app's bin/ directory contains executables that are versioned -like any other source code, rather than stubs that are generated on demand. +If you already have Rails binstubs in source control, you might be +inadverently overwriting them during deployment by using bundle install +with the --binstubs option. -Here's how to upgrade: +If your application was created prior to Rails 4, here's how to upgrade: bundle config --delete bin # Turn off Bundler's stub generator - rake rails:update:bin # Use the new Rails 4 executables + rails app:update:bin # Use the new Rails executables git add bin # Add bin/ to source control You may need to remove bin/ from your .gitignore as well. @@ -36,24 +49,24 @@ EOS if exe = find_executable contents = File.read(exe) - if contents =~ /(APP|ENGINE)_PATH/ + if /(APP|ENGINE)_PATH/.match?(contents) exec RUBY, exe, *ARGV break # non reachable, hack to be able to stub exec in the test suite - elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler') + elsif exe.end_with?("bin/rails") && contents.include?("This file was generated by Bundler") $stderr.puts(BUNDLER_WARNING) - Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd)) - require File.expand_path('../boot', APP_PATH) - require 'rails/commands' + Object.const_set(:APP_PATH, File.expand_path("config/application", Dir.pwd)) + require File.expand_path("../boot", APP_PATH) + require "rails/commands" break end end # If we exhaust the search there is no executable, this could be a # call to generate a new application, so restore the original cwd. - Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root? + Dir.chdir(original_cwd) && return if Pathname.new(Dir.pwd).root? # Otherwise keep moving upwards in search of an executable. - Dir.chdir('..') + Dir.chdir("..") end end diff --git a/railties/lib/rails/app_updater.rb b/railties/lib/rails/app_updater.rb new file mode 100644 index 0000000000..19d136e041 --- /dev/null +++ b/railties/lib/rails/app_updater.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "rails/generators" +require "rails/generators/rails/app/app_generator" + +module Rails + class AppUpdater # :nodoc: + class << self + def invoke_from_app_generator(method) + app_generator.send(method) + end + + def app_generator + @app_generator ||= begin + gen = Rails::Generators::AppGenerator.new ["rails"], generator_options, destination_root: Rails.root + File.exist?(Rails.root.join("config", "application.rb")) ? gen.send(:app_const) : gen.send(:valid_const?) + gen + end + end + + private + def generator_options + options = { api: !!Rails.application.config.api_only, update: true } + options[:skip_javascript] = !File.exist?(Rails.root.join("bin", "yarn")) + options[:skip_active_record] = !defined?(ActiveRecord::Railtie) + options[:skip_active_storage] = !defined?(ActiveStorage::Engine) || !defined?(ActiveRecord::Railtie) + options[:skip_action_mailer] = !defined?(ActionMailer::Railtie) + options[:skip_action_cable] = !defined?(ActionCable::Engine) + options[:skip_sprockets] = !defined?(Sprockets::Railtie) + options[:skip_puma] = !defined?(Puma) + options[:skip_bootsnap] = !defined?(Bootsnap) + options[:skip_spring] = !defined?(Spring) + options + end + end + end +end diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 8075068b3f..656786246d 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -1,10 +1,14 @@ -require 'fileutils' -require 'yaml' -require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/object/blank' -require 'active_support/key_generator' -require 'active_support/message_verifier' -require 'rails/engine' +# frozen_string_literal: true + +require "yaml" +require "active_support/core_ext/hash/keys" +require "active_support/core_ext/object/blank" +require "active_support/key_generator" +require "active_support/message_verifier" +require "active_support/encrypted_configuration" +require "active_support/deprecation" +require "rails/engine" +require "rails/secrets" module Rails # An Engine with the responsibility of coordinating the whole boot process. @@ -73,22 +77,22 @@ module Rails # on one of the applications to create a copy of the application which shares # the configuration. # - # If you decide to define rake tasks, runners, or initializers in an - # application other than +Rails.application+, then you must run those - # these manually. + # If you decide to define Rake tasks, runners, or initializers in an + # application other than +Rails.application+, then you must run them manually. class Application < Engine - autoload :Bootstrap, 'rails/application/bootstrap' - autoload :Configuration, 'rails/application/configuration' - autoload :DefaultMiddlewareStack, 'rails/application/default_middleware_stack' - autoload :Finisher, 'rails/application/finisher' - autoload :Railties, 'rails/engine/railties' - autoload :RoutesReloader, 'rails/application/routes_reloader' + autoload :Bootstrap, "rails/application/bootstrap" + autoload :Configuration, "rails/application/configuration" + autoload :DefaultMiddlewareStack, "rails/application/default_middleware_stack" + autoload :Finisher, "rails/application/finisher" + autoload :Railties, "rails/engine/railties" + autoload :RoutesReloader, "rails/application/routes_reloader" class << self def inherited(base) super Rails.app_class = base add_lib_to_load_path!(find_root(base.called_from)) + ActiveSupport.run_load_hooks(:before_configuration, base) end def instance @@ -113,7 +117,7 @@ module Rails attr_accessor :assets, :sandbox alias_method :sandbox?, :sandbox - attr_reader :reloaders + attr_reader :reloaders, :reloader, :executor delegate :default_url_options, :default_url_options=, to: :routes @@ -131,6 +135,10 @@ module Rails @message_verifiers = {} @ran_load_hooks = false + @executor = Class.new(ActiveSupport::Executor) + @reloader = Class.new(ActiveSupport::Reloader) + @reloader.executor = @executor + # are these actually used? @initial_variable_values = initial_variable_values @block = block @@ -144,7 +152,6 @@ module Rails def run_load_hooks! # :nodoc: return self if @ran_load_hooks @ran_load_hooks = true - ActiveSupport.run_load_hooks(:before_configuration, self) @initial_variable_values.each do |variable_name, value| if INITIAL_VARIABLES.include?(variable_name) @@ -156,28 +163,20 @@ module Rails self end - # Implements call according to the Rack API. It simply - # dispatches the request to the underlying middleware stack. - def call(env) - req = ActionDispatch::Request.new env - env["ORIGINAL_FULLPATH"] = req.fullpath - env["ORIGINAL_SCRIPT_NAME"] = req.script_name - super(env) - end - # Reload application routes regardless if they changed or not. def reload_routes! routes_reloader.reload! end - # Return the application's KeyGenerator + # Returns the application's KeyGenerator def key_generator # number of iterations selected based on consultation with the google security # team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220 @caching_key_generator ||= - if secrets.secret_key_base - key_generator = ActiveSupport::KeyGenerator.new(secrets.secret_key_base, iterations: 1000) - ActiveSupport::CachingKeyGenerator.new(key_generator) + if secret_key_base + ActiveSupport::CachingKeyGenerator.new( + ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000) + ) else ActiveSupport::LegacyKeyGenerator.new(secrets.secret_token) end @@ -220,16 +219,23 @@ module Rails # url: http://localhost:3001 # namespace: my_app_development # - # # config/production.rb + # # config/environments/production.rb # Rails.application.configure do # config.middleware.use ExceptionNotifier, config_for(:exception_notification) # end - def config_for(name) - yaml = Pathname.new("#{paths["config"].existent.first}/#{name}.yml") + def config_for(name, env: Rails.env) + if name.is_a?(Pathname) + yaml = name + else + yaml = Pathname.new("#{paths["config"].existent.first}/#{name}.yml") + end if yaml.exist? require "erb" - (YAML.load(ERB.new(yaml.read).result) || {})[Rails.env] || {} + require "active_support/ordered_options" + + config = (YAML.load(ERB.new(yaml.read).result) || {})[env] || {} + ActiveSupport::InheritableOptions.new(config.deep_symbolize_keys) else raise "Could not load configuration. No such file - #{yaml}" end @@ -243,13 +249,11 @@ module Rails # will be used by middlewares and engines to configure themselves. def env_config @app_env_config ||= begin - validate_secret_key_config! - - super.merge({ + super.merge( "action_dispatch.parameter_filter" => config.filter_parameters, "action_dispatch.redirect_filter" => config.filter_redirect, "action_dispatch.secret_token" => secrets.secret_token, - "action_dispatch.secret_key_base" => secrets.secret_key_base, + "action_dispatch.secret_key_base" => secret_key_base, "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions, "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local, "action_dispatch.logger" => Rails.logger, @@ -259,14 +263,23 @@ module Rails "action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt, "action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt, "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt, + "action_dispatch.authenticated_encrypted_cookie_salt" => config.action_dispatch.authenticated_encrypted_cookie_salt, + "action_dispatch.use_authenticated_cookie_encryption" => config.action_dispatch.use_authenticated_cookie_encryption, + "action_dispatch.encrypted_cookie_cipher" => config.action_dispatch.encrypted_cookie_cipher, + "action_dispatch.signed_cookie_digest" => config.action_dispatch.signed_cookie_digest, "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer, - "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest - }) + "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest, + "action_dispatch.cookies_rotations" => config.action_dispatch.cookies_rotations, + "action_dispatch.use_cookies_with_metadata" => config.action_dispatch.use_cookies_with_metadata, + "action_dispatch.content_security_policy" => config.content_security_policy, + "action_dispatch.content_security_policy_report_only" => config.content_security_policy_report_only, + "action_dispatch.content_security_policy_nonce_generator" => config.content_security_policy_nonce_generator + ) end end - # If you try to define a set of rake tasks on the instance, these will get - # passed up to the rake tasks defined on the application's class. + # If you try to define a set of Rake tasks on the instance, these will get + # passed up to the Rake tasks defined on the application's class. def rake_tasks(&block) self.class.rake_tasks(&block) end @@ -274,7 +287,7 @@ module Rails # Sends the initializers to the +initializer+ method defined in the # Rails::Initializable module. Each Rails::Application class has its own # set of initializers, as defined by the Initializable module. - def initializer(name, opts={}, &block) + def initializer(name, opts = {}, &block) self.class.initializer(name, opts, &block) end @@ -317,7 +330,7 @@ module Rails # Rails application, you will need to add lib to $LOAD_PATH on your own in case # you need to load files in lib/ during the application configuration as well. def self.add_lib_to_load_path!(root) #:nodoc: - path = File.join root, 'lib' + path = File.join root, "lib" if File.exist?(path) && !$LOAD_PATH.include?(path) $LOAD_PATH.unshift(path) end @@ -347,7 +360,7 @@ module Rails # Initialize the application passing the given group. By default, the # group is :default - def initialize!(group=:default) #:nodoc: + def initialize!(group = :default) #:nodoc: raise "Application has been already initialized." if @initialized run_initializers(group, self) @initialized = true @@ -364,9 +377,7 @@ module Rails @config ||= Application::Configuration.new(self.class.find_root(self.class.called_from)) end - def config=(configuration) #:nodoc: - @config = configuration - end + attr_writer :config # Returns secrets added to config/secrets.yml. # @@ -385,25 +396,90 @@ module Rails def secrets @secrets ||= begin secrets = ActiveSupport::OrderedOptions.new - yaml = config.paths["config/secrets"].first - if File.exist?(yaml) - require "erb" - all_secrets = YAML.load(ERB.new(IO.read(yaml)).result) || {} - env_secrets = all_secrets[Rails.env] - secrets.merge!(env_secrets.symbolize_keys) if env_secrets - end + files = config.paths["config/secrets"].existent + files = files.reject { |path| path.end_with?(".enc") } unless config.read_encrypted_secrets + secrets.merge! Rails::Secrets.parse(files, env: Rails.env) # Fallback to config.secret_key_base if secrets.secret_key_base isn't set secrets.secret_key_base ||= config.secret_key_base # Fallback to config.secret_token if secrets.secret_token isn't set secrets.secret_token ||= config.secret_token + if secrets.secret_token.present? + ActiveSupport::Deprecation.warn( + "`secrets.secret_token` is deprecated in favor of `secret_key_base` and will be removed in Rails 6.0." + ) + end + secrets end end - def secrets=(secrets) #:nodoc: - @secrets = secrets + attr_writer :secrets + + # The secret_key_base is used as the input secret to the application's key generator, which in turn + # is used to create all MessageVerifiers/MessageEncryptors, including the ones that sign and encrypt cookies. + # + # In test and development, this is simply derived as a MD5 hash of the application's name. + # + # In all other environments, we look for it first in ENV["SECRET_KEY_BASE"], + # then credentials.secret_key_base, and finally secrets.secret_key_base. For most applications, + # the correct place to store it is in the encrypted credentials file. + def secret_key_base + if Rails.env.test? || Rails.env.development? + secrets.secret_key_base || Digest::MD5.hexdigest(self.class.name) + else + validate_secret_key_base( + ENV["SECRET_KEY_BASE"] || credentials.secret_key_base || secrets.secret_key_base + ) + end + end + + # Decrypts the credentials hash as kept in +config/credentials.yml.enc+. This file is encrypted with + # the Rails master key, which is either taken from <tt>ENV["RAILS_MASTER_KEY"]</tt> or from loading + # +config/master.key+. + # If specific credentials file exists for current environment, it takes precedence, thus for +production+ + # environment look first for +config/credentials/production.yml.enc+ with master key taken + # from <tt>ENV["RAILS_MASTER_KEY"]</tt> or from loading +config/credentials/production.key+. + # Default behavior can be overwritten by setting +config.credentials.content_path+ and +config.credentials.key_path+. + def credentials + @credentials ||= encrypted(config.credentials.content_path, key_path: config.credentials.key_path) + end + + # Shorthand to decrypt any encrypted configurations or files. + # + # For any file added with <tt>rails encrypted:edit</tt> call +read+ to decrypt + # the file with the master key. + # The master key is either stored in +config/master.key+ or <tt>ENV["RAILS_MASTER_KEY"]</tt>. + # + # Rails.application.encrypted("config/mystery_man.txt.enc").read + # # => "We've met before, haven't we?" + # + # It's also possible to interpret encrypted YAML files with +config+. + # + # Rails.application.encrypted("config/credentials.yml.enc").config + # # => { next_guys_line: "I don't think so. Where was it you think we met?" } + # + # Any top-level configs are also accessible directly on the return value: + # + # Rails.application.encrypted("config/credentials.yml.enc").next_guys_line + # # => "I don't think so. Where was it you think we met?" + # + # The files or configs can also be encrypted with a custom key. To decrypt with + # a key in the +ENV+, use: + # + # Rails.application.encrypted("config/special_tokens.yml.enc", env_key: "SPECIAL_TOKENS") + # + # Or to decrypt with a file, that should be version control ignored, relative to +Rails.root+: + # + # Rails.application.encrypted("config/special_tokens.yml.enc", key_path: "config/special_tokens.key") + def encrypted(path, key_path: "config/master.key", env_key: "RAILS_MASTER_KEY") + ActiveSupport::EncryptedConfiguration.new( + config_path: Rails.root.join(path), + key_path: Rails.root.join(key_path), + env_key: env_key, + raise_if_missing_key: config.require_master_key + ) end def to_app #:nodoc: @@ -504,15 +580,27 @@ module Rails default_stack.build_stack end - def validate_secret_key_config! #:nodoc: - if secrets.secret_key_base.blank? - ActiveSupport::Deprecation.warn "You didn't set `secret_key_base`. " + - "Read the upgrade documentation to learn more about this new config option." - - if secrets.secret_token.blank? - raise "Missing `secret_key_base` for '#{Rails.env}' environment, set this value in `config/secrets.yml`" - end + def validate_secret_key_base(secret_key_base) + if secret_key_base.is_a?(String) && secret_key_base.present? + secret_key_base + elsif secret_key_base + raise ArgumentError, "`secret_key_base` for #{Rails.env} environment must be a type of String`" + elsif secrets.secret_token.blank? + raise ArgumentError, "Missing `secret_key_base` for '#{Rails.env}' environment, set this string with `rails credentials:edit`" end end + + private + + def build_request(env) + req = super + env["ORIGINAL_FULLPATH"] = req.fullpath + env["ORIGINAL_SCRIPT_NAME"] = req.script_name + req + end + + def build_middleware + config.app_middleware + super + end end end diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index 0f4d932749..e3c0759f95 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -1,6 +1,10 @@ +# frozen_string_literal: true + +require "fileutils" require "active_support/notifications" require "active_support/dependencies" require "active_support/descendants_tracker" +require "rails/secrets" module Rails class Application @@ -35,7 +39,7 @@ INFO FileUtils.mkdir_p File.dirname path end - f = File.open path, 'a' + f = File.open path, "a" f.binmode f.sync = config.autoflush_log # if true make sure every write flushes @@ -47,8 +51,8 @@ INFO logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDERR)) logger.level = ActiveSupport::Logger::WARN logger.warn( - "Rails Error: Unable to access log file. Please ensure that #{path} exists and is writable " + - "(ie, make it writable for user and group: chmod 0664 #{path}). " + + "Rails Error: Unable to access log file. Please ensure that #{path} exists and is writable " \ + "(ie, make it writable for user and group: chmod 0664 #{path}). " \ "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed." ) logger @@ -63,7 +67,7 @@ INFO Rails.cache = ActiveSupport::Cache.lookup_store(config.cache_store) if Rails.cache.respond_to?(:middleware) - config.middleware.insert_before("Rack::Runtime", Rails.cache.middleware) + config.middleware.insert_before(::Rack::Runtime, Rails.cache.middleware) end end end @@ -76,6 +80,10 @@ INFO initializer :bootstrap_hook, group: :all do |app| ActiveSupport.run_load_hooks(:before_initialize, app) end + + initializer :set_secrets_root, group: :all do + Rails::Secrets.root = root + end end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 4fc7a1db62..d6f8c4f47c 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -1,7 +1,9 @@ -require 'active_support/core_ext/kernel/reporting' -require 'active_support/file_update_checker' -require 'rails/engine/configuration' -require 'rails/source_annotation_extractor' +# frozen_string_literal: true + +require "active_support/core_ext/kernel/reporting" +require "active_support/file_update_checker" +require "rails/engine/configuration" +require "rails/source_annotation_extractor" module Rails class Application @@ -11,46 +13,125 @@ module Rails :eager_load, :exceptions_app, :file_watcher, :filter_parameters, :force_ssl, :helpers_paths, :logger, :log_formatter, :log_tags, :railties_order, :relative_url_root, :secret_key_base, :secret_token, - :serve_static_files, :ssl_options, :static_cache_control, :static_index, + :ssl_options, :public_file_server, :session_options, :time_zone, :reload_classes_only_on_change, - :beginning_of_week, :filter_redirect, :x + :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading, + :read_encrypted_secrets, :log_level, :content_security_policy_report_only, + :content_security_policy_nonce_generator, :require_master_key, :credentials - attr_writer :log_level - attr_reader :encoding, :api_only + attr_reader :encoding, :api_only, :loaded_config_version def initialize(*) super - self.encoding = "utf-8" - @allow_concurrency = nil - @consider_all_requests_local = false - @filter_parameters = [] - @filter_redirect = [] - @helpers_paths = [] - @serve_static_files = true - @static_cache_control = nil - @static_index = "index" - @force_ssl = false - @ssl_options = {} - @session_store = :cookie_store - @session_options = {} - @time_zone = "UTC" - @beginning_of_week = :monday - @log_level = nil - @middleware = app_middleware - @generators = app_generators - @cache_store = [ :file_store, "#{root}/tmp/cache/" ] - @railties_order = [:all] - @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"] - @reload_classes_only_on_change = true - @file_watcher = ActiveSupport::FileUpdateChecker - @exceptions_app = nil - @autoflush_log = true - @log_formatter = ActiveSupport::Logger::SimpleFormatter.new - @eager_load = nil - @secret_token = nil - @secret_key_base = nil - @api_only = false - @x = Custom.new + self.encoding = Encoding::UTF_8 + @allow_concurrency = nil + @consider_all_requests_local = false + @filter_parameters = [] + @filter_redirect = [] + @helpers_paths = [] + @public_file_server = ActiveSupport::OrderedOptions.new + @public_file_server.enabled = true + @public_file_server.index_name = "index" + @force_ssl = false + @ssl_options = {} + @session_store = nil + @time_zone = "UTC" + @beginning_of_week = :monday + @log_level = :debug + @generators = app_generators + @cache_store = [ :file_store, "#{root}/tmp/cache/" ] + @railties_order = [:all] + @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"] + @reload_classes_only_on_change = true + @file_watcher = ActiveSupport::FileUpdateChecker + @exceptions_app = nil + @autoflush_log = true + @log_formatter = ActiveSupport::Logger::SimpleFormatter.new + @eager_load = nil + @secret_token = nil + @secret_key_base = nil + @api_only = false + @debug_exception_response_format = nil + @x = Custom.new + @enable_dependency_loading = false + @read_encrypted_secrets = false + @content_security_policy = nil + @content_security_policy_report_only = false + @content_security_policy_nonce_generator = nil + @require_master_key = false + @loaded_config_version = nil + @credentials = ActiveSupport::OrderedOptions.new + @credentials.content_path = default_credentials_content_path + @credentials.key_path = default_credentials_key_path + end + + def load_defaults(target_version) + case target_version.to_s + when "5.0" + if respond_to?(:action_controller) + action_controller.per_form_csrf_tokens = true + action_controller.forgery_protection_origin_check = true + end + + ActiveSupport.to_time_preserves_timezone = true + + if respond_to?(:active_record) + active_record.belongs_to_required_by_default = true + end + + self.ssl_options = { hsts: { subdomains: true } } + when "5.1" + load_defaults "5.0" + + if respond_to?(:assets) + assets.unknown_asset_fallback = false + end + + if respond_to?(:action_view) + action_view.form_with_generates_remote_forms = true + end + when "5.2" + load_defaults "5.1" + + if respond_to?(:active_record) + active_record.cache_versioning = true + # Remove the temporary load hook from SQLite3Adapter when this is removed + ActiveSupport.on_load(:active_record_sqlite3adapter) do + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true + end + end + + if respond_to?(:action_dispatch) + action_dispatch.use_authenticated_cookie_encryption = true + end + + if respond_to?(:active_support) + active_support.use_authenticated_message_encryption = true + active_support.use_sha1_digests = true + end + + if respond_to?(:action_controller) + action_controller.default_protect_from_forgery = true + end + + if respond_to?(:action_view) + action_view.form_with_generates_ids = true + end + when "6.0" + load_defaults "5.2" + + if respond_to?(:action_view) + action_view.default_enforce_utf8 = false + end + + if respond_to?(:action_dispatch) + action_dispatch.use_cookies_with_metadata = true + end + else + raise "Unknown version #{target_version.to_s.inspect}" + end + + @loaded_config_version = target_version end def encoding=(value) @@ -64,13 +145,21 @@ module Rails def api_only=(value) @api_only = value generators.api_only = value + + @debug_exception_response_format ||= :api + end + + def debug_exception_response_format + @debug_exception_response_format || :default end + attr_writer :debug_exception_response_format + def paths @paths ||= begin paths = super paths.add "config/database", with: "config/database.yml" - paths.add "config/secrets", with: "config/secrets.yml" + paths.add "config/secrets", with: "config", glob: "secrets.yml{,.enc}" paths.add "config/environment", with: "config/environment.rb" paths.add "lib/templates" paths.add "log", with: "log/#{Rails.env}.log" @@ -83,7 +172,7 @@ module Rails end # Loads and returns the entire raw configuration of database from - # values stored in `config/database.yml`. + # values stored in <tt>config/database.yml</tt>. def database_configuration path = paths["config/database"].existent.first yaml = Pathname.new(path) if path @@ -91,8 +180,15 @@ module Rails config = if yaml && yaml.exist? require "yaml" require "erb" - YAML.load(ERB.new(yaml.read).result) || {} - elsif ENV['DATABASE_URL'] + loaded_yaml = YAML.load(ERB.new(yaml.read).result) || {} + shared = loaded_yaml.delete("shared") + if shared + loaded_yaml.each do |_k, values| + values.reverse_merge!(shared) + end + end + Hash.new(shared).merge(loaded_yaml) + elsif ENV["DATABASE_URL"] # Value from ENV['DATABASE_URL'] is set to default database connection # by Active Record. {} @@ -106,11 +202,7 @@ module Rails "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \ "Error: #{e.message}" rescue => e - raise e, "Cannot load `Rails.application.database_configuration`:\n#{e.message}", e.backtrace - end - - def log_level - @log_level ||= (Rails.env.production? ? :info : :debug) + raise e, "Cannot load database configuration:\n#{e.message}", e.backtrace end def colorize_logging @@ -119,50 +211,90 @@ module Rails def colorize_logging=(val) ActiveSupport::LogSubscriber.colorize_logging = val - self.generators.colorize_logging = val + generators.colorize_logging = val end - def session_store(*args) - if args.empty? - case @session_store - when :disabled - nil - when :active_record_store + def session_store(new_session_store = nil, **options) + if new_session_store + if new_session_store == :active_record_store begin ActionDispatch::Session::ActiveRecordStore rescue NameError raise "`ActiveRecord::SessionStore` is extracted out of Rails into a gem. " \ "Please add `activerecord-session_store` to your Gemfile to use it." end + end + + @session_store = new_session_store + @session_options = options || {} + else + case @session_store + when :disabled + nil + when :active_record_store + ActionDispatch::Session::ActiveRecordStore when Symbol ActionDispatch::Session.const_get(@session_store.to_s.camelize) else @session_store end - else - @session_store = args.shift - @session_options = args.shift || {} end end + def session_store? #:nodoc: + @session_store + end + def annotations - SourceAnnotationExtractor::Annotation + Rails::SourceAnnotationExtractor::Annotation + end + + def content_security_policy(&block) + if block_given? + @content_security_policy = ActionDispatch::ContentSecurityPolicy.new(&block) + else + @content_security_policy + end + end + + class Custom #:nodoc: + def initialize + @configurations = Hash.new + end + + def method_missing(method, *args) + if method =~ /=$/ + @configurations[$`.to_sym] = args.first + else + @configurations.fetch(method) { + @configurations[method] = ActiveSupport::OrderedOptions.new + } + end + end + + def respond_to_missing?(symbol, *) + true + end end private - class Custom #:nodoc: - def initialize - @configurations = Hash.new - end - - def method_missing(method, *args) - if method =~ /=$/ - @configurations[$`.to_sym] = args.first - else - @configurations.fetch(method) { - @configurations[method] = ActiveSupport::OrderedOptions.new - } - end + def credentials_available_for_current_env? + File.exist?("#{root}/config/credentials/#{Rails.env}.yml.enc") + end + + def default_credentials_content_path + if credentials_available_for_current_env? + File.join(root, "config", "credentials", "#{Rails.env}.yml.enc") + else + File.join(root, "config", "credentials.yml.enc") + end + end + + def default_credentials_key_path + if credentials_available_for_current_env? + File.join(root, "config", "credentials", "#{Rails.env}.key") + else + File.join(root, "config", "master.key") end end end diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index 88eade5c5a..433a7ab41f 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails class Application class DefaultMiddlewareStack @@ -10,15 +12,17 @@ module Rails end def build_stack - ActionDispatch::MiddlewareStack.new.tap do |middleware| + ActionDispatch::MiddlewareStack.new do |middleware| if config.force_ssl middleware.use ::ActionDispatch::SSL, config.ssl_options end middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header - if config.serve_static_files - middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control, index: config.static_index + if config.public_file_server.enabled + headers = config.public_file_server.headers || {} + + middleware.use ::ActionDispatch::Static, paths["public"].first, index: config.public_file_server.index_name, headers: headers end if rack_cache = load_rack_cache @@ -31,68 +35,56 @@ module Rails # handling: presumably their code is not threadsafe middleware.use ::Rack::Lock - - elsif config.allow_concurrency == :unsafe - # Do nothing, even if we know this is dangerous. This is the - # historical behaviour for true. - - else - # Default concurrency setting: enabled, but safe - - unless config.cache_classes && config.eager_load - # Without cache_classes + eager_load, the load interlock - # is required for proper operation - - middleware.use ::ActionDispatch::LoadInterlock - end end + middleware.use ::ActionDispatch::Executor, app.executor + middleware.use ::Rack::Runtime middleware.use ::Rack::MethodOverride unless config.api_only middleware.use ::ActionDispatch::RequestId + middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies - # Must come after Rack::MethodOverride to properly log overridden methods middleware.use ::Rails::Rack::Logger, config.log_tags middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app - middleware.use ::ActionDispatch::DebugExceptions, app - middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies + middleware.use ::ActionDispatch::DebugExceptions, app, config.debug_exception_response_format unless config.cache_classes - middleware.use ::ActionDispatch::Reloader, lambda { reload_dependencies? } + middleware.use ::ActionDispatch::Reloader, app.reloader end middleware.use ::ActionDispatch::Callbacks middleware.use ::ActionDispatch::Cookies unless config.api_only if !config.api_only && config.session_store - if config.force_ssl && !config.session_options.key?(:secure) + if config.force_ssl && config.ssl_options.fetch(:secure_cookies, true) && !config.session_options.key?(:secure) config.session_options[:secure] = true end middleware.use config.session_store, config.session_options middleware.use ::ActionDispatch::Flash end - middleware.use ::ActionDispatch::ParamsParser + unless config.api_only + middleware.use ::ActionDispatch::ContentSecurityPolicy::Middleware + end + middleware.use ::Rack::Head middleware.use ::Rack::ConditionalGet middleware.use ::Rack::ETag, "no-cache" + + middleware.use ::Rack::TempfileReaper unless config.api_only end end private - def reload_dependencies? - config.reload_classes_only_on_change != true || app.reloaders.map(&:updated?).any? - end - def load_rack_cache rack_cache = config.action_dispatch.rack_cache return unless rack_cache begin - require 'rack/cache' + require "rack/cache" rescue LoadError => error - error.message << ' Be sure to add rack-cache to your Gemfile' + error.message << " Be sure to add rack-cache to your Gemfile" raise end diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index f8f92792a7..04aaf6dd9a 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails class Application module Finisher @@ -21,15 +23,26 @@ module Rails initializer :add_builtin_route do |app| if Rails.env.development? + app.routes.prepend do + get "/rails/info/properties" => "rails/info#properties", internal: true + get "/rails/info/routes" => "rails/info#routes", internal: true + get "/rails/info" => "rails/info#index", internal: true + end + app.routes.append do - get '/rails/info/properties' => "rails/info#properties" - get '/rails/info/routes' => "rails/info#routes" - get '/rails/info' => "rails/info#index" - get '/' => "rails/welcome#index" + get "/" => "rails/welcome#index", internal: true end end end + # Setup default session store if not already set in config/application.rb + initializer :setup_default_session_store, before: :build_middleware_stack do |app| + unless app.config.session_store? + app_name = app.class.name ? app.railtie_name.chomp("_application") : "" + app.config.session_store :cookie_store, key: "_#{app_name}_session" + end + end + initializer :build_middleware_stack do build_middleware_stack end @@ -38,16 +51,16 @@ module Rails app.routes.define_mounted_helper(:main_app) end - initializer :add_to_prepare_blocks do + initializer :add_to_prepare_blocks do |app| config.to_prepare_blocks.each do |block| - ActionDispatch::Reloader.to_prepare(&block) + app.reloader.to_prepare(&block) end end # This needs to happen before eager load so it happens - # in exactly the same point regardless of config.cache_classes - initializer :run_prepare_callbacks do - ActionDispatch::Reloader.prepare! + # in exactly the same point regardless of config.eager_load + initializer :run_prepare_callbacks do |app| + app.reloader.prepare! end initializer :eager_load! do @@ -62,13 +75,61 @@ module Rails ActiveSupport.run_load_hooks(:after_initialize, self) end + class MutexHook + def initialize(mutex = Mutex.new) + @mutex = mutex + end + + def run + @mutex.lock + end + + def complete(_state) + @mutex.unlock + end + end + + module InterlockHook + def self.run + ActiveSupport::Dependencies.interlock.start_running + end + + def self.complete(_state) + ActiveSupport::Dependencies.interlock.done_running + end + end + + initializer :configure_executor_for_concurrency do |app| + if config.allow_concurrency == false + # User has explicitly opted out of concurrent request + # handling: presumably their code is not threadsafe + + app.executor.register_hook(MutexHook.new, outer: true) + + elsif config.allow_concurrency == :unsafe + # Do nothing, even if we know this is dangerous. This is the + # historical behavior for true. + + else + # Default concurrency setting: enabled, but safe + + unless config.cache_classes && config.eager_load + # Without cache_classes + eager_load, the load interlock + # is required for proper operation + + app.executor.register_hook(InterlockHook, outer: true) + end + end + end + # Set routes reload after the finisher hook to ensure routes added in # the hook are taken into account. - initializer :set_routes_reloader_hook do + initializer :set_routes_reloader_hook do |app| reloader = routes_reloader - reloader.execute_if_updated - self.reloaders << reloader - ActionDispatch::Reloader.to_prepare do + reloader.eager_load = app.config.eager_load + reloader.execute + reloaders << reloader + app.reloader.to_run do # We configure #execute rather than #execute_if_updated because if # autoloaded constants are cleared we need to reload routes also in # case any was used there, as in @@ -78,42 +139,55 @@ module Rails # This means routes are also reloaded if i18n is updated, which # might not be necessary, but in order to be more precise we need # some sort of reloaders dependency support, to be added. + require_unload_lock! reloader.execute end end # Set clearing dependencies after the finisher hook to ensure paths # added in the hook are taken into account. - initializer :set_clear_dependencies_hook, group: :all do + initializer :set_clear_dependencies_hook, group: :all do |app| callback = lambda do - ActiveSupport::Dependencies.interlock.attempt_loading do - ActiveSupport::DescendantsTracker.clear - ActiveSupport::Dependencies.clear + ActiveSupport::DescendantsTracker.clear + ActiveSupport::Dependencies.clear + end + + if config.cache_classes + app.reloader.check = lambda { false } + elsif config.reload_classes_only_on_change + app.reloader.check = lambda do + app.reloaders.map(&:updated?).any? end + else + app.reloader.check = lambda { true } end if config.reload_classes_only_on_change reloader = config.file_watcher.new(*watchable_args, &callback) - self.reloaders << reloader + reloaders << reloader # Prepend this callback to have autoloaded constants cleared before # any other possible reloading, in case they need to autoload fresh # constants. - ActionDispatch::Reloader.to_prepare(prepend: true) do + app.reloader.to_run(prepend: true) do # In addition to changes detected by the file watcher, if routes # or i18n have been updated we also need to clear constants, # that's why we run #execute rather than #execute_if_updated, this # callback has to clear autoloaded constants after any update. - reloader.execute + class_unload! do + reloader.execute + end end else - ActionDispatch::Reloader.to_cleanup(&callback) + app.reloader.to_complete do + class_unload!(&callback) + end end end # Disable dependency loading during request cycle initializer :disable_dependency_loading do - if config.eager_load && config.cache_classes + if config.eager_load && config.cache_classes && !config.enable_dependency_loading ActiveSupport::Dependencies.unhook! end end diff --git a/railties/lib/rails/application/routes_reloader.rb b/railties/lib/rails/application/routes_reloader.rb index cf0a4e128f..3ecb8e264e 100644 --- a/railties/lib/rails/application/routes_reloader.rb +++ b/railties/lib/rails/application/routes_reloader.rb @@ -1,20 +1,25 @@ +# frozen_string_literal: true + require "active_support/core_ext/module/delegation" module Rails class Application class RoutesReloader attr_reader :route_sets, :paths + attr_accessor :eager_load delegate :execute_if_updated, :execute, :updated?, to: :updater def initialize @paths = [] @route_sets = [] + @eager_load = false end def reload! clear! load_paths finalize! + route_sets.each(&:eager_load!) if eager_load ensure revert end @@ -22,11 +27,7 @@ module Rails private def updater - @updater ||= begin - updater = ActiveSupport::FileUpdateChecker.new(paths) { reload! } - updater.execute - updater - end + @updater ||= ActiveSupport::FileUpdateChecker.new(paths) { reload! } end def clear! diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb index 9a29ec21cf..b3fe822218 100644 --- a/railties/lib/rails/application_controller.rb +++ b/railties/lib/rails/application_controller.rb @@ -1,16 +1,29 @@ +# frozen_string_literal: true + class Rails::ApplicationController < ActionController::Base # :nodoc: - self.view_paths = File.expand_path('../templates', __FILE__) - layout 'application' + self.view_paths = File.expand_path("templates", __dir__) + layout "application" - protected + before_action :disable_content_security_policy_nonce! - def require_local! - unless local_request? - render text: '<p>For security purposes, this information is only available to local requests.</p>', status: :forbidden - end + content_security_policy do |policy| + policy.script_src :unsafe_inline + policy.style_src :unsafe_inline end - def local_request? - Rails.application.config.consider_all_requests_local || request.local? - end + private + + def require_local! + unless local_request? + render html: "<p>For security purposes, this information is only available to local requests.</p>".html_safe, status: :forbidden + end + end + + def local_request? + Rails.application.config.consider_all_requests_local || request.local? + end + + def disable_content_security_policy_nonce! + request.content_security_policy_nonce_generator = nil + end end diff --git a/railties/lib/rails/backtrace_cleaner.rb b/railties/lib/rails/backtrace_cleaner.rb index 5276eb33c9..7c2eb1dc42 100644 --- a/railties/lib/rails/backtrace_cleaner.rb +++ b/railties/lib/rails/backtrace_cleaner.rb @@ -1,32 +1,22 @@ -require 'active_support/backtrace_cleaner' +# frozen_string_literal: true + +require "active_support/backtrace_cleaner" module Rails class BacktraceCleaner < ActiveSupport::BacktraceCleaner - APP_DIRS_PATTERN = /^\/?(app|config|lib|test)/ - RENDER_TEMPLATE_PATTERN = /:in `_render_template_\w*'/ - EMPTY_STRING = ''.freeze - SLASH = '/'.freeze - DOT_SLASH = './'.freeze + APP_DIRS_PATTERN = /^\/?(app|config|lib|test|\(\w*\))/ + RENDER_TEMPLATE_PATTERN = /:in `.*_\w+_{2,3}\d+_\d+'/ + EMPTY_STRING = "" + SLASH = "/" + DOT_SLASH = "./" def initialize super - @root = "#{Rails.root}/".freeze + @root = "#{Rails.root}/" add_filter { |line| line.sub(@root, EMPTY_STRING) } add_filter { |line| line.sub(RENDER_TEMPLATE_PATTERN, EMPTY_STRING) } add_filter { |line| line.sub(DOT_SLASH, SLASH) } # for tests - - add_gem_filters - add_silencer { |line| line !~ APP_DIRS_PATTERN } + add_silencer { |line| !APP_DIRS_PATTERN.match?(line) } end - - private - def add_gem_filters - gems_paths = (Gem.path | [Gem.default_dir]).map { |p| Regexp.escape(p) } - return if gems_paths.empty? - - gems_regexp = %r{(#{gems_paths.join('|')})/gems/([^/]+)-([\w.]+)/(.*)} - gems_result = '\2 (\3) \4'.freeze - add_filter { |line| line.sub(gems_regexp, gems_result) } - end end end diff --git a/railties/lib/rails/cli.rb b/railties/lib/rails/cli.rb index a8794bc0de..e56e604fdc 100644 --- a/railties/lib/rails/cli.rb +++ b/railties/lib/rails/cli.rb @@ -1,15 +1,19 @@ -require 'rails/app_loader' +# frozen_string_literal: true + +require "rails/app_loader" # If we are inside a Rails application this method performs an exec and thus # the rest of this script is not run. Rails::AppLoader.exec_app -require 'rails/ruby_version_check' +require "rails/ruby_version_check" Signal.trap("INT") { puts; exit(1) } -if ARGV.first == 'plugin' +require "rails/command" + +if ARGV.first == "plugin" ARGV.shift - require 'rails/commands/plugin' + Rails::Command.invoke :plugin, ARGV else - require 'rails/commands/application' + Rails::Command.invoke :application, ARGV end diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb index fd352dc9b7..19d331ff30 100644 --- a/railties/lib/rails/code_statistics.rb +++ b/railties/lib/rails/code_statistics.rb @@ -1,13 +1,18 @@ -require 'rails/code_statistics_calculator' +# frozen_string_literal: true + +require "rails/code_statistics_calculator" +require "active_support/core_ext/enumerable" class CodeStatistics #:nodoc: + TEST_TYPES = ["Controller tests", + "Helper tests", + "Model tests", + "Mailer tests", + "Job tests", + "Integration tests", + "System tests"] - TEST_TYPES = ['Controller tests', - 'Helper tests', - 'Model tests', - 'Mailer tests', - 'Job tests', - 'Integration tests'] + HEADERS = { lines: " Lines", code_lines: " LOC", classes: "Classes", methods: "Methods" } def initialize(*pairs) @pairs = pairs @@ -30,10 +35,10 @@ class CodeStatistics #:nodoc: private def calculate_statistics - Hash[@pairs.map{|pair| [pair.first, calculate_directory_statistics(pair.last)]}] + Hash[@pairs.map { |pair| [pair.first, calculate_directory_statistics(pair.last)] }] end - def calculate_directory_statistics(directory, pattern = /.*\.(rb|js|coffee)$/) + def calculate_directory_statistics(directory, pattern = /^(?!\.).*?\.(rb|js|coffee|rake)$/) stats = CodeStatisticsCalculator.new Dir.foreach(directory) do |file_name| @@ -41,7 +46,7 @@ class CodeStatistics #:nodoc: if File.directory?(path) && (/^\./ !~ file_name) stats.add(calculate_directory_statistics(path, pattern)) - elsif file_name =~ pattern + elsif file_name&.match?(pattern) stats.add_by_file_path(path) end end @@ -67,34 +72,44 @@ class CodeStatistics #:nodoc: test_loc end + def width_for(label) + [@statistics.values.sum { |s| s.send(label) }.to_s.size, HEADERS[label].length].max + end + def print_header print_splitter - puts "| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |" + print "| Name " + HEADERS.each do |k, v| + print " | #{v.rjust(width_for(k))}" + end + puts " | M/C | LOC/M |" print_splitter end def print_splitter - puts "+----------------------+--------+--------+---------+---------+-----+-------+" + print "+----------------------" + HEADERS.each_key do |k| + print "+#{'-' * (width_for(k) + 2)}" + end + puts "+-----+-------+" end def print_line(name, statistics) m_over_c = (statistics.methods / statistics.classes) rescue m_over_c = 0 loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue loc_over_m = 0 - puts "| #{name.ljust(20)} " \ - "| #{statistics.lines.to_s.rjust(6)} " \ - "| #{statistics.code_lines.to_s.rjust(6)} " \ - "| #{statistics.classes.to_s.rjust(7)} " \ - "| #{statistics.methods.to_s.rjust(7)} " \ - "| #{m_over_c.to_s.rjust(3)} " \ - "| #{loc_over_m.to_s.rjust(5)} |" + print "| #{name.ljust(20)} " + HEADERS.each_key do |k| + print "| #{statistics.send(k).to_s.rjust(width_for(k))} " + end + puts "| #{m_over_c.to_s.rjust(3)} | #{loc_over_m.to_s.rjust(5)} |" end def print_code_test_stats code = calculate_code tests = calculate_tests - puts " Code LOC: #{code} Test LOC: #{tests} Code to Test Ratio: 1:#{sprintf("%.1f", tests.to_f/code)}" + puts " Code LOC: #{code} Test LOC: #{tests} Code to Test Ratio: 1:#{sprintf("%.1f", tests.to_f / code)}" puts "" end end diff --git a/railties/lib/rails/code_statistics_calculator.rb b/railties/lib/rails/code_statistics_calculator.rb index a142236dbe..85f86bdbd0 100644 --- a/railties/lib/rails/code_statistics_calculator.rb +++ b/railties/lib/rails/code_statistics_calculator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CodeStatisticsCalculator #:nodoc: attr_reader :lines, :code_lines, :classes, :methods @@ -25,6 +27,7 @@ class CodeStatisticsCalculator #:nodoc: } PATTERNS[:minitest] = PATTERNS[:rb].merge method: /^\s*(def|test)\s+['"_a-z]/ + PATTERNS[:rake] = PATTERNS[:rb] def initialize(lines = 0, code_lines = 0, classes = 0, methods = 0) @lines = lines @@ -42,7 +45,7 @@ class CodeStatisticsCalculator #:nodoc: def add_by_file_path(file_path) File.open(file_path) do |f| - self.add_by_io(f, file_type(file_path)) + add_by_io(f, file_type(file_path)) end end @@ -76,10 +79,10 @@ class CodeStatisticsCalculator #:nodoc: private def file_type(file_path) - if file_path.end_with? '_test.rb' + if file_path.end_with? "_test.rb" :minitest else - File.extname(file_path).sub(/\A\./, '').downcase.to_sym + File.extname(file_path).sub(/\A\./, "").downcase.to_sym end end end diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb new file mode 100644 index 0000000000..f09aa3ae0d --- /dev/null +++ b/railties/lib/rails/command.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require "active_support" +require "active_support/dependencies/autoload" +require "active_support/core_ext/enumerable" +require "active_support/core_ext/object/blank" + +require "thor" + +module Rails + module Command + extend ActiveSupport::Autoload + + autoload :Spellchecker + autoload :Behavior + autoload :Base + + include Behavior + + HELP_MAPPINGS = %w(-h -? --help) + + class << self + def hidden_commands # :nodoc: + @hidden_commands ||= [] + end + + def environment # :nodoc: + ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development" + end + + # Receives a namespace, arguments and the behavior to invoke the command. + def invoke(full_namespace, args = [], **config) + namespace = full_namespace = full_namespace.to_s + + if char = namespace =~ /:(\w+)$/ + command_name, namespace = $1, namespace.slice(0, char) + else + command_name = namespace + end + + command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name) + command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name) + + command = find_by_namespace(namespace, command_name) + if command && command.all_commands[command_name] + command.perform(command_name, args, config) + else + find_by_namespace("rake").perform(full_namespace, args, config) + end + end + + # Rails finds namespaces similar to Thor, it only adds one rule: + # + # Command names must end with "_command.rb". This is required because Rails + # looks in load paths and loads the command just before it's going to be used. + # + # find_by_namespace :webrat, :rails, :integration + # + # Will search for the following commands: + # + # "rails:webrat", "webrat:integration", "webrat" + # + # Notice that "rails:commands:webrat" could be loaded as well, what + # Rails looks for is the first and last parts of the namespace. + def find_by_namespace(namespace, command_name = nil) # :nodoc: + lookups = [ namespace ] + lookups << "#{namespace}:#{command_name}" if command_name + lookups.concat lookups.map { |lookup| "rails:#{lookup}" } + + lookup(lookups) + + namespaces = subclasses.index_by(&:namespace) + namespaces[(lookups & namespaces.keys).first] + end + + # Returns the root of the Rails engine or app running the command. + def root + if defined?(ENGINE_ROOT) + Pathname.new(ENGINE_ROOT) + elsif defined?(APP_PATH) + Pathname.new(File.expand_path("../..", APP_PATH)) + end + end + + def print_commands # :nodoc: + commands.each { |command| puts(" #{command}") } + end + + private + COMMANDS_IN_USAGE = %w(generate console server test test:system dbconsole new) + private_constant :COMMANDS_IN_USAGE + + def commands + lookup! + + visible_commands = (subclasses - hidden_commands).flat_map(&:printing_commands) + + (visible_commands - COMMANDS_IN_USAGE).sort + end + + def command_type # :doc: + @command_type ||= "command" + end + + def lookup_paths # :doc: + @lookup_paths ||= %w( rails/commands commands ) + end + + def file_lookup_paths # :doc: + @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_command.rb" ] + end + end + end +end diff --git a/railties/lib/rails/command/actions.rb b/railties/lib/rails/command/actions.rb new file mode 100644 index 0000000000..cbb743346b --- /dev/null +++ b/railties/lib/rails/command/actions.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Rails + module Command + module Actions + # Change to the application's path if there is no <tt>config.ru</tt> file in current directory. + # This allows us to run <tt>rails server</tt> from other directories, but still get + # the main <tt>config.ru</tt> and properly set the <tt>tmp</tt> directory. + def set_application_directory! + Dir.chdir(File.expand_path("../..", APP_PATH)) unless File.exist?(File.expand_path("config.ru")) + end + + def require_application_and_environment! + require ENGINE_PATH if defined?(ENGINE_PATH) + + if defined?(APP_PATH) + require APP_PATH + Rails.application.require_environment! + end + end + + if defined?(ENGINE_PATH) + def load_tasks + Rake.application.init("rails") + Rake.application.load_rakefile + end + + def load_generators + engine = ::Rails::Engine.find(ENGINE_ROOT) + Rails::Generators.namespace = engine.railtie_namespace + engine.load_generators + end + else + def load_tasks + Rails.application.load_tasks + end + + def load_generators + Rails.application.load_generators + end + end + end + end +end diff --git a/railties/lib/rails/command/base.rb b/railties/lib/rails/command/base.rb new file mode 100644 index 0000000000..766872de8a --- /dev/null +++ b/railties/lib/rails/command/base.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + +require "thor" +require "erb" + +require "active_support/core_ext/string/filters" +require "active_support/core_ext/string/inflections" + +require "rails/command/actions" + +module Rails + module Command + class Base < Thor + class Error < Thor::Error # :nodoc: + end + + include Actions + + class << self + # Returns true when the app is a Rails engine. + def engine? + defined?(ENGINE_ROOT) + end + + # Tries to get the description from a USAGE file one folder above the command + # root. + def desc(usage = nil, description = nil, options = {}) + if usage + super + else + @desc ||= ERB.new(File.read(usage_path)).result(binding) if usage_path + end + end + + # Convenience method to get the namespace from the class name. It's the + # same as Thor default except that the Command at the end of the class + # is removed. + def namespace(name = nil) + if name + super + else + @namespace ||= super.chomp("_command").sub(/:command:/, ":") + end + end + + # Convenience method to hide this command from the available ones when + # running rails command. + def hide_command! + Rails::Command.hidden_commands << self + end + + def inherited(base) #:nodoc: + super + + if base.name && base.name !~ /Base$/ + Rails::Command.subclasses << base + end + end + + def perform(command, args, config) # :nodoc: + if Rails::Command::HELP_MAPPINGS.include?(args.first) + command, args = "help", [] + end + + dispatch(command, args.dup, nil, config) + end + + def printing_commands + namespaced_commands + end + + def executable + "rails #{command_name}" + end + + # Use Rails' default banner. + def banner(*) + "#{executable} #{arguments.map(&:usage).join(' ')} [options]".squish + end + + # Sets the base_name taking into account the current class namespace. + # + # Rails::Command::TestCommand.base_name # => 'rails' + def base_name + @base_name ||= begin + if base = name.to_s.split("::").first + base.underscore + end + end + end + + # Return command name without namespaces. + # + # Rails::Command::TestCommand.command_name # => 'test' + def command_name + @command_name ||= begin + if command = name.to_s.split("::").last + command.chomp!("Command") + command.underscore + end + end + end + + # Path to lookup a USAGE description in a file. + def usage_path + if default_command_root + path = File.join(default_command_root, "USAGE") + path if File.exist?(path) + end + end + + # Default file root to place extra files a command might need, placed + # one folder above the command file. + # + # For a Rails::Command::TestCommand placed in <tt>rails/command/test_command.rb</tt> + # would return <tt>rails/test</tt>. + def default_command_root + path = File.expand_path(File.join("../commands", command_root_namespace), __dir__) + path if File.exist?(path) + end + + private + # Allow the command method to be called perform. + def create_command(meth) + if meth == "perform" + alias_method command_name, meth + else + # Prevent exception about command without usage. + # Some commands define their documentation differently. + @usage ||= "" + @desc ||= "" + + super + end + end + + def command_root_namespace + (namespace.split(":") - %w( rails )).first + end + + def namespaced_commands + commands.keys.map do |key| + key == command_root_namespace ? key : "#{command_root_namespace}:#{key}" + end + end + end + + def help + if command_name = self.class.command_name + self.class.command_help(shell, command_name) + else + super + end + end + end + end +end diff --git a/railties/lib/rails/command/behavior.rb b/railties/lib/rails/command/behavior.rb new file mode 100644 index 0000000000..718e2d9ab2 --- /dev/null +++ b/railties/lib/rails/command/behavior.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require "active_support" + +module Rails + module Command + module Behavior #:nodoc: + extend ActiveSupport::Concern + + class_methods do + # Remove the color from output. + def no_color! + Thor::Base.shell = Thor::Shell::Basic + end + + # Track all command subclasses. + def subclasses + @subclasses ||= [] + end + + private + # Prints a list of generators. + def print_list(base, namespaces) + return if namespaces.empty? + puts "#{base.camelize}:" + + namespaces.each do |namespace| + puts(" #{namespace}") + end + + puts + end + + # Receives namespaces in an array and tries to find matching generators + # in the load path. + def lookup(namespaces) + paths = namespaces_to_paths(namespaces) + + paths.each do |raw_path| + lookup_paths.each do |base| + path = "#{base}/#{raw_path}_#{command_type}" + + begin + require path + return + rescue LoadError => e + raise unless e.message =~ /#{Regexp.escape(path)}$/ + rescue Exception => e + warn "[WARNING] Could not load #{command_type} #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}" + end + end + end + end + + # This will try to load any command in the load path to show in help. + def lookup! + $LOAD_PATH.each do |base| + Dir[File.join(base, *file_lookup_paths)].each do |path| + begin + path = path.sub("#{base}/", "") + require path + rescue Exception + # No problem + end + end + end + end + + # Convert namespaces to paths by replacing ":" for "/" and adding + # an extra lookup. For example, "rails:model" should be searched + # in both: "rails/model/model_generator" and "rails/model_generator". + def namespaces_to_paths(namespaces) + paths = [] + namespaces.each do |namespace| + pieces = namespace.split(":") + paths << pieces.dup.push(pieces.last).join("/") + paths << pieces.join("/") + end + paths.uniq! + paths + end + end + end + end +end diff --git a/railties/lib/rails/command/environment_argument.rb b/railties/lib/rails/command/environment_argument.rb new file mode 100644 index 0000000000..5dc98b113d --- /dev/null +++ b/railties/lib/rails/command/environment_argument.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "active_support" + +module Rails + module Command + module EnvironmentArgument #:nodoc: + extend ActiveSupport::Concern + + included do + argument :environment, optional: true, banner: "environment" + + class_option :environment, aliases: "-e", type: :string, + desc: "Specifies the environment to run this console under (test/development/production)." + end + + private + def extract_environment_option_from_argument + if environment + self.options = options.merge(environment: acceptable_environment(environment)) + + ActiveSupport::Deprecation.warn "Passing the environment's name as a " \ + "regular argument is deprecated and " \ + "will be removed in the next Rails " \ + "version. Please, use the -e option " \ + "instead." + elsif options[:environment] + self.options = options.merge(environment: acceptable_environment(options[:environment])) + else + self.options = options.merge(environment: Rails::Command.environment) + end + end + + def acceptable_environment(env = nil) + if available_environments.include? env + env + else + %w( production development test ).detect { |e| e =~ /^#{env}/ } || env + end + end + + def available_environments + Dir["config/environments/*.rb"].map { |fname| File.basename(fname, ".*") } + end + end + end +end diff --git a/railties/lib/rails/command/helpers/editor.rb b/railties/lib/rails/command/helpers/editor.rb new file mode 100644 index 0000000000..6191d97672 --- /dev/null +++ b/railties/lib/rails/command/helpers/editor.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "active_support/encrypted_file" + +module Rails + module Command + module Helpers + module Editor + private + def ensure_editor_available(command:) + if ENV["EDITOR"].to_s.empty? + say "No $EDITOR to open file in. Assign one like this:" + say "" + say %(EDITOR="mate --wait" #{command}) + say "" + say "For editors that fork and exit immediately, it's important to pass a wait flag," + say "otherwise the credentials will be saved immediately with no chance to edit." + + false + else + true + end + end + + def catch_editing_exceptions + yield + rescue Interrupt + say "Aborted changing file: nothing saved." + rescue ActiveSupport::EncryptedFile::MissingKeyError => error + say error.message + end + end + end + end +end diff --git a/railties/lib/rails/command/spellchecker.rb b/railties/lib/rails/command/spellchecker.rb new file mode 100644 index 0000000000..085d5b16df --- /dev/null +++ b/railties/lib/rails/command/spellchecker.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Rails + module Command + module Spellchecker # :nodoc: + class << self + def suggest(word, from:) + if defined?(DidYouMean::SpellChecker) + DidYouMean::SpellChecker.new(dictionary: from.map(&:to_s)).correct(word).first + else + from.sort_by { |w| levenshtein_distance(word, w) }.first + end + end + + private + + # This code is based directly on the Text gem implementation. + # Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher. + # + # Returns a value representing the "cost" of transforming str1 into str2. + def levenshtein_distance(str1, str2) # :doc: + s = str1 + t = str2 + n = s.length + m = t.length + + return m if 0 == n + return n if 0 == m + + d = (0..m).to_a + x = nil + + # avoid duplicating an enumerable object in the loop + str2_codepoint_enumerable = str2.each_codepoint + + str1.each_codepoint.with_index do |char1, i| + e = i + 1 + + str2_codepoint_enumerable.with_index do |char2, j| + cost = (char1 == char2) ? 0 : 1 + x = [ + d[j + 1] + 1, # insertion + e + 1, # deletion + d[j] + cost # substitution + ].min + d[j] = e + e = x + end + + d[m] = x + end + + x + end + end + end + end +end diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index 12bd73db24..77961a0292 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -1,4 +1,6 @@ -ARGV << '--help' if ARGV.empty? +# frozen_string_literal: true + +require "rails/command" aliases = { "g" => "generate", @@ -7,12 +9,10 @@ aliases = { "s" => "server", "db" => "dbconsole", "r" => "runner", - "t" => "test", + "t" => "test" } command = ARGV.shift command = aliases[command] || command -require 'rails/commands/commands_tasks' - -Rails::CommandsTasks.new(ARGV).run_command!(command) +Rails::Command.invoke command, ARGV diff --git a/railties/lib/rails/commands/application.rb b/railties/lib/rails/commands/application.rb deleted file mode 100644 index c998e6b6a8..0000000000 --- a/railties/lib/rails/commands/application.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'rails/generators' -require 'rails/generators/rails/app/app_generator' - -module Rails - module Generators - class AppGenerator # :nodoc: - # We want to exit on failure to be kind to other libraries - # This is only when accessing via CLI - def self.exit_on_failure? - true - end - end - end -end - -args = Rails::Generators::ARGVScrubber.new(ARGV).prepare! -Rails::Generators::AppGenerator.start args diff --git a/railties/lib/rails/commands/application/application_command.rb b/railties/lib/rails/commands/application/application_command.rb new file mode 100644 index 0000000000..f77553b830 --- /dev/null +++ b/railties/lib/rails/commands/application/application_command.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "rails/generators" +require "rails/generators/rails/app/app_generator" + +module Rails + module Generators + class AppGenerator # :nodoc: + # We want to exit on failure to be kind to other libraries + # This is only when accessing via CLI + def self.exit_on_failure? + true + end + end + end + + module Command + class ApplicationCommand < Base # :nodoc: + hide_command! + + def help + perform # Punt help output to the generator. + end + + def perform(*args) + Rails::Generators::AppGenerator.start \ + Rails::Generators::ARGVScrubber.new(args).prepare! + end + end + end +end diff --git a/railties/lib/rails/commands/commands_tasks.rb b/railties/lib/rails/commands/commands_tasks.rb deleted file mode 100644 index 685d55eea8..0000000000 --- a/railties/lib/rails/commands/commands_tasks.rb +++ /dev/null @@ -1,185 +0,0 @@ -module Rails - # This is a class which takes in a rails command and initiates the appropriate - # initiation sequence. - # - # Warning: This class mutates ARGV because some commands require manipulating - # it before they are run. - class CommandsTasks # :nodoc: - attr_reader :argv - - HELP_MESSAGE = <<-EOT -Usage: rails COMMAND [ARGS] - -The most common rails commands are: - generate Generate new code (short-cut alias: "g") - console Start the Rails console (short-cut alias: "c") - server Start the Rails server (short-cut alias: "s") - test Run tests (short-cut alias: "t") - dbconsole Start a console for the database specified in config/database.yml - (short-cut alias: "db") - new Create a new Rails application. "rails new my_app" creates a - new application called MyApp in "./my_app" - -In addition to those, there are: - destroy Undo code generated with "generate" (short-cut alias: "d") - plugin new Generates skeleton for developing a Rails plugin - runner Run a piece of code in the application environment (short-cut alias: "r") - -All commands can be run with -h (or --help) for more information. -EOT - - COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help test) - - def initialize(argv) - @argv = argv - end - - def run_command!(command) - command = parse_command(command) - if COMMAND_WHITELIST.include?(command) - send(command) - else - write_error_message(command) - end - end - - def plugin - require_command!("plugin") - end - - def generate - generate_or_destroy(:generate) - end - - def destroy - generate_or_destroy(:destroy) - end - - def console - require_command!("console") - options = Rails::Console.parse_arguments(argv) - - # RAILS_ENV needs to be set before config/application is required - ENV['RAILS_ENV'] = options[:environment] if options[:environment] - - # shift ARGV so IRB doesn't freak - shift_argv! - - require_application_and_environment! - Rails::Console.start(Rails.application, options) - end - - def server - set_application_directory! - require_command!("server") - - Rails::Server.new.tap do |server| - # We need to require application after the server sets environment, - # otherwise the --environment option given to the server won't propagate. - require APP_PATH - Dir.chdir(Rails.application.root) - server.start - end - end - - def test - require_command!("test") - end - - def dbconsole - require_command!("dbconsole") - Rails::DBConsole.start - end - - def runner - require_command!("runner") - end - - def new - if %w(-h --help).include?(argv.first) - require_command!("application") - else - exit_with_initialization_warning! - end - end - - def version - argv.unshift '--version' - require_command!("application") - end - - def help - write_help_message - end - - private - - def exit_with_initialization_warning! - puts "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n" - puts "Type 'rails' for help." - exit(1) - end - - def shift_argv! - argv.shift if argv.first && argv.first[0] != '-' - end - - def require_command!(command) - require "rails/commands/#{command}" - end - - def generate_or_destroy(command) - require 'rails/generators' - require_application_and_environment! - Rails.application.load_generators - require_command!(command) - end - - # Change to the application's path if there is no config.ru file in current directory. - # This allows us to run `rails server` from other directories, but still get - # the main config.ru and properly set the tmp directory. - def set_application_directory! - Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru")) - end - - def require_application_and_environment! - require APP_PATH - Rails.application.require_environment! - end - - def write_help_message - puts HELP_MESSAGE - end - - # Output an error message stating that the attempted command is not a valid rails command. - # Run the attempted command as a rake command with the --dry-run flag. If successful, suggest - # to the user that they possibly meant to run the given rails command as a rake command. - # Append the help message. - # - # Example: - # $ rails db:migrate - # Error: Command 'db:migrate' not recognized - # Did you mean: `$ rake db:migrate` ? - # (Help message output) - # - def write_error_message(command) - puts "Error: Command '#{command}' not recognized" - if %x{rake #{command} --dry-run 2>&1 } && $?.success? - puts "Did you mean: `$ rake #{command}` ?\n\n" - end - write_help_message - exit(1) - end - - def parse_command(command) - case command - when '--version', '-v' - 'version' - when '--help', '-h' - 'help' - else - command - end - end - end -end diff --git a/railties/lib/rails/commands/console.rb b/railties/lib/rails/commands/console.rb deleted file mode 100644 index ea5d20ea24..0000000000 --- a/railties/lib/rails/commands/console.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'optparse' -require 'irb' -require 'irb/completion' -require 'rails/commands/console_helper' - -module Rails - class Console - include ConsoleHelper - - class << self - def parse_arguments(arguments) - options = {} - - OptionParser.new do |opt| - opt.banner = "Usage: rails console [environment] [options]" - opt.on('-s', '--sandbox', 'Rollback database modifications on exit.') { |v| options[:sandbox] = v } - opt.on("-e", "--environment=name", String, - "Specifies the environment to run this console under (test/development/production).", - "Default: development") { |v| options[:environment] = v.strip } - opt.parse!(arguments) - end - - set_options_env(arguments, options) - end - end - - attr_reader :options, :app, :console - - def initialize(app, options={}) - @app = app - @options = options - - app.sandbox = sandbox? - app.load_console - - @console = app.config.console || IRB - end - - def sandbox? - options[:sandbox] - end - - def environment - options[:environment] ||= super - end - alias_method :environment?, :environment - - def set_environment! - Rails.env = environment - end - - def start - set_environment! if environment? - - if sandbox? - puts "Loading #{Rails.env} environment in sandbox (Rails #{Rails.version})" - puts "Any modifications you make will be rolled back on exit" - else - puts "Loading #{Rails.env} environment (Rails #{Rails.version})" - end - - if defined?(console::ExtendCommandBundle) - console::ExtendCommandBundle.include(Rails::ConsoleMethods) - end - console.start - end - end -end diff --git a/railties/lib/rails/commands/console/console_command.rb b/railties/lib/rails/commands/console/console_command.rb new file mode 100644 index 0000000000..e35faa5b01 --- /dev/null +++ b/railties/lib/rails/commands/console/console_command.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require "irb" +require "irb/completion" + +require "rails/command/environment_argument" + +module Rails + class Console + module BacktraceCleaner + def filter_backtrace(bt) + if result = super + Rails.backtrace_cleaner.filter([result]).first + end + end + end + + def self.start(*args) + new(*args).start + end + + attr_reader :options, :app, :console + + def initialize(app, options = {}) + @app = app + @options = options + + app.sandbox = sandbox? + app.load_console + + @console = app.config.console || IRB + + if @console == IRB + IRB::WorkSpace.prepend(BacktraceCleaner) + end + end + + def sandbox? + options[:sandbox] + end + + def environment + options[:environment] + end + alias_method :environment?, :environment + + def set_environment! + Rails.env = environment + end + + def start + set_environment! if environment? + + if sandbox? + puts "Loading #{Rails.env} environment in sandbox (Rails #{Rails.version})" + puts "Any modifications you make will be rolled back on exit" + else + puts "Loading #{Rails.env} environment (Rails #{Rails.version})" + end + + if defined?(console::ExtendCommandBundle) + console::ExtendCommandBundle.include(Rails::ConsoleMethods) + end + console.start + end + end + + module Command + class ConsoleCommand < Base # :nodoc: + include EnvironmentArgument + + class_option :sandbox, aliases: "-s", type: :boolean, default: false, + desc: "Rollback database modifications on exit." + + def initialize(args = [], local_options = {}, config = {}) + console_options = [] + + # For the same behavior as OptionParser, leave only options after "--" in ARGV. + termination = local_options.find_index("--") + if termination + console_options = local_options[termination + 1..-1] + local_options = local_options[0...termination] + end + + ARGV.replace(console_options) + super(args, local_options, config) + end + + def perform + extract_environment_option_from_argument + + # RAILS_ENV needs to be set before config/application is required. + ENV["RAILS_ENV"] = options[:environment] + + require_application_and_environment! + Rails::Console.start(Rails.application, options) + end + end + end +end diff --git a/railties/lib/rails/commands/console_helper.rb b/railties/lib/rails/commands/console_helper.rb deleted file mode 100644 index 8ee0b60012..0000000000 --- a/railties/lib/rails/commands/console_helper.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'active_support/concern' - -module Rails - module ConsoleHelper # :nodoc: - extend ActiveSupport::Concern - - module ClassMethods - def start(*args) - new(*args).start - end - - private - def set_options_env(arguments, options) - if arguments.first && arguments.first[0] != '-' - env = arguments.first - if available_environments.include? env - options[:environment] = env - else - options[:environment] = %w(production development test).detect { |e| e =~ /^#{env}/ } || env - end - end - options - end - - def available_environments - Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') } - end - end - - def environment - ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" - end - end -end
\ No newline at end of file diff --git a/railties/lib/rails/commands/credentials/USAGE b/railties/lib/rails/commands/credentials/USAGE new file mode 100644 index 0000000000..6b33d1ab74 --- /dev/null +++ b/railties/lib/rails/commands/credentials/USAGE @@ -0,0 +1,49 @@ +=== Storing Encrypted Credentials in Source Control + +The Rails `credentials` commands provide access to encrypted credentials, +so you can safely store access tokens, database passwords, and the like +safely inside the app without relying on a mess of ENVs. + +This also allows for atomic deploys: no need to coordinate key changes +to get everything working as the keys are shipped with the code. + +=== Setup + +Applications after Rails 5.2 automatically have a basic credentials file generated +that just contains the secret_key_base used by MessageVerifiers/MessageEncryptors, like the ones +signing and encrypting cookies. + +For applications created prior to Rails 5.2, we'll automatically generate a new +credentials file in `config/credentials.yml.enc` the first time you run `rails credentials:edit`. +If you didn't have a master key saved in `config/master.key`, that'll be created too. + +Don't lose this master key! Put it in a password manager your team can access. +Should you lose it no one, including you, will be able to access any encrypted +credentials. + +Don't commit the key! Add `config/master.key` to your source control's +ignore file. If you use Git, Rails handles this for you. + +Rails also looks for the master key in `ENV["RAILS_MASTER_KEY"]`, if that's easier to manage. + +You could prepend that to your server's start command like this: + + RAILS_MASTER_KEY="very-secret-and-secure" server.start + +=== Editing Credentials + +This will open a temporary file in `$EDITOR` with the decrypted contents to edit +the encrypted credentials. + +When the temporary file is next saved the contents are encrypted and written to +`config/credentials.yml.enc` while the file itself is destroyed to prevent credentials +from leaking. + +=== Environment Specific Credentials + +It is possible to have credentials for each environment. If the file for current environment exists it will take +precedence over `config/credentials.yml.enc`, thus for `production` environment first look for +`config/credentials/production.yml.enc` that can be decrypted using master key taken from `ENV["RAILS_MASTER_KEY"]` +or stored in `config/credentials/production.key`. +To edit given file use command `rails credentials:edit --environment production` +Default paths can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`. diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb new file mode 100644 index 0000000000..4b30d208e0 --- /dev/null +++ b/railties/lib/rails/commands/credentials/credentials_command.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require "active_support" +require "rails/command/helpers/editor" + +module Rails + module Command + class CredentialsCommand < Rails::Command::Base # :nodoc: + include Helpers::Editor + + class_option :environment, aliases: "-e", type: :string, + desc: "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key" + + no_commands do + def help + say "Usage:\n #{self.class.banner}" + say "" + say self.class.desc + end + end + + def edit + require_application_and_environment! + + ensure_editor_available(command: "bin/rails credentials:edit") || (return) + + encrypted = Rails.application.encrypted(content_path, key_path: key_path) + + ensure_encryption_key_has_been_added(key_path) if encrypted.key.nil? + ensure_encrypted_file_has_been_added(content_path, key_path) + + catch_editing_exceptions do + change_encrypted_file_in_system_editor(content_path, key_path) + end + + say "File encrypted and saved." + rescue ActiveSupport::MessageEncryptor::InvalidMessage + say "Couldn't decrypt #{content_path}. Perhaps you passed the wrong key?" + end + + def show + require_application_and_environment! + + encrypted = Rails.application.encrypted(content_path, key_path: key_path) + + say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: key_path, file_path: content_path) + end + + private + def content_path + options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc" + end + + def key_path + options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key" + end + + + def ensure_encryption_key_has_been_added(key_path) + encryption_key_file_generator.add_key_file(key_path) + encryption_key_file_generator.ignore_key_file(key_path) + end + + def ensure_encrypted_file_has_been_added(file_path, key_path) + encrypted_file_generator.add_encrypted_file_silently(file_path, key_path) + end + + def change_encrypted_file_in_system_editor(file_path, key_path) + Rails.application.encrypted(file_path, key_path: key_path).change do |tmp_path| + system("#{ENV["EDITOR"]} #{tmp_path}") + end + end + + + def encryption_key_file_generator + require "rails/generators" + require "rails/generators/rails/encryption_key_file/encryption_key_file_generator" + + Rails::Generators::EncryptionKeyFileGenerator.new + end + + def encrypted_file_generator + require "rails/generators" + require "rails/generators/rails/encrypted_file/encrypted_file_generator" + + Rails::Generators::EncryptedFileGenerator.new + end + + def missing_encrypted_message(key:, key_path:, file_path:) + if key.nil? + "Missing '#{key_path}' to decrypt credentials. See `rails credentials:help`" + else + "File '#{file_path}' does not exist. Use `rails credentials:edit` to change that." + end + end + end + end +end diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb deleted file mode 100644 index dca60f948f..0000000000 --- a/railties/lib/rails/commands/dbconsole.rb +++ /dev/null @@ -1,173 +0,0 @@ -require 'erb' -require 'yaml' -require 'optparse' -require 'rails/commands/console_helper' - -module Rails - class DBConsole - include ConsoleHelper - - attr_reader :arguments - - class << self - def parse_arguments(arguments) - options = {} - - OptionParser.new do |opt| - opt.banner = "Usage: rails dbconsole [environment] [options]" - opt.on("-p", "--include-password", "Automatically provide the password from database.yml") do |v| - options['include_password'] = true - end - - opt.on("--mode [MODE]", ['html', 'list', 'line', 'column'], - "Automatically put the sqlite3 database in the specified mode (html, list, line, column).") do |mode| - options['mode'] = mode - end - - opt.on("--header") do |h| - options['header'] = h - end - - opt.on("-h", "--help", "Show this help message.") do - puts opt - exit - end - - opt.on("-e", "--environment=name", String, - "Specifies the environment to run this console under (test/development/production).", - "Default: development" - ) { |v| options[:environment] = v.strip } - - opt.parse!(arguments) - abort opt.to_s unless (0..1).include?(arguments.size) - end - - set_options_env(arguments, options) - end - end - - def initialize(arguments = ARGV) - @arguments = arguments - end - - def start - options = self.class.parse_arguments(arguments) - ENV['RAILS_ENV'] = options[:environment] || environment - - case config["adapter"] - when /^(jdbc)?mysql/ - args = { - 'host' => '--host', - 'port' => '--port', - 'socket' => '--socket', - 'username' => '--user', - 'encoding' => '--default-character-set', - 'sslca' => '--ssl-ca', - 'sslcert' => '--ssl-cert', - 'sslcapath' => '--ssl-capath', - 'sslcipher' => '--ssh-cipher', - 'sslkey' => '--ssl-key' - }.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact - - if config['password'] && options['include_password'] - args << "--password=#{config['password']}" - elsif config['password'] && !config['password'].to_s.empty? - args << "-p" - end - - args << config['database'] - - find_cmd_and_exec(['mysql', 'mysql5'], *args) - - when /^postgres|^postgis/ - ENV['PGUSER'] = config["username"] if config["username"] - ENV['PGHOST'] = config["host"] if config["host"] - ENV['PGPORT'] = config["port"].to_s if config["port"] - ENV['PGPASSWORD'] = config["password"].to_s if config["password"] && options['include_password'] - find_cmd_and_exec('psql', config["database"]) - - when "sqlite3" - args = [] - - args << "-#{options['mode']}" if options['mode'] - args << "-header" if options['header'] - args << File.expand_path(config['database'], Rails.respond_to?(:root) ? Rails.root : nil) - - find_cmd_and_exec('sqlite3', *args) - - when "oracle", "oracle_enhanced" - logon = "" - - if config['username'] - logon = config['username'] - logon << "/#{config['password']}" if config['password'] && options['include_password'] - logon << "@#{config['database']}" if config['database'] - end - - find_cmd_and_exec('sqlplus', logon) - - when "sqlserver" - args = [] - - args += ["-D", "#{config['database']}"] if config['database'] - args += ["-U", "#{config['username']}"] if config['username'] - args += ["-P", "#{config['password']}"] if config['password'] - - if config['host'] - host_arg = "#{config['host']}" - host_arg << ":#{config['port']}" if config['port'] - args += ["-S", host_arg] - end - - find_cmd_and_exec("sqsh", *args) - - else - abort "Unknown command-line client for #{config['database']}." - end - end - - def config - @config ||= begin - if configurations[environment].blank? - raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}" - else - configurations[environment] - end - end - end - - def environment - Rails.respond_to?(:env) ? Rails.env : super - end - - protected - def configurations - require APP_PATH - ActiveRecord::Base.configurations = Rails.application.config.database_configuration - ActiveRecord::Base.configurations - end - - def find_cmd_and_exec(commands, *args) - commands = Array(commands) - - dirs_on_path = ENV['PATH'].to_s.split(File::PATH_SEPARATOR) - unless (ext = RbConfig::CONFIG['EXEEXT']).empty? - commands = commands.map{|cmd| "#{cmd}#{ext}"} - end - - full_path_command = nil - found = commands.detect do |cmd| - dirs_on_path.detect do |path| - full_path_command = File.join(path, cmd) - File.file?(full_path_command) && File.executable?(full_path_command) - end - end - - if found - exec full_path_command, *args - else - abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") - end - end - end -end diff --git a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb new file mode 100644 index 0000000000..0fac7d34a0 --- /dev/null +++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +require "rails/command/environment_argument" + +module Rails + class DBConsole + def self.start(*args) + new(*args).start + end + + def initialize(options = {}) + @options = options + end + + def start + ENV["RAILS_ENV"] ||= @options[:environment] || environment + + case config["adapter"] + when /^(jdbc)?mysql/ + args = { + "host" => "--host", + "port" => "--port", + "socket" => "--socket", + "username" => "--user", + "encoding" => "--default-character-set", + "sslca" => "--ssl-ca", + "sslcert" => "--ssl-cert", + "sslcapath" => "--ssl-capath", + "sslcipher" => "--ssl-cipher", + "sslkey" => "--ssl-key" + }.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact + + if config["password"] && @options["include_password"] + args << "--password=#{config['password']}" + elsif config["password"] && !config["password"].to_s.empty? + args << "-p" + end + + args << config["database"] + + find_cmd_and_exec(["mysql", "mysql5"], *args) + + when /^postgres|^postgis/ + ENV["PGUSER"] = config["username"] if config["username"] + ENV["PGHOST"] = config["host"] if config["host"] + ENV["PGPORT"] = config["port"].to_s if config["port"] + ENV["PGPASSWORD"] = config["password"].to_s if config["password"] && @options["include_password"] + find_cmd_and_exec("psql", config["database"]) + + when "sqlite3" + args = [] + + args << "-#{@options['mode']}" if @options["mode"] + args << "-header" if @options["header"] + args << File.expand_path(config["database"], Rails.respond_to?(:root) ? Rails.root : nil) + + find_cmd_and_exec("sqlite3", *args) + + when "oracle", "oracle_enhanced" + logon = "" + + if config["username"] + logon = config["username"].dup + logon << "/#{config['password']}" if config["password"] && @options["include_password"] + logon << "@#{config['database']}" if config["database"] + end + + find_cmd_and_exec("sqlplus", logon) + + when "sqlserver" + args = [] + + args += ["-D", "#{config['database']}"] if config["database"] + args += ["-U", "#{config['username']}"] if config["username"] + args += ["-P", "#{config['password']}"] if config["password"] + + if config["host"] + host_arg = +"#{config['host']}" + host_arg << ":#{config['port']}" if config["port"] + args += ["-S", host_arg] + end + + find_cmd_and_exec("sqsh", *args) + + else + abort "Unknown command-line client for #{config['database']}." + end + end + + def config + @config ||= begin + # We need to check whether the user passed the connection the + # first time around to show a consistent error message to people + # relying on 2-level database configuration. + if @options["connection"] && configurations[connection].blank? + raise ActiveRecord::AdapterNotSpecified, "'#{connection}' connection is not configured. Available configuration: #{configurations.inspect}" + elsif configurations[environment].blank? && configurations[connection].blank? + raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}" + else + configurations[connection] || configurations[environment].presence + end + end + end + + def environment + Rails.respond_to?(:env) ? Rails.env : Rails::Command.environment + end + + def connection + @options.fetch(:connection, "primary") + end + + private + def configurations # :doc: + require APP_PATH + ActiveRecord::Base.configurations = Rails.application.config.database_configuration + ActiveRecord::Base.configurations + end + + def find_cmd_and_exec(commands, *args) # :doc: + commands = Array(commands) + + dirs_on_path = ENV["PATH"].to_s.split(File::PATH_SEPARATOR) + unless (ext = RbConfig::CONFIG["EXEEXT"]).empty? + commands = commands.map { |cmd| "#{cmd}#{ext}" } + end + + full_path_command = nil + found = commands.detect do |cmd| + dirs_on_path.detect do |path| + full_path_command = File.join(path, cmd) + File.file?(full_path_command) && File.executable?(full_path_command) + end + end + + if found + exec full_path_command, *args + else + abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") + end + end + end + + module Command + class DbconsoleCommand < Base # :nodoc: + include EnvironmentArgument + + class_option :include_password, aliases: "-p", type: :boolean, + desc: "Automatically provide the password from database.yml" + + class_option :mode, enum: %w( html list line column ), type: :string, + desc: "Automatically put the sqlite3 database in the specified mode (html, list, line, column)." + + class_option :header, type: :boolean + + class_option :connection, aliases: "-c", type: :string, + desc: "Specifies the connection to use." + + def perform + extract_environment_option_from_argument + + # RAILS_ENV needs to be set before config/application is required. + ENV["RAILS_ENV"] = options[:environment] + + require_application_and_environment! + Rails::DBConsole.start(options) + end + end + end +end diff --git a/railties/lib/rails/commands/destroy.rb b/railties/lib/rails/commands/destroy.rb deleted file mode 100644 index ce26cc3fde..0000000000 --- a/railties/lib/rails/commands/destroy.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'rails/generators' - -#if no argument/-h/--help is passed to rails destroy command, then -#it generates the help associated. -if [nil, "-h", "--help"].include?(ARGV.first) - Rails::Generators.help 'destroy' - exit -end - -name = ARGV.shift -Rails::Generators.invoke name, ARGV, behavior: :revoke, destination_root: Rails.root diff --git a/railties/lib/rails/commands/destroy/destroy_command.rb b/railties/lib/rails/commands/destroy/destroy_command.rb new file mode 100644 index 0000000000..dd432d28fd --- /dev/null +++ b/railties/lib/rails/commands/destroy/destroy_command.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "rails/generators" + +module Rails + module Command + class DestroyCommand < Base # :nodoc: + no_commands do + def help + require_application_and_environment! + load_generators + + Rails::Generators.help self.class.command_name + end + end + + def perform(*) + generator = args.shift + return help unless generator + + require_application_and_environment! + load_generators + + Rails::Generators.invoke generator, args, behavior: :revoke, destination_root: Rails::Command.root + end + end + end +end diff --git a/railties/lib/rails/commands/dev/dev_command.rb b/railties/lib/rails/commands/dev/dev_command.rb new file mode 100644 index 0000000000..a3f02f3172 --- /dev/null +++ b/railties/lib/rails/commands/dev/dev_command.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "rails/dev_caching" + +module Rails + module Command + class DevCommand < Base # :nodoc: + def help + say "rails dev:cache # Toggle development mode caching on/off." + end + + def cache + Rails::DevCaching.enable_by_file + end + end + end +end diff --git a/railties/lib/rails/commands/encrypted/encrypted_command.rb b/railties/lib/rails/commands/encrypted/encrypted_command.rb new file mode 100644 index 0000000000..8d5947652a --- /dev/null +++ b/railties/lib/rails/commands/encrypted/encrypted_command.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "pathname" +require "active_support" +require "rails/command/helpers/editor" + +module Rails + module Command + class EncryptedCommand < Rails::Command::Base # :nodoc: + include Helpers::Editor + + class_option :key, aliases: "-k", type: :string, + default: "config/master.key", desc: "The Rails.root relative path to the encryption key" + + no_commands do + def help + say "Usage:\n #{self.class.banner}" + say "" + end + end + + def edit(file_path) + require_application_and_environment! + encrypted = Rails.application.encrypted(file_path, key_path: options[:key]) + + ensure_editor_available(command: "bin/rails encrypted:edit") || (return) + ensure_encryption_key_has_been_added(options[:key]) if encrypted.key.nil? + ensure_encrypted_file_has_been_added(file_path, options[:key]) + + catch_editing_exceptions do + change_encrypted_file_in_system_editor(file_path, options[:key]) + end + + say "File encrypted and saved." + rescue ActiveSupport::MessageEncryptor::InvalidMessage + say "Couldn't decrypt #{file_path}. Perhaps you passed the wrong key?" + end + + def show(file_path) + require_application_and_environment! + encrypted = Rails.application.encrypted(file_path, key_path: options[:key]) + + say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: options[:key], file_path: file_path) + end + + private + def ensure_encryption_key_has_been_added(key_path) + encryption_key_file_generator.add_key_file(key_path) + encryption_key_file_generator.ignore_key_file(key_path) + end + + def ensure_encrypted_file_has_been_added(file_path, key_path) + encrypted_file_generator.add_encrypted_file_silently(file_path, key_path) + end + + def change_encrypted_file_in_system_editor(file_path, key_path) + Rails.application.encrypted(file_path, key_path: key_path).change do |tmp_path| + system("#{ENV["EDITOR"]} #{tmp_path}") + end + end + + + def encryption_key_file_generator + require "rails/generators" + require "rails/generators/rails/encryption_key_file/encryption_key_file_generator" + + Rails::Generators::EncryptionKeyFileGenerator.new + end + + def encrypted_file_generator + require "rails/generators" + require "rails/generators/rails/encrypted_file/encrypted_file_generator" + + Rails::Generators::EncryptedFileGenerator.new + end + + def missing_encrypted_message(key:, key_path:, file_path:) + if key.nil? + "Missing '#{key_path}' to decrypt data. See `rails encrypted:help`" + else + "File '#{file_path}' does not exist. Use `rails encrypted:edit #{file_path}` to change that." + end + end + end + end +end diff --git a/railties/lib/rails/commands/generate.rb b/railties/lib/rails/commands/generate.rb deleted file mode 100644 index 926c36b967..0000000000 --- a/railties/lib/rails/commands/generate.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'rails/generators' - -#if no argument/-h/--help is passed to rails generate command, then -#it generates the help associated. -if [nil, "-h", "--help"].include?(ARGV.first) - Rails::Generators.help 'generate' - exit -end - -name = ARGV.shift - -root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root -Rails::Generators.invoke name, ARGV, behavior: :invoke, destination_root: root diff --git a/railties/lib/rails/commands/generate/generate_command.rb b/railties/lib/rails/commands/generate/generate_command.rb new file mode 100644 index 0000000000..93d7a0ce3a --- /dev/null +++ b/railties/lib/rails/commands/generate/generate_command.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "rails/generators" + +module Rails + module Command + class GenerateCommand < Base # :nodoc: + no_commands do + def help + require_application_and_environment! + load_generators + + Rails::Generators.help self.class.command_name + end + end + + def perform(*) + generator = args.shift + return help unless generator + + require_application_and_environment! + load_generators + + ARGV.shift + + Rails::Generators.invoke generator, args, behavior: :invoke, destination_root: Rails::Command.root + end + end + end +end diff --git a/railties/lib/rails/commands/help/USAGE b/railties/lib/rails/commands/help/USAGE new file mode 100644 index 0000000000..8eb98319d2 --- /dev/null +++ b/railties/lib/rails/commands/help/USAGE @@ -0,0 +1,16 @@ +The most common rails commands are: + generate Generate new code (short-cut alias: "g") + console Start the Rails console (short-cut alias: "c") + server Start the Rails server (short-cut alias: "s") + test Run tests except system tests (short-cut alias: "t") + test:system Run system tests + dbconsole Start a console for the database specified in config/database.yml + (short-cut alias: "db") +<% unless engine? %> + new Create a new Rails application. "rails new my_app" creates a + new application called MyApp in "./my_app" +<% end %> + +All commands can be run with -h (or --help) for more information. +In addition to those commands, there are: + diff --git a/railties/lib/rails/commands/help/help_command.rb b/railties/lib/rails/commands/help/help_command.rb new file mode 100644 index 0000000000..9df34e9b79 --- /dev/null +++ b/railties/lib/rails/commands/help/help_command.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Rails + module Command + class HelpCommand < Base # :nodoc: + hide_command! + + def help(*) + say self.class.desc + + Rails::Command.print_commands + end + end + end +end diff --git a/railties/lib/rails/commands/initializers/initializers_command.rb b/railties/lib/rails/commands/initializers/initializers_command.rb new file mode 100644 index 0000000000..33596177af --- /dev/null +++ b/railties/lib/rails/commands/initializers/initializers_command.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Rails + module Command + class InitializersCommand < Base # :nodoc: + desc "initializers", "Print out all defined initializers in the order they are invoked by Rails." + def perform + require_application_and_environment! + + Rails.application.initializers.tsort_each do |initializer| + say "#{initializer.context_class}.#{initializer.name}" + end + end + end + end +end diff --git a/railties/lib/rails/commands/new/new_command.rb b/railties/lib/rails/commands/new/new_command.rb new file mode 100644 index 0000000000..a4f2081510 --- /dev/null +++ b/railties/lib/rails/commands/new/new_command.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Rails + module Command + class NewCommand < Base # :nodoc: + no_commands do + def help + Rails::Command.invoke :application, [ "--help" ] + end + end + + def perform(*) + say "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n" + say "Type 'rails' for help." + exit 1 + end + end + end +end diff --git a/railties/lib/rails/commands/notes/notes_command.rb b/railties/lib/rails/commands/notes/notes_command.rb new file mode 100644 index 0000000000..64b339b3cd --- /dev/null +++ b/railties/lib/rails/commands/notes/notes_command.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "rails/source_annotation_extractor" + +module Rails + module Command + class NotesCommand < Base # :nodoc: + class_option :annotations, aliases: "-a", desc: "Filter by specific annotations, e.g. Foobar TODO", type: :array, default: %w(OPTIMIZE FIXME TODO) + + def perform(*) + require_application_and_environment! + + deprecation_warning + display_annotations + end + + private + def display_annotations + annotations = options[:annotations] + tag = (annotations.length > 1) + + Rails::SourceAnnotationExtractor.enumerate annotations.join("|"), tag: tag, dirs: directories + end + + def directories + Rails::SourceAnnotationExtractor::Annotation.directories + source_annotation_directories + end + + def deprecation_warning + return if source_annotation_directories.empty? + ActiveSupport::Deprecation.warn("`SOURCE_ANNOTATION_DIRECTORIES` is deprecated and will be removed in Rails 6.1. You can add default directories by using config.annotations.register_directories instead.") + end + + def source_annotation_directories + ENV["SOURCE_ANNOTATION_DIRECTORIES"].to_s.split(",") + end + end + end +end diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb deleted file mode 100644 index 52d8966ead..0000000000 --- a/railties/lib/rails/commands/plugin.rb +++ /dev/null @@ -1,23 +0,0 @@ -if ARGV.first != "new" - ARGV[0] = "--help" -else - ARGV.shift - unless ARGV.delete("--no-rc") - customrc = ARGV.index{ |x| x.include?("--rc=") } - railsrc = if customrc - File.expand_path(ARGV.delete_at(customrc).gsub(/--rc=/, "")) - else - File.join(File.expand_path("~"), '.railsrc') - end - if File.exist?(railsrc) - extra_args_string = File.read(railsrc) - extra_args = extra_args_string.split(/\n+/).flat_map(&:split) - puts "Using #{extra_args.join(" ")} from #{railsrc}" - ARGV.insert(1, *extra_args) - end - end -end - -require 'rails/generators' -require 'rails/generators/rails/plugin/plugin_generator' -Rails::Generators::PluginGenerator.start diff --git a/railties/lib/rails/commands/plugin/plugin_command.rb b/railties/lib/rails/commands/plugin/plugin_command.rb new file mode 100644 index 0000000000..96187aa952 --- /dev/null +++ b/railties/lib/rails/commands/plugin/plugin_command.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Rails + module Command + class PluginCommand < Base # :nodoc: + hide_command! + + def help + run_plugin_generator %w( --help ) + end + + def self.banner(*) # :nodoc: + "#{executable} new [options]" + end + + class_option :rc, type: :string, default: File.join("~", ".railsrc"), + desc: "Initialize the plugin command with previous defaults. Uses .railsrc in your home directory by default." + + class_option :no_rc, desc: "Skip evaluating .railsrc." + + def perform(type = nil, *plugin_args) + plugin_args << "--help" unless type == "new" + + unless options.key?("no_rc") # Thor's not so indifferent access hash. + railsrc = File.expand_path(options[:rc]) + + if File.exist?(railsrc) + extra_args = File.read(railsrc).split(/\n+/).flat_map(&:split) + say "Using #{extra_args.join(" ")} from #{railsrc}" + plugin_args.insert(1, *extra_args) + end + end + + run_plugin_generator plugin_args + end + + private + def run_plugin_generator(plugin_args) + require "rails/generators" + require "rails/generators/rails/plugin/plugin_generator" + Rails::Generators::PluginGenerator.start plugin_args + end + end + end +end diff --git a/railties/lib/rails/commands/rake/rake_command.rb b/railties/lib/rails/commands/rake/rake_command.rb new file mode 100644 index 0000000000..535df0c430 --- /dev/null +++ b/railties/lib/rails/commands/rake/rake_command.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Rails + module Command + class RakeCommand < Base # :nodoc: + extend Rails::Command::Actions + + namespace "rake" + + class << self + def printing_commands + formatted_rake_tasks.map(&:first) + end + + def perform(task, *) + require_rake + + ARGV.unshift(task) # Prepend the task, so Rake knows how to run it. + + Rake.application.standard_exception_handling do + Rake.application.init("rails") + Rake.application.load_rakefile + Rake.application.top_level + end + end + + private + def rake_tasks + require_rake + + return @rake_tasks if defined?(@rake_tasks) + + require_application_and_environment! + + Rake::TaskManager.record_task_metadata = true + Rake.application.instance_variable_set(:@name, "rails") + load_tasks + @rake_tasks = Rake.application.tasks.select(&:comment) + end + + def formatted_rake_tasks + rake_tasks.map { |t| [ t.name_with_args, t.comment ] } + end + + def require_rake + require "rake" # Defer booting Rake until we know it's needed. + end + end + end + end +end diff --git a/railties/lib/rails/commands/routes/routes_command.rb b/railties/lib/rails/commands/routes/routes_command.rb new file mode 100644 index 0000000000..b592a5212f --- /dev/null +++ b/railties/lib/rails/commands/routes/routes_command.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "rails/command" + +module Rails + module Command + class RoutesCommand < Base # :nodoc: + class_option :controller, aliases: "-c", desc: "Filter by a specific controller, e.g. PostsController or Admin::PostsController." + class_option :grep, aliases: "-g", desc: "Grep routes by a specific pattern." + class_option :expanded, type: :boolean, aliases: "-E", desc: "Print routes expanded vertically with parts explained." + + def perform(*) + require_application_and_environment! + require "action_dispatch/routing/inspector" + + say inspector.format(formatter, routes_filter) + end + + private + def inspector + ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes) + end + + def formatter + if options.key?("expanded") + ActionDispatch::Routing::ConsoleFormatter::Expanded.new + else + ActionDispatch::Routing::ConsoleFormatter::Sheet.new + end + end + + def routes_filter + options.symbolize_keys.slice(:controller, :grep) + end + end + end +end diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb deleted file mode 100644 index 86bce9b2fe..0000000000 --- a/railties/lib/rails/commands/runner.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'optparse' - -options = { environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup } -code_or_file = nil - -if ARGV.first.nil? - ARGV.push "-h" -end - -ARGV.clone.options do |opts| - opts.banner = "Usage: rails runner [options] [<'Some.ruby(code)'> | <filename.rb>]" - - opts.separator "" - - opts.on("-e", "--environment=name", String, - "Specifies the environment for the runner to operate under (test/development/production).", - "Default: development") { |v| options[:environment] = v } - - opts.separator "" - - opts.on("-h", "--help", - "Show this help message.") { $stdout.puts opts; exit } - - opts.separator "" - opts.separator "Examples: " - - opts.separator " rails runner 'puts Rails.env'" - opts.separator " This runs the code `puts Rails.env` after loading the app" - opts.separator "" - opts.separator " rails runner path/to/filename.rb" - opts.separator " This runs the Ruby file located at `path/to/filename.rb` after loading the app" - - if RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ - opts.separator "" - opts.separator "You can also use runner as a shebang line for your executables:" - opts.separator " -------------------------------------------------------------" - opts.separator " #!/usr/bin/env #{File.expand_path($0)} runner" - opts.separator "" - opts.separator " Product.all.each { |p| p.price *= 2 ; p.save! }" - opts.separator " -------------------------------------------------------------" - end - - opts.order! { |o| code_or_file ||= o } rescue retry -end - -ARGV.delete(code_or_file) - -ENV["RAILS_ENV"] = options[:environment] - -require APP_PATH -Rails.application.require_environment! -Rails.application.load_runner - -if code_or_file.nil? - $stderr.puts "Run '#{$0} -h' for help." - exit 1 -elsif File.exist?(code_or_file) - $0 = code_or_file - Kernel.load code_or_file -else - eval(code_or_file, binding, __FILE__, __LINE__) -end diff --git a/railties/lib/rails/commands/runner/USAGE b/railties/lib/rails/commands/runner/USAGE new file mode 100644 index 0000000000..24b60037f0 --- /dev/null +++ b/railties/lib/rails/commands/runner/USAGE @@ -0,0 +1,20 @@ +Examples: + +Run `puts Rails.env` after loading the app: + + <%= executable %> 'puts Rails.env' + +Run the Ruby file located at `path/to/filename.rb` after loading the app: + + <%= executable %> path/to/filename.rb + +Run the Ruby script read from stdin after loading the app: + <%= executable %> - + +<% unless Gem.win_platform? %> +You can also use the runner command as a shebang line for your executables: + + #!/usr/bin/env <%= File.expand_path(executable) %> + + Product.all.each { |p| p.price *= 2 ; p.save! } +<% end %> diff --git a/railties/lib/rails/commands/runner/runner_command.rb b/railties/lib/rails/commands/runner/runner_command.rb new file mode 100644 index 0000000000..cb693bcf34 --- /dev/null +++ b/railties/lib/rails/commands/runner/runner_command.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Rails + module Command + class RunnerCommand < Base # :nodoc: + class_option :environment, aliases: "-e", type: :string, + default: Rails::Command.environment.dup, + desc: "The environment for the runner to operate under (test/development/production)" + + no_commands do + def help + super + say self.class.desc + end + end + + def self.banner(*) + "#{super} [<'Some.ruby(code)'> | <filename.rb> | -]" + end + + def perform(code_or_file = nil, *command_argv) + unless code_or_file + help + exit 1 + end + + ENV["RAILS_ENV"] = options[:environment] + + require_application_and_environment! + Rails.application.load_runner + + ARGV.replace(command_argv) + + if code_or_file == "-" + eval($stdin.read, TOPLEVEL_BINDING, "stdin") + elsif File.exist?(code_or_file) + $0 = code_or_file + Kernel.load code_or_file + else + begin + eval(code_or_file, TOPLEVEL_BINDING, __FILE__, __LINE__) + rescue SyntaxError, NameError => e + error "Please specify a valid ruby command or the path of a script to run." + error "Run '#{self.class.executable} -h' for help." + error "" + error e + exit 1 + end + end + end + end + end +end diff --git a/railties/lib/rails/commands/secrets/USAGE b/railties/lib/rails/commands/secrets/USAGE new file mode 100644 index 0000000000..e205cdc001 --- /dev/null +++ b/railties/lib/rails/commands/secrets/USAGE @@ -0,0 +1,60 @@ +=== Storing Encrypted Secrets in Source Control + +The Rails `secrets` commands helps encrypting secrets to slim a production +environment's `ENV` hash. It's also useful for atomic deploys: no need to +coordinate key changes to get everything working as the keys are shipped +with the code. + +=== Setup + +Run `rails secrets:setup` to opt in and generate the `config/secrets.yml.key` +and `config/secrets.yml.enc` files. + +The latter contains all the keys to be encrypted while the former holds the +encryption key. + +Don't lose the key! Put it in a password manager your team can access. +Should you lose it no one, including you, will be able to access any encrypted +secrets. +Don't commit the key! Add `config/secrets.yml.key` to your source control's +ignore file. If you use Git, Rails handles this for you. + +Rails also looks for the key in `ENV["RAILS_MASTER_KEY"]` if that's easier to +manage. + +You could prepend that to your server's start command like this: + + RAILS_MASTER_KEY="im-the-master-now-hahaha" server.start + + +The `config/secrets.yml.enc` has much the same format as `config/secrets.yml`: + + production: + secret_key_base: so-secret-very-hidden-wow + payment_processing_gateway_key: much-safe-very-gaedwey-wow + +But that's where the similarities between `secrets.yml` and `secrets.yml.enc` +end, e.g. no keys from `secrets.yml` will be moved to `secrets.yml.enc` and +be encrypted. + +A `shared:` top level key is also supported such that any keys there is merged +into the other environments. + +Additionally, Rails won't read encrypted secrets out of the box even if you have +the key. Add this: + + config.read_encrypted_secrets = true + +to the environment you'd like to read encrypted secrets. `rails secrets:setup` +inserts this into the production environment by default. + +=== Editing Secrets + +After `rails secrets:setup`, run `rails secrets:edit`. + +That command opens a temporary file in `$EDITOR` with the decrypted contents of +`config/secrets.yml.enc` to edit the encrypted secrets. + +When the temporary file is next saved the contents are encrypted and written to +`config/secrets.yml.enc` while the file itself is destroyed to prevent secrets +from leaking. diff --git a/railties/lib/rails/commands/secrets/secrets_command.rb b/railties/lib/rails/commands/secrets/secrets_command.rb new file mode 100644 index 0000000000..2eebc0f35f --- /dev/null +++ b/railties/lib/rails/commands/secrets/secrets_command.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "active_support" +require "rails/secrets" + +module Rails + module Command + class SecretsCommand < Rails::Command::Base # :nodoc: + no_commands do + def help + say "Usage:\n #{self.class.banner}" + say "" + say self.class.desc + end + end + + def setup + deprecate_in_favor_of_credentials_and_exit + end + + def edit + if ENV["EDITOR"].to_s.empty? + say "No $EDITOR to open decrypted secrets in. Assign one like this:" + say "" + say %(EDITOR="mate --wait" rails secrets:edit) + say "" + say "For editors that fork and exit immediately, it's important to pass a wait flag," + say "otherwise the secrets will be saved immediately with no chance to edit." + + return + end + + require_application_and_environment! + + Rails::Secrets.read_for_editing do |tmp_path| + system("#{ENV["EDITOR"]} #{tmp_path}") + end + + say "New secrets encrypted and saved." + rescue Interrupt + say "Aborted changing encrypted secrets: nothing saved." + rescue Rails::Secrets::MissingKeyError => error + say error.message + rescue Errno::ENOENT => error + if /secrets\.yml\.enc/.match?(error.message) + deprecate_in_favor_of_credentials_and_exit + else + raise + end + end + + def show + say Rails::Secrets.read + end + + private + def deprecate_in_favor_of_credentials_and_exit + say "Encrypted secrets is deprecated in favor of credentials. Run:" + say "rails credentials:help" + + exit 1 + end + end + end +end diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb deleted file mode 100644 index d1e445ac70..0000000000 --- a/railties/lib/rails/commands/server.rb +++ /dev/null @@ -1,120 +0,0 @@ -require 'fileutils' -require 'optparse' -require 'action_dispatch' -require 'rails' - -module Rails - class Server < ::Rack::Server - class Options - def parse!(args) - args, options = args.dup, {} - - option_parser(options).parse! args - - options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" - options[:server] = args.shift - options - end - - private - - def option_parser(options) - OptionParser.new do |opts| - opts.banner = "Usage: rails server [mongrel, thin etc] [options]" - opts.on("-p", "--port=port", Integer, - "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v } - opts.on("-b", "--binding=IP", String, - "Binds Rails to the specified IP.", "Default: localhost") { |v| options[:Host] = v } - opts.on("-c", "--config=file", String, - "Uses a custom rackup configuration.") { |v| options[:config] = v } - opts.on("-d", "--daemon", "Runs server as a Daemon.") { options[:daemonize] = true } - opts.on("-e", "--environment=name", String, - "Specifies the environment to run this server under (test/development/production).", - "Default: development") { |v| options[:environment] = v } - opts.on("-P", "--pid=pid", String, - "Specifies the PID file.", - "Default: tmp/pids/server.pid") { |v| options[:pid] = v } - - opts.separator "" - - opts.on("-h", "--help", "Shows this help message.") { puts opts; exit } - end - end - end - - def initialize(*) - super - set_environment - end - - # TODO: this is no longer required but we keep it for the moment to support older config.ru files. - def app - @app ||= begin - app = super - app.respond_to?(:to_app) ? app.to_app : app - end - end - - def opt_parser - Options.new - end - - def set_environment - ENV["RAILS_ENV"] ||= options[:environment] - end - - def start - print_boot_information - trap(:INT) { exit } - create_tmp_directories - log_to_stdout if options[:log_stdout] - - super - ensure - # The '-h' option calls exit before @options is set. - # If we call 'options' with it unset, we get double help banners. - puts 'Exiting' unless @options && options[:daemonize] - end - - def middleware - Hash.new([]) - end - - def default_options - super.merge({ - Port: 3000, - DoNotReverseLookup: true, - environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup, - daemonize: false, - pid: File.expand_path("tmp/pids/server.pid") - }) - end - - private - - def print_boot_information - url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" - puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}" - puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}" - puts "=> Run `rails server -h` for more startup options" - - puts "=> Ctrl-C to shutdown server" unless options[:daemonize] - end - - def create_tmp_directories - %w(cache pids sockets).each do |dir_to_make| - FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make)) - end - end - - def log_to_stdout - wrapped_app # touch the app so the logger is set up - - console = ActiveSupport::Logger.new($stdout) - console.formatter = Rails.logger.formatter - console.level = Rails.logger.level - - Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) - end - end -end diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb new file mode 100644 index 0000000000..70789e0303 --- /dev/null +++ b/railties/lib/rails/commands/server/server_command.rb @@ -0,0 +1,322 @@ +# frozen_string_literal: true + +require "fileutils" +require "action_dispatch" +require "rails" +require "active_support/deprecation" +require "active_support/core_ext/string/filters" +require "rails/dev_caching" + +module Rails + class Server < ::Rack::Server + class Options + def parse!(args) + Rails::Command::ServerCommand.new([], args).server_options + end + end + + def initialize(options = nil) + @default_options = options || {} + super(@default_options) + set_environment + end + + def app + @app ||= begin + app = super + if app.is_a?(Class) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Using `Rails::Application` subclass to start the server is deprecated and will be removed in Rails 6.0. + Please change `run #{app}` to `run Rails.application` in config.ru. + MSG + end + app.respond_to?(:to_app) ? app.to_app : app + end + end + + def opt_parser + Options.new + end + + def set_environment + ENV["RAILS_ENV"] ||= options[:environment] + end + + def start(after_stop_callback = nil) + trap(:INT) { exit } + create_tmp_directories + setup_dev_caching + log_to_stdout if options[:log_stdout] + + super() + ensure + after_stop_callback.call if after_stop_callback + end + + def serveable? # :nodoc: + server + true + rescue LoadError, NameError + false + end + + def middleware + Hash.new([]) + end + + def default_options + super.merge(@default_options) + end + + def served_url + "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" unless use_puma? + end + + private + def setup_dev_caching + if options[:environment] == "development" + Rails::DevCaching.enable_by_argument(options[:caching]) + end + end + + def create_tmp_directories + %w(cache pids sockets).each do |dir_to_make| + FileUtils.mkdir_p(File.join(Rails.root, "tmp", dir_to_make)) + end + end + + def log_to_stdout + wrapped_app # touch the app so the logger is set up + + console = ActiveSupport::Logger.new(STDOUT) + console.formatter = Rails.logger.formatter + console.level = Rails.logger.level + + unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT) + Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) + end + end + + def use_puma? + server.to_s == "Rack::Handler::Puma" + end + end + + module Command + class ServerCommand < Base # :nodoc: + # Hard-coding a bunch of handlers here as we don't have a public way of + # querying them from the Rack::Handler registry. + RACK_SERVERS = %w(cgi fastcgi webrick lsws scgi thin puma unicorn) + + DEFAULT_PORT = 3000 + DEFAULT_PID_PATH = "tmp/pids/server.pid" + + argument :using, optional: true + + class_option :port, aliases: "-p", type: :numeric, + desc: "Runs Rails on the specified port - defaults to 3000.", banner: :port + class_option :binding, aliases: "-b", type: :string, + desc: "Binds Rails to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments'.", + banner: :IP + class_option :config, aliases: "-c", type: :string, default: "config.ru", + desc: "Uses a custom rackup configuration.", banner: :file + class_option :daemon, aliases: "-d", type: :boolean, default: false, + desc: "Runs server as a Daemon." + class_option :environment, aliases: "-e", type: :string, + desc: "Specifies the environment to run this server under (development/test/production).", banner: :name + class_option :using, aliases: "-u", type: :string, + desc: "Specifies the Rack server used to run the application (thin/puma/webrick).", banner: :name + class_option :pid, aliases: "-P", type: :string, default: DEFAULT_PID_PATH, + desc: "Specifies the PID file." + class_option :dev_caching, aliases: "-C", type: :boolean, default: nil, + desc: "Specifies whether to perform caching in development." + class_option :restart, type: :boolean, default: nil, hide: true + class_option :early_hints, type: :boolean, default: nil, desc: "Enables HTTP/2 early hints." + class_option :log_to_stdout, type: :boolean, default: nil, optional: true, + desc: "Whether to log to stdout. Enabled by default in development when not daemonized." + + def initialize(args, local_options, *) + super + + @original_options = local_options - %w( --restart ) + deprecate_positional_rack_server_and_rewrite_to_option(@original_options) + end + + def perform + set_application_directory! + prepare_restart + + Rails::Server.new(server_options).tap do |server| + # Require application after server sets environment to propagate + # the --environment option. + require APP_PATH + Dir.chdir(Rails.application.root) + + if server.serveable? + print_boot_information(server.server, server.served_url) + after_stop_callback = -> { say "Exiting" unless options[:daemon] } + server.start(after_stop_callback) + else + say rack_server_suggestion(using) + end + end + end + + no_commands do + def server_options + { + user_supplied_options: user_supplied_options, + server: using, + log_stdout: log_to_stdout?, + Port: port, + Host: host, + DoNotReverseLookup: true, + config: options[:config], + environment: environment, + daemonize: options[:daemon], + pid: pid, + caching: options[:dev_caching], + restart_cmd: restart_command, + early_hints: early_hints + } + end + end + + private + def user_supplied_options + @user_supplied_options ||= begin + # Convert incoming options array to a hash of flags + # ["-p3001", "-C", "--binding", "127.0.0.1"] # => {"-p"=>true, "-C"=>true, "--binding"=>true} + user_flag = {} + @original_options.each do |command| + if command.to_s.start_with?("--") + option = command.split("=")[0] + user_flag[option] = true + elsif command =~ /\A(-.)/ + user_flag[Regexp.last_match[0]] = true + end + end + + # Collect all options that the user has explicitly defined so we can + # differentiate them from defaults + user_supplied_options = [] + self.class.class_options.select do |key, option| + if option.aliases.any? { |name| user_flag[name] } || user_flag["--#{option.name}"] + name = option.name.to_sym + case name + when :port + name = :Port + when :binding + name = :Host + when :dev_caching + name = :caching + when :daemonize + name = :daemon + end + user_supplied_options << name + end + end + user_supplied_options << :Host if ENV["HOST"] || ENV["BINDING"] + user_supplied_options << :Port if ENV["PORT"] + user_supplied_options.uniq + end + end + + def port + options[:port] || ENV.fetch("PORT", DEFAULT_PORT).to_i + end + + def host + if options[:binding] + options[:binding] + else + default_host = environment == "development" ? "localhost" : "0.0.0.0" + + if ENV["HOST"] && !ENV["BINDING"] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Using the `HOST` environment to specify the IP is deprecated and will be removed in Rails 6.1. + Please use `BINDING` environment instead. + MSG + + return ENV["HOST"] + end + + ENV.fetch("BINDING", default_host) + end + end + + def environment + options[:environment] || Rails::Command.environment + end + + def restart_command + "bin/rails server #{@original_options.join(" ")} --restart" + end + + def early_hints + options[:early_hints] + end + + def log_to_stdout? + options.fetch(:log_to_stdout) do + options[:daemon].blank? && environment == "development" + end + end + + def pid + File.expand_path(options[:pid]) + end + + def self.banner(*) + "rails server [thin/puma/webrick] [options]" + end + + def prepare_restart + FileUtils.rm_f(options[:pid]) if options[:restart] + end + + def deprecate_positional_rack_server_and_rewrite_to_option(original_options) + if using + ActiveSupport::Deprecation.warn(<<~MSG) + Passing the Rack server name as a regular argument is deprecated + and will be removed in the next Rails version. Please, use the -u + option instead. + MSG + + original_options.concat [ "-u", using ] + else + # Use positional internally to get around Thor's immutable options. + # TODO: Replace `using` occurrences with `options[:using]` after deprecation removal. + @using = options[:using] + end + end + + def rack_server_suggestion(server) + if server.in?(RACK_SERVERS) + <<~MSG + Could not load server "#{server}". Maybe you need to the add it to the Gemfile? + + gem "#{server}" + + Run `rails server --help` for more options. + MSG + else + suggestion = Rails::Command::Spellchecker.suggest(server, from: RACK_SERVERS) + + <<~MSG + Could not find server "#{server}". Maybe you meant #{suggestion.inspect}? + Run `rails server --help` for more options. + MSG + end + end + + def print_boot_information(server, url) + say <<~MSG + => Booting #{ActiveSupport::Inflector.demodulize(server)} + => Rails #{Rails.version} application starting in #{Rails.env} #{url} + => Run `rails server --help` for more startup options + MSG + end + end + end +end diff --git a/railties/lib/rails/commands/test.rb b/railties/lib/rails/commands/test.rb deleted file mode 100644 index fe5307788a..0000000000 --- a/railties/lib/rails/commands/test.rb +++ /dev/null @@ -1,5 +0,0 @@ -require "rails/test_unit/minitest_plugin" - -$: << File.expand_path("../../test", APP_PATH) - -exit Minitest.run(ARGV) diff --git a/railties/lib/rails/commands/test/test_command.rb b/railties/lib/rails/commands/test/test_command.rb new file mode 100644 index 0000000000..00ea9ac4a6 --- /dev/null +++ b/railties/lib/rails/commands/test/test_command.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "rails/command" +require "rails/test_unit/runner" +require "rails/test_unit/reporter" + +module Rails + module Command + class TestCommand < Base # :nodoc: + no_commands do + def help + say "Usage: #{Rails::TestUnitReporter.executable} [options] [files or directories]" + say "" + say "You can run a single test by appending a line number to a filename:" + say "" + say " #{Rails::TestUnitReporter.executable} test/models/user_test.rb:27" + say "" + say "You can run multiple files and directories at the same time:" + say "" + say " #{Rails::TestUnitReporter.executable} test/controllers test/integration/login_test.rb" + say "" + say "By default test failures and errors are reported inline during a run." + say "" + + Minitest.run(%w(--help)) + end + end + + def perform(*) + $LOAD_PATH << Rails::Command.root.join("test").to_s + + Rails::TestUnit::Runner.parse_options(ARGV) + Rails::TestUnit::Runner.run(ARGV) + end + end + end +end diff --git a/railties/lib/rails/commands/version/version_command.rb b/railties/lib/rails/commands/version/version_command.rb new file mode 100644 index 0000000000..3e2112b6d4 --- /dev/null +++ b/railties/lib/rails/commands/version/version_command.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Rails + module Command + class VersionCommand < Base # :nodoc: + def perform + Rails::Command.invoke :application, [ "--version" ] + end + end + end +end diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb index d99d27a756..e8741a50ba 100644 --- a/railties/lib/rails/configuration.rb +++ b/railties/lib/rails/configuration.rb @@ -1,7 +1,9 @@ -require 'active_support/ordered_options' -require 'active_support/core_ext/object' -require 'rails/paths' -require 'rails/rack' +# frozen_string_literal: true + +require "active_support/ordered_options" +require "active_support/core_ext/object" +require "rails/paths" +require "rails/rack" module Rails module Configuration @@ -33,9 +35,9 @@ module Rails # config.middleware.delete ActionDispatch::Flash # class MiddlewareStackProxy - def initialize - @operations = [] - @delete_operations = [] + def initialize(operations = [], delete_operations = []) + @operations = operations + @delete_operations = delete_operations end def insert_before(*args, &block) @@ -71,6 +73,13 @@ module Rails other end + + def +(other) # :nodoc: + MiddlewareStackProxy.new(@operations + other.operations, @delete_operations + other.delete_operations) + end + + protected + attr_reader :operations, :delete_operations end class Generators #:nodoc: @@ -78,8 +87,8 @@ module Rails attr_reader :hidden_namespaces def initialize - @aliases = Hash.new { |h,k| h[k] = {} } - @options = Hash.new { |h,k| h[k] = {} } + @aliases = Hash.new { |h, k| h[k] = {} } + @options = Hash.new { |h, k| h[k] = {} } @fallbacks = {} @templates = [] @colorize_logging = true @@ -99,7 +108,7 @@ module Rails end def method_missing(method, *args) - method = method.to_s.sub(/=$/, '').to_sym + method = method.to_s.sub(/=$/, "").to_sym return @options[method] if args.empty? diff --git a/railties/lib/rails/console/app.rb b/railties/lib/rails/console/app.rb index ac5836a588..c37583ce9a 100644 --- a/railties/lib/rails/console/app.rb +++ b/railties/lib/rails/console/app.rb @@ -1,11 +1,13 @@ -require 'active_support/all' -require 'action_controller' +# frozen_string_literal: true + +require "active_support/all" +require "action_controller" module Rails module ConsoleMethods # reference the global "app" instance, created on demand. To recreate the # instance, pass a non-false value as the parameter. - def app(create=false) + def app(create = false) @app_integration_instance = nil if create @app_integration_instance ||= new_session do |sess| sess.host! "www.example.com" @@ -27,10 +29,9 @@ module Rails end # reloads the environment - def reload!(print=true) + def reload!(print = true) puts "Reloading..." if print - ActionDispatch::Reloader.cleanup! - ActionDispatch::Reloader.prepare! + Rails.application.reloader.reload! true end end diff --git a/railties/lib/rails/console/helpers.rb b/railties/lib/rails/console/helpers.rb index b775f1ff8d..39fbc55606 100644 --- a/railties/lib/rails/console/helpers.rb +++ b/railties/lib/rails/console/helpers.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module Rails module ConsoleMethods # Gets the helper methods available to the controller. # # This method assumes an +ApplicationController+ exists, and it extends +ActionController::Base+ def helper - @helper ||= ApplicationController.helpers + ApplicationController.helpers end # Gets a new instance of a controller object. diff --git a/railties/lib/rails/dev_caching.rb b/railties/lib/rails/dev_caching.rb new file mode 100644 index 0000000000..ff629b2527 --- /dev/null +++ b/railties/lib/rails/dev_caching.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "fileutils" + +module Rails + module DevCaching # :nodoc: + class << self + FILE = "tmp/caching-dev.txt" + + def enable_by_file + FileUtils.mkdir_p("tmp") + + if File.exist?(FILE) + delete_cache_file + puts "Development mode is no longer being cached." + else + create_cache_file + puts "Development mode is now being cached." + end + + FileUtils.touch "tmp/restart.txt" + end + + def enable_by_argument(caching) + FileUtils.mkdir_p("tmp") + + if caching + create_cache_file + elsif caching == false && File.exist?(FILE) + delete_cache_file + end + end + + private + def create_cache_file + FileUtils.touch FILE + end + + def delete_cache_file + File.delete FILE + end + end + end +end diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 1dede32dd4..6a13a84108 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -1,7 +1,10 @@ -require 'rails/railtie' -require 'rails/engine/railties' -require 'active_support/core_ext/module/delegation' -require 'pathname' +# frozen_string_literal: true + +require "rails/railtie" +require "rails/engine/railties" +require "active_support/core_ext/module/delegation" +require "pathname" +require "thread" module Rails # <tt>Rails::Engine</tt> allows you to wrap a specific Rails application or subset of @@ -39,7 +42,7 @@ module Rails # # class MyEngine < Rails::Engine # # Add a load path for this specific Engine - # config.autoload_paths << File.expand_path("../lib/some/path", __FILE__) + # config.autoload_paths << File.expand_path("lib/some/path", __dir__) # # initializer "my_engine.add_middleware" do |app| # app.middleware.use MyEngine::Middleware @@ -108,7 +111,7 @@ module Rails # # == Endpoint # - # An engine can also be a rack application. It can be useful if you have a rack application that + # An engine can also be a Rack application. It can be useful if you have a Rack application that # you would like to wrap with +Engine+ and provide with some of the +Engine+'s features. # # To do that, use the +endpoint+ method: @@ -127,7 +130,7 @@ module Rails # # == Middleware stack # - # As an engine can now be a rack endpoint, it can also have a middleware + # As an engine can now be a Rack endpoint, it can also have a middleware # stack. The usage is exactly the same as in <tt>Application</tt>: # # module MyEngine @@ -218,7 +221,7 @@ module Rails # The next thing that changes in isolated engines is the behavior of routes. # Normally, when you namespace your controllers, you also need to namespace # the related routes. With an isolated engine, the engine's namespace is - # automatically applied, so you don't need to specify it explicity in your + # automatically applied, so you don't need to specify it explicitly in your # routes: # # MyEngine::Engine.routes.draw do @@ -336,7 +339,7 @@ module Rails # == Loading priority # # In order to change engine's priority you can use +config.railties_order+ in the main application. - # It will affect the priority of loading views, helpers, assets and all the other files + # It will affect the priority of loading views, helpers, assets, and all the other files # related to engine or application. # # # load Blog::Engine with highest priority, followed by application and other railties @@ -357,12 +360,7 @@ module Rails Rails::Railtie::Configuration.eager_load_namespaces << base base.called_from = begin - call_stack = if Kernel.respond_to?(:caller_locations) - caller_locations.map { |l| l.absolute_path || l.path } - else - # Remove the line number from backtraces making sure we don't leave anything behind - caller.map { |p| p.sub(/:\d+.*/, '') } - end + call_stack = caller_locations.map { |l| l.absolute_path || l.path } File.dirname(call_stack.detect { |p| p !~ %r[railties[\w.-]*/lib/rails|rack[\w.-]*/lib/rack] }) end @@ -384,7 +382,7 @@ module Rails def isolate_namespace(mod) engine_name(generate_railtie_name(mod.name)) - self.routes.default_scope = { module: ActiveSupport::Inflector.underscore(mod.name) } + routes.default_scope = { module: ActiveSupport::Inflector.underscore(mod.name) } self.isolated = true unless mod.respond_to?(:railtie_namespace) @@ -406,7 +404,7 @@ module Rails end unless mod.respond_to?(:railtie_routes_url_helpers) - define_method(:railtie_routes_url_helpers) {|include_path_helpers = true| railtie.routes.url_helpers(include_path_helpers) } + define_method(:railtie_routes_url_helpers) { |include_path_helpers = true| railtie.routes.url_helpers(include_path_helpers) } end end end @@ -434,12 +432,13 @@ module Rails @env_config = nil @helpers = nil @routes = nil + @app_build_lock = Mutex.new super end # Load console and invoke the registered hooks. # Check <tt>Rails::Railtie.console</tt> for more info. - def load_console(app=self) + def load_console(app = self) require "rails/console/app" require "rails/console/helpers" run_console_blocks(app) @@ -448,14 +447,14 @@ module Rails # Load Rails runner and invoke the registered hooks. # Check <tt>Rails::Railtie.runner</tt> for more info. - def load_runner(app=self) + def load_runner(app = self) run_runner_blocks(app) self end # Load Rake, railties tasks and invoke the registered hooks. # Check <tt>Rails::Railtie.rake_tasks</tt> for more info. - def load_tasks(app=self) + def load_tasks(app = self) require "rake" run_tasks_blocks(app) self @@ -463,7 +462,7 @@ module Rails # Load Rails generators and invoke the registered hooks. # Check <tt>Rails::Railtie.generators</tt> for more info. - def load_generators(app=self) + def load_generators(app = self) require "rails/generators" run_generators_blocks(app) Rails::Generators.configure!(app.config.generators) @@ -502,12 +501,15 @@ module Rails paths["app/helpers"].existent end - # Returns the underlying rack application for this engine. + # Returns the underlying Rack application for this engine. def app - @app ||= begin - config.middleware = config.middleware.merge_into(default_middleware_stack) - config.middleware.build(endpoint) - end + @app || @app_build_lock.synchronize { + @app ||= begin + stack = default_middleware_stack + config.middleware = build_middleware.merge_into(stack) + config.middleware.build(endpoint) + end + } end # Returns the endpoint for this engine. If none is registered, @@ -518,18 +520,13 @@ module Rails # Define the Rack API for this engine. def call(env) - env.merge!(env_config) - if env['SCRIPT_NAME'] - env[routes.env_key] = env['SCRIPT_NAME'].dup - end - app.call(env) + req = build_request env + app.call req.env end # Defines additional Rack env configuration that is added on each call. def env_config - @env_config ||= { - 'action_dispatch.routes' => routes - } + @env_config ||= {} end # Defines the routes for this engine. If a block is given to @@ -554,7 +551,7 @@ module Rails load(seed_file) if seed_file end - # Add configured load paths to ruby load paths and remove duplicates. + # Add configured load paths to Ruby's load path, and remove duplicate entries. initializer :set_load_path, before: :bootstrap_hook do _all_load_paths.reverse_each do |path| $LOAD_PATH.unshift(path) if File.directory?(path) @@ -578,7 +575,7 @@ module Rails end initializer :add_routing_paths do |app| - routing_paths = self.paths["config/routes.rb"].existent + routing_paths = paths["config/routes.rb"].existent if routes? || routing_paths.any? app.routes_reloader.paths.unshift(*routing_paths) @@ -589,14 +586,14 @@ module Rails # I18n load paths are a special case since the ones added # later have higher priority. initializer :add_locales do - config.i18n.railties_load_path.concat(paths["config/locales"].existent) + config.i18n.railties_load_path << paths["config/locales"] end initializer :add_view_paths do views = paths["app/views"].existent unless views.empty? - ActiveSupport.on_load(:action_controller){ prepend_view_path(views) if respond_to?(:prepend_view_path) } - ActiveSupport.on_load(:action_mailer){ prepend_view_path(views) } + ActiveSupport.on_load(:action_controller) { prepend_view_path(views) if respond_to?(:prepend_view_path) } + ActiveSupport.on_load(:action_mailer) { prepend_view_path(views) } end end @@ -624,7 +621,7 @@ module Rails end rake_tasks do - next if self.is_a?(Rails::Application) + next if is_a?(Rails::Application) next unless has_migrations? namespace railtie_name do @@ -648,48 +645,61 @@ module Rails protected - def load_config_initializer(initializer) - ActiveSupport::Notifications.instrument('load_config_initializer.railties', initializer: initializer) do - load(initializer) + def run_tasks_blocks(*) #:nodoc: + super + paths["lib/tasks"].existent.sort.each { |ext| load(ext) } end - end - def run_tasks_blocks(*) #:nodoc: - super - paths["lib/tasks"].existent.sort.each { |ext| load(ext) } - end + private - def has_migrations? #:nodoc: - paths["db/migrate"].existent.any? - end + def load_config_initializer(initializer) # :doc: + ActiveSupport::Notifications.instrument("load_config_initializer.railties", initializer: initializer) do + load(initializer) + end + end - def self.find_root_with_flag(flag, root_path, default=nil) #:nodoc: + def has_migrations? + paths["db/migrate"].existent.any? + end + + def self.find_root_with_flag(flag, root_path, default = nil) #:nodoc: + while root_path && File.directory?(root_path) && !File.exist?("#{root_path}/#{flag}") + parent = File.dirname(root_path) + root_path = parent != root_path && parent + end + + root = File.exist?("#{root_path}/#{flag}") ? root_path : default + raise "Could not find root path for #{self}" unless root - while root_path && File.directory?(root_path) && !File.exist?("#{root_path}/#{flag}") - parent = File.dirname(root_path) - root_path = parent != root_path && parent + Pathname.new File.realpath root end - root = File.exist?("#{root_path}/#{flag}") ? root_path : default - raise "Could not find root path for #{self}" unless root + def default_middleware_stack + ActionDispatch::MiddlewareStack.new + end - Pathname.new File.realpath root - end + def _all_autoload_once_paths + config.autoload_once_paths + end - def default_middleware_stack #:nodoc: - ActionDispatch::MiddlewareStack.new - end + def _all_autoload_paths + @_all_autoload_paths ||= (config.autoload_paths + config.eager_load_paths + config.autoload_once_paths).uniq + end - def _all_autoload_once_paths #:nodoc: - config.autoload_once_paths - end + def _all_load_paths + @_all_load_paths ||= (config.paths.load_paths + _all_autoload_paths).uniq + end - def _all_autoload_paths #:nodoc: - @_all_autoload_paths ||= (config.autoload_paths + config.eager_load_paths + config.autoload_once_paths).uniq - end + def build_request(env) + env.merge!(env_config) + req = ActionDispatch::Request.new env + req.routes = routes + req.engine_script_name = req.script_name + req + end - def _all_load_paths #:nodoc: - @_all_load_paths ||= (config.paths.load_paths + _all_autoload_paths).uniq - end + def build_middleware + config.middleware + end end end diff --git a/railties/lib/rails/engine/commands.rb b/railties/lib/rails/engine/commands.rb index f39f926109..05218640c6 100644 --- a/railties/lib/rails/engine/commands.rb +++ b/railties/lib/rails/engine/commands.rb @@ -1,41 +1,9 @@ -ARGV << '--help' if ARGV.empty? +# frozen_string_literal: true -aliases = { - "g" => "generate", - "d" => "destroy" -} - -command = ARGV.shift -command = aliases[command] || command - -require ENGINE_PATH -engine = ::Rails::Engine.find(ENGINE_ROOT) - -case command -when 'generate', 'destroy' - require 'rails/generators' - Rails::Generators.namespace = engine.railtie_namespace - engine.load_generators - require "rails/commands/#{command}" - -when '--version', '-v' - ARGV.unshift '--version' - require 'rails/commands/application' - -else - puts "Error: Command not recognized" unless %w(-h --help).include?(command) - puts <<-EOT -Usage: rails COMMAND [ARGS] - -The common Rails commands available for engines are: - generate Generate new code (short-cut alias: "g") - destroy Undo code generated with "generate" (short-cut alias: "d") - -All commands can be run with -h for more information. - -If you want to run any commands that need to be run in context -of the application, like `rails server` or `rails console`, -you should do it from application's directory (typically test/dummy). - EOT - exit(1) +unless defined?(APP_PATH) + if File.exist?(File.expand_path("test/dummy/config/application.rb", ENGINE_ROOT)) + APP_PATH = File.expand_path("test/dummy/config/application", ENGINE_ROOT) + end end + +require "rails/commands" diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb index 62a4139d07..4143b3c881 100644 --- a/railties/lib/rails/engine/configuration.rb +++ b/railties/lib/rails/engine/configuration.rb @@ -1,20 +1,19 @@ -require 'rails/railtie/configuration' +# frozen_string_literal: true + +require "rails/railtie/configuration" module Rails class Engine class Configuration < ::Rails::Railtie::Configuration attr_reader :root - attr_writer :middleware, :eager_load_paths, :autoload_once_paths, :autoload_paths + attr_accessor :middleware + attr_writer :eager_load_paths, :autoload_once_paths, :autoload_paths - def initialize(root=nil) + def initialize(root = nil) super() @root = root @generators = app_generators.dup - end - - # Returns the middleware stack for the engine. - def middleware - @middleware ||= Rails::Configuration::MiddlewareStackProxy.new + @middleware = Rails::Configuration::MiddlewareStackProxy.new end # Holds generators configuration: @@ -29,7 +28,7 @@ module Rails # # config.generators.colorize_logging = false # - def generators #:nodoc: + def generators @generators ||= Rails::Configuration::Generators.new yield(@generators) if block_given? @generators @@ -39,9 +38,12 @@ module Rails @paths ||= begin paths = Rails::Paths::Root.new(@root) - paths.add "app", eager_load: true, glob: "{*,*/concerns}" + paths.add "app", eager_load: true, + glob: "{*,*/concerns}", + exclude: %w(assets javascript) paths.add "app/assets", glob: "*" paths.add "app/controllers", eager_load: true + paths.add "app/channels", eager_load: true, glob: "**/*_channel.rb" paths.add "app/helpers", eager_load: true paths.add "app/models", eager_load: true paths.add "app/mailers", eager_load: true diff --git a/railties/lib/rails/engine/railties.rb b/railties/lib/rails/engine/railties.rb index 9969a1475d..052b74c880 100644 --- a/railties/lib/rails/engine/railties.rb +++ b/railties/lib/rails/engine/railties.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails class Engine < Railtie class Railties diff --git a/railties/lib/rails/engine/updater.rb b/railties/lib/rails/engine/updater.rb new file mode 100644 index 0000000000..be7a47124a --- /dev/null +++ b/railties/lib/rails/engine/updater.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "rails/generators" +require "rails/generators/rails/plugin/plugin_generator" + +module Rails + class Engine + class Updater + class << self + def generator + @generator ||= Rails::Generators::PluginGenerator.new ["plugin"], + { engine: true }, { destination_root: ENGINE_ROOT } + end + + def run(action) + generator.send(action) + end + end + end + end +end diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb index 7d74b1bfe5..54bfbdd516 100644 --- a/railties/lib/rails/gem_version.rb +++ b/railties/lib/rails/gem_version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails # Returns the version of the currently loaded Rails as a <tt>Gem::Version</tt> def self.gem_version @@ -5,7 +7,7 @@ module Rails end module VERSION - MAJOR = 5 + MAJOR = 6 MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index b430cf1909..5e8cebc50a 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -1,45 +1,49 @@ -activesupport_path = File.expand_path('../../../../activesupport/lib', __FILE__) +# frozen_string_literal: true + +activesupport_path = File.expand_path("../../../activesupport/lib", __dir__) $:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) -require 'thor/group' +require "thor/group" +require "rails/command" -require 'active_support' -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/array/extract_options' -require 'active_support/core_ext/hash/deep_merge' -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/string/inflections' +require "active_support" +require "active_support/core_ext/object/blank" +require "active_support/core_ext/kernel/singleton_class" +require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/hash/deep_merge" +require "active_support/core_ext/module/attribute_accessors" +require "active_support/core_ext/string/indent" +require "active_support/core_ext/string/inflections" module Rails module Generators - autoload :Actions, 'rails/generators/actions' - autoload :ActiveModel, 'rails/generators/active_model' - autoload :Base, 'rails/generators/base' - autoload :Migration, 'rails/generators/migration' - autoload :NamedBase, 'rails/generators/named_base' - autoload :ResourceHelpers, 'rails/generators/resource_helpers' - autoload :TestCase, 'rails/generators/test_case' + include Rails::Command::Behavior + + autoload :Actions, "rails/generators/actions" + autoload :ActiveModel, "rails/generators/active_model" + autoload :Base, "rails/generators/base" + autoload :Migration, "rails/generators/migration" + autoload :NamedBase, "rails/generators/named_base" + autoload :ResourceHelpers, "rails/generators/resource_helpers" + autoload :TestCase, "rails/generators/test_case" mattr_accessor :namespace DEFAULT_ALIASES = { rails: { - actions: '-a', - orm: '-o', - javascripts: '-j', - javascript_engine: '-je', - resource_controller: '-c', - scaffold_controller: '-c', - stylesheets: '-y', - stylesheet_engine: '-se', - scaffold_stylesheet: '-ss', - template_engine: '-e', - test_framework: '-t' + actions: "-a", + orm: "-o", + resource_controller: "-c", + scaffold_controller: "-c", + stylesheets: "-y", + stylesheet_engine: "-se", + scaffold_stylesheet: "-ss", + template_engine: "-e", + test_framework: "-t" }, test_unit: { - fixture_replacement: '-r', + fixture_replacement: "-r", } } @@ -50,8 +54,6 @@ module Rails force_plural: false, helper: true, integration_tool: nil, - javascripts: true, - javascript_engine: :js, orm: false, resource_controller: :controller, resource_route: true, @@ -59,335 +61,258 @@ module Rails stylesheets: true, stylesheet_engine: :css, scaffold_stylesheet: true, - test_framework: false, + system_tests: nil, + test_framework: nil, template_engine: :erb } } - def self.configure!(config) #:nodoc: - api_only! if config.api_only - no_color! unless config.colorize_logging - aliases.deep_merge! config.aliases - options.deep_merge! config.options - fallbacks.merge! config.fallbacks - templates_path.concat config.templates - templates_path.uniq! - hide_namespaces(*config.hidden_namespaces) - end - - def self.templates_path #:nodoc: - @templates_path ||= [] - end - - def self.aliases #:nodoc: - @aliases ||= DEFAULT_ALIASES.dup - end - - def self.options #:nodoc: - @options ||= DEFAULT_OPTIONS.dup - end - - # Hold configured generators fallbacks. If a plugin developer wants a - # generator group to fallback to another group in case of missing generators, - # they can add a fallback. - # - # For example, shoulda is considered a test_framework and is an extension - # of test_unit. However, most part of shoulda generators are similar to - # test_unit ones. - # - # Shoulda then can tell generators to search for test_unit generators when - # some of them are not available by adding a fallback: - # - # Rails::Generators.fallbacks[:shoulda] = :test_unit - def self.fallbacks - @fallbacks ||= {} - end - - # Configure generators for API only applications. It basically hides - # everything that is usually browser related, such as assets and session - # migration generators, and completely disable views, helpers and assets - # so generators such as scaffold won't create them. - def self.api_only! - hide_namespaces "assets", "helper", "css", "js" - - options[:rails].merge!( - api: true, - assets: false, - helper: false, - template_engine: nil - ) - end - - # Remove the color from output. - def self.no_color! - Thor::Base.shell = Thor::Shell::Basic - end - - # Track all generators subclasses. - def self.subclasses - @subclasses ||= [] - end + class << self + def configure!(config) #:nodoc: + api_only! if config.api_only + no_color! unless config.colorize_logging + aliases.deep_merge! config.aliases + options.deep_merge! config.options + fallbacks.merge! config.fallbacks + templates_path.concat config.templates + templates_path.uniq! + hide_namespaces(*config.hidden_namespaces) + end - # Rails finds namespaces similar to thor, it only adds one rule: - # - # Generators names must end with "_generator.rb". This is required because Rails - # looks in load paths and loads the generator just before it's going to be used. - # - # find_by_namespace :webrat, :rails, :integration - # - # Will search for the following generators: - # - # "rails:webrat", "webrat:integration", "webrat" - # - # Notice that "rails:generators:webrat" could be loaded as well, what - # Rails looks for is the first and last parts of the namespace. - def self.find_by_namespace(name, base=nil, context=nil) #:nodoc: - lookups = [] - lookups << "#{base}:#{name}" if base - lookups << "#{name}:#{context}" if context - - unless base || context - unless name.to_s.include?(?:) - lookups << "#{name}:#{name}" - lookups << "rails:#{name}" - end - lookups << "#{name}" + def templates_path #:nodoc: + @templates_path ||= [] end - lookup(lookups) + def aliases #:nodoc: + @aliases ||= DEFAULT_ALIASES.dup + end - namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }] + def options #:nodoc: + @options ||= DEFAULT_OPTIONS.dup + end - lookups.each do |namespace| - klass = namespaces[namespace] - return klass if klass + # Hold configured generators fallbacks. If a plugin developer wants a + # generator group to fallback to another group in case of missing generators, + # they can add a fallback. + # + # For example, shoulda is considered a test_framework and is an extension + # of test_unit. However, most part of shoulda generators are similar to + # test_unit ones. + # + # Shoulda then can tell generators to search for test_unit generators when + # some of them are not available by adding a fallback: + # + # Rails::Generators.fallbacks[:shoulda] = :test_unit + def fallbacks + @fallbacks ||= {} end - invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name) - end + # Configure generators for API only applications. It basically hides + # everything that is usually browser related, such as assets and session + # migration generators, and completely disable helpers and assets + # so generators such as scaffold won't create them. + def api_only! + hide_namespaces "assets", "helper", "css", "js" + + options[:rails].merge!( + api: true, + assets: false, + helper: false, + template_engine: nil + ) + + if ARGV.first == "mailer" + options[:rails][:template_engine] = :erb + end + end - # Receives a namespace, arguments and the behavior to invoke the generator. - # It's used as the default entry point for generate, destroy and update - # commands. - def self.invoke(namespace, args=ARGV, config={}) - names = namespace.to_s.split(':') - if klass = find_by_namespace(names.pop, names.any? && names.join(':')) - args << "--help" if args.empty? && klass.arguments.any?(&:required?) - klass.start(args, config) - else - options = sorted_groups.flat_map(&:last) - suggestions = options.sort_by {|suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3) - msg = "Could not find generator '#{namespace}'. " - msg << "Maybe you meant #{ suggestions.map {|s| "'#{s}'"}.to_sentence(last_word_connector: " or ") }\n" - msg << "Run `rails generate --help` for more options." - puts msg + # Remove the color from output. + def no_color! + Thor::Base.shell = Thor::Shell::Basic end - end - # Returns an array of generator namespaces that are hidden. - # Generator namespaces may be hidden for a variety of reasons. - # Some are aliased such as "rails:migration" and can be - # invoked with the shorter "migration", others are private to other generators - # such as "css:scaffold". - def self.hidden_namespaces - @hidden_namespaces ||= begin - orm = options[:rails][:orm] - test = options[:rails][:test_framework] - template = options[:rails][:template_engine] - css = options[:rails][:stylesheet_engine] - - [ - "rails", - "resource_route", - "#{orm}:migration", - "#{orm}:model", - "#{test}:controller", - "#{test}:helper", - "#{test}:integration", - "#{test}:mailer", - "#{test}:model", - "#{test}:scaffold", - "#{test}:view", - "#{template}:controller", - "#{template}:scaffold", - "#{template}:mailer", - "#{css}:scaffold", - "#{css}:assets", - "css:assets", - "css:scaffold" - ] + # Returns an array of generator namespaces that are hidden. + # Generator namespaces may be hidden for a variety of reasons. + # Some are aliased such as "rails:migration" and can be + # invoked with the shorter "migration", others are private to other generators + # such as "css:scaffold". + def hidden_namespaces + @hidden_namespaces ||= begin + orm = options[:rails][:orm] + test = options[:rails][:test_framework] + template = options[:rails][:template_engine] + css = options[:rails][:stylesheet_engine] + + [ + "rails", + "resource_route", + "#{orm}:migration", + "#{orm}:model", + "#{test}:controller", + "#{test}:helper", + "#{test}:integration", + "#{test}:system", + "#{test}:mailer", + "#{test}:model", + "#{test}:scaffold", + "#{test}:view", + "#{test}:job", + "#{template}:controller", + "#{template}:scaffold", + "#{template}:mailer", + "#{css}:scaffold", + "#{css}:assets", + "css:assets", + "css:scaffold" + ] + end end - end - class << self def hide_namespaces(*namespaces) hidden_namespaces.concat(namespaces) end alias hide_namespace hide_namespaces - end - # Show help message with available generators. - def self.help(command = 'generate') - puts "Usage: rails #{command} GENERATOR [args] [options]" - puts - puts "General options:" - puts " -h, [--help] # Print generator's options and usage" - puts " -p, [--pretend] # Run but do not make any changes" - puts " -f, [--force] # Overwrite files that already exist" - puts " -s, [--skip] # Skip files that already exist" - puts " -q, [--quiet] # Suppress status output" - puts - puts "Please choose a generator below." - puts - - print_generators - end + # Show help message with available generators. + def help(command = "generate") + puts "Usage: rails #{command} GENERATOR [args] [options]" + puts + puts "General options:" + puts " -h, [--help] # Print generator's options and usage" + puts " -p, [--pretend] # Run but do not make any changes" + puts " -f, [--force] # Overwrite files that already exist" + puts " -s, [--skip] # Skip files that already exist" + puts " -q, [--quiet] # Suppress status output" + puts + puts "Please choose a generator below." + puts - def self.public_namespaces - lookup! - subclasses.map(&:namespace) - end + print_generators + end - def self.print_generators - sorted_groups.each { |b, n| print_list(b, n) } - end + def public_namespaces + lookup! + subclasses.map(&:namespace) + end - def self.sorted_groups - namespaces = public_namespaces - namespaces.sort! - groups = Hash.new { |h,k| h[k] = [] } - namespaces.each do |namespace| - base = namespace.split(':').first - groups[base] << namespace + def print_generators + sorted_groups.each { |b, n| print_list(b, n) } end - rails = groups.delete("rails") - rails.map! { |n| n.sub(/^rails:/, '') } - rails.delete("app") - rails.delete("plugin") - hidden_namespaces.each { |n| groups.delete(n.to_s) } + def sorted_groups + namespaces = public_namespaces + namespaces.sort! - [["rails", rails]] + groups.sort.to_a - end + groups = Hash.new { |h, k| h[k] = [] } + namespaces.each do |namespace| + base = namespace.split(":").first + groups[base] << namespace + end - protected - - # This code is based directly on the Text gem implementation - # Returns a value representing the "cost" of transforming str1 into str2 - def self.levenshtein_distance str1, str2 - s = str1 - t = str2 - n = s.length - m = t.length - - return m if (0 == n) - return n if (0 == m) - - d = (0..m).to_a - x = nil - - # avoid duplicating an enumerable object in the loop - str2_codepoint_enumerable = str2.each_codepoint - - str1.each_codepoint.with_index do |char1, i| - e = i+1 - - str2_codepoint_enumerable.with_index do |char2, j| - cost = (char1 == char2) ? 0 : 1 - x = [ - d[j+1] + 1, # insertion - e + 1, # deletion - d[j] + cost # substitution - ].min - d[j] = e - e = x - end + rails = groups.delete("rails") + rails.map! { |n| n.sub(/^rails:/, "") } + rails.delete("app") + rails.delete("plugin") + rails.delete("encrypted_secrets") + rails.delete("encrypted_file") + rails.delete("encryption_key_file") + rails.delete("master_key") + rails.delete("credentials") - d[m] = x - end + hidden_namespaces.each { |n| groups.delete(n.to_s) } - x + [[ "rails", rails ]] + groups.sort.to_a end - # Prints a list of generators. - def self.print_list(base, namespaces) #:nodoc: - namespaces = namespaces.reject do |n| - hidden_namespaces.include?(n) + # Rails finds namespaces similar to Thor, it only adds one rule: + # + # Generators names must end with "_generator.rb". This is required because Rails + # looks in load paths and loads the generator just before it's going to be used. + # + # find_by_namespace :webrat, :rails, :integration + # + # Will search for the following generators: + # + # "rails:webrat", "webrat:integration", "webrat" + # + # Notice that "rails:generators:webrat" could be loaded as well, what + # Rails looks for is the first and last parts of the namespace. + def find_by_namespace(name, base = nil, context = nil) #:nodoc: + lookups = [] + lookups << "#{base}:#{name}" if base + lookups << "#{name}:#{context}" if context + + unless base || context + unless name.to_s.include?(?:) + lookups << "#{name}:#{name}" + lookups << "rails:#{name}" + end + lookups << "#{name}" end - return if namespaces.empty? - puts "#{base.camelize}:" + lookup(lookups) - namespaces.each do |namespace| - puts(" #{namespace}") + namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }] + lookups.each do |namespace| + klass = namespaces[namespace] + return klass if klass end - puts + invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name) end - # Try fallbacks for the given base. - def self.invoke_fallbacks_for(name, base) #:nodoc: - return nil unless base && fallbacks[base.to_sym] - invoked_fallbacks = [] + # Receives a namespace, arguments and the behavior to invoke the generator. + # It's used as the default entry point for generate, destroy and update + # commands. + def invoke(namespace, args = ARGV, config = {}) + names = namespace.to_s.split(":") + if klass = find_by_namespace(names.pop, names.any? && names.join(":")) + args << "--help" if args.empty? && klass.arguments.any?(&:required?) + klass.start(args, config) + else + options = sorted_groups.flat_map(&:last) + suggestion = Rails::Command::Spellchecker.suggest(namespace.to_s, from: options) + puts <<~MSG + Could not find generator '#{namespace}'. Maybe you meant #{suggestion.inspect}? + Run `rails generate --help` for more options. + MSG + end + end - Array(fallbacks[base.to_sym]).each do |fallback| - next if invoked_fallbacks.include?(fallback) - invoked_fallbacks << fallback + private - klass = find_by_namespace(name, fallback) - return klass if klass + def print_list(base, namespaces) # :doc: + namespaces = namespaces.reject { |n| hidden_namespaces.include?(n) } + super end - nil - end + # Try fallbacks for the given base. + def invoke_fallbacks_for(name, base) + return nil unless base && fallbacks[base.to_sym] + invoked_fallbacks = [] + + Array(fallbacks[base.to_sym]).each do |fallback| + next if invoked_fallbacks.include?(fallback) + invoked_fallbacks << fallback - # Receives namespaces in an array and tries to find matching generators - # in the load path. - def self.lookup(namespaces) #:nodoc: - paths = namespaces_to_paths(namespaces) - - paths.each do |raw_path| - ["rails/generators", "generators"].each do |base| - path = "#{base}/#{raw_path}_generator" - - begin - require path - return - rescue LoadError => e - raise unless e.message =~ /#{Regexp.escape(path)}$/ - rescue Exception => e - warn "[WARNING] Could not load generator #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}" - end + klass = find_by_namespace(name, fallback) + return klass if klass end + + nil end - end - # This will try to load any generator in the load path to show in help. - def self.lookup! #:nodoc: - $LOAD_PATH.each do |base| - Dir[File.join(base, "{rails/generators,generators}", "**", "*_generator.rb")].each do |path| - begin - path = path.sub("#{base}/", "") - require path - rescue Exception - # No problem - end - end + def command_type # :doc: + @command_type ||= "generator" end - end - # Convert namespaces to paths by replacing ":" for "/" and adding - # an extra lookup. For example, "rails:model" should be searched - # in both: "rails/model/model_generator" and "rails/model_generator". - def self.namespaces_to_paths(namespaces) #:nodoc: - paths = [] - namespaces.each do |namespace| - pieces = namespace.split(":") - paths << pieces.dup.push(pieces.last).join("/") - paths << pieces.join("/") + def lookup_paths # :doc: + @lookup_paths ||= %w( rails/generators generators ) end - paths.uniq! - paths - end + + def file_lookup_paths # :doc: + @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_generator.rb" ] + end + end end end diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index 560a553789..78d2471890 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -1,11 +1,13 @@ -require 'open-uri' +# frozen_string_literal: true + +require "active_support/core_ext/string/strip" module Rails module Generators module Actions def initialize(*) # :nodoc: super - @in_group = nil + @indentation = 0 @after_bundle_callbacks = [] end @@ -13,29 +15,32 @@ module Rails # # gem "rspec", group: :test # gem "technoweenie-restful-authentication", lib: "restful-authentication", source: "http://gems.github.com/" - # gem "rails", "3.0", git: "git://github.com/rails/rails" + # gem "rails", "3.0", git: "https://github.com/rails/rails" + # gem "RedCloth", ">= 4.1.0", "< 4.2.0" def gem(*args) options = args.extract_options! - name, version = args + name, *versions = args # Set the message to be shown in logs. Uses the git repo if one is given, # otherwise use name (version). - parts, message = [ quote(name) ], name - if version ||= options.delete(:version) - parts << quote(version) - message << " (#{version})" + parts, message = [ quote(name) ], name.dup + + if versions = versions.any? ? versions : options.delete(:version) + _versions = Array(versions) + _versions.each do |version| + parts << quote(version) + end + message << " (#{_versions.join(", ")})" end message = options[:git] if options[:git] log :gemfile, message - options.each do |option, value| - parts << "#{option}: #{quote(value)}" - end + parts << quote(options) unless options.empty? in_root do str = "gem #{parts.join(", ")}" - str = " " + str if @in_group + str = indentation + str str = "\n" + str append_file "Gemfile", str, verbose: false end @@ -47,17 +52,29 @@ module Rails # gem "rspec-rails" # end def gem_group(*names, &block) - name = names.map(&:inspect).join(", ") - log :gemfile, "group #{name}" + options = names.extract_options! + str = names.map(&:inspect) + str << quote(options) unless options.empty? + str = str.join(", ") + log :gemfile, "group #{str}" in_root do - append_file "Gemfile", "\ngroup #{name} do", force: true + append_file "Gemfile", "\ngroup #{str} do", force: true + with_indentation(&block) + append_file "Gemfile", "\nend\n", force: true + end + end - @in_group = true - instance_eval(&block) - @in_group = false + def github(repo, options = {}, &block) + str = [quote(repo)] + str << quote(options) unless options.empty? + str = str.join(", ") + log :github, "github #{str}" - append_file "Gemfile", "\nend\n", force: true + in_root do + append_file "Gemfile", "\n#{indentation}github #{str} do", force: true + with_indentation(&block) + append_file "Gemfile", "\n#{indentation}end", force: true end end @@ -70,15 +87,13 @@ module Rails # add_source "http://gems.github.com/" do # gem "rspec-rails" # end - def add_source(source, options={}, &block) + def add_source(source, options = {}, &block) log :source, source in_root do if block - append_file "Gemfile", "source #{quote(source)} do", force: true - @in_group = true - instance_eval(&block) - @in_group = false + append_file "Gemfile", "\nsource #{quote(source)} do", force: true + with_indentation(&block) append_file "Gemfile", "\nend\n", force: true else prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false @@ -92,23 +107,23 @@ module Rails # file in <tt>config/environments</tt>. # # environment do - # "config.autoload_paths += %W(#{config.root}/extras)" + # "config.action_controller.asset_host = 'cdn.provider.com'" # end # # environment(nil, env: "development") do - # "config.autoload_paths += %W(#{config.root}/extras)" + # "config.action_controller.asset_host = 'localhost:3000'" # end - def environment(data=nil, options={}) - sentinel = /class [a-z_:]+ < Rails::Application/i - env_file_sentinel = /Rails\.application\.configure do/ - data = yield if !data && block_given? + def environment(data = nil, options = {}) + sentinel = "class Application < Rails::Application\n" + env_file_sentinel = "Rails.application.configure do\n" + data ||= yield if block_given? in_root do if options[:env].nil? - inject_into_file 'config/application.rb', "\n #{data}", after: sentinel, verbose: false + inject_into_file "config/application.rb", optimize_indentation(data, 4), after: sentinel, verbose: false else Array(options[:env]).each do |env| - inject_into_file "config/environments/#{env}.rb", "\n #{data}", after: env_file_sentinel, verbose: false + inject_into_file "config/environments/#{env}.rb", optimize_indentation(data, 2), after: env_file_sentinel, verbose: false end end end @@ -120,7 +135,7 @@ module Rails # git :init # git add: "this.file that.rb" # git add: "onefile.rb", rm: "badfile.cxx" - def git(commands={}) + def git(commands = {}) if commands.is_a?(Symbol) run "git #{commands}" else @@ -139,12 +154,13 @@ module Rails # end # # vendor("foreign.rb", "# Foreign code is fun") - def vendor(filename, data=nil, &block) + def vendor(filename, data = nil) log :vendor, filename - create_file("vendor/#{filename}", data, verbose: false, &block) + data ||= yield if block_given? + create_file("vendor/#{filename}", optimize_indentation(data), verbose: false) end - # Create a new file in the lib/ directory. Code can be specified + # Create a new file in the <tt>lib/</tt> directory. Code can be specified # in a block or a data string can be given. # # lib("crypto.rb") do @@ -152,9 +168,10 @@ module Rails # end # # lib("foreign.rb", "# Foreign code is fun") - def lib(filename, data=nil, &block) + def lib(filename, data = nil) log :lib, filename - create_file("lib/#{filename}", data, verbose: false, &block) + data ||= yield if block_given? + create_file("lib/#{filename}", optimize_indentation(data), verbose: false) end # Create a new +Rakefile+ with the provided code (either in a block or a string). @@ -172,9 +189,10 @@ module Rails # end # # rakefile('seed.rake', 'puts "Planting seeds"') - def rakefile(filename, data=nil, &block) + def rakefile(filename, data = nil) log :rakefile, filename - create_file("lib/tasks/#{filename}", data, verbose: false, &block) + data ||= yield if block_given? + create_file("lib/tasks/#{filename}", optimize_indentation(data), verbose: false) end # Create a new initializer with the provided code (either in a block or a string). @@ -190,9 +208,10 @@ module Rails # end # # initializer("api.rb", "API_KEY = '123456'") - def initializer(filename, data=nil, &block) + def initializer(filename, data = nil) log :initializer, filename - create_file("config/initializers/#{filename}", data, verbose: false, &block) + data ||= yield if block_given? + create_file("config/initializers/#{filename}", optimize_indentation(data), verbose: false) end # Generate something using a generator from Rails or a plugin. @@ -207,22 +226,31 @@ module Rails in_root { run_ruby_script("bin/rails generate #{what} #{argument}", verbose: false) } end - # Runs the supplied rake task + # Runs the supplied rake task (invoked with 'rake ...') # # rake("db:migrate") # rake("db:migrate", env: "production") # rake("gems:install", sudo: true) - def rake(command, options={}) - log :rake, command - env = options[:env] || ENV["RAILS_ENV"] || 'development' - sudo = options[:sudo] && RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ ? 'sudo ' : '' - in_root { run("#{sudo}#{extify(:rake)} #{command} RAILS_ENV=#{env}", verbose: false) } + # rake("gems:install", capture: true) + def rake(command, options = {}) + execute_command :rake, command, options + end + + # Runs the supplied rake task (invoked with 'rails ...') + # + # rails_command("db:migrate") + # rails_command("db:migrate", env: "production") + # rails_command("gems:install", sudo: true) + # rails_command("gems:install", capture: true) + def rails_command(command, options = {}) + execute_command :rails, command, options end # Just run the capify command in root # # capify! def capify! + ActiveSupport::Deprecation.warn("`capify!` is deprecated and will be removed in the next version of Rails.") log :capify, "" in_root { run("#{extify(:capify)} .", verbose: false) } end @@ -235,7 +263,7 @@ module Rails sentinel = /\.routes\.draw do\s*\n/m in_root do - inject_into_file 'config/routes.rb', " #{routing_code}\n", { after: sentinel, verbose: false, force: true } + inject_into_file "config/routes.rb", optimize_indentation(routing_code, 2), after: sentinel, verbose: false, force: false end end @@ -256,23 +284,36 @@ module Rails @after_bundle_callbacks << block end - protected + private # Define log for backwards compatibility. If just one argument is sent, # invoke say, otherwise invoke say_status. Differently from say and # similarly to say_status, this method respects the quiet? option given. - def log(*args) + def log(*args) # :doc: if args.size == 1 say args.first.to_s unless options.quiet? else - args << (self.behavior == :invoke ? :green : :red) + args << (behavior == :invoke ? :green : :red) say_status(*args) end end + # Runs the supplied command using either "rake ..." or "rails ..." + # based on the executor parameter provided. + def execute_command(executor, command, options = {}) # :doc: + log executor, command + env = options[:env] || ENV["RAILS_ENV"] || "development" + sudo = options[:sudo] && !Gem.win_platform? ? "sudo " : "" + config = { verbose: false } + + config[:capture] = options[:capture] if options[:capture] + + in_root { run("#{sudo}#{extify(executor)} #{command} RAILS_ENV=#{env}", config) } + end + # Add an extension to the given name based on the platform. - def extify(name) - if RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ + def extify(name) # :doc: + if Gem.win_platform? "#{name}.bat" else name @@ -281,7 +322,12 @@ module Rails # Surround string with single quotes if there is no quotes. # Otherwise fall back to double quotes - def quote(value) + def quote(value) # :doc: + if value.respond_to? :each_pair + return value.map do |k, v| + "#{k}: #{quote(v)}" + end.join(", ") + end return value.inspect unless value.is_a? String if value.include?("'") @@ -290,6 +336,30 @@ module Rails "'#{value}'" end end + + # Returns optimized string with indentation + def optimize_indentation(value, amount = 0) # :doc: + return "#{value}\n" unless value.is_a?(String) + + if value.lines.size > 1 + value.strip_heredoc.indent(amount) + else + "#{value.strip.indent(amount)}\n" + end + end + + # Indent the +Gemfile+ to the depth of @indentation + def indentation # :doc: + " " * @indentation + end + + # Manage +Gemfile+ indentation for a DSL action block + def with_indentation(&block) # :doc: + @indentation += 1 + instance_eval(&block) + ensure + @indentation -= 1 + end end end end diff --git a/railties/lib/rails/generators/actions/create_migration.rb b/railties/lib/rails/generators/actions/create_migration.rb index cffdef6ec9..05bc242447 100644 --- a/railties/lib/rails/generators/actions/create_migration.rb +++ b/railties/lib/rails/generators/actions/create_migration.rb @@ -1,10 +1,12 @@ -require 'thor/actions' +# frozen_string_literal: true + +require "fileutils" +require "thor/actions" module Rails module Generators module Actions - class CreateMigration < Thor::Actions::CreateFile - + class CreateMigration < Thor::Actions::CreateFile #:nodoc: def migration_dir File.dirname(@destination) end @@ -37,32 +39,32 @@ module Rails end alias :exists? :existing_migration - protected + private - def on_conflict_behavior - options = base.options.merge(config) - if identical? - say_status :identical, :blue, relative_existing_migration - elsif options[:force] - say_status :remove, :green, relative_existing_migration - say_status :create, :green - unless pretend? - ::FileUtils.rm_rf(existing_migration) - yield + def on_conflict_behavior # :doc: + options = base.options.merge(config) + if identical? + say_status :identical, :blue, relative_existing_migration + elsif options[:force] + say_status :remove, :green, relative_existing_migration + say_status :create, :green + unless pretend? + ::FileUtils.rm_rf(existing_migration) + yield + end + elsif options[:skip] + say_status :skip, :yellow + else + say_status :conflict, :red + raise Error, "Another migration is already named #{migration_file_name}: " \ + "#{existing_migration}. Use --force to replace this migration " \ + "or --skip to ignore conflicted file." end - elsif options[:skip] - say_status :skip, :yellow - else - say_status :conflict, :red - raise Error, "Another migration is already named #{migration_file_name}: " + - "#{existing_migration}. Use --force to replace this migration " + - "or --skip to ignore conflicted file." end - end - def say_status(status, color, message = relative_destination) - base.shell.say_status(status, message, color) if config[:verbose] - end + def say_status(status, color, message = relative_destination) # :doc: + base.shell.say_status(status, message, color) if config[:verbose] + end end end end diff --git a/railties/lib/rails/generators/active_model.rb b/railties/lib/rails/generators/active_model.rb index 6183944bb0..8df8eb2438 100644 --- a/railties/lib/rails/generators/active_model.rb +++ b/railties/lib/rails/generators/active_model.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails module Generators # ActiveModel is a class to be implemented by each ORM to allow Rails to @@ -39,13 +41,13 @@ module Rails # GET edit # PATCH/PUT update # DELETE destroy - def self.find(klass, params=nil) + def self.find(klass, params = nil) "#{klass}.find(#{params})" end # GET new # POST create - def self.build(klass, params=nil) + def self.build(klass, params = nil) if params "#{klass}.new(#{params})" else @@ -59,7 +61,7 @@ module Rails end # PATCH/PUT update - def update(params=nil) + def update(params = nil) "#{name}.update(#{params})" end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 249fe96772..4dc4d27a46 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -1,15 +1,17 @@ -require 'digest/md5' -require 'active_support/core_ext/string/strip' -require 'rails/version' unless defined?(Rails::VERSION) -require 'open-uri' -require 'uri' -require 'rails/generators' -require 'active_support/core_ext/array/extract_options' +# frozen_string_literal: true + +require "fileutils" +require "digest/md5" +require "rails/version" unless defined?(Rails::VERSION) +require "open-uri" +require "uri" +require "rails/generators" +require "active_support/core_ext/array/extract_options" module Rails module Generators class AppBase < Base # :nodoc: - DATABASES = %w( mysql oracle postgresql sqlite3 frontbase ibm_db sqlserver ) + DATABASES = %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver ) JDBC_DATABASES = %w( jdbcmysql jdbcsqlite3 jdbcpostgresql jdbc ) DATABASES.concat(JDBC_DATABASES) @@ -23,63 +25,75 @@ module Rails end def self.add_shared_options_for(name) - class_option :template, type: :string, aliases: '-m', - desc: "Path to some #{name} template (can be a filesystem path or URL)" + class_option :template, type: :string, aliases: "-m", + desc: "Path to some #{name} template (can be a filesystem path or URL)" + + class_option :database, type: :string, aliases: "-d", default: "sqlite3", + desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})" + + class_option :skip_gemfile, type: :boolean, default: false, + desc: "Don't create a Gemfile" + + class_option :skip_git, type: :boolean, aliases: "-G", default: false, + desc: "Skip .gitignore file" - class_option :skip_gemfile, type: :boolean, default: false, - desc: "Don't create a Gemfile" + class_option :skip_keeps, type: :boolean, default: false, + desc: "Skip source control .keep files" - class_option :skip_bundle, type: :boolean, aliases: '-B', default: false, - desc: "Don't run bundle install" + class_option :skip_action_mailer, type: :boolean, aliases: "-M", + default: false, + desc: "Skip Action Mailer files" - class_option :skip_git, type: :boolean, aliases: '-G', default: false, - desc: 'Skip .gitignore file' + class_option :skip_active_record, type: :boolean, aliases: "-O", default: false, + desc: "Skip Active Record files" - class_option :skip_keeps, type: :boolean, default: false, - desc: 'Skip source control .keep files' + class_option :skip_active_storage, type: :boolean, default: false, + desc: "Skip Active Storage files" - class_option :skip_action_mailer, type: :boolean, aliases: "-M", - default: false, - desc: "Skip Action Mailer files" + class_option :skip_puma, type: :boolean, aliases: "-P", default: false, + desc: "Skip Puma related files" - class_option :skip_active_record, type: :boolean, aliases: '-O', default: false, - desc: 'Skip Active Record files' + class_option :skip_action_cable, type: :boolean, aliases: "-C", default: false, + desc: "Skip Action Cable files" - class_option :skip_sprockets, type: :boolean, aliases: '-S', default: false, - desc: 'Skip Sprockets files' + class_option :skip_sprockets, type: :boolean, aliases: "-S", default: false, + desc: "Skip Sprockets files" - class_option :skip_spring, type: :boolean, default: false, - desc: "Don't install Spring application preloader" + class_option :skip_spring, type: :boolean, default: false, + desc: "Don't install Spring application preloader" - class_option :database, type: :string, aliases: '-d', default: 'sqlite3', - desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})" + class_option :skip_listen, type: :boolean, default: false, + desc: "Don't generate configuration that depends on the listen gem" - class_option :javascript, type: :string, aliases: '-j', default: 'jquery', - desc: 'Preconfigure for selected JavaScript library' + class_option :skip_javascript, type: :boolean, aliases: "-J", default: name == "plugin", + desc: "Skip JavaScript files" - class_option :skip_javascript, type: :boolean, aliases: '-J', default: false, - desc: 'Skip JavaScript files' + class_option :skip_turbolinks, type: :boolean, default: false, + desc: "Skip turbolinks gem" - class_option :dev, type: :boolean, default: false, - desc: "Setup the #{name} with Gemfile pointing to your Rails checkout" + class_option :skip_test, type: :boolean, aliases: "-T", default: false, + desc: "Skip test files" - class_option :edge, type: :boolean, default: false, - desc: "Setup the #{name} with Gemfile pointing to Rails repository" + class_option :skip_system_test, type: :boolean, default: false, + desc: "Skip system test files" - class_option :skip_turbolinks, type: :boolean, default: false, - desc: 'Skip turbolinks gem' + class_option :skip_bootsnap, type: :boolean, default: false, + desc: "Skip bootsnap gem" - class_option :skip_test, type: :boolean, aliases: '-T', default: false, - desc: 'Skip test files' + class_option :dev, type: :boolean, default: false, + desc: "Setup the #{name} with Gemfile pointing to your Rails checkout" - class_option :rc, type: :string, default: false, - desc: "Path to file containing extra configuration options for rails command" + class_option :edge, type: :boolean, default: false, + desc: "Setup the #{name} with Gemfile pointing to Rails repository" - class_option :no_rc, type: :boolean, default: false, - desc: 'Skip loading of extra configuration options from .railsrc file' + class_option :rc, type: :string, default: nil, + desc: "Path to file containing extra configuration options for rails command" - class_option :help, type: :boolean, aliases: '-h', group: :rails, - desc: 'Show this help message and quit' + class_option :no_rc, type: :boolean, default: false, + desc: "Skip loading of extra configuration options from .railsrc file" + + class_option :help, type: :boolean, aliases: "-h", group: :rails, + desc: "Show this help message and quit" end def initialize(*args) @@ -89,9 +103,9 @@ module Rails convert_database_option_for_jruby end - protected + private - def gemfile_entry(name, *args) + def gemfile_entry(name, *args) # :doc: options = args.extract_options! version = args.first github = options[:github] @@ -107,23 +121,26 @@ module Rails self end - def gemfile_entries + def gemfile_entries # :doc: [rails_gemfile_entry, database_gemfile_entry, + webserver_gemfile_entry, assets_gemfile_entry, + webpacker_gemfile_entry, javascript_gemfile_entry, jbuilder_gemfile_entry, psych_gemfile_entry, + cable_gemfile_entry, @extra_entries].flatten.find_all(&@gem_filter) end - def add_gem_entry_filter + def add_gem_entry_filter # :doc: @gem_filter = lambda { |next_filter, entry| yield(entry) && next_filter.call(entry) }.curry[@gem_filter] end - def builder + def builder # :doc: @builder ||= begin builder_class = get_builder_class builder_class.include(ActionMethods) @@ -131,55 +148,85 @@ module Rails end end - def build(meth, *args) + def build(meth, *args) # :doc: builder.send(meth, *args) if builder.respond_to?(meth) end - def create_root + def create_root # :doc: valid_const? - empty_directory '.' + empty_directory "." FileUtils.cd(destination_root) unless options[:pretend] end - def apply_rails_template + def apply_rails_template # :doc: apply rails_template if rails_template rescue Thor::Error, LoadError, Errno::ENOENT => e raise Error, "The template [#{rails_template}] could not be loaded. Error: #{e}" end - def set_default_accessors! + def set_default_accessors! # :doc: self.destination_root = File.expand_path(app_path, destination_root) - self.rails_template = case options[:template] + self.rails_template = \ + case options[:template] when /^https?:\/\// options[:template] when String File.expand_path(options[:template], Dir.pwd) else options[:template] - end + end end - def database_gemfile_entry + def database_gemfile_entry # :doc: return [] if options[:skip_active_record] - GemfileEntry.version gem_for_database, nil, + gem_name, gem_version = gem_for_database + GemfileEntry.version gem_name, gem_version, "Use #{options[:database]} as the database for Active Record" end - def include_all_railties? - options.values_at(:skip_active_record, :skip_action_mailer, :skip_test, :skip_sprockets).none? + def webserver_gemfile_entry # :doc: + return [] if options[:skip_puma] + comment = "Use Puma as the app server" + GemfileEntry.new("puma", "~> 3.11", comment) + end + + def include_all_railties? # :doc: + [ + options.values_at( + :skip_active_record, + :skip_action_mailer, + :skip_test, + :skip_sprockets, + :skip_action_cable + ), + skip_active_storage? + ].flatten.none? end - def comment_if(value) - options[value] ? '# ' : '' + def comment_if(value) # :doc: + question = "#{value}?" + + comment = + if respond_to?(question, true) + send(question) + else + options[value] + end + + comment ? "# " : "" end - def keeps? + def keeps? # :doc: !options[:skip_keeps] end - def sqlite3? - !options[:skip_active_record] && options[:database] == 'sqlite3' + def sqlite3? # :doc: + !options[:skip_active_record] && options[:database] == "sqlite3" + end + + def skip_active_storage? # :doc: + options[:skip_active_storage] || options[:skip_active_record] end class GemfileEntry < Struct.new(:name, :version, :comment, :options, :commented_out) @@ -202,117 +249,119 @@ module Rails def self.path(name, path, comment = nil) new(name, nil, comment, path: path) end + + def version + version = super + + if version.is_a?(Array) + version.join("', '") + else + version + end + end end def rails_gemfile_entry if options.dev? [ - GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH), - GemfileEntry.github('sprockets-rails', 'rails/sprockets-rails'), - GemfileEntry.github('arel', 'rails/arel') + GemfileEntry.path("rails", Rails::Generators::RAILS_DEV_PATH) ] elsif options.edge? [ - GemfileEntry.github('rails', 'rails/rails'), - GemfileEntry.github('sprockets-rails', 'rails/sprockets-rails'), - GemfileEntry.github('arel', 'rails/arel') + GemfileEntry.github("rails", "rails/rails") ] else - [GemfileEntry.version('rails', - Rails::VERSION::STRING, + [GemfileEntry.version("rails", + rails_version_specifier, "Bundle edge Rails instead: gem 'rails', github: 'rails/rails'")] end end + def rails_version_specifier(gem_version = Rails.gem_version) + if gem_version.segments.size == 3 || gem_version.release.segments.size == 3 + # ~> 1.2.3 + # ~> 1.2.3.pre4 + "~> #{gem_version}" + else + # ~> 1.2.3, >= 1.2.3.4 + # ~> 1.2.3, >= 1.2.3.4.pre5 + patch = gem_version.segments[0, 3].join(".") + ["~> #{patch}", ">= #{gem_version}"] + end + end + def gem_for_database - # %w( mysql oracle postgresql sqlite3 frontbase ibm_db sqlserver jdbcmysql jdbcsqlite3 jdbcpostgresql ) + # %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver jdbcmysql jdbcsqlite3 jdbcpostgresql ) case options[:database] - when "oracle" then "ruby-oci8" - when "postgresql" then "pg" - when "frontbase" then "ruby-frontbase" - when "mysql" then "mysql2" - when "sqlserver" then "activerecord-sqlserver-adapter" - when "jdbcmysql" then "activerecord-jdbcmysql-adapter" - when "jdbcsqlite3" then "activerecord-jdbcsqlite3-adapter" - when "jdbcpostgresql" then "activerecord-jdbcpostgresql-adapter" - when "jdbc" then "activerecord-jdbc-adapter" - else options[:database] + when "mysql" then ["mysql2", [">= 0.4.4"]] + when "postgresql" then ["pg", [">= 0.18", "< 2.0"]] + when "oracle" then ["activerecord-oracle_enhanced-adapter", nil] + when "frontbase" then ["ruby-frontbase", nil] + when "sqlserver" then ["activerecord-sqlserver-adapter", nil] + when "jdbcmysql" then ["activerecord-jdbcmysql-adapter", nil] + when "jdbcsqlite3" then ["activerecord-jdbcsqlite3-adapter", nil] + when "jdbcpostgresql" then ["activerecord-jdbcpostgresql-adapter", nil] + when "jdbc" then ["activerecord-jdbc-adapter", nil] + else [options[:database], nil] end end def convert_database_option_for_jruby if defined?(JRUBY_VERSION) - case options[:database] - when "oracle" then options[:database].replace "jdbc" - when "postgresql" then options[:database].replace "jdbcpostgresql" - when "mysql" then options[:database].replace "jdbcmysql" - when "sqlite3" then options[:database].replace "jdbcsqlite3" + opt = options.dup + case opt[:database] + when "postgresql" then opt[:database] = "jdbcpostgresql" + when "mysql" then opt[:database] = "jdbcmysql" + when "sqlite3" then opt[:database] = "jdbcsqlite3" end + self.options = opt.freeze end end def assets_gemfile_entry return [] if options[:skip_sprockets] - gems = [] - gems << GemfileEntry.version('sass-rails', '~> 5.0', - 'Use SCSS for stylesheets') - - gems << GemfileEntry.version('uglifier', - '>= 1.3.0', - 'Use Uglifier as compressor for JavaScript assets') - - gems + GemfileEntry.version("sass-rails", "~> 5.0", "Use SCSS for stylesheets") end - def jbuilder_gemfile_entry - return [] if options[:api] - - comment = 'Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder' - GemfileEntry.version('jbuilder', '~> 2.0', comment) - end + def webpacker_gemfile_entry + return [] if options[:skip_javascript] - def coffee_gemfile_entry - comment = 'Use CoffeeScript for .coffee assets and views' if options.dev? || options.edge? - GemfileEntry.github 'coffee-rails', 'rails/coffee-rails', nil, comment + GemfileEntry.github "webpacker", "rails/webpacker", nil, "Use development version of Webpacker" else - GemfileEntry.version 'coffee-rails', '~> 4.1.0', comment + GemfileEntry.new "webpacker", nil, "Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker" end end - def javascript_gemfile_entry - if options[:skip_javascript] - [] - else - gems = [coffee_gemfile_entry, javascript_runtime_gemfile_entry] - gems << GemfileEntry.version("#{options[:javascript]}-rails", nil, - "Use #{options[:javascript]} as the JavaScript library") - - unless options[:skip_turbolinks] - gems << GemfileEntry.version("turbolinks", nil, - "Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks") - end - - gems - end + def jbuilder_gemfile_entry + comment = "Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder" + GemfileEntry.new "jbuilder", "~> 2.5", comment, {}, options[:api] end - def javascript_runtime_gemfile_entry - comment = 'See https://github.com/rails/execjs#readme for more supported runtimes' - if defined?(JRUBY_VERSION) - GemfileEntry.version 'therubyrhino', nil, comment + def javascript_gemfile_entry + if options[:skip_javascript] || options[:skip_turbolinks] + [] else - GemfileEntry.new 'therubyracer', nil, comment, { platforms: :ruby }, true + [ GemfileEntry.version("turbolinks", "~> 5", + "Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks") ] end end def psych_gemfile_entry return [] unless defined?(Rubinius) - comment = 'Use Psych as the YAML engine, instead of Syck, so serialized ' \ - 'data can be read safely from different rubies (see http://git.io/uuLVag)' - GemfileEntry.new('psych', '~> 2.0', comment, platforms: :rbx) + comment = "Use Psych as the YAML engine, instead of Syck, so serialized " \ + "data can be read safely from different rubies (see http://git.io/uuLVag)" + GemfileEntry.new("psych", "~> 2.0", comment, platforms: :rbx) + end + + def cable_gemfile_entry + return [] if options[:skip_action_cable] + comment = "Use Redis adapter to run Action Cable in production" + gems = [] + gems << GemfileEntry.new("redis", "~> 4.0", comment, {}, true) + gems end def bundle_command(command) @@ -326,9 +375,9 @@ module Rails # We unset temporary bundler variables to load proper bundler and Gemfile. # # Thanks to James Tucker for the Gem tricks involved in this call. - _bundle_command = Gem.bin_path('bundler', 'bundle') + _bundle_command = Gem.bin_path("bundler", "bundle") - require 'bundler' + require "bundler" Bundler.with_clean_env do full_command = %Q["#{Gem.ruby}" "#{_bundle_command}" #{command}] if options[:quiet] @@ -347,8 +396,41 @@ module Rails !options[:skip_spring] && !options.dev? && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin") end + def webpack_install? + !(options[:skip_javascript] || options[:skip_webpack_install]) + end + + def depends_on_system_test? + !(options[:skip_system_test] || options[:skip_test] || options[:api]) + end + + def depend_on_listen? + !options[:skip_listen] && os_supports_listen_out_of_the_box? + end + + def depend_on_bootsnap? + !options[:skip_bootsnap] && !options[:dev] && !defined?(JRUBY_VERSION) + end + + def os_supports_listen_out_of_the_box? + RbConfig::CONFIG["host_os"] =~ /darwin|linux/ + end + def run_bundle - bundle_command('install') if bundle_install? + bundle_command("install") if bundle_install? + end + + def run_webpack + if webpack_install? + rails_command "webpacker:install" + rails_command "webpacker:install:#{options[:webpack]}" if options[:webpack] && options[:webpack] != "webpack" + end + end + + def generate_bundler_binstub + if bundle_install? + bundle_command("binstubs bundler") + end end def generate_spring_binstubs diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index 813b8b629e..5523a3f659 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + begin - require 'thor/group' + require "thor/group" rescue LoadError puts "Thor is not available.\nIf you ran this command from a git checkout " \ "of Rails, please make sure thor is installed,\nand run this command " \ @@ -16,18 +18,21 @@ module Rails include Thor::Actions include Rails::Generators::Actions + class_option :skip_namespace, type: :boolean, default: false, + desc: "Skip namespace (affects only isolated applications)" + add_runtime_options! strict_args_position! # Returns the source root for this generator using default_source_root as default. - def self.source_root(path=nil) + def self.source_root(path = nil) @_source_root = path if path @_source_root ||= default_source_root end # Tries to get the description from a USAGE file one folder above the source # root otherwise uses a default description. - def self.desc(description=nil) + def self.desc(description = nil) return super if description @desc ||= if usage_path @@ -40,15 +45,15 @@ module Rails # Convenience method to get the namespace from the class name. It's the # same as Thor default except that the Generator at the end of the class # is removed. - def self.namespace(name=nil) + def self.namespace(name = nil) return super if name - @namespace ||= super.sub(/_generator$/, '').sub(/:generators:/, ':') + @namespace ||= super.sub(/_generator$/, "").sub(/:generators:/, ":") end # Convenience method to hide this generator from the available ones when # running rails generator command. def self.hide! - Rails::Generators.hide_namespace self.namespace + Rails::Generators.hide_namespace(namespace) end # Invoke a generator based on the value supplied by the user to the @@ -103,12 +108,12 @@ module Rails # hook_for :test_framework, as: :controller # end # - # And now it will lookup at: + # And now it will look up at: # # "test_unit:controller", "test_unit" # - # Similarly, if you want it to also lookup in the rails namespace, you just - # need to provide the :in value: + # Similarly, if you want it to also look up in the rails namespace, you + # just need to provide the :in value: # # class AwesomeGenerator < Rails::Generators::Base # hook_for :test_framework, in: :rails, as: :controller @@ -168,7 +173,7 @@ module Rails names.each do |name| unless class_options.key?(name) defaults = if options[:type] == :boolean - { } + {} elsif [true, false].include?(default_value_for_option(name, options)) { banner: "" } else @@ -195,7 +200,7 @@ module Rails end # Make class option aware of Rails::Generators.options and Rails::Generators.aliases. - def self.class_option(name, options={}) #:nodoc: + def self.class_option(name, options = {}) #:nodoc: options[:desc] = "Indicates when to generate #{name.to_s.humanize.downcase}" unless options.key?(:desc) options[:aliases] = default_aliases_for_option(name, options) options[:default] = default_value_for_option(name, options) @@ -208,14 +213,14 @@ module Rails def self.default_source_root return unless base_name && generator_name return unless default_generator_root - path = File.join(default_generator_root, 'templates') + path = File.join(default_generator_root, "templates") path if File.exist?(path) end # Returns the base root for a common set of generators. This is used to dynamically # guess the default source root. def self.base_root - File.dirname(__FILE__) + __dir__ end # Cache source root and add lib/generators/base/generator/templates to @@ -230,7 +235,7 @@ module Rails Rails::Generators.subclasses << base Rails::Generators.templates_path.each do |path| - if base.name.include?('::') + if base.name.include?("::") base.source_paths << File.join(path, base.base_name, base.generator_name) else base.source_paths << File.join(path, base.generator_name) @@ -239,11 +244,11 @@ module Rails end end - protected + private # Check whether the given class names are already taken by user # application or Ruby on Rails. - def class_collisions(*class_names) #:nodoc: + def class_collisions(*class_names) return unless behavior == :invoke class_names.flatten.each do |class_name| @@ -251,35 +256,69 @@ module Rails next if class_name.strip.empty? # Split the class from its module nesting - nesting = class_name.split('::') + nesting = class_name.split("::") last_name = nesting.pop last = extract_last_module(nesting) if last && last.const_defined?(last_name.camelize, false) - raise Error, "The name '#{class_name}' is either already used in your application " << - "or reserved by Ruby on Rails. Please choose an alternative and run " << + raise Error, "The name '#{class_name}' is either already used in your application " \ + "or reserved by Ruby on Rails. Please choose an alternative and run " \ "this generator again." end end end # Takes in an array of nested modules and extracts the last module - def extract_last_module(nesting) + def extract_last_module(nesting) # :doc: nesting.inject(Object) do |last_module, nest| break unless last_module.const_defined?(nest, false) last_module.const_get(nest) end end + # Wrap block with namespace of current application + # if namespace exists and is not skipped + def module_namespacing(&block) # :doc: + content = capture(&block) + content = wrap_with_namespace(content) if namespaced? + concat(content) + end + + def indent(content, multiplier = 2) # :doc: + spaces = " " * multiplier + content.each_line.map { |line| line.blank? ? line : "#{spaces}#{line}" }.join + end + + def wrap_with_namespace(content) # :doc: + content = indent(content).chomp + "module #{namespace.name}\n#{content}\nend\n" + end + + def namespace # :doc: + Rails::Generators.namespace + end + + def namespaced? # :doc: + !options[:skip_namespace] && namespace + end + + def namespace_dirs + @namespace_dirs ||= namespace.name.split("::").map(&:underscore) + end + + def namespaced_path # :doc: + @namespaced_path ||= namespace_dirs.join("/") + end + # Use Rails default banner. - def self.banner - "rails generate #{namespace.sub(/^rails:/,'')} #{self.arguments.map(&:usage).join(' ')} [options]".gsub(/\s+/, ' ') + def self.banner # :doc: + "rails generate #{namespace.sub(/^rails:/, '')} #{arguments.map(&:usage).join(' ')} [options]".gsub(/\s+/, " ") end # Sets the base_name taking into account the current class namespace. - def self.base_name + def self.base_name # :doc: @base_name ||= begin - if base = name.to_s.split('::').first + if base = name.to_s.split("::").first base.underscore end end @@ -287,10 +326,10 @@ module Rails # Removes the namespaces and get the generator name. For example, # Rails::Generators::ModelGenerator will return "model" as generator name. - def self.generator_name + def self.generator_name # :doc: @generator_name ||= begin - if generator = name.to_s.split('::').last - generator.sub!(/Generator$/, '') + if generator = name.to_s.split("::").last + generator.sub!(/Generator$/, "") generator.underscore end end @@ -298,21 +337,21 @@ module Rails # Returns the default value for the option name given doing a lookup in # Rails::Generators.options. - def self.default_value_for_option(name, options) + def self.default_value_for_option(name, options) # :doc: default_for_option(Rails::Generators.options, name, options, options[:default]) end - # Return default aliases for the option name given doing a lookup in + # Returns default aliases for the option name given doing a lookup in # Rails::Generators.aliases. - def self.default_aliases_for_option(name, options) + def self.default_aliases_for_option(name, options) # :doc: default_for_option(Rails::Generators.aliases, name, options, options[:aliases]) end - # Return default for the option name given doing a lookup in config. - def self.default_for_option(config, name, options, default) - if generator_name and c = config[generator_name.to_sym] and c.key?(name) + # Returns default for the option name given doing a lookup in config. + def self.default_for_option(config, name, options, default) # :doc: + if generator_name && (c = config[generator_name.to_sym]) && c.key?(name) c[name] - elsif base_name and c = config[base_name.to_sym] and c.key?(name) + elsif base_name && (c = config[base_name.to_sym]) && c.key?(name) c[name] elsif config[:rails].key?(name) config[:rails][name] @@ -331,7 +370,7 @@ module Rails def self.prepare_for_invocation(name, value) #:nodoc: return super unless value.is_a?(String) || value.is_a?(Symbol) - if value && constants = self.hooks[name] + if value && constants = hooks[name] value = name if TrueClass === value Rails::Generators.find_by_namespace(value, *constants) elsif klass = Rails::Generators.find_by_namespace(value) @@ -343,7 +382,7 @@ module Rails # Small macro to add ruby as an option to the generator with proper # default value plus an instance helper method called shebang. - def self.add_shebang_option! + def self.add_shebang_option! # :doc: class_option :ruby, type: :string, aliases: "-r", default: Thor::Util.ruby_command, desc: "Path to the Ruby binary of your choice", banner: "PATH" @@ -361,7 +400,7 @@ module Rails } end - def self.usage_path + def self.usage_path # :doc: paths = [ source_root && File.expand_path("../USAGE", source_root), default_generator_root && File.join(default_generator_root, "USAGE") @@ -369,11 +408,10 @@ module Rails paths.compact.detect { |path| File.exist? path } end - def self.default_generator_root + def self.default_generator_root # :doc: path = File.expand_path(File.join(base_name, generator_name), base_root) path if File.exist?(path) end - end end end diff --git a/railties/lib/rails/generators/css/assets/assets_generator.rb b/railties/lib/rails/generators/css/assets/assets_generator.rb index e4a305f4b3..f657d1e50f 100644 --- a/railties/lib/rails/generators/css/assets/assets_generator.rb +++ b/railties/lib/rails/generators/css/assets/assets_generator.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + require "rails/generators/named_base" module Css # :nodoc: module Generators # :nodoc: class AssetsGenerator < Rails::Generators::NamedBase # :nodoc: - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) def copy_stylesheet - copy_file "stylesheet.css", File.join('app/assets/stylesheets', class_path, "#{file_name}.css") + copy_file "stylesheet.css", File.join("app/assets/stylesheets", class_path, "#{file_name}.css") end end end diff --git a/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb index cf534030f9..89c560f382 100644 --- a/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/css/scaffold/scaffold_generator.rb @@ -1,15 +1,17 @@ +# frozen_string_literal: true + require "rails/generators/named_base" module Css # :nodoc: module Generators # :nodoc: class ScaffoldGenerator < Rails::Generators::NamedBase # :nodoc: + source_root Rails::Generators::ScaffoldGenerator.source_root + # In order to allow the Sass generators to pick up the default Rails CSS and # transform it, we leave it in a standard location for the CSS stylesheet # generators to handle. For the simple, default case, just copy it over. def copy_stylesheet - dir = Rails::Generators::ScaffoldGenerator.source_root - file = File.join(dir, "scaffold.css") - create_file "app/assets/stylesheets/scaffold.css", File.read(file) + copy_file "scaffold.css", "app/assets/stylesheets/scaffold.css" end end end diff --git a/railties/lib/rails/generators/erb.rb b/railties/lib/rails/generators/erb.rb index 0755ac335c..ba20bcd32a 100644 --- a/railties/lib/rails/generators/erb.rb +++ b/railties/lib/rails/generators/erb.rb @@ -1,25 +1,27 @@ -require 'rails/generators/named_base' +# frozen_string_literal: true + +require "rails/generators/named_base" module Erb # :nodoc: module Generators # :nodoc: class Base < Rails::Generators::NamedBase #:nodoc: - protected + private - def formats - [format] - end + def formats + [format] + end - def format - :html - end + def format + :html + end - def handler - :erb - end + def handler + :erb + end - def filename_with_extensions(name, format = self.format) - [name, format, handler].compact.join(".") - end + def filename_with_extensions(name, file_format = format) + [name, file_format, handler].compact.join(".") + end end end end diff --git a/railties/lib/rails/generators/erb/controller/controller_generator.rb b/railties/lib/rails/generators/erb/controller/controller_generator.rb index 94c1b835d1..8e13744b2a 100644 --- a/railties/lib/rails/generators/erb/controller/controller_generator.rb +++ b/railties/lib/rails/generators/erb/controller/controller_generator.rb @@ -1,4 +1,6 @@ -require 'rails/generators/erb' +# frozen_string_literal: true + +require "rails/generators/erb" module Erb # :nodoc: module Generators # :nodoc: diff --git a/railties/lib/rails/generators/erb/controller/templates/view.html.erb b/railties/lib/rails/generators/erb/controller/templates/view.html.erb.tt index cd54d13d83..cd54d13d83 100644 --- a/railties/lib/rails/generators/erb/controller/templates/view.html.erb +++ b/railties/lib/rails/generators/erb/controller/templates/view.html.erb.tt diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb index 65563aa6db..997602cb8c 100644 --- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb @@ -1,4 +1,6 @@ -require 'rails/generators/erb' +# frozen_string_literal: true + +require "rails/generators/erb" module Erb # :nodoc: module Generators # :nodoc: @@ -6,13 +8,13 @@ module Erb # :nodoc: argument :actions, type: :array, default: [], banner: "method method" def copy_view_files - view_base_path = File.join("app/views", class_path, file_name + '_mailer') + view_base_path = File.join("app/views", class_path, file_name + "_mailer") empty_directory view_base_path - if self.behavior == :invoke + if behavior == :invoke formats.each do |format| - layout_path = File.join("app/views/layouts", filename_with_extensions("mailer", format)) - template filename_with_extensions(:layout, format), layout_path + layout_path = File.join("app/views/layouts", class_path, filename_with_extensions("mailer", format)) + template filename_with_extensions(:layout, format), layout_path unless File.exist?(layout_path) end end @@ -26,15 +28,15 @@ module Erb # :nodoc: end end - protected + private - def formats - [:text, :html] - end + def formats + [:text, :html] + end - def file_name - @_file_name ||= super.gsub(/\_mailer/i, '') - end + def file_name + @_file_name ||= super.sub(/_mailer\z/i, "") + end end end end diff --git a/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb b/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb deleted file mode 100644 index 93110e74ad..0000000000 --- a/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<html> - <body> - <%%= yield %> - </body> -</html> diff --git a/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb.tt b/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb.tt new file mode 100644 index 0000000000..55f3675d49 --- /dev/null +++ b/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb.tt @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <style> + /* Email styles need to be inline */ + </style> + </head> + + <body> + <%%= yield %> + </body> +</html> diff --git a/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb b/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb.tt index 6363733e6e..6363733e6e 100644 --- a/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb +++ b/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb.tt diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.html.erb b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb.tt index b5045671b3..b5045671b3 100644 --- a/railties/lib/rails/generators/erb/mailer/templates/view.html.erb +++ b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb.tt diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.text.erb b/railties/lib/rails/generators/erb/mailer/templates/view.text.erb.tt index 342285df19..342285df19 100644 --- a/railties/lib/rails/generators/erb/mailer/templates/view.text.erb +++ b/railties/lib/rails/generators/erb/mailer/templates/view.text.erb.tt diff --git a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb index c94829a0ae..2fc04e4094 100644 --- a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb @@ -1,5 +1,7 @@ -require 'rails/generators/erb' -require 'rails/generators/resource_helpers' +# frozen_string_literal: true + +require "rails/generators/erb" +require "rails/generators/resource_helpers" module Erb # :nodoc: module Generators # :nodoc: @@ -21,7 +23,7 @@ module Erb # :nodoc: end end - protected + private def available_views %w(index edit show new _form) diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt index d99b27cb2d..518cb1121e 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt @@ -1,4 +1,4 @@ -<%%= form_for(<%= singular_table_name %>) do |f| %> +<%%= form_with(model: <%= model_resource_name %>, local: true) do |form| %> <%% if <%= singular_table_name %>.errors.any? %> <div id="error_explanation"> <h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2> @@ -14,21 +14,21 @@ <% attributes.each do |attribute| -%> <div class="field"> <% if attribute.password_digest? -%> - <%%= f.label :password %><br> - <%%= f.password_field :password %> + <%%= form.label :password %> + <%%= form.password_field :password %> </div> <div class="field"> - <%%= f.label :password_confirmation %><br> - <%%= f.password_field :password_confirmation %> + <%%= form.label :password_confirmation %> + <%%= form.password_field :password_confirmation %> <% else -%> - <%%= f.label :<%= attribute.column_name %> %><br> - <%%= f.<%= attribute.field_type %> :<%= attribute.column_name %> %> + <%%= form.label :<%= attribute.column_name %> %> + <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %> <% end -%> </div> <% end -%> <div class="actions"> - <%%= f.submit %> + <%%= form.submit %> </div> <%% end %> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb.tt index 81329473d9..81329473d9 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb.tt diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt index c3b8ef1181..2cf4e5c9d0 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt @@ -16,11 +16,11 @@ <%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %> <tr> <% attributes.reject(&:password_digest?).each do |attribute| -%> - <td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td> + <td><%%= <%= singular_table_name %>.<%= attribute.column_name %> %></td> <% end -%> - <td><%%= link_to 'Show', <%= singular_table_name %> %></td> - <td><%%= link_to 'Edit', edit_<%= singular_table_name %>_path(<%= singular_table_name %>) %></td> - <td><%%= link_to 'Destroy', <%= singular_table_name %>, method: :delete, data: { confirm: 'Are you sure?' } %></td> + <td><%%= link_to 'Show', <%= model_resource_name %> %></td> + <td><%%= link_to 'Edit', edit_<%= singular_route_name %>_path(<%= singular_table_name %>) %></td> + <td><%%= link_to 'Destroy', <%= model_resource_name %>, method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr> <%% end %> </tbody> @@ -28,4 +28,4 @@ <br> -<%%= link_to 'New <%= human_name %>', new_<%= singular_table_name %>_path %> +<%%= link_to 'New <%= singular_table_name.titleize %>', new_<%= singular_route_name %>_path %> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb.tt index 9b2b2f4875..9b2b2f4875 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb.tt diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt index 5e634153be..7deba07926 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt @@ -3,7 +3,7 @@ <% attributes.reject(&:password_digest?).each do |attribute| -%> <p> <strong><%= attribute.human_name %>:</strong> - <%%= @<%= singular_table_name %>.<%= attribute.name %> %> + <%%= @<%= singular_table_name %>.<%= attribute.column_name %> %> </p> <% end -%> diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb index 8145a26e22..3f20f5a718 100644 --- a/railties/lib/rails/generators/generated_attribute.rb +++ b/railties/lib/rails/generators/generated_attribute.rb @@ -1,4 +1,6 @@ -require 'active_support/time' +# frozen_string_literal: true + +require "active_support/time" module Rails module Generators @@ -12,7 +14,7 @@ module Rails class << self def parse(column_definition) - name, type, has_index = column_definition.split(':') + name, type, has_index = column_definition.split(":") # if user provided "name:index" instead of "name:string:index" # type should be set blank so GeneratedAttribute's constructor @@ -23,8 +25,9 @@ module Rails type = type.to_sym if type if type && reference?(type) - references_index = UNIQ_INDEX_OPTIONS.include?(has_index) ? { unique: true } : true - attr_options[:index] = references_index + if UNIQ_INDEX_OPTIONS.include?(has_index) + attr_options[:index] = { unique: true } + end end new(name, type, has_index, attr_options) @@ -55,7 +58,7 @@ module Rails end end - def initialize(name, type=nil, index_type=false, attr_options={}) + def initialize(name, type = nil, index_type = false, attr_options = {}) @name = name @type = type || :string @has_index = INDEX_OPTIONS.include?(index_type) @@ -65,40 +68,40 @@ module Rails def field_type @field_type ||= case type - when :integer then :number_field - when :float, :decimal then :text_field - when :time then :time_select - when :datetime, :timestamp then :datetime_select - when :date then :date_select - when :text then :text_area - when :boolean then :check_box - else - :text_field + when :integer then :number_field + when :float, :decimal then :text_field + when :time then :time_select + when :datetime, :timestamp then :datetime_select + when :date then :date_select + when :text then :text_area + when :boolean then :check_box + else + :text_field end end def default @default ||= case type - when :integer then 1 - when :float then 1.5 - when :decimal then "9.99" - when :datetime, :timestamp, :time then Time.now.to_s(:db) - when :date then Date.today.to_s(:db) - when :string then name == "type" ? "" : "MyString" - when :text then "MyText" - when :boolean then false - when :references, :belongs_to then nil - else - "" + when :integer then 1 + when :float then 1.5 + when :decimal then "9.99" + when :datetime, :timestamp, :time then Time.now.to_s(:db) + when :date then Date.today.to_s(:db) + when :string then name == "type" ? "" : "MyString" + when :text then "MyText" + when :boolean then false + when :references, :belongs_to then nil + else + "" end end def plural_name - name.sub(/_id$/, '').pluralize + name.sub(/_id$/, "").pluralize end def singular_name - name.sub(/_id$/, '').singularize + name.sub(/_id$/, "").singularize end def human_name @@ -126,11 +129,11 @@ module Rails end def polymorphic? - self.attr_options[:polymorphic] + attr_options[:polymorphic] end def required? - self.attr_options[:required] + attr_options[:required] end def has_index? @@ -142,7 +145,7 @@ module Rails end def password_digest? - name == 'password' && type == :digest + name == "password" && type == :digest end def token? @@ -150,7 +153,7 @@ module Rails end def inject_options - "".tap { |s| options_for_migration.each { |k,v| s << ", #{k}: #{v.inspect}" } } + (+"").tap { |s| options_for_migration.each { |k, v| s << ", #{k}: #{v.inspect}" } } end def inject_index_options diff --git a/railties/lib/rails/generators/js/assets/assets_generator.rb b/railties/lib/rails/generators/js/assets/assets_generator.rb deleted file mode 100644 index 1e925b2cd2..0000000000 --- a/railties/lib/rails/generators/js/assets/assets_generator.rb +++ /dev/null @@ -1,13 +0,0 @@ -require "rails/generators/named_base" - -module Js # :nodoc: - module Generators # :nodoc: - class AssetsGenerator < Rails::Generators::NamedBase # :nodoc: - source_root File.expand_path("../templates", __FILE__) - - def copy_javascript - copy_file "javascript.js", File.join('app/assets/javascripts', class_path, "#{file_name}.js") - end - end - end -end diff --git a/railties/lib/rails/generators/js/assets/templates/javascript.js b/railties/lib/rails/generators/js/assets/templates/javascript.js deleted file mode 100644 index dee720facd..0000000000 --- a/railties/lib/rails/generators/js/assets/templates/javascript.js +++ /dev/null @@ -1,2 +0,0 @@ -// Place all the behaviors and hooks related to the matching controller here. -// All this logic will automatically be available in application.js. diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb index 51e6d68bf0..5081060895 100644 --- a/railties/lib/rails/generators/migration.rb +++ b/railties/lib/rails/generators/migration.rb @@ -1,5 +1,7 @@ -require 'active_support/concern' -require 'rails/generators/actions/create_migration' +# frozen_string_literal: true + +require "active_support/concern" +require "rails/generators/actions/create_migration" module Rails module Generators @@ -10,22 +12,22 @@ module Rails extend ActiveSupport::Concern attr_reader :migration_number, :migration_file_name, :migration_class_name - module ClassMethods - def migration_lookup_at(dirname) #:nodoc: + module ClassMethods #:nodoc: + def migration_lookup_at(dirname) Dir.glob("#{dirname}/[0-9]*_*.rb") end - def migration_exists?(dirname, file_name) #:nodoc: + def migration_exists?(dirname, file_name) migration_lookup_at(dirname).grep(/\d+_#{file_name}.rb$/).first end - def current_migration_number(dirname) #:nodoc: + def current_migration_number(dirname) migration_lookup_at(dirname).collect do |file| File.basename(file).split("_").first.to_i end.max.to_i end - def next_migration_number(dirname) #:nodoc: + def next_migration_number(dirname) raise NotImplementedError end end @@ -35,11 +37,11 @@ module Rails end def set_migration_assigns!(destination) - destination = File.expand_path(destination, self.destination_root) + destination = File.expand_path(destination, destination_root) migration_dir = File.dirname(destination) @migration_number = self.class.next_migration_number(migration_dir) - @migration_file_name = File.basename(destination, '.rb') + @migration_file_name = File.basename(destination, ".rb") @migration_class_name = @migration_file_name.camelize end @@ -52,16 +54,21 @@ module Rails # # migration_template "migration.rb", "db/migrate/add_foo_to_bar.rb" def migration_template(source, destination, config = {}) - source = File.expand_path(find_in_source_paths(source.to_s)) + source = File.expand_path(find_in_source_paths(source.to_s)) set_migration_assigns!(destination) - context = instance_eval('binding') + context = instance_eval("binding") dir, base = File.split(destination) - numbered_destination = File.join(dir, ["%migration_number%", base].join('_')) + numbered_destination = File.join(dir, ["%migration_number%", base].join("_")) create_migration numbered_destination, nil, config do - ERB.new(::File.binread(source), nil, '-', '@output_buffer').result(context) + match = ERB.version.match(/\Aerb\.rb \[(?<version>[^ ]+) /) + if match && match[:version] >= "2.2.0" # Ruby 2.6+ + ERB.new(::File.binread(source), trim_mode: "-", eoutvar: "@output_buffer").result(context) + else + ERB.new(::File.binread(source), nil, "-", "@output_buffer").result(context) + end end end end diff --git a/railties/lib/rails/generators/model_helpers.rb b/railties/lib/rails/generators/model_helpers.rb index 42c646543e..3676432d5c 100644 --- a/railties/lib/rails/generators/model_helpers.rb +++ b/railties/lib/rails/generators/model_helpers.rb @@ -1,14 +1,20 @@ -require 'rails/generators/active_model' +# frozen_string_literal: true + +require "rails/generators/active_model" module Rails module Generators module ModelHelpers # :nodoc: PLURAL_MODEL_NAME_WARN_MESSAGE = "[WARNING] The model name '%s' was recognized as a plural, using the singular '%s' instead. " \ "Override with --force-plural or setup custom inflection rules for this noun before running the generator." + IRREGULAR_MODEL_NAME_WARN_MESSAGE = <<~WARNING + [WARNING] Rails cannot recover singular form from its plural form '%s'. + Please setup custom inflection rules for this noun before running the generator in config/initializers/inflections.rb. + WARNING mattr_accessor :skip_warn def self.included(base) #:nodoc: - base.class_option :force_plural, type: :boolean, default: false, desc: 'Forces the use of the given model name' + base.class_option :force_plural, type: :boolean, default: false, desc: "Forces the use of the given model name" end def initialize(args, *_options) @@ -17,11 +23,14 @@ module Rails singular = name.singularize unless ModelHelpers.skip_warn say PLURAL_MODEL_NAME_WARN_MESSAGE % [name, singular] - ModelHelpers.skip_warn = true end name.replace singular assign_names!(name) end + if name.singularize != name.pluralize.singularize && ! ModelHelpers.skip_warn + say IRREGULAR_MODEL_NAME_WARN_MESSAGE % [name.pluralize] + end + ModelHelpers.skip_warn = true end end end diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index 243694f38e..d6732f8ff1 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -1,20 +1,19 @@ -require 'active_support/core_ext/module/introspection' -require 'rails/generators/base' -require 'rails/generators/generated_attribute' +# frozen_string_literal: true + +require "rails/generators/base" +require "rails/generators/generated_attribute" module Rails module Generators class NamedBase < Base argument :name, type: :string - class_option :skip_namespace, type: :boolean, default: false, - desc: "Skip namespace (affects only isolated applications)" def initialize(args, *options) #:nodoc: @inside_template = nil # Unfreeze name in case it's given as a frozen string args[0] = args[0].dup if args[0].is_a?(String) && args[0].frozen? super - assign_names!(self.name) + assign_names!(name) parse_attributes! if respond_to?(:attributes) end @@ -26,164 +25,179 @@ module Rails super end end + + def js_template(source, destination) + template(source + ".js", destination + ".js") + end end - protected + private attr_reader :file_name # FIXME: We are avoiding to use alias because a bug on thor that make # this method public and add it to the task list. - def singular_name + def singular_name # :doc: file_name end - # Wrap block with namespace of current application - # if namespace exists and is not skipped - def module_namespacing(&block) - content = capture(&block) - content = wrap_with_namespace(content) if namespaced? - concat(content) - end - - def indent(content, multiplier = 2) - spaces = " " * multiplier - content.each_line.map {|line| line.blank? ? line : "#{spaces}#{line}" }.join - end - - def wrap_with_namespace(content) - content = indent(content).chomp - "module #{namespace.name}\n#{content}\nend\n" - end - - def inside_template + def inside_template # :doc: @inside_template = true yield ensure @inside_template = false end - def inside_template? + def inside_template? # :doc: @inside_template end - def namespace - Rails::Generators.namespace - end - - def namespaced? - !options[:skip_namespace] && namespace - end - - def file_path - @file_path ||= (class_path + [file_name]).join('/') + def file_path # :doc: + @file_path ||= (class_path + [file_name]).join("/") end - def class_path + def class_path # :doc: inside_template? || !namespaced? ? regular_class_path : namespaced_class_path end - def regular_class_path + def regular_class_path # :doc: @class_path end - def namespaced_file_path - @namespaced_file_path ||= namespaced_class_path.join("/") - end - - def namespaced_class_path - @namespaced_class_path ||= [namespaced_path] + @class_path + def namespaced_class_path # :doc: + @namespaced_class_path ||= namespace_dirs + @class_path end - def namespaced_path - @namespaced_path ||= namespace.name.split("::").first.underscore + def class_name # :doc: + (class_path + [file_name]).map!(&:camelize).join("::") end - def class_name - (class_path + [file_name]).map!(&:camelize).join('::') - end - - def human_name + def human_name # :doc: @human_name ||= singular_name.humanize end - def plural_name + def plural_name # :doc: @plural_name ||= singular_name.pluralize end - def i18n_scope - @i18n_scope ||= file_path.tr('/', '.') + def i18n_scope # :doc: + @i18n_scope ||= file_path.tr("/", ".") end - def table_name + def table_name # :doc: @table_name ||= begin base = pluralize_table_names? ? plural_name : singular_name - (class_path + [base]).join('_') + (class_path + [base]).join("_") end end - def uncountable? + def uncountable? # :doc: singular_name == plural_name end - def index_helper - uncountable? ? "#{plural_table_name}_index" : plural_table_name + def index_helper # :doc: + uncountable? ? "#{plural_route_name}_index" : plural_route_name end - def singular_table_name + def show_helper # :doc: + "#{singular_route_name}_url(@#{singular_table_name})" + end + + def edit_helper # :doc: + "edit_#{show_helper}" + end + + def new_helper # :doc: + "new_#{singular_route_name}_url" + end + + def singular_table_name # :doc: @singular_table_name ||= (pluralize_table_names? ? table_name.singularize : table_name) end - def plural_table_name + def plural_table_name # :doc: @plural_table_name ||= (pluralize_table_names? ? table_name : table_name.pluralize) end - def plural_file_name + def plural_file_name # :doc: @plural_file_name ||= file_name.pluralize end - def fixture_file_name + def fixture_file_name # :doc: @fixture_file_name ||= (pluralize_table_names? ? plural_file_name : file_name) end - def route_url - @route_url ||= class_path.collect {|dname| "/" + dname }.join + "/" + plural_file_name + def route_url # :doc: + @route_url ||= class_path.collect { |dname| "/" + dname }.join + "/" + plural_file_name + end + + def url_helper_prefix # :doc: + @url_helper_prefix ||= (class_path + [file_name]).join("_") end # Tries to retrieve the application name or simply return application. - def application_name + def application_name # :doc: if defined?(Rails) && Rails.application - Rails.application.class.name.split('::').first.underscore + Rails.application.class.name.split("::").first.underscore else "application" end end - def assign_names!(name) #:nodoc: - @class_path = name.include?('/') ? name.split('/') : name.split('::') + def redirect_resource_name # :doc: + model_resource_name(prefix: "@") + end + + def model_resource_name(prefix: "") # :doc: + resource_name = "#{prefix}#{singular_table_name}" + if options[:model_name] + "[#{controller_class_path.map { |name| ":" + name }.join(", ")}, #{resource_name}]" + else + resource_name + end + end + + def singular_route_name # :doc: + if options[:model_name] + "#{controller_class_path.join('_')}_#{singular_table_name}" + else + singular_table_name + end + end + + def plural_route_name # :doc: + if options[:model_name] + "#{controller_class_path.join('_')}_#{plural_table_name}" + else + plural_table_name + end + end + + def assign_names!(name) + @class_path = name.include?("/") ? name.split("/") : name.split("::") @class_path.map!(&:underscore) @file_name = @class_path.pop end # Convert attributes array into GeneratedAttribute objects. - def parse_attributes! #:nodoc: + def parse_attributes! self.attributes = (attributes || []).map do |attr| Rails::Generators::GeneratedAttribute.parse(attr) end end - def attributes_names + def attributes_names # :doc: @attributes_names ||= attributes.each_with_object([]) do |a, names| names << a.column_name - names << 'password_confirmation' if a.password_digest? + names << "password_confirmation" if a.password_digest? names << "#{a.name}_type" if a.polymorphic? end end - def pluralize_table_names? + def pluralize_table_names? # :doc: !defined?(ActiveRecord::Base) || ActiveRecord::Base.pluralize_table_names end - def mountable_engine? + def mountable_engine? # :doc: defined?(ENGINE_ROOT) && namespaced? end @@ -197,9 +211,9 @@ module Rails # If the generator is invoked with class name Admin, it will check for # the presence of "AdminDecorator". # - def self.check_class_collision(options={}) + def self.check_class_collision(options = {}) # :doc: define_method :check_class_collision do - name = if self.respond_to?(:controller_class_name) # for ScaffoldBase + name = if respond_to?(:controller_class_name) # for ResourceHelpers controller_class_name else class_name diff --git a/railties/lib/rails/generators/rails/app/USAGE b/railties/lib/rails/generators/rails/app/USAGE index 691095f33f..28df6ebf44 100644 --- a/railties/lib/rails/generators/rails/app/USAGE +++ b/railties/lib/rails/generators/rails/app/USAGE @@ -12,4 +12,3 @@ Example: rails new ~/Code/Ruby/weblog This generates a skeletal Rails installation in ~/Code/Ruby/weblog. - See the README in the newly created application to get going. diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index b813b083f4..33002790d4 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -1,4 +1,6 @@ -require 'rails/generators/app_base' +# frozen_string_literal: true + +require "rails/generators/app_base" module Rails module ActionMethods # :nodoc: @@ -19,7 +21,6 @@ module Rails RUBY end - # TODO: Remove once this is fully in place def method_missing(meth, *args, &block) @generator.send(meth, *args, &block) end @@ -32,6 +33,14 @@ module Rails # This allows you to override entire operations, like the creation of the # Gemfile, README, or JavaScript files, without needing to know exactly # what those operations do so you can create another template action. + # + # class CustomAppBuilder < Rails::AppBuilder + # def test + # @generator.gem "rspec-rails", group: [:development, :test] + # run "bundle install" + # generate "rspec:install" + # end + # end class AppBuilder def rakefile template "Rakefile" @@ -41,6 +50,10 @@ module Rails copy_file "README.md", "README.md" end + def ruby_version + template "ruby-version", ".ruby-version" + end + def gemfile template "Gemfile" end @@ -53,15 +66,23 @@ module Rails template "gitignore", ".gitignore" end + def version_control + if !options[:skip_git] && !options[:pretend] + run "git init", capture: options[:quiet] + end + end + + def package_json + template "package.json" + end + def app - directory 'app' + directory "app" - keep_file 'app/assets/images' - keep_file 'app/mailers' - keep_file 'app/models' + keep_file "app/assets/images" - keep_file 'app/controllers/concerns' - keep_file 'app/models/concerns' + keep_file "app/controllers/concerns" + keep_file "app/models/concerns" end def bin @@ -71,6 +92,14 @@ module Rails chmod "bin", 0755 & ~File.umask, verbose: false end + def bin_when_updating + bin + + if options[:skip_javascript] + remove_file "bin/yarn" + end + end + def config empty_directory "config" @@ -78,7 +107,10 @@ module Rails template "routes.rb" template "application.rb" template "environment.rb" - template "secrets.yml" + template "cable.yml" unless options[:skip_action_cable] + template "puma.rb" unless options[:skip_puma] + template "spring.rb" if spring_install? + template "storage.yml" unless skip_active_storage? directory "environments" directory "initializers" @@ -87,25 +119,64 @@ module Rails end def config_when_updating - cookie_serializer_config_exist = File.exist?('config/initializers/cookies_serializer.rb') - callback_terminator_config_exist = File.exist?('config/initializers/callback_terminator.rb') - active_record_belongs_to_required_by_default_config_exist = File.exist?('config/initializers/active_record_belongs_to_required_by_default.rb') + cookie_serializer_config_exist = File.exist?("config/initializers/cookies_serializer.rb") + action_cable_config_exist = File.exist?("config/cable.yml") + active_storage_config_exist = File.exist?("config/storage.yml") + rack_cors_config_exist = File.exist?("config/initializers/cors.rb") + assets_config_exist = File.exist?("config/initializers/assets.rb") + csp_config_exist = File.exist?("config/initializers/content_security_policy.rb") + + @config_target_version = Rails.application.config.loaded_config_version || "5.0" config - unless callback_terminator_config_exist - remove_file 'config/initializers/callback_terminator.rb' + unless cookie_serializer_config_exist + gsub_file "config/initializers/cookies_serializer.rb", /json(?!,)/, "marshal" + end + + if !options[:skip_action_cable] && !action_cable_config_exist + template "config/cable.yml" end - unless cookie_serializer_config_exist - gsub_file 'config/initializers/cookies_serializer.rb', /json/, 'marshal' + if !skip_active_storage? && !active_storage_config_exist + template "config/storage.yml" end - unless active_record_belongs_to_required_by_default_config_exist - remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb' + if options[:skip_sprockets] && !assets_config_exist + remove_file "config/initializers/assets.rb" + end + + unless rack_cors_config_exist + remove_file "config/initializers/cors.rb" + end + + if options[:api] + unless cookie_serializer_config_exist + remove_file "config/initializers/cookies_serializer.rb" + end + + unless csp_config_exist + remove_file "config/initializers/content_security_policy.rb" + end end end + def master_key + return if options[:pretend] || options[:dummy_app] + + require "rails/generators/rails/master_key/master_key_generator" + master_key_generator = Rails::Generators::MasterKeyGenerator.new([], quiet: options[:quiet], force: options[:force]) + master_key_generator.add_master_key_file_silently + master_key_generator.ignore_master_key_file_silently + end + + def credentials + return if options[:pretend] || options[:dummy_app] + + require "rails/generators/rails/credentials/credentials_generator" + Rails::Generators::CredentialsGenerator.new([], quiet: options[:quiet]).add_credentials_file_silently + end + def database_yml template "config/databases/#{options[:database]}.yml", "config/database.yml" end @@ -115,29 +186,40 @@ module Rails end def lib - empty_directory 'lib' - empty_directory_with_keep_file 'lib/tasks' - empty_directory_with_keep_file 'lib/assets' + empty_directory "lib" + empty_directory_with_keep_file "lib/tasks" + empty_directory_with_keep_file "lib/assets" end def log - empty_directory_with_keep_file 'log' + empty_directory_with_keep_file "log" end def public_directory directory "public", "public", recursive: false end + def storage + empty_directory_with_keep_file "storage" + empty_directory_with_keep_file "tmp/storage" + end + def test - empty_directory_with_keep_file 'test/fixtures' - empty_directory_with_keep_file 'test/fixtures/files' - empty_directory_with_keep_file 'test/controllers' - empty_directory_with_keep_file 'test/mailers' - empty_directory_with_keep_file 'test/models' - empty_directory_with_keep_file 'test/helpers' - empty_directory_with_keep_file 'test/integration' - - template 'test/test_helper.rb' + empty_directory_with_keep_file "test/fixtures" + empty_directory_with_keep_file "test/fixtures/files" + empty_directory_with_keep_file "test/controllers" + empty_directory_with_keep_file "test/mailers" + empty_directory_with_keep_file "test/models" + empty_directory_with_keep_file "test/helpers" + empty_directory_with_keep_file "test/integration" + + template "test/test_helper.rb" + end + + def system_test + empty_directory_with_keep_file "test/system" + + template "test/application_system_test_case.rb" end def tmp @@ -147,51 +229,53 @@ module Rails end def vendor - vendor_javascripts - vendor_stylesheets + empty_directory_with_keep_file "vendor" end - def vendor_javascripts - unless options[:skip_javascript] - empty_directory_with_keep_file 'vendor/assets/javascripts' - end - end - - def vendor_stylesheets - empty_directory_with_keep_file 'vendor/assets/stylesheets' + def config_target_version + defined?(@config_target_version) ? @config_target_version : Rails::VERSION::STRING.to_f end end module Generators # We need to store the RAILS_DEV_PATH in a constant, otherwise the path # can change in Ruby 1.8.7 when we FileUtils.cd. - RAILS_DEV_PATH = File.expand_path("../../../../../..", File.dirname(__FILE__)) + RAILS_DEV_PATH = File.expand_path("../../../../../..", __dir__) RESERVED_NAMES = %w[application destroy plugin runner test] class AppGenerator < AppBase # :nodoc: + WEBPACKS = %w( react vue angular elm stimulus ) + add_shared_options_for "application" - # Add bin/rails options + # Add rails command options class_option :version, type: :boolean, aliases: "-v", group: :rails, desc: "Show Rails version number and quit" class_option :api, type: :boolean, desc: "Preconfigure smaller stack for API only apps" + class_option :skip_bundle, type: :boolean, aliases: "-B", default: false, + desc: "Don't run bundle install" + + class_option :webpack, type: :string, aliases: "--webpacker", default: nil, + desc: "Preconfigure Webpack with a particular framework (options: #{WEBPACKS.join(", ")})" + + class_option :skip_webpack_install, type: :boolean, default: false, + desc: "Don't run Webpack install" + def initialize(*args) super - unless app_path - raise Error, "Application name should be provided in arguments. For details run: rails --help" - end - if !options[:skip_active_record] && !DATABASES.include?(options[:database]) raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}." end - # Force sprockets to be skipped when generating API only apps. + # Force sprockets and yarn to be skipped when generating API only apps. # Can't modify options hash as it's frozen by default. - self.options = options.merge(skip_sprockets: true, skip_javascript: true).freeze if options[:api] + if options[:api] + self.options = options.merge(skip_sprockets: true, skip_javascript: true).freeze + end end public_task :set_default_accessors! @@ -200,9 +284,12 @@ module Rails def create_root_files build(:readme) build(:rakefile) + build(:ruby_version) build(:configru) - build(:gitignore) unless options[:skip_git] - build(:gemfile) unless options[:skip_gemfile] + build(:gitignore) unless options[:skip_git] + build(:gemfile) unless options[:skip_gemfile] + build(:version_control) + build(:package_json) unless options[:skip_javascript] end def create_app_files @@ -213,6 +300,11 @@ module Rails build(:bin) end + def update_bin_files + build(:bin_when_updating) + end + remove_task :update_bin_files + def create_config_files build(:config) end @@ -222,6 +314,19 @@ module Rails end remove_task :update_config_files + def create_master_key + build(:master_key) + end + + def create_credentials + build(:credentials) + end + + def display_upgrade_guide_info + say "\nAfter this, check Rails upgrade guide at https://guides.rubyonrails.org/upgrading_ruby_on_rails.html for more details about upgrading your app." + end + remove_task :display_upgrade_guide_info + def create_boot_file template "config/boot.rb" end @@ -232,6 +337,7 @@ module Rails end def create_db_files + return if options[:skip_active_record] build(:db) end @@ -247,10 +353,6 @@ module Rails build(:public_directory) end - def create_test_files - build(:test) unless options[:skip_test] - end - def create_tmp_files build(:tmp) end @@ -259,77 +361,140 @@ module Rails build(:vendor) end + def create_test_files + build(:test) unless options[:skip_test] + end + + def create_system_test_files + build(:system_test) if depends_on_system_test? + end + + def create_storage_files + build(:storage) unless skip_active_storage? + end + def delete_app_assets_if_api_option if options[:api] - remove_dir 'app/assets' - remove_dir 'lib/assets' - remove_dir 'tmp/cache/assets' - remove_dir 'vendor/assets' + remove_dir "app/assets" + remove_dir "lib/assets" + remove_dir "tmp/cache/assets" end end def delete_app_helpers_if_api_option if options[:api] - remove_dir 'app/helpers' - remove_dir 'test/helpers' + remove_dir "app/helpers" + remove_dir "test/helpers" end end def delete_app_views_if_api_option if options[:api] - remove_dir 'app/views' + if options[:skip_action_mailer] + remove_dir "app/views" + else + remove_file "app/views/layouts/application.html.erb" + end + end + end + + def delete_public_files_if_api_option + if options[:api] + remove_file "public/404.html" + remove_file "public/422.html" + remove_file "public/500.html" + remove_file "public/apple-touch-icon-precomposed.png" + remove_file "public/apple-touch-icon.png" + remove_file "public/favicon.ico" end end def delete_js_folder_skipping_javascript if options[:skip_javascript] - remove_dir 'app/assets/javascripts' + remove_dir "app/javascript" end end def delete_assets_initializer_skipping_sprockets if options[:skip_sprockets] - remove_file 'config/initializers/assets.rb' + remove_file "config/initializers/assets.rb" end end - def delete_active_record_initializers_skipping_active_record + def delete_application_record_skipping_active_record if options[:skip_active_record] - remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb' + remove_file "app/models/application_record.rb" + end + end + + def delete_action_mailer_files_skipping_action_mailer + if options[:skip_action_mailer] + remove_file "app/views/layouts/mailer.html.erb" + remove_file "app/views/layouts/mailer.text.erb" + remove_dir "app/mailers" + remove_dir "test/mailers" + end + end + + def delete_action_cable_files_skipping_action_cable + if options[:skip_action_cable] + remove_dir "app/javascript/channels" + remove_dir "app/channels" end end def delete_non_api_initializers_if_api_option if options[:api] - remove_file 'config/initializers/session_store.rb' - remove_file 'config/initializers/cookies_serializer.rb' + remove_file "config/initializers/cookies_serializer.rb" + remove_file "config/initializers/content_security_policy.rb" + end + end + + def delete_api_initializers + unless options[:api] + remove_file "config/initializers/cors.rb" end end + def delete_new_framework_defaults + unless options[:update] + remove_file "config/initializers/new_framework_defaults_6_0.rb" + end + end + + def delete_bin_yarn + remove_file "bin/yarn" if options[:skip_javascript] + end + def finish_template build(:leftovers) end public_task :apply_rails_template, :run_bundle - public_task :generate_spring_binstubs + public_task :generate_bundler_binstub, :generate_spring_binstubs + public_task :run_webpack def run_after_bundle_callbacks @after_bundle_callbacks.each(&:call) end - protected - def self.banner - "rails new #{self.arguments.map(&:usage).join(' ')} [options]" + "rails new #{arguments.map(&:usage).join(' ')} [options]" end + private + # Define file as an alias to create_file for backwards compatibility. def file(*args, &block) create_file(*args, &block) end def app_name - @app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', '').tr(". ", "_") + @app_name ||= original_app_name.tr("-", "_") + end + + def original_app_name + @original_app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', "").tr(". ", "_") end def defined_app_name @@ -344,7 +509,7 @@ module Rails alias :defined_app_const_base? :defined_app_const_base def app_const_base - @app_const_base ||= defined_app_const_base || app_name.gsub(/\W/, '_').squeeze('_').camelize + @app_const_base ||= defined_app_const_base || app_name.gsub(/\W/, "_").squeeze("_").camelize end alias :camelized :app_const_base @@ -353,21 +518,17 @@ module Rails end def valid_const? - if app_const =~ /^\d/ - raise Error, "Invalid application name #{app_name}. Please give a name which does not start with numbers." - elsif RESERVED_NAMES.include?(app_name) - raise Error, "Invalid application name #{app_name}. Please give a " \ + if /^\d/.match?(app_const) + raise Error, "Invalid application name #{original_app_name}. Please give a name which does not start with numbers." + elsif RESERVED_NAMES.include?(original_app_name) + raise Error, "Invalid application name #{original_app_name}. Please give a " \ "name which does not match one of the reserved rails " \ "words: #{RESERVED_NAMES.join(", ")}" elsif Object.const_defined?(app_const_base) - raise Error, "Invalid application name #{app_name}, constant #{app_const_base} is already in use. Please choose another application name." + raise Error, "Invalid application name #{original_app_name}, constant #{app_const_base} is already in use. Please choose another application name." end end - def app_secret - SecureRandom.hex(64) - end - def mysql_socket @mysql_socket ||= [ "/tmp/mysql.sock", # default @@ -379,7 +540,7 @@ module Rails "/opt/local/var/run/mysql4/mysqld.sock", # mac + darwinports + mysql4 "/opt/local/var/run/mysql5/mysqld.sock", # mac + darwinports + mysql5 "/opt/lampp/var/mysql/mysql.sock" # xampp for linux - ].find { |f| File.exist?(f) } unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ + ].find { |f| File.exist?(f) } unless Gem.win_platform? end def get_builder_class @@ -407,14 +568,14 @@ module Rails end def self.default_rc_file - File.expand_path('~/.railsrc') + File.expand_path("~/.railsrc") end private def handle_version_request!(argument) - if ['--version', '-v'].include?(argument) - require 'rails/version' + if ["--version", "-v"].include?(argument) + require "rails/version" puts "Rails #{Rails::VERSION::STRING}" exit(0) end @@ -424,20 +585,20 @@ module Rails if argument == "new" yield else - ['--help'] + argv.drop(1) + ["--help"] + argv.drop(1) end end def handle_rails_rc!(argv) - if argv.find { |arg| arg == '--no-rc' } - argv.reject { |arg| arg == '--no-rc' } + if argv.find { |arg| arg == "--no-rc" } + argv.reject { |arg| arg == "--no-rc" } else railsrc(argv) { |rc_argv, rc| insert_railsrc_into_argv!(rc_argv, rc) } end end def railsrc(argv) - if (customrc = argv.index{ |x| x.include?("--rc=") }) + if (customrc = argv.index { |x| x.include?("--rc=") }) fname = File.expand_path(argv[customrc].gsub(/--rc=/, "")) yield(argv.take(customrc) + argv.drop(customrc + 1), fname) else diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt index b083381255..fb264935bd 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt @@ -1,5 +1,11 @@ source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } +ruby <%= "'#{RUBY_VERSION}'" -%> + +<% unless gemfile_entries.first.comment -%> + +<% end -%> <% gemfile_entries.each do |gem| -%> <% if gem.comment -%> @@ -14,17 +20,21 @@ source 'https://rubygems.org' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' +<% unless skip_active_storage? -%> -# Use Unicorn as the app server -# gem 'unicorn' +# Use ActiveStorage variant +# gem 'image_processing', '~> 1.2' +<% end -%> # Use Capistrano for deployment # gem 'capistrano-rails', group: :development -<%- if options.api? -%> -# Use ActiveModelSerializers to serialize JSON responses -gem 'active_model_serializers', '~> 0.10.0.rc2' +<% if depend_on_bootsnap? -%> +# Reduces boot times through caching; required in config/boot.rb +gem 'bootsnap', '>= 1.1.0', require: false +<%- end -%> +<%- if options.api? -%> # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible # gem 'rack-cors' @@ -32,24 +42,40 @@ gem 'active_model_serializers', '~> 0.10.0.rc2' <% if RUBY_ENGINE == 'ruby' -%> group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console - gem 'byebug' + gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end +<% end -%> group :development do - # Access an IRB console on exception pages or by using <%%= console %> in views +<%- unless options.api? -%> + # Access an interactive console on exception pages or by calling 'console' anywhere in the code. <%- if options.dev? || options.edge? -%> gem 'web-console', github: 'rails/web-console' <%- else -%> - gem 'web-console', '~> 2.0' + gem 'web-console', '>= 3.3.0' <%- end -%> -<%- if spring_install? %> +<%- end -%> +<% if depend_on_listen? -%> + gem 'listen', '>= 3.0.5', '< 3.2' +<% end -%> +<% if spring_install? -%> # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' +<% if depend_on_listen? -%> + gem 'spring-watcher-listen', '~> 2.0.0' <% end -%> -end <% end -%> -<% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince|java/) -%> +end + +<%- if depends_on_system_test? -%> +group :test do + # Adds support for Capybara system testing and selenium driver + gem 'capybara', '>= 2.15' + gem 'selenium-webdriver' + # Easy installation and use of chromedriver to run system tests with Chrome + gem 'chromedriver-helper' +end +<%- end -%> # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] -<% end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/README.md b/railties/lib/rails/generators/rails/app/templates/README.md.tt index 55e144da18..7db80e4ca1 100644 --- a/railties/lib/rails/generators/rails/app/templates/README.md +++ b/railties/lib/rails/generators/rails/app/templates/README.md.tt @@ -1,4 +1,4 @@ -## README +# README This README would normally document whatever steps are necessary to get the application up and running. diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile b/railties/lib/rails/generators/rails/app/templates/Rakefile.tt index ba6b733dd2..e85f913914 100644 --- a/railties/lib/rails/generators/rails/app/templates/Rakefile +++ b/railties/lib/rails/generators/rails/app/templates/Rakefile.tt @@ -1,6 +1,6 @@ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require File.expand_path('../config/application', __FILE__) +require_relative 'config/application' Rails.application.load_tasks diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt new file mode 100644 index 0000000000..591819335f --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt @@ -0,0 +1,2 @@ +//= link_tree ../images +//= link_directory ../stylesheets .css diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt deleted file mode 100644 index cb86978d4c..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt +++ /dev/null @@ -1,20 +0,0 @@ -// This is a manifest file that'll be compiled into application.js, which will include all the files -// listed below. -// -// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. -// -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. -// -// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details -// about supported directives. -// -<% unless options[:skip_javascript] -%> -//= require <%= options[:javascript] %> -//= require <%= options[:javascript] %>_ujs -<% if gemfile_entries.any? { |m| m.name == "turbolinks" } -%> -//= require turbolinks -<% end -%> -<% end -%> -//= require_tree . diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css.tt index 0cdd2788d0..d05ea0f511 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css.tt @@ -2,12 +2,13 @@ * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, - * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's + * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS - * files in this directory. It is generally better to create a new file per style scope. + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. * *= require_tree . *= require_self diff --git a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb.tt new file mode 100644 index 0000000000..d672697283 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb.tt @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb.tt new file mode 100644 index 0000000000..0ff5442f47 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb.tt @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt index f726fd6305..938eff8ed0 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/controllers/application_controller.rb.tt @@ -1,7 +1,2 @@ -class ApplicationController < ActionController::<%= options[:api] ? "API" : "Base" %> -<%- unless options[:api] -%> - # Prevent CSRF attacks by raising an exception. - # For APIs, you may want to use :null_session instead. - protect_from_forgery with: :exception -<%- end -%> +class ApplicationController < ActionController::<%= options.api? ? "API" : "Base" %> end diff --git a/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb b/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb.tt index de6be7945c..de6be7945c 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb +++ b/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js new file mode 100644 index 0000000000..76ca3d0f2f --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js @@ -0,0 +1,6 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the `rails generate channel` command. + +import ActionCable from "actioncable" + +export default ActionCable.createConsumer() diff --git a/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/index.js b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/index.js new file mode 100644 index 0000000000..0cfcf74919 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/index.js @@ -0,0 +1,5 @@ +// Load all the channels within this directory and all subdirectories. +// Channel files must be named *_channel.js. + +const channels = require.context('.', true, /_channel\.js$/) +channels.keys().forEach(channels) diff --git a/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt new file mode 100644 index 0000000000..4d7a145cd6 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt @@ -0,0 +1,21 @@ +// This file is automatically compiled by Webpack, along with any other files +// present in this directory. You're encouraged to place your actual application logic in +// a relevant structure within app/javascript and only use these pack files to reference +// that code so it'll be compiled. + +import Rails from "rails-ujs" +Rails.start() +<%- unless options[:skip_turbolinks] -%> + +import Turbolinks from "turbolinks" +Turbolinks.start() +<%- end -%> +<%- unless skip_active_storage? -%> + +import * as ActiveStorage from "activestorage" +ActiveStorage.start() +<%- end -%> +<%- unless options[:skip_action_cable] -%> + +import "channels" +<%- end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb deleted file mode 100644 index a009ace51c..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb +++ /dev/null @@ -1,2 +0,0 @@ -class ApplicationJob < ActiveJob::Base -end diff --git a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt new file mode 100644 index 0000000000..d394c3d106 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb.tt new file mode 100644 index 0000000000..286b2239d1 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb.tt @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end diff --git a/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb.tt new file mode 100644 index 0000000000..10a4cba84d --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb.tt @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt index 75ea52828e..9a7267c783 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt @@ -1,23 +1,24 @@ <!DOCTYPE html> <html> -<head> - <title><%= camelized %></title> - <%- if options[:skip_javascript] -%> - <%%= stylesheet_link_tag 'application', media: 'all' %> - <%- else -%> - <%- if gemfile_entries.any? { |m| m.name == 'turbolinks' } -%> - <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> - <%%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> + <head> + <title><%= camelized %></title> + <%%= csrf_meta_tags %> + <%%= csp_meta_tag %> + + <%- if options[:skip_javascript] -%> + <%%= stylesheet_link_tag 'application', media: 'all' %> <%- else -%> - <%%= stylesheet_link_tag 'application', media: 'all' %> - <%%= javascript_include_tag 'application' %> + <%- unless options[:skip_turbolinks] -%> + <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> + <%%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> + <%- else -%> + <%%= stylesheet_link_tag 'application', media: 'all' %> + <%%= javascript_pack_tag 'application' %> + <%- end -%> <%- end -%> - <%- end -%> - <%%= csrf_meta_tags %> -</head> -<body> - -<%%= yield %> + </head> -</body> + <body> + <%%= yield %> + </body> </html> diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.html.erb.tt new file mode 100644 index 0000000000..55f3675d49 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.html.erb.tt @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <style> + /* Email styles need to be inline */ + </style> + </head> + + <body> + <%%= yield %> + </body> +</html> diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.text.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.text.erb.tt new file mode 100644 index 0000000000..6363733e6e --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/mailer.text.erb.tt @@ -0,0 +1 @@ +<%%= yield %> diff --git a/railties/lib/rails/generators/rails/app/templates/bin/bundle b/railties/lib/rails/generators/rails/app/templates/bin/bundle deleted file mode 100644 index 1123dcf501..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/bin/bundle +++ /dev/null @@ -1,2 +0,0 @@ -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -load Gem.bin_path('bundler', 'bundle') diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rails b/railties/lib/rails/generators/rails/app/templates/bin/rails deleted file mode 100644 index 80ec8080ab..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/bin/rails +++ /dev/null @@ -1,3 +0,0 @@ -APP_PATH = File.expand_path('../../config/application', __FILE__) -require_relative '../config/boot' -require 'rails/commands' diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rails.tt b/railties/lib/rails/generators/rails/app/templates/bin/rails.tt new file mode 100644 index 0000000000..513a2e0183 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/bin/rails.tt @@ -0,0 +1,3 @@ +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rake b/railties/lib/rails/generators/rails/app/templates/bin/rake.tt index d14fc8395b..d14fc8395b 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/rake +++ b/railties/lib/rails/generators/rails/app/templates/bin/rake.tt diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup b/railties/lib/rails/generators/rails/app/templates/bin/setup deleted file mode 100644 index 0d41f2fe4c..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup +++ /dev/null @@ -1,29 +0,0 @@ -require 'pathname' -require 'fileutils' -include FileUtils - -# path to your application root. -APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) - -chdir APP_ROOT do - # This script is a starting point to setup your application. - # Add necessary setup steps to this file. - - puts '== Installing dependencies ==' - system 'gem install bundler --conservative' - system('bundle check') or system('bundle install') - - # puts "\n== Copying sample files ==" - # unless File.exist?('config/database.yml') - # cp 'config/database.yml.sample', 'config/database.yml' - # end - - puts "\n== Preparing database ==" - system 'ruby bin/rake db:setup' - - puts "\n== Removing old logs and tempfiles ==" - system 'ruby bin/rake log:clear tmp:clear' - - puts "\n== Restarting application server ==" - system 'ruby bin/rake restart' -end diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt new file mode 100644 index 0000000000..3f73bae3da --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt @@ -0,0 +1,38 @@ +require 'fileutils' + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +FileUtils.chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') +<% unless options.skip_javascript? -%> + + # Install JavaScript dependencies + # system('bin/yarn') +<% end -%> +<% unless options.skip_active_record? -%> + + # puts "\n== Copying sample files ==" + # unless File.exist?('config/database.yml') + # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:setup' +<% end -%> + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update.tt b/railties/lib/rails/generators/rails/app/templates/bin/update.tt new file mode 100644 index 0000000000..03b77d0d46 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/bin/update.tt @@ -0,0 +1,33 @@ +require 'fileutils' + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +FileUtils.chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') +<% unless options.skip_javascript? -%> + + # Install JavaScript dependencies + # system('bin/yarn') +<% end -%> +<% unless options.skip_active_record? -%> + + puts "\n== Updating database ==" + system! 'rails db:migrate' +<% end -%> + + puts "\n== Removing old logs and tempfiles ==" + system! 'rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'rails restart' +end diff --git a/railties/lib/rails/generators/rails/app/templates/bin/yarn.tt b/railties/lib/rails/generators/rails/app/templates/bin/yarn.tt new file mode 100644 index 0000000000..90ddcc520e --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/bin/yarn.tt @@ -0,0 +1,10 @@ +APP_ROOT = File.expand_path('..', __dir__) +Dir.chdir(APP_ROOT) do + begin + exec "yarnpkg", *ARGV + rescue Errno::ENOENT + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru b/railties/lib/rails/generators/rails/app/templates/config.ru.tt index bd83b25412..f7ba0b527b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config.ru +++ b/railties/lib/rails/generators/rails/app/templates/config.ru.tt @@ -1,4 +1,5 @@ # This file is used by Rack-based servers to start the application. -require ::File.expand_path('../config/environment', __FILE__) +require_relative 'config/environment' + run Rails.application diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt index 6b7d7abd0b..9a427113c7 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt @@ -1,4 +1,4 @@ -require File.expand_path('../boot', __FILE__) +require_relative 'boot' <% if include_all_railties? -%> require 'rails/all' @@ -8,9 +8,11 @@ require "rails" require "active_model/railtie" require "active_job/railtie" <%= comment_if :skip_active_record %>require "active_record/railtie" +<%= comment_if :skip_active_storage %>require "active_storage/engine" require "action_controller/railtie" <%= comment_if :skip_action_mailer %>require "action_mailer/railtie" require "action_view/railtie" +<%= comment_if :skip_action_cable %>require "action_cable/engine" <%= comment_if :skip_sprockets %>require "sprockets/railtie" <%= comment_if :skip_test %>require "rails/test_unit/railtie" <% end -%> @@ -21,23 +23,23 @@ Bundler.require(*Rails.groups) module <%= app_const_base %> class Application < Rails::Application - # Settings in config/environments/* take precedence over those specified here. - # Application configuration should go into files in config/initializers - # -- all .rb files in that directory are automatically loaded. - - # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. - # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - # config.time_zone = 'Central Time (US & Canada)' + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults <%= build(:config_target_version) %> - # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. - # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] - # config.i18n.default_locale = :de -<%- if options[:api] -%> + # Settings in config/environments/* take precedence over those specified here. + # Application configuration can go into files in config/initializers + # -- all .rb files in that directory are automatically loaded after loading + # the framework and any gems in your application. +<%- if options.api? -%> # Only loads a smaller set of middleware suitable for API only apps. # Middleware like session, flash, cookies can be added back manually. # Skip views, helpers and assets when generating a new resource. config.api_only = true +<%- elsif !depends_on_system_test? -%> + + # Don't generate system test files. + config.generators.system_tests = nil <%- end -%> end end diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb b/railties/lib/rails/generators/rails/app/templates/config/boot.rb deleted file mode 100644 index 6b750f00b1..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/boot.rb +++ /dev/null @@ -1,3 +0,0 @@ -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) - -require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/boot.rb.tt new file mode 100644 index 0000000000..42d46b8175 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/boot.rb.tt @@ -0,0 +1,6 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. +<% if depend_on_bootsnap? -%> +require 'bootsnap/setup' # Speed up boot time by caching expensive operations. +<%- end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt new file mode 100644 index 0000000000..f69dc91b92 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/cable.yml.tt @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: <%= app_name %>_production diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt index 34fc0e3465..33f422c622 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt @@ -8,6 +8,7 @@ # default: &default adapter: frontbase + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> host: localhost username: <%= app_name %> password: '' @@ -28,7 +29,7 @@ test: # ever seen by anyone, they now have access to your database. # # Instead, provide the password as a unix environment variable when you boot -# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database +# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database # for a full rundown on how to provide these environment variables in a # production deployment. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt index 187ff01bac..681c765e93 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt @@ -34,6 +34,7 @@ # default: &default adapter: ibm_db + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: db2inst1 password: #schema: db2inst1 @@ -64,7 +65,7 @@ test: # ever seen by anyone, they now have access to your database. # # Instead, provide the password as a unix environment variable when you boot -# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database +# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database # for a full rundown on how to provide these environment variables in a # production deployment. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt index db0a429753..af69f12059 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt @@ -38,6 +38,7 @@ default: &default adapter: jdbc + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: <%= app_name %> password: driver: @@ -58,7 +59,7 @@ test: # ever seen by anyone, they now have access to your database. # # Instead, provide the password as a unix environment variable when you boot -# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database +# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database # for a full rundown on how to provide these environment variables in a # production deployment. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt index f5b62e8fb3..f39593372c 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt @@ -1,4 +1,4 @@ -# MySQL. Versions 4.1 and 5.0 are recommended. +# MySQL. Versions 5.5.8 and up are supported. # # Install the MySQL driver: # gem install activerecord-jdbcmysql-adapter @@ -7,10 +7,11 @@ # gem 'activerecord-jdbcmysql-adapter' # # And be sure to use new-style password hashing: -# http://dev.mysql.com/doc/refman/5.6/en/old-client.html +# https://dev.mysql.com/doc/refman/5.7/en/password-hashing.html # default: &default adapter: mysql + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: host: localhost @@ -31,7 +32,7 @@ test: # ever seen by anyone, they now have access to your database. # # Instead, provide the password as a unix environment variable when you boot -# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database +# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database # for a full rundown on how to provide these environment variables in a # production deployment. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt index 9e99264d33..df8a6ad627 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt @@ -1,4 +1,4 @@ -# PostgreSQL. Versions 8.2 and up are supported. +# PostgreSQL. Versions 9.1 and up are supported. # # Configure Using Gemfile # gem 'activerecord-jdbcpostgresql-adapter' @@ -6,6 +6,7 @@ default: &default adapter: postgresql encoding: unicode + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default @@ -47,7 +48,7 @@ test: # ever seen by anyone, they now have access to your database. # # Instead, provide the password as a unix environment variable when you boot -# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database +# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database # for a full rundown on how to provide these environment variables in a # production deployment. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml.tt index 28c36eb82f..371415e6a8 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml.tt @@ -6,6 +6,7 @@ # default: &default adapter: sqlite3 + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt index b0767bd93a..b6c2e7448a 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt @@ -1,4 +1,4 @@ -# MySQL. Versions 5.0+ are recommended. +# MySQL. Versions 5.5.8 and up are supported. # # Install the MySQL driver # gem install mysql2 @@ -7,12 +7,12 @@ # gem 'mysql2' # # And be sure to use new-style password hashing: -# http://dev.mysql.com/doc/refman/5.6/en/old-client.html +# https://dev.mysql.com/doc/refman/5.7/en/password-hashing.html # default: &default adapter: mysql2 - encoding: utf8 - pool: 5 + encoding: utf8mb4 + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: <% if mysql_socket -%> @@ -37,7 +37,7 @@ test: # ever seen by anyone, they now have access to your database. # # Instead, provide the password as a unix environment variable when you boot -# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database +# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database # for a full rundown on how to provide these environment variables in a # production deployment. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt index 9aedcc15cb..8d9d33ba6c 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt @@ -1,4 +1,4 @@ -# Oracle/OCI 8i, 9, 10g +# Oracle/OCI 11g or higher recommended # # Requires Ruby/OCI8: # https://github.com/kubo/ruby-oci8 @@ -17,7 +17,8 @@ # cursor_sharing: similar # default: &default - adapter: oracle + adapter: oracle_enhanced + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: <%= app_name %> password: @@ -37,14 +38,16 @@ test: # ever seen by anyone, they now have access to your database. # # Instead, provide the password as a unix environment variable when you boot -# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database +# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database # for a full rundown on how to provide these environment variables in a # production deployment. # # On Heroku and other platform providers, you may have a full connection URL # available as an environment variable. For example: # -# DATABASE_URL="oracle://myuser:mypass@localhost/somedatabase" +# DATABASE_URL="oracle-enhanced://myuser:mypass@localhost/somedatabase" +# +# Note that the adapter name uses a dash instead of an underscore. # # You can use this database configuration with: # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt index feb25bbc6b..dcd57425e2 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt @@ -1,10 +1,10 @@ -# PostgreSQL. Versions 8.2 and up are supported. +# PostgreSQL. Versions 9.1 and up are supported. # # Install the pg driver: # gem install pg -# On OS X with Homebrew: +# On macOS with Homebrew: # gem install pg -- --with-pg-config=/usr/local/bin/pg_config -# On OS X with MacPorts: +# On macOS with MacPorts: # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config # On Windows: # gem install pg @@ -17,9 +17,9 @@ default: &default adapter: postgresql encoding: unicode - # For details on connection pooling, see rails configuration guide - # http://guides.rubyonrails.org/configuring.html#database-pooling - pool: 5 + # For details on connection pooling, see Rails configuration guide + # https://guides.rubyonrails.org/configuring.html#database-pooling + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: <<: *default @@ -64,7 +64,7 @@ test: # ever seen by anyone, they now have access to your database. # # Instead, provide the password as a unix environment variable when you boot -# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database +# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database # for a full rundown on how to provide these environment variables in a # production deployment. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml.tt index 1c1a37ca8d..9510568124 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml.tt @@ -6,7 +6,7 @@ # default: &default adapter: sqlite3 - pool: 5 + pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 development: diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt index 30b0df34a8..0246fb0d02 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt @@ -1,4 +1,4 @@ -# SQL Server (2005 or higher recommended) +# SQL Server (2012 or higher required) # # Install the adapters and driver # gem install tiny_tds @@ -8,28 +8,12 @@ # gem 'tiny_tds' # gem 'activerecord-sqlserver-adapter' # -# You should make sure freetds is configured correctly first. -# freetds.conf contains host/port/protocol_versions settings. -# http://freetds.schemamania.org/userguide/freetdsconf.htm -# -# A typical Microsoft server -# [mssql] -# host = mssqlserver.yourdomain.com -# port = 1433 -# tds version = 7.1 - -# If you can connect with "tsql -S servername", your basic FreeTDS installation is working. -# 'man tsql' for more info -# Set timeout to a larger number if valid queries against a live db fail -# default: &default adapter: sqlserver encoding: utf8 - reconnect: false - username: <%= app_name %> - password: - timeout: 25 - dataserver: from_freetds.conf + username: sa + password: <%%= ENV['SA_PASSWORD'] %> + host: localhost development: <<: *default @@ -47,7 +31,7 @@ test: # ever seen by anyone, they now have access to your database. # # Instead, provide the password as a unix environment variable when you boot -# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database +# the app. Read https://guides.rubyonrails.org/configuring.html#configuring-a-database # for a full rundown on how to provide these environment variables in a # production deployment. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/environment.rb b/railties/lib/rails/generators/rails/app/templates/config/environment.rb.tt index ee8d90dc65..426333bb46 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environment.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/environment.rb.tt @@ -1,5 +1,5 @@ # Load the Rails application. -require File.expand_path('../application', __FILE__) +require_relative 'application' # Initialize the Rails application. Rails.application.initialize! diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index ecb5d4170f..3807c8a9aa 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -9,13 +9,34 @@ Rails.application.configure do # Do not eager load code on boot. config.eager_load = false - # Show full error reports and disable caching. - config.consider_all_requests_local = true - config.action_controller.perform_caching = false + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join('tmp', 'caching-dev.txt').exist? + config.action_controller.perform_caching = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + <%- unless skip_active_storage? -%> + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + <%- end -%> <%- unless options.skip_action_mailer? -%> # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false <%- end -%> # Print deprecation notices to the Rails logger. @@ -25,6 +46,9 @@ Rails.application.configure do # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + <%- end -%> <%- unless options.skip_sprockets? -%> # Debug mode disables concatenation and preprocessing of assets. @@ -32,16 +56,14 @@ Rails.application.configure do # number of complex assets. config.assets.debug = true - # Asset digests allow you to set far-future HTTP expiration dates on all assets, - # yet still be able to expire them through the digest params. - config.assets.digest = true - - # Adds additional error checking when serving assets at runtime. - # Checks for improperly declared sprockets dependencies. - # Raises helpful error messages. - config.assets.raise_runtime_errors = true + # Suppress logger output for asset requests. + config.assets.quiet = true <%- end -%> - # Raises error for missing translations + # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + <%= '# ' unless depend_on_listen? %>config.file_watcher = ActiveSupport::EventedFileUpdateChecker end 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 8c09396fc1..08befd9196 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -14,25 +14,22 @@ Rails.application.configure do config.consider_all_requests_local = false config.action_controller.perform_caching = true + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. - config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? <%- unless options.skip_sprockets? -%> - # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # Asset digests allow you to set far-future HTTP expiration dates on all assets, - # yet still be able to expire them through the digest params. - config.assets.digest = true - - # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb <%- end -%> - # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' @@ -40,6 +37,18 @@ Rails.application.configure do # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + <%- unless skip_active_storage? -%> + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + <%- end -%> + <%- unless options[:skip_action_cable] -%> + # Mount Action Cable outside main process or domain. + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + + <%- end -%> # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true @@ -48,24 +57,23 @@ Rails.application.configure do config.log_level = :debug # Prepend all log lines with the following tags. - # config.log_tags = [ :subdomain, :request_id ] - - # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + config.log_tags = [ :request_id ] # Use a different cache store in production. # config.cache_store = :mem_cache_store - # Use a real queuing backend for Active Job (and separate queues per environment) + # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "<%= app_name %>_#{Rails.env}" + # config.active_job.queue_name_prefix = "<%= app_name %>_production" + <%- unless options.skip_action_mailer? -%> + config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false - <%- end -%> + <%- end -%> # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true @@ -75,6 +83,16 @@ Rails.application.configure do # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end <%- unless options.skip_active_record? -%> # Do not dump schema after migrations. diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt index 0306deb18c..223aa56187 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt @@ -12,33 +12,43 @@ Rails.application.configure do # preloads Rails for running tests, you may have to set it to true. config.eager_load = false - # Configure static file server for tests with Cache-Control for performance. - config.serve_static_files = true - config.static_cache_control = 'public, max-age=3600' + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false + config.cache_store = :null_store # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false + + <%- unless skip_active_storage? -%> + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + <%- end -%> <%- unless options.skip_action_mailer? -%> + config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test - <%- end -%> - - # Randomize the order test cases are executed. - config.active_support.test_order = :random + <%- end -%> # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - # Raises error for missing translations + # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true + + # Prevent expensive template finalization at end of test suite runs. + config.action_view.finalize_compiled_template_methods = false end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/active_record_belongs_to_required_by_default.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/active_record_belongs_to_required_by_default.rb deleted file mode 100644 index 30c4f89792..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/active_record_belongs_to_required_by_default.rb +++ /dev/null @@ -1,4 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Require `belongs_to` associations by default. -Rails.application.config.active_record.belongs_to_required_by_default = true diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb deleted file mode 100644 index ea930f54da..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb +++ /dev/null @@ -1,6 +0,0 @@ -## Change renderer defaults here. -# -# ApplicationController.renderer.defaults.merge!( -# http_host: 'example.org', -# https: false -# ) diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb.tt new file mode 100644 index 0000000000..89d2efab2b --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb.tt @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt index 01ef3e6630..fe48fc34ee 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt @@ -3,9 +3,10 @@ # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' -# Add additional assets to the asset load path +# Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path # Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -# Rails.application.config.assets.precompile += %w( search.js ) +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt index 59385cdf37..59385cdf37 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb deleted file mode 100644 index a70a1b9cde..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb +++ /dev/null @@ -1,4 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Do not halt callback chains when a callback returns false. -ActiveSupport.halt_callback_chains_on_return_false = false diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt new file mode 100644 index 0000000000..c517b0f96b --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt @@ -0,0 +1,29 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +# Rails.application.config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +<%- unless options[:skip_javascript] -%> +# # If you are using webpack-dev-server then specify webpack-dev-server host +# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? +<%- end -%> + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end + +# If you are using UJS then enable automatic nonce generation +# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } + +# Report CSP violations to a specified URI +# For further information see the following documentation: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# Rails.application.config.content_security_policy_report_only = true diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb.tt index 7f70458dee..5a6a32d371 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb.tt @@ -1,3 +1,5 @@ # Be sure to restart your server when you modify this file. +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb.tt index 45c44d24f8..3b1c1b5ed1 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb.tt @@ -1,9 +1,11 @@ -# Avoid CORS issues when API is called from the frontend app -# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests +# Be sure to restart your server when you modify this file. + +# Avoid CORS issues when API is called from the frontend app. +# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. # Read more: https://github.com/cyu/rack-cors -# Rails.application.config.middleware.insert_before 0, "Rack::Cors" do +# Rails.application.config.middleware.insert_before 0, Rack::Cors do # allow do # origins 'example.com' # diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt index 4a994e1e7b..4a994e1e7b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb.tt index ac033bf9dc..ac033bf9dc 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb.tt index dc1899682b..dc1899682b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb.tt diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt new file mode 100644 index 0000000000..54eb0cb1d2 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt @@ -0,0 +1,17 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 6.0 upgrade. +# +# Once upgraded flip defaults one by one to migrate to the new default. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. + +# Don't force requests from old versions of IE to be UTF-8 encoded +# Rails.application.config.action_view.default_enforce_utf8 = false + +# Embed purpose and expiry metadata inside signed and encrypted +# cookies for increased security. +# +# This option is not backwards compatible with earlier Rails versions. +# It's best enabled when your entire app is migrated and stable on 6.0. +# Rails.application.config.action_dispatch.use_cookies_with_metadata = true diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt deleted file mode 100644 index 2bb9b82c61..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt +++ /dev/null @@ -1,3 +0,0 @@ -# Be sure to restart your server when you modify this file. - -Rails.application.config.session_store :cookie_store, key: <%= "'_#{app_name}_session'" %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml b/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml index 0653957166..cf9b342d0a 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml @@ -16,8 +16,18 @@ # # This would use the information in config/locales/es.yml. # +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# # To learn more, please read the Rails Internationalization guide -# available at http://guides.rubyonrails.org/i18n.html. +# available at https://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" diff --git a/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt new file mode 100644 index 0000000000..f6146e7259 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt @@ -0,0 +1,35 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked webserver processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# +# preload_app! + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/railties/lib/rails/generators/rails/app/templates/config/routes.rb b/railties/lib/rails/generators/rails/app/templates/config/routes.rb deleted file mode 100644 index 3f66539d54..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/routes.rb +++ /dev/null @@ -1,56 +0,0 @@ -Rails.application.routes.draw do - # The priority is based upon order of creation: first created -> highest priority. - # See how all your routes lay out with "rake routes". - - # You can have the root of your site routed with "root" - # root 'welcome#index' - - # Example of regular route: - # get 'products/:id' => 'catalog#view' - - # Example of named route that can be invoked with purchase_url(id: product.id) - # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase - - # Example resource route (maps HTTP verbs to controller actions automatically): - # resources :products - - # Example resource route with options: - # resources :products do - # member do - # get 'short' - # post 'toggle' - # end - # - # collection do - # get 'sold' - # end - # end - - # Example resource route with sub-resources: - # resources :products do - # resources :comments, :sales - # resource :seller - # end - - # Example resource route with more complex sub-resources: - # resources :products do - # resources :comments - # resources :sales do - # get 'recent', on: :collection - # end - # end - - # Example resource route with concerns: - # concern :toggleable do - # post 'toggle' - # end - # resources :posts, concerns: :toggleable - # resources :photos, concerns: :toggleable - - # Example resource route within a namespace: - # namespace :admin do - # # Directs /admin/products/* to Admin::ProductsController - # # (app/controllers/admin/products_controller.rb) - # resources :products - # end -end diff --git a/railties/lib/rails/generators/rails/app/templates/config/routes.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/routes.rb.tt new file mode 100644 index 0000000000..c06383a172 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/routes.rb.tt @@ -0,0 +1,3 @@ +Rails.application.routes.draw do + # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html +end diff --git a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml deleted file mode 100644 index b2669a0f79..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/secrets.yml +++ /dev/null @@ -1,22 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Your secret key is used for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! - -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. -# You can use `rake secret` to generate a secure secret key. - -# Make sure the secrets in this file are kept private -# if you're sharing your code publicly. - -development: - secret_key_base: <%= app_secret %> - -test: - secret_key_base: <%= app_secret %> - -# Do not keep production secrets in the repository, -# instead read values from the environment. -production: - secret_key_base: <%%= ENV["SECRET_KEY_BASE"] %> diff --git a/railties/lib/rails/generators/rails/app/templates/config/spring.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/spring.rb.tt new file mode 100644 index 0000000000..db5bf1307a --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/spring.rb.tt @@ -0,0 +1,6 @@ +Spring.watch( + ".ruby-version", + ".rbenv-vars", + "tmp/restart.txt", + "tmp/caching-dev.txt" +) diff --git a/railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt new file mode 100644 index 0000000000..7207c75086 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%%= Rails.root.join("storage") %> + +# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket + +# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt b/railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt index 4edb1e857e..1beea2accd 100644 --- a/railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/db/seeds.rb.tt @@ -1,7 +1,7 @@ # This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). +# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). # # Examples: # -# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) -# Mayor.create(name: 'Emanuel', city: cities.first) +# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) +# Character.create(name: 'Luke', movie: movies.first) diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore.tt index 1b8cf8a9fa..860baa1595 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore +++ b/railties/lib/rails/generators/rails/app/templates/gitignore.tt @@ -20,3 +20,16 @@ !/log/.keep !/tmp/.keep <% end -%> + +<% unless skip_active_storage? -%> +# Ignore uploaded files in development. +/storage/* +<% if keeps? -%> +!/storage/.keep +<% end -%> +<% end -%> +<% unless options.api? -%> + +/public/assets +<% end -%> +.byebug_history diff --git a/railties/lib/rails/generators/rails/app/templates/package.json.tt b/railties/lib/rails/generators/rails/app/templates/package.json.tt new file mode 100644 index 0000000000..7174116989 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/package.json.tt @@ -0,0 +1,11 @@ +{ + "name": "<%= app_name %>", + "private": true, + "dependencies": { + "rails-ujs": ">=5.2.1"<% unless options[:skip_turbolinks] %>, + "turbolinks": "5.1.1"<% end -%><% unless skip_active_storage? %>, + "activestorage": ">=5.2.1"<% end -%><% unless options[:skip_action_cable] %>, + "actioncable": ">=5.2.1"<% end %> + }, + "version": "0.1.0" +} diff --git a/railties/lib/rails/generators/rails/app/templates/public/404.html b/railties/lib/rails/generators/rails/app/templates/public/404.html index b612547fc2..2be3af26fc 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/404.html +++ b/railties/lib/rails/generators/rails/app/templates/public/404.html @@ -4,7 +4,7 @@ <title>The page you were looking for doesn't exist (404)</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> - body { + .rails-default-error-page { background-color: #EFEFEF; color: #2E2F30; text-align: center; @@ -12,13 +12,13 @@ margin: 0; } - div.dialog { + .rails-default-error-page div.dialog { width: 95%; max-width: 33em; margin: 4em auto 0; } - div.dialog > div { + .rails-default-error-page div.dialog > div { border: 1px solid #CCC; border-right-color: #999; border-left-color: #999; @@ -31,13 +31,13 @@ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } - h1 { + .rails-default-error-page h1 { font-size: 100%; color: #730E15; line-height: 1.5em; } - div.dialog > p { + .rails-default-error-page div.dialog > p { margin: 0 0 1em; padding: 1em; background-color: #F7F7F7; @@ -54,7 +54,7 @@ </style> </head> -<body> +<body class="rails-default-error-page"> <!-- This file lives in public/404.html --> <div class="dialog"> <div> diff --git a/railties/lib/rails/generators/rails/app/templates/public/422.html b/railties/lib/rails/generators/rails/app/templates/public/422.html index a21f82b3bd..c08eac0d1d 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/422.html +++ b/railties/lib/rails/generators/rails/app/templates/public/422.html @@ -4,7 +4,7 @@ <title>The change you wanted was rejected (422)</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> - body { + .rails-default-error-page { background-color: #EFEFEF; color: #2E2F30; text-align: center; @@ -12,13 +12,13 @@ margin: 0; } - div.dialog { + .rails-default-error-page div.dialog { width: 95%; max-width: 33em; margin: 4em auto 0; } - div.dialog > div { + .rails-default-error-page div.dialog > div { border: 1px solid #CCC; border-right-color: #999; border-left-color: #999; @@ -31,13 +31,13 @@ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } - h1 { + .rails-default-error-page h1 { font-size: 100%; color: #730E15; line-height: 1.5em; } - div.dialog > p { + .rails-default-error-page div.dialog > p { margin: 0 0 1em; padding: 1em; background-color: #F7F7F7; @@ -54,7 +54,7 @@ </style> </head> -<body> +<body class="rails-default-error-page"> <!-- This file lives in public/422.html --> <div class="dialog"> <div> diff --git a/railties/lib/rails/generators/rails/app/templates/public/500.html b/railties/lib/rails/generators/rails/app/templates/public/500.html index 061abc587d..78a030af22 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/500.html +++ b/railties/lib/rails/generators/rails/app/templates/public/500.html @@ -4,7 +4,7 @@ <title>We're sorry, but something went wrong (500)</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> - body { + .rails-default-error-page { background-color: #EFEFEF; color: #2E2F30; text-align: center; @@ -12,13 +12,13 @@ margin: 0; } - div.dialog { + .rails-default-error-page div.dialog { width: 95%; max-width: 33em; margin: 4em auto 0; } - div.dialog > div { + .rails-default-error-page div.dialog > div { border: 1px solid #CCC; border-right-color: #999; border-left-color: #999; @@ -31,13 +31,13 @@ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); } - h1 { + .rails-default-error-page h1 { font-size: 100%; color: #730E15; line-height: 1.5em; } - div.dialog > p { + .rails-default-error-page div.dialog > p { margin: 0 0 1em; padding: 1em; background-color: #F7F7F7; @@ -54,7 +54,7 @@ </style> </head> -<body> +<body class="rails-default-error-page"> <!-- This file lives in public/500.html --> <div class="dialog"> <div> diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory b/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon-precomposed.png index e69de29bb2..e69de29bb2 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory +++ b/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon-precomposed.png diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory b/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon.png index e69de29bb2..e69de29bb2 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory +++ b/railties/lib/rails/generators/rails/app/templates/public/apple-touch-icon.png diff --git a/railties/lib/rails/generators/rails/app/templates/public/robots.txt b/railties/lib/rails/generators/rails/app/templates/public/robots.txt index 3c9c7c01f3..37b576a4a0 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/robots.txt +++ b/railties/lib/rails/generators/rails/app/templates/public/robots.txt @@ -1,5 +1 @@ # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file -# -# To ban all spiders from the entire site uncomment the next two lines: -# User-agent: * -# Disallow: / diff --git a/railties/lib/rails/generators/rails/app/templates/ruby-version.tt b/railties/lib/rails/generators/rails/app/templates/ruby-version.tt new file mode 100644 index 0000000000..bac1339923 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/ruby-version.tt @@ -0,0 +1 @@ +<%= ENV["RBENV_VERSION"] || ENV["rvm_ruby_string"] || "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" -%> diff --git a/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb.tt b/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb.tt new file mode 100644 index 0000000000..d19212abd5 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb.tt @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt index 87b8fe3516..c06cd525d7 100644 --- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb +++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt @@ -1,8 +1,15 @@ ENV['RAILS_ENV'] ||= 'test' -require File.expand_path('../../config/environment', __FILE__) +require_relative '../config/environment' require 'rails/test_help' class ActiveSupport::TestCase + # Run tests in parallel with specified workers +<% if defined?(JRUBY_VERSION) || Gem.win_platform? -%> + parallelize(workers: 2, with: :threads) +<%- else -%> + parallelize(workers: 2) +<% end -%> + <% unless options[:skip_active_record] -%> # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all diff --git a/railties/lib/rails/generators/rails/application_record/application_record_generator.rb b/railties/lib/rails/generators/rails/application_record/application_record_generator.rb new file mode 100644 index 0000000000..f6b6e76b1d --- /dev/null +++ b/railties/lib/rails/generators/rails/application_record/application_record_generator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Rails + module Generators + class ApplicationRecordGenerator < Base # :nodoc: + hook_for :orm, required: true, desc: "ORM to be invoked" + end + end +end diff --git a/railties/lib/rails/generators/rails/assets/USAGE b/railties/lib/rails/generators/rails/assets/USAGE index d2e5ed4482..ee73d05808 100644 --- a/railties/lib/rails/generators/rails/assets/USAGE +++ b/railties/lib/rails/generators/rails/assets/USAGE @@ -5,16 +5,13 @@ Description: To create an asset within a folder, specify the asset's name as a path like 'parent/name'. - This generates a JavaScript stub in app/assets/javascripts and a stylesheet - stub in app/assets/stylesheets. + This generates a stylesheet stub in app/assets/stylesheets. - If CoffeeScript is available, JavaScripts will be generated with the .coffee extension. If Sass 3 is available, stylesheets will be generated with the .scss extension. Example: `rails generate assets posts` Posts assets. - JavaScript: app/assets/javascripts/posts.js Stylesheet: app/assets/stylesheets/posts.css diff --git a/railties/lib/rails/generators/rails/assets/assets_generator.rb b/railties/lib/rails/generators/rails/assets/assets_generator.rb index 6f4b86e708..9ce8570172 100644 --- a/railties/lib/rails/generators/rails/assets/assets_generator.rb +++ b/railties/lib/rails/generators/rails/assets/assets_generator.rb @@ -1,25 +1,19 @@ +# frozen_string_literal: true + module Rails module Generators class AssetsGenerator < NamedBase # :nodoc: - class_option :javascripts, type: :boolean, desc: "Generate JavaScripts" class_option :stylesheets, type: :boolean, desc: "Generate Stylesheets" - - class_option :javascript_engine, desc: "Engine for JavaScripts" class_option :stylesheet_engine, desc: "Engine for Stylesheets" - protected - - def asset_name - file_name - end - - hook_for :javascript_engine do |javascript_engine| - invoke javascript_engine, [name] if options[:javascripts] - end + private + def asset_name + file_name + end - hook_for :stylesheet_engine do |stylesheet_engine| - invoke stylesheet_engine, [name] if options[:stylesheets] - end + hook_for :stylesheet_engine do |stylesheet_engine| + invoke stylesheet_engine, [name] if options[:stylesheets] + end end end end diff --git a/railties/lib/rails/generators/rails/assets/templates/javascript.js b/railties/lib/rails/generators/rails/assets/templates/javascript.js deleted file mode 100644 index dee720facd..0000000000 --- a/railties/lib/rails/generators/rails/assets/templates/javascript.js +++ /dev/null @@ -1,2 +0,0 @@ -// Place all the behaviors and hooks related to the matching controller here. -// All this logic will automatically be available in application.js. diff --git a/railties/lib/rails/generators/rails/assets/templates/stylesheet.css b/railties/lib/rails/generators/rails/assets/templates/stylesheet.css index 7594abf268..afad32db02 100644 --- a/railties/lib/rails/generators/rails/assets/templates/stylesheet.css +++ b/railties/lib/rails/generators/rails/assets/templates/stylesheet.css @@ -1,4 +1,4 @@ -/* +/* Place all the styles related to the matching controller here. They will automatically be included in application.css. */ diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb index 0a4c509a31..eb75e7e661 100644 --- a/railties/lib/rails/generators/rails/controller/controller_generator.rb +++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb @@ -1,59 +1,76 @@ +# frozen_string_literal: true + module Rails module Generators class ControllerGenerator < NamedBase # :nodoc: argument :actions, type: :array, default: [], banner: "action action" class_option :skip_routes, type: :boolean, desc: "Don't add routes to config/routes.rb." + class_option :helper, type: :boolean + class_option :assets, type: :boolean check_class_collision suffix: "Controller" def create_controller_files - template 'controller.rb', File.join('app/controllers', class_path, "#{file_name}_controller.rb") + template "controller.rb", File.join("app/controllers", class_path, "#{file_name}_controller.rb") end def add_routes - unless options[:skip_routes] - actions.reverse_each do |action| - # route prepends two spaces onto the front of the string that is passed, this corrects that. - route generate_routing_code(action)[2..-1] - end - end + return if options[:skip_routes] + return if actions.empty? + route generate_routing_code end - hook_for :template_engine, :test_framework - hook_for :helper, :assets, hide: true + hook_for :template_engine, :test_framework, :helper, :assets do |generator| + invoke generator, [ remove_possible_suffix(name), actions ] + end private + def file_name + @_file_name ||= remove_possible_suffix(super) + end + + def remove_possible_suffix(name) + name.sub(/_?controller$/i, "") + end + # This method creates nested route entry for namespaced resources. - # For eg. rails g controller foo/bar/baz index + # For eg. rails g controller foo/bar/baz index show # Will generate - # namespace :foo do # namespace :bar do # get 'baz/index' + # get 'baz/show' # end # end - def generate_routing_code(action) - depth = regular_class_path.length + def generate_routing_code + depth = 0 + lines = [] + # Create 'namespace' ladder # namespace :foo do # namespace :bar do - namespace_ladder = regular_class_path.each_with_index.map do |ns, i| - indent(" namespace :#{ns} do\n", i * 2) - end.join + regular_class_path.each do |ns| + lines << indent("namespace :#{ns} do\n", depth * 2) + depth += 1 + end # Create route # get 'baz/index' - route = indent(%{ get '#{file_name}/#{action}'\n}, depth * 2) + # get 'baz/show' + actions.each do |action| + lines << indent(%{get '#{file_name}/#{action}'\n}, depth * 2) + end # Create `end` ladder # end # end - end_ladder = (1..depth).reverse_each.map do |i| - indent("end\n", i * 2) - end.join + until depth.zero? + depth -= 1 + lines << indent("end\n", depth * 2) + end - # Combine the 3 parts to generate complete route entry - namespace_ladder + route + end_ladder + lines.join end end end diff --git a/railties/lib/rails/generators/rails/controller/templates/controller.rb b/railties/lib/rails/generators/rails/controller/templates/controller.rb.tt index 633e0b3177..633e0b3177 100644 --- a/railties/lib/rails/generators/rails/controller/templates/controller.rb +++ b/railties/lib/rails/generators/rails/controller/templates/controller.rb.tt diff --git a/railties/lib/rails/generators/rails/credentials/credentials_generator.rb b/railties/lib/rails/generators/rails/credentials/credentials_generator.rb new file mode 100644 index 0000000000..99b935aa6a --- /dev/null +++ b/railties/lib/rails/generators/rails/credentials/credentials_generator.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "rails/generators/base" +require "rails/generators/rails/master_key/master_key_generator" +require "active_support/encrypted_configuration" + +module Rails + module Generators + class CredentialsGenerator < Base # :nodoc: + def add_credentials_file + unless credentials.content_path.exist? + template = credentials_template + + say "Adding #{credentials.content_path} to store encrypted credentials." + say "" + say "The following content has been encrypted with the Rails master key:" + say "" + say template, :on_green + say "" + + add_credentials_file_silently(template) + + say "You can edit encrypted credentials with `rails credentials:edit`." + say "" + end + end + + def add_credentials_file_silently(template = nil) + unless credentials.content_path.exist? + credentials.write(credentials_template) + end + end + + private + def credentials + ActiveSupport::EncryptedConfiguration.new( + config_path: "config/credentials.yml.enc", + key_path: "config/master.key", + env_key: "RAILS_MASTER_KEY", + raise_if_missing_key: true + ) + end + + def credentials_template + <<~YAML + # aws: + # access_key_id: 123 + # secret_access_key: 345 + + # Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies. + secret_key_base: #{SecureRandom.hex(64)} + YAML + end + end + end +end diff --git a/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb b/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb new file mode 100644 index 0000000000..867e28c6db --- /dev/null +++ b/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "rails/generators/base" +require "active_support/encrypted_file" + +module Rails + module Generators + class EncryptedFileGenerator < Base # :nodoc: + def add_encrypted_file_silently(file_path, key_path, template = encrypted_file_template) + unless File.exist?(file_path) + setup = { content_path: file_path, key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: true } + ActiveSupport::EncryptedFile.new(setup).write(template) + end + end + + private + def encrypted_file_template + <<~YAML + # aws: + # access_key_id: 123 + # secret_access_key: 345 + + YAML + end + end + end +end diff --git a/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb b/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb new file mode 100644 index 0000000000..e2359e9ded --- /dev/null +++ b/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require "pathname" +require "rails/generators/base" +require "active_support/encrypted_file" + +module Rails + module Generators + class EncryptionKeyFileGenerator < Base # :nodoc: + def add_key_file(key_path) + key_path = Pathname.new(key_path) + + unless key_path.exist? + key = ActiveSupport::EncryptedFile.generate_key + + log "Adding #{key_path} to store the encryption key: #{key}" + log "" + log "Save this in a password manager your team can access." + log "" + log "If you lose the key, no one, including you, can access anything encrypted with it." + + log "" + add_key_file_silently(key_path, key) + log "" + end + end + + def add_key_file_silently(key_path, key = nil) + create_file key_path, key || ActiveSupport::EncryptedFile.generate_key + key_path.chmod 0600 + end + + def ignore_key_file(key_path, ignore: key_ignore(key_path)) + if File.exist?(".gitignore") + unless File.read(".gitignore").include?(ignore) + log "Ignoring #{key_path} so it won't end up in Git history:" + log "" + append_to_file ".gitignore", ignore + log "" + end + else + log "IMPORTANT: Don't commit #{key_path}. Add this to your ignore file:" + log ignore, :on_green + log "" + end + end + + def ignore_key_file_silently(key_path, ignore: key_ignore(key_path)) + append_to_file ".gitignore", ignore if File.exist?(".gitignore") + end + + private + def key_ignore(key_path) + [ "", "/#{key_path}", "" ].join("\n") + end + end + end +end diff --git a/railties/lib/rails/generators/rails/generator/generator_generator.rb b/railties/lib/rails/generators/rails/generator/generator_generator.rb index 15d88f06ac..747acd68d1 100644 --- a/railties/lib/rails/generators/rails/generator/generator_generator.rb +++ b/railties/lib/rails/generators/rails/generator/generator_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails module Generators class GeneratorGenerator < NamedBase # :nodoc: @@ -7,12 +9,12 @@ module Rails desc: "Namespace generator under lib/generators/name" def create_generator_files - directory '.', generator_dir + directory ".", generator_dir end hook_for :test_framework - protected + private def generator_dir if options[:namespace] @@ -21,7 +23,6 @@ module Rails File.join("lib", "generators", regular_class_path) end end - end end end diff --git a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt index d0575772bc..178d5c3f9f 100644 --- a/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt +++ b/railties/lib/rails/generators/rails/generator/templates/%file_name%_generator.rb.tt @@ -1,3 +1,3 @@ class <%= class_name %>Generator < Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) end diff --git a/railties/lib/rails/generators/rails/helper/helper_generator.rb b/railties/lib/rails/generators/rails/helper/helper_generator.rb index 5ff38e4111..3837c10ca0 100644 --- a/railties/lib/rails/generators/rails/helper/helper_generator.rb +++ b/railties/lib/rails/generators/rails/helper/helper_generator.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module Rails module Generators class HelperGenerator < NamedBase # :nodoc: check_class_collision suffix: "Helper" def create_helper_files - template 'helper.rb', File.join('app/helpers', class_path, "#{file_name}_helper.rb") + template "helper.rb", File.join("app/helpers", class_path, "#{file_name}_helper.rb") end hook_for :test_framework diff --git a/railties/lib/rails/generators/rails/helper/templates/helper.rb b/railties/lib/rails/generators/rails/helper/templates/helper.rb.tt index b4173151b4..b4173151b4 100644 --- a/railties/lib/rails/generators/rails/helper/templates/helper.rb +++ b/railties/lib/rails/generators/rails/helper/templates/helper.rb.tt diff --git a/railties/lib/rails/generators/rails/integration_test/integration_test_generator.rb b/railties/lib/rails/generators/rails/integration_test/integration_test_generator.rb index 70770ddcb8..975dd8b90c 100644 --- a/railties/lib/rails/generators/rails/integration_test/integration_test_generator.rb +++ b/railties/lib/rails/generators/rails/integration_test/integration_test_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails module Generators class IntegrationTestGenerator < NamedBase # :nodoc: diff --git a/railties/lib/rails/generators/rails/master_key/master_key_generator.rb b/railties/lib/rails/generators/rails/master_key/master_key_generator.rb new file mode 100644 index 0000000000..21664ea86d --- /dev/null +++ b/railties/lib/rails/generators/rails/master_key/master_key_generator.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "pathname" +require "rails/generators/base" +require "rails/generators/rails/encryption_key_file/encryption_key_file_generator" +require "active_support/encrypted_file" + +module Rails + module Generators + class MasterKeyGenerator < Base # :nodoc: + MASTER_KEY_PATH = Pathname.new("config/master.key") + + def add_master_key_file + unless MASTER_KEY_PATH.exist? + key = ActiveSupport::EncryptedFile.generate_key + + log "Adding #{MASTER_KEY_PATH} to store the master encryption key: #{key}" + log "" + log "Save this in a password manager your team can access." + log "" + log "If you lose the key, no one, including you, can access anything encrypted with it." + + log "" + add_master_key_file_silently(key) + log "" + end + end + + def add_master_key_file_silently(key = nil) + unless MASTER_KEY_PATH.exist? + key_file_generator.add_key_file_silently(MASTER_KEY_PATH, key) + end + end + + def ignore_master_key_file + key_file_generator.ignore_key_file(MASTER_KEY_PATH, ignore: key_ignore) + end + + def ignore_master_key_file_silently + key_file_generator.ignore_key_file_silently(MASTER_KEY_PATH, ignore: key_ignore) + end + + private + def key_file_generator + EncryptionKeyFileGenerator.new([], options) + end + + def key_ignore + [ "", "# Ignore master key for decrypting credentials and more.", "/#{MASTER_KEY_PATH}", "" ].join("\n") + end + end + end +end diff --git a/railties/lib/rails/generators/rails/migration/migration_generator.rb b/railties/lib/rails/generators/rails/migration/migration_generator.rb index fca2a8fef4..c331c135e3 100644 --- a/railties/lib/rails/generators/rails/migration/migration_generator.rb +++ b/railties/lib/rails/generators/rails/migration/migration_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails module Generators class MigrationGenerator < NamedBase # :nodoc: diff --git a/railties/lib/rails/generators/rails/model/USAGE b/railties/lib/rails/generators/rails/model/USAGE index 11daa5c3cb..025bcf4774 100644 --- a/railties/lib/rails/generators/rails/model/USAGE +++ b/railties/lib/rails/generators/rails/model/USAGE @@ -8,14 +8,14 @@ Description: As a special case, specifying 'password:digest' will generate a password_digest field of string type, and configure your generated model and - tests for use with ActiveModel has_secure_password (assuming the default ORM + tests for use with Active Model has_secure_password (assuming the default ORM and test framework are being used). You don't have to think up every attribute up front, but it helps to sketch out a few so you can start working with the model immediately. This generator invokes your configured ORM and test framework, which - defaults to ActiveRecord and TestUnit. + defaults to Active Record and TestUnit. Finally, if --parent option is given, it's used as superclass of the created model. This allows you create Single Table Inheritance models. @@ -91,7 +91,7 @@ Available field types: Examples: `rails generate model account` - For ActiveRecord and TestUnit it creates: + For Active Record and TestUnit it creates: Model: app/models/account.rb Test: test/models/account_test.rb @@ -104,7 +104,7 @@ Examples: `rails generate model admin/account` - For ActiveRecord and TestUnit it creates: + For Active Record and TestUnit it creates: Module: app/models/admin.rb Model: app/models/admin/account.rb diff --git a/railties/lib/rails/generators/rails/model/model_generator.rb b/railties/lib/rails/generators/rails/model/model_generator.rb index ec78fd855d..de4de2cae2 100644 --- a/railties/lib/rails/generators/rails/model/model_generator.rb +++ b/railties/lib/rails/generators/rails/model/model_generator.rb @@ -1,4 +1,6 @@ -require 'rails/generators/model_helpers' +# frozen_string_literal: true + +require "rails/generators/model_helpers" module Rails module Generators diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 66111004aa..239b3a5739 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -1,6 +1,7 @@ -require 'active_support/core_ext/hash/slice' +# frozen_string_literal: true + require "rails/generators/rails/app/app_generator" -require 'date' +require "date" module Rails # The plugin builder allows you to override elements of the plugin @@ -17,20 +18,27 @@ module Rails def app if mountable? - directory 'app' - empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" + if api? + directory "app", exclude_pattern: %r{app/(views|helpers)} + else + directory "app" + empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" + end elsif full? - empty_directory_with_keep_file 'app/models' - empty_directory_with_keep_file 'app/controllers' - empty_directory_with_keep_file 'app/views' - empty_directory_with_keep_file 'app/helpers' - empty_directory_with_keep_file 'app/mailers' - empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" + empty_directory_with_keep_file "app/models" + empty_directory_with_keep_file "app/controllers" + empty_directory_with_keep_file "app/mailers" + + unless api? + empty_directory_with_keep_file "app/assets/images/#{namespaced_name}" + empty_directory_with_keep_file "app/helpers" + empty_directory_with_keep_file "app/views" + end end end def readme - template "README.rdoc" + template "README.md" end def gemfile @@ -53,7 +61,12 @@ module Rails template "lib/%namespaced_name%.rb" template "lib/tasks/%namespaced_name%_tasks.rake" template "lib/%namespaced_name%/version.rb" - template "lib/%namespaced_name%/engine.rb" if engine? + + if engine? + template "lib/%namespaced_name%/engine.rb" + else + template "lib/%namespaced_name%/railtie.rb" + end end def config @@ -64,8 +77,8 @@ module Rails template "test/test_helper.rb" template "test/%namespaced_name%_test.rb" append_file "Rakefile", <<-EOF -#{rakefile_test_tasks} +#{rakefile_test_tasks} task default: :test EOF if engine? @@ -74,14 +87,19 @@ task default: :test end PASSTHROUGH_OPTIONS = [ - :skip_active_record, :skip_action_mailer, :skip_javascript, :database, - :javascript, :quiet, :pretend, :force, :skip + :skip_active_record, :skip_active_storage, :skip_action_mailer, :skip_javascript, :skip_action_cable, :skip_sprockets, :database, + :api, :quiet, :pretend, :skip ] def generate_test_dummy(force = false) - opts = (options || {}).slice(*PASSTHROUGH_OPTIONS) + opts = (options.dup || {}).keep_if { |k, _| PASSTHROUGH_OPTIONS.map(&:to_s).include?(k) } opts[:force] = force opts[:skip_bundle] = true + opts[:skip_listen] = true + opts[:skip_git] = true + opts[:skip_turbolinks] = true + opts[:skip_webpack_install] = true + opts[:dummy_app] = true invoke Rails::Generators::AppGenerator, [ File.expand_path(dummy_path, destination_root) ], opts @@ -96,24 +114,27 @@ task default: :test end def test_dummy_assets - template "rails/javascripts.js", "#{dummy_path}/app/assets/javascripts/application.js", force: true - template "rails/stylesheets.css", "#{dummy_path}/app/assets/stylesheets/application.css", force: true + template "rails/javascripts.js", "#{dummy_path}/app/javascript/packs/application.js", force: true + template "rails/stylesheets.css", "#{dummy_path}/app/assets/stylesheets/application.css", force: true + template "rails/dummy_manifest.js", "#{dummy_path}/app/assets/config/manifest.js", force: true end def test_dummy_clean inside dummy_path do - remove_file ".gitignore" remove_file "db/seeds.rb" - remove_file "doc" remove_file "Gemfile" remove_file "lib/tasks" remove_file "public/robots.txt" - remove_file "README" + remove_file "README.md" remove_file "test" remove_file "vendor" end end + def assets_manifest + template "rails/engine_manifest.js", "app/assets/config/#{underscored_name}_manifest.js" + end + def stylesheets if mountable? copy_file "rails/stylesheets.css", @@ -135,9 +156,8 @@ task default: :test end def bin(force = false) - return unless engine? - - directory "bin", force: force do |content| + bin_file = engine? ? "bin/rails.tt" : "bin/test.tt" + template bin_file, force: force do |content| "#{shebang}\n" + content end chmod "bin", 0755, verbose: false @@ -148,7 +168,7 @@ task default: :test gemfile_in_app_path = File.join(rails_app_path, "Gemfile") if File.exist? gemfile_in_app_path - entry = "gem '#{name}', path: '#{relative_path}'" + entry = "\ngem '#{name}', path: '#{relative_path}'" append_file gemfile_in_app_path, entry end end @@ -173,16 +193,15 @@ task default: :test desc: "Skip gemspec file" class_option :skip_gemfile_entry, type: :boolean, default: false, - desc: "If creating plugin in application's directory " + + desc: "If creating plugin in application's directory " \ "skip adding entry to Gemfile" + class_option :api, type: :boolean, default: false, + desc: "Generate a smaller stack for API application plugins" + def initialize(*args) @dummy_path = nil super - - unless plugin_path - raise Error, "Plugin name should be provided in arguments. For details run: rails plugin new --help" - end end public_task :set_default_accessors! @@ -209,16 +228,16 @@ task default: :test build(:lib) end - def create_public_stylesheets_files - build(:stylesheets) + def create_assets_manifest_file + build(:assets_manifest) if !api? && engine? end - def create_javascript_files - build(:javascripts) + def create_public_stylesheets_files + build(:stylesheets) unless api? end - def create_images_directory - build(:images) + def create_javascript_files + build(:javascripts) unless api? end def create_bin_files @@ -242,14 +261,24 @@ task default: :test build(:leftovers) end - public_task :apply_rails_template, :run_bundle + public_task :apply_rails_template + + def run_after_bundle_callbacks + unless @after_bundle_callbacks.empty? + ActiveSupport::Deprecation.warn("`after_bundle` is deprecated and will be removed in the next version of Rails. ") + end + + @after_bundle_callbacks.each do |callback| + callback.call + end + end def name @name ||= begin # same as ActiveSupport::Inflector#underscore except not replacing '-' underscored = original_name.dup - underscored.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') - underscored.gsub!(/([a-z\d])([A-Z])/,'\1_\2') + underscored.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2') + underscored.gsub!(/([a-z\d])([A-Z])/, '\1_\2') underscored.downcase! underscored @@ -261,14 +290,10 @@ task default: :test end def namespaced_name - @namespaced_name ||= name.gsub('-', '/') + @namespaced_name ||= name.tr("-", "/") end - protected - - def app_templates_dir - "../../app/templates" - end + private def create_dummy_app(path = nil) dummy_path(path) if path @@ -286,7 +311,7 @@ task default: :test end def engine? - full? || mountable? + full? || mountable? || options[:engine] end def full? @@ -302,11 +327,15 @@ task default: :test end def with_dummy_app? - options[:skip_test].blank? || options[:dummy_path] != 'test/dummy' + options[:skip_test].blank? || options[:dummy_path] != "test/dummy" + end + + def api? + options[:api] end def self.banner - "rails plugin new #{self.arguments.map(&:usage).join(' ')} [options]" + "rails plugin new #{arguments.map(&:usage).join(' ')} [options]" end def original_name @@ -318,7 +347,7 @@ task default: :test end def wrap_in_modules(unwrapped_code) - unwrapped_code = "#{unwrapped_code}".strip.gsub(/\W$\n/, '') + unwrapped_code = "#{unwrapped_code}".strip.gsub(/\s$\n/, "") modules.reverse.inject(unwrapped_code) do |content, mod| str = "module #{mod}\n" str += content.lines.map { |line| " #{line}" }.join @@ -335,7 +364,7 @@ task default: :test end def camelized - @camelized ||= name.gsub(/\W/, '_').squeeze('_').camelize + @camelized ||= name.gsub(/\W/, "_").squeeze("_").camelize end def author @@ -357,11 +386,11 @@ task default: :test end def valid_const? - if original_name =~ /-\d/ + if /-\d/.match?(original_name) raise Error, "Invalid plugin name #{original_name}. Please give a name which does not contain a namespace starting with numeric characters." - elsif original_name =~ /[^\w-]+/ + elsif /[^\w-]+/.match?(original_name) raise Error, "Invalid plugin name #{original_name}. Please give a name which uses only alphabetic, numeric, \"_\" or \"-\" characters." - elsif camelized =~ /^\d/ + elsif /^\d/.match?(camelized) raise Error, "Invalid plugin name #{original_name}. Please give a name which does not start with numbers." elsif RESERVED_NAMES.include?(name) raise Error, "Invalid plugin name #{original_name}. Please give a " \ @@ -393,7 +422,6 @@ task default: :test require 'rake/testtask' Rake::TestTask.new(:test) do |t| - t.libs << 'lib' t.libs << 'test' t.pattern = 'test/**/*_test.rb' t.verbose = false @@ -415,12 +443,12 @@ end end def inside_application? - rails_app_path && app_path =~ /^#{rails_app_path}/ + rails_app_path && destination_root.start_with?(rails_app_path.to_s) end def relative_path return unless inside_application? - app_path.sub(/^#{rails_app_path}\//, '') + app_path.sub(/^#{rails_app_path}\//, "") end end end diff --git a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec deleted file mode 100644 index f8ece4fe73..0000000000 --- a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec +++ /dev/null @@ -1,27 +0,0 @@ -$:.push File.expand_path("../lib", __FILE__) - -# Maintain your gem's version: -require "<%= namespaced_name %>/version" - -# Describe your gem and declare its dependencies: -Gem::Specification.new do |s| - s.name = "<%= name %>" - s.version = <%= camelized_modules %>::VERSION - s.authors = ["<%= author %>"] - s.email = ["<%= email %>"] - s.homepage = "TODO" - s.summary = "TODO: Summary of <%= camelized_modules %>." - s.description = "TODO: Description of <%= camelized_modules %>." - s.license = "MIT" - - s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"] -<% unless options.skip_test? -%> - s.test_files = Dir["test/**/*"] -<% end -%> - - <%= '# ' if options.dev? || options.edge? -%>s.add_dependency "rails", "~> <%= Rails::VERSION::STRING %>" -<% unless options[:skip_active_record] -%> - - s.add_development_dependency "<%= gem_for_database %>" -<% end -%> -end diff --git a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt new file mode 100644 index 0000000000..405642c850 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec.tt @@ -0,0 +1,33 @@ +$:.push File.expand_path("lib", __dir__) + +# Maintain your gem's version: +require "<%= namespaced_name %>/version" + +# Describe your gem and declare its dependencies: +Gem::Specification.new do |spec| + spec.name = "<%= name %>" + spec.version = <%= camelized_modules %>::VERSION + spec.authors = ["<%= author %>"] + spec.email = ["<%= email %>"] + spec.homepage = "TODO" + spec.summary = "TODO: Summary of <%= camelized_modules %>." + spec.description = "TODO: Description of <%= camelized_modules %>." + spec.license = "MIT" + + # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' + # to allow pushing to a single host or delete this section to allow pushing to any host. + if spec.respond_to?(:metadata) + spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" + else + raise "RubyGems 2.0 or newer is required to protect against " \ + "public gem pushes." + end + + spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] + + <%= '# ' if options.dev? || options.edge? -%>spec.add_dependency "rails", "<%= Array(rails_version_specifier).join('", "') %>" +<% unless options[:skip_active_record] -%> + + spec.add_development_dependency "<%= gem_for_database[0] %>" +<% end -%> +end diff --git a/railties/lib/rails/generators/rails/plugin/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile.tt index 2c91c6a0ea..290259b4db 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Gemfile +++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile.tt @@ -1,7 +1,8 @@ source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } <% if options[:skip_gemspec] -%> -<%= '# ' if options.dev? || options.edge? -%>gem 'rails', '~> <%= Rails::VERSION::STRING %>' +<%= '# ' if options.dev? || options.edge? -%>gem 'rails', '<%= Array(rails_version_specifier).join("', '") %>' <% else -%> # Declare your gem's dependencies in <%= name %>.gemspec. # Bundler will treat runtime dependencies like base dependencies, and @@ -11,7 +12,7 @@ gemspec <% if options[:skip_gemspec] -%> group :development do - gem '<%= gem_for_database %>' + gem '<%= gem_for_database[0] %>' end <% else -%> # Declare any dependencies that are still in development here instead of in diff --git a/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE.tt index ff2fb3ba4e..ff2fb3ba4e 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE +++ b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/README.md.tt b/railties/lib/rails/generators/rails/plugin/templates/README.md.tt new file mode 100644 index 0000000000..1632409bea --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/README.md.tt @@ -0,0 +1,28 @@ +# <%= camelized_modules %> +Short description and motivation. + +## Usage +How to use my plugin. + +## Installation +Add this line to your application's Gemfile: + +```ruby +gem '<%= name %>' +``` + +And then execute: +```bash +$ bundle +``` + +Or install it yourself as: +```bash +$ gem install <%= name %> +``` + +## Contributing +Contribution directions go here. + +## License +The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). diff --git a/railties/lib/rails/generators/rails/plugin/templates/README.rdoc b/railties/lib/rails/generators/rails/plugin/templates/README.rdoc deleted file mode 100644 index 25983ca5da..0000000000 --- a/railties/lib/rails/generators/rails/plugin/templates/README.rdoc +++ /dev/null @@ -1,3 +0,0 @@ -= <%= camelized_modules %> - -This project rocks and uses MIT-LICENSE.
\ No newline at end of file diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt index bda55bae29..f3efe21cf1 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile +++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile.tt @@ -10,20 +10,19 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_dir = 'rdoc' rdoc.title = '<%= camelized_modules %>' rdoc.options << '--line-numbers' - rdoc.rdoc_files.include('README.rdoc') + rdoc.rdoc_files.include('README.md') rdoc.rdoc_files.include('lib/**/*.rb') end - <% if engine? && !options[:skip_active_record] && with_dummy_app? -%> -APP_RAKEFILE = File.expand_path("../<%= dummy_path -%>/Rakefile", __FILE__) -load 'rails/tasks/engine.rake' -<% end %> +APP_RAKEFILE = File.expand_path("<%= dummy_path -%>/Rakefile", __dir__) +load 'rails/tasks/engine.rake' +<% end -%> <% if engine? -%> -load 'rails/tasks/statistics.rake' -<% end %> +load 'rails/tasks/statistics.rake' +<% end -%> <% unless options[:skip_gemspec] -%> -Bundler::GemHelper.install_tasks -<% end %> +require 'bundler/gem_tasks' +<% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt index 7157e48c42..b86ef0f2f8 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt @@ -1,5 +1,6 @@ -<%= wrap_in_modules <<-rb.strip_heredoc - class ApplicationController < ActionController::Base +<%= wrap_in_modules <<~rb + class ApplicationController < ActionController::#{api? ? "API" : "Base"} + #{ api? ? '# ' : '' }protect_from_forgery with: :exception end rb %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt index 25d692732d..be078f36de 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt @@ -1,4 +1,4 @@ -<%= wrap_in_modules <<-rb.strip_heredoc +<%= wrap_in_modules <<~rb module ApplicationHelper end rb diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt new file mode 100644 index 0000000000..846863bc13 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt @@ -0,0 +1,5 @@ +<%= wrap_in_modules <<~rb + class ApplicationJob < ActiveJob::Base + end +rb +%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt new file mode 100644 index 0000000000..246e274348 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt @@ -0,0 +1,7 @@ +<%= wrap_in_modules <<~rb + class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' + end +rb +%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt new file mode 100644 index 0000000000..21465278be --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt @@ -0,0 +1,6 @@ +<%= wrap_in_modules <<~rb + class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + end +rb +%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt index 6bc480161d..6e54a1ce9d 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt @@ -2,9 +2,13 @@ <html> <head> <title><%= humanized %></title> + <%%= csrf_meta_tags %> + <%%= csp_meta_tag %> + <%%= stylesheet_link_tag "<%= namespaced_name %>/application", media: "all" %> + <%- unless options[:skip_javascript] -%> <%%= javascript_include_tag "<%= namespaced_name %>/application" %> - <%%= csrf_meta_tags %> + <%- end -%> </head> <body> diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt index 3edaac35c9..ee8e469da2 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt @@ -1,11 +1,30 @@ -# This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. +# This command will automatically be run when you run "rails" with Rails gems +# installed from the root of your application. -ENGINE_ROOT = File.expand_path('../..', __FILE__) -ENGINE_PATH = File.expand_path('../../lib/<%= namespaced_name -%>/engine', __FILE__) +ENGINE_ROOT = File.expand_path('..', __dir__) +ENGINE_PATH = File.expand_path('../lib/<%= namespaced_name -%>/engine', __dir__) +<% if with_dummy_app? -%> +APP_PATH = File.expand_path('../<%= dummy_path -%>/config/application', __dir__) +<% end -%> # Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +<% if include_all_railties? -%> require 'rails/all' +<% else -%> +require "rails" +# Pick the frameworks you want: +require "active_model/railtie" +require "active_job/railtie" +<%= comment_if :skip_active_record %>require "active_record/railtie" +<%= comment_if :skip_active_storage %>require "active_storage/engine" +require "action_controller/railtie" +<%= comment_if :skip_action_mailer %>require "action_mailer/railtie" +require "action_view/railtie" +<%= comment_if :skip_action_cable %>require "action_cable/engine" +<%= comment_if :skip_sprockets %>require "sprockets/railtie" +<%= comment_if :skip_test %>require "rails/test_unit/railtie" +<% end -%> require 'rails/engine/commands' diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt new file mode 100644 index 0000000000..8e7d321626 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt @@ -0,0 +1,4 @@ +$: << File.expand_path("../test", __dir__) + +require "bundler/setup" +require "rails/plugin/test" diff --git a/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb.tt index 154452bfe5..154452bfe5 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/gitignore b/railties/lib/rails/generators/rails/plugin/templates/gitignore deleted file mode 100644 index d524fcbc4e..0000000000 --- a/railties/lib/rails/generators/rails/plugin/templates/gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.bundle/ -log/*.log -pkg/ -<% unless options[:skip_test] && options[:dummy_path] == 'test/dummy' -%> -<%= dummy_path %>/db/*.sqlite3 -<%= dummy_path %>/db/*.sqlite3-journal -<%= dummy_path %>/log/*.log -<%= dummy_path %>/tmp/ -<%= dummy_path %>/.sass-cache -<% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt b/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt new file mode 100644 index 0000000000..0aabf09252 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt @@ -0,0 +1,18 @@ +.bundle/ +log/*.log +pkg/ +<% if with_dummy_app? -%> +<% if sqlite3? -%> +<%= dummy_path %>/db/*.sqlite3 +<%= dummy_path %>/db/*.sqlite3-journal +<% end -%> +<%= dummy_path %>/log/*.log +<% unless options[:skip_javascript] -%> +<%= dummy_path %>/node_modules/ +<%= dummy_path %>/yarn-error.log +<% end -%> +<% unless skip_active_storage? -%> +<%= dummy_path %>/storage/ +<% end -%> +<%= dummy_path %>/tmp/ +<% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb.tt index 40b1c4cee7..3285055eb7 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%.rb.tt @@ -1,5 +1,7 @@ <% if engine? -%> require "<%= namespaced_name %>/engine" - +<% else -%> +require "<%= namespaced_name %>/railtie" <% end -%> + <%= wrap_in_modules "# Your code goes here..." %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt index 17afd52177..4ec1804940 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt @@ -1,6 +1,7 @@ -<%= wrap_in_modules <<-rb.strip_heredoc +<%= wrap_in_modules <<~rb class Engine < ::Rails::Engine #{mountable? ? ' isolate_namespace ' + camelized_modules : ' '} + #{api? ? " config.generators.api_only = true" : ' '} end rb %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt new file mode 100644 index 0000000000..b853fabcc3 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt @@ -0,0 +1,5 @@ +<%= wrap_in_modules <<~rb + class Railtie < ::Rails::Railtie + end +rb +%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb deleted file mode 100644 index d257295988..0000000000 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb +++ /dev/null @@ -1 +0,0 @@ -<%= wrap_in_modules 'VERSION = "0.0.1"' %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt new file mode 100644 index 0000000000..b08f4ef9ae --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt @@ -0,0 +1 @@ +<%= wrap_in_modules "VERSION = '0.1.0'" %> diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt index 88a2c4120f..88a2c4120f 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake +++ b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb.tt index b1038c839e..06ffe2f1ed 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb.tt @@ -1,13 +1,18 @@ -require File.expand_path('../boot', __FILE__) +require_relative 'boot' <% if include_all_railties? -%> require 'rails/all' <% else -%> +require "rails" # Pick the frameworks you want: +require "active_model/railtie" +require "active_job/railtie" <%= comment_if :skip_active_record %>require "active_record/railtie" +<%= comment_if :skip_active_storage %>require "active_storage/engine" require "action_controller/railtie" <%= comment_if :skip_action_mailer %>require "action_mailer/railtie" require "action_view/railtie" +<%= comment_if :skip_action_cable %>require "action_cable/engine" <%= comment_if :skip_sprockets %>require "sprockets/railtie" <%= comment_if :skip_test %>require "rails/test_unit/railtie" <% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb deleted file mode 100644 index 6266cfc509..0000000000 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb +++ /dev/null @@ -1,5 +0,0 @@ -# Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) - -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) -$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb.tt new file mode 100644 index 0000000000..c9aef85d40 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb.tt @@ -0,0 +1,5 @@ +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) + +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js.tt b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js.tt new file mode 100644 index 0000000000..03937cf8ff --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js.tt @@ -0,0 +1,10 @@ +<% unless api? -%> +//= link_tree ../images +<% end -%> +<% unless options.skip_javascript -%> +//= link_directory ../javascripts .js +<% end -%> +//= link_directory ../stylesheets .css +<% if mountable? && !api? -%> +//= link <%= underscored_name %>_manifest.js +<% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js.tt b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js.tt new file mode 100644 index 0000000000..2f23844f5e --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js.tt @@ -0,0 +1,6 @@ +<% if mountable? -%> +<% if !options.skip_javascript -%> +//= link_directory ../javascripts/<%= namespaced_name %> .js +<% end -%> +//= link_directory ../stylesheets/<%= namespaced_name %> .css +<% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt index 8913b40f69..51049826bf 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt @@ -5,9 +5,13 @@ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. +// compiled file. JavaScript code in this file should be added after the last require_* statement. // // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. // +//= require rails-ujs +<% unless skip_active_storage? -%> +//= require activestorage +<% end -%> //= require_tree . diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb.tt index 673de44108..694510edc0 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb.tt @@ -1,4 +1,3 @@ Rails.application.routes.draw do - mount <%= camelized_modules %>::Engine => "/<%= name %>" end diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css index 0cdd2788d0..0ebd7fe829 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css @@ -7,7 +7,8 @@ * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS - * files in this directory. It is generally better to create a new file per style scope. + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. * *= require_tree . *= require_self diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt index 1ee05d7871..1ee05d7871 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb.tt new file mode 100644 index 0000000000..d19212abd5 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb.tt @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb.tt index f5d1ec2046..29e59d8407 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb.tt @@ -5,4 +5,3 @@ class NavigationTest < ActionDispatch::IntegrationTest # assert true # end end - diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt index 95adcc06ff..4f7a8d3d6e 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb.tt @@ -1,25 +1,29 @@ # Configure Rails Environment ENV["RAILS_ENV"] = "test" -require File.expand_path("../../<%= options[:dummy_path] -%>/config/environment.rb", __FILE__) +require_relative "<%= File.join('..', options[:dummy_path], 'config/environment') -%>" <% unless options[:skip_active_record] -%> -ActiveRecord::Migrator.migrations_paths = [File.expand_path("../../<%= options[:dummy_path] -%>/db/migrate", __FILE__)] +ActiveRecord::Migrator.migrations_paths = [File.expand_path("../<%= options[:dummy_path] -%>/db/migrate", __dir__)] <% if options[:mountable] -%> -ActiveRecord::Migrator.migrations_paths << File.expand_path('../../db/migrate', __FILE__) +ActiveRecord::Migrator.migrations_paths << File.expand_path('../db/migrate', __dir__) <% end -%> <% end -%> require "rails/test_help" -# Filter out Minitest backtrace while allowing backtrace from other libraries -# to be shown. +# Filter out the backtrace from minitest while preserving the one from other libraries. Minitest.backtrace_filter = Minitest::BacktraceFilter.new -# Load support files -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } +<% unless engine? -%> +require "rails/test_unit/reporter" +Rails::TestUnitReporter.executable = 'bin/test' +<% end -%> +<% unless options[:skip_active_record] -%> # Load fixtures from the engine if ActiveSupport::TestCase.respond_to?(:fixture_path=) - ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__) + ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) + ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" ActiveSupport::TestCase.fixtures :all end +<% end -%> diff --git a/railties/lib/rails/generators/rails/resource/USAGE b/railties/lib/rails/generators/rails/resource/USAGE index e359cd574f..66d0ee546a 100644 --- a/railties/lib/rails/generators/rails/resource/USAGE +++ b/railties/lib/rails/generators/rails/resource/USAGE @@ -1,6 +1,6 @@ Description: Stubs out a new resource including an empty model and controller suitable - for a restful, resource-oriented application. Pass the singular model name, + for a RESTful, resource-oriented application. Pass the singular model name, either CamelCased or under_scored, as the first argument, and an optional list of attribute pairs. diff --git a/railties/lib/rails/generators/rails/resource/resource_generator.rb b/railties/lib/rails/generators/rails/resource/resource_generator.rb index 3acf21df13..3ba25ef0fe 100644 --- a/railties/lib/rails/generators/rails/resource/resource_generator.rb +++ b/railties/lib/rails/generators/rails/resource/resource_generator.rb @@ -1,5 +1,7 @@ -require 'rails/generators/resource_helpers' -require 'rails/generators/rails/model/model_generator' +# frozen_string_literal: true + +require "rails/generators/resource_helpers" +require "rails/generators/rails/model/model_generator" module Rails module Generators diff --git a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb index 42705107ae..9a92991efe 100644 --- a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb +++ b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails module Generators class ResourceRouteGenerator < NamedBase # :nodoc: @@ -15,37 +17,32 @@ module Rails def add_resource_route return if options[:actions].present? - # iterates over all namespaces and opens up blocks - regular_class_path.each_with_index do |namespace, index| - write("namespace :#{namespace} do", index + 1) + depth = 0 + lines = [] + + # Create 'namespace' ladder + # namespace :foo do + # namespace :bar do + regular_class_path.each do |ns| + lines << indent("namespace :#{ns} do\n", depth * 2) + depth += 1 end # inserts the primary resource - write("resources :#{file_name.pluralize}", route_length + 1) + # Create route + # resources 'products' + lines << indent(%{resources :#{file_name.pluralize}\n}, depth * 2) - # ends blocks - regular_class_path.each_index do |index| - write("end", route_length - index) + # Create `end` ladder + # end + # end + until depth.zero? + depth -= 1 + lines << indent("end\n", depth * 2) end - # route prepends two spaces onto the front of the string that is passed, this corrects that. - # Also it adds a \n to the end of each line, as route already adds that - # we need to correct that too. - route route_string[2..-2] + route lines.join end - - private - def route_string - @route_string ||= "" - end - - def write(str, indent) - route_string << "#{" " * indent}#{str}\n" - end - - def route_length - regular_class_path.length - end end end end diff --git a/railties/lib/rails/generators/rails/scaffold/USAGE b/railties/lib/rails/generators/rails/scaffold/USAGE index d2e495758d..c9283eda87 100644 --- a/railties/lib/rails/generators/rails/scaffold/USAGE +++ b/railties/lib/rails/generators/rails/scaffold/USAGE @@ -16,7 +16,7 @@ Description: As a special case, specifying 'password:digest' will generate a password_digest field of string type, and configure your generated model, - controller, views, and test suite for use with ActiveModel + controller, views, and test suite for use with Active Model has_secure_password (assuming they are using Rails defaults). Timestamps are added by default, so you don't have to specify them by hand diff --git a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb index 17c32bfdb3..8beb7416c0 100644 --- a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb @@ -1,4 +1,6 @@ -require 'rails/generators/rails/resource/resource_generator' +# frozen_string_literal: true + +require "rails/generators/rails/resource/resource_generator" module Rails module Generators @@ -6,6 +8,7 @@ module Rails remove_hook_for :resource_controller remove_class_option :actions + class_option :api, type: :boolean class_option :stylesheets, type: :boolean, desc: "Generate Stylesheets" class_option :stylesheet_engine, desc: "Engine for Stylesheets" class_option :assets, type: :boolean diff --git a/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css index b7818883d1..cd4f3de38d 100644 --- a/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css +++ b/railties/lib/rails/generators/rails/scaffold/templates/scaffold.css @@ -1,13 +1,13 @@ body { background-color: #fff; color: #333; + margin: 33px; } body, p, ol, ul, td { font-family: verdana, arial, helvetica, sans-serif; font-size: 13px; line-height: 18px; - margin: 33px; } pre { @@ -34,9 +34,7 @@ th { } td { - padding-bottom: 7px; - padding-left: 5px; - padding-right: 5px; + padding: 0 5px 7px; } div.field, @@ -57,8 +55,7 @@ div.actions { #error_explanation { width: 450px; border: 2px solid red; - padding: 7px; - padding-bottom: 0; + padding: 7px 7px 0; margin-bottom: 20px; background-color: #f0f0f0; } @@ -68,8 +65,7 @@ div.actions { font-weight: bold; padding: 5px 5px 5px 15px; font-size: 12px; - margin: -7px; - margin-bottom: 0; + margin: -7px -7px 0; background-color: #c00; color: #fff; } @@ -78,3 +74,7 @@ div.actions { font-size: 12px; list-style: square; } + +label { + display: block; +} diff --git a/railties/lib/rails/generators/rails/scaffold_controller/USAGE b/railties/lib/rails/generators/rails/scaffold_controller/USAGE index 8ba4c5ccbc..28f229510b 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/USAGE +++ b/railties/lib/rails/generators/rails/scaffold_controller/USAGE @@ -12,7 +12,7 @@ Description: Example: `rails generate scaffold_controller CreditCard` - Credit card controller with URLs like /credit_card/debit. + Credit card controller with URLs like /credit_cards. Controller: app/controllers/credit_cards_controller.rb Test: test/controllers/credit_cards_controller_test.rb Views: app/views/credit_cards/index.html.erb [...] diff --git a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb index d0b8cad896..7030561a33 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb @@ -1,4 +1,6 @@ -require 'rails/generators/resource_helpers' +# frozen_string_literal: true + +require "rails/generators/resource_helpers" module Rails module Generators @@ -17,10 +19,14 @@ module Rails def create_controller_files template_file = options.api? ? "api_controller.rb" : "controller.rb" - template template_file, File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb") + template template_file, File.join("app/controllers", controller_class_path, "#{controller_file_name}_controller.rb") + end + + hook_for :template_engine, as: :scaffold do |template_engine| + invoke template_engine unless options.api? end - hook_for :template_engine, :test_framework, as: :scaffold + hook_for :test_framework, as: :scaffold # Invoke the helper using the controller name (pluralized) hook_for :helper, as: :scaffold do |invoked| diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt index bc3c9b3f6b..400afec6dc 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt @@ -1,5 +1,5 @@ <% if namespaced? -%> -require_dependency "<%= namespaced_file_path %>/application_controller" +require_dependency "<%= namespaced_path %>/application_controller" <% end -%> <% module_namespacing do -%> @@ -52,7 +52,7 @@ class <%= controller_class_name %>Controller < ApplicationController # Only allow a trusted parameter "white list" through. def <%= "#{singular_table_name}_params" %> <%- if attributes_names.empty? -%> - params[:<%= singular_table_name %>] + params.fetch(:<%= singular_table_name %>, {}) <%- else -%> params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>) <%- end -%> diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt index 2c3b04043f..05f1c2b2d3 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt @@ -1,5 +1,5 @@ <% if namespaced? -%> -require_dependency "<%= namespaced_file_path %>/application_controller" +require_dependency "<%= namespaced_path %>/application_controller" <% end -%> <% module_namespacing do -%> @@ -29,7 +29,7 @@ class <%= controller_class_name %>Controller < ApplicationController @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %> if @<%= orm_instance.save %> - redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %> + redirect_to <%= redirect_resource_name %>, notice: <%= "'#{human_name} was successfully created.'" %> else render :new end @@ -38,7 +38,7 @@ class <%= controller_class_name %>Controller < ApplicationController # PATCH/PUT <%= route_url %>/1 def update if @<%= orm_instance.update("#{singular_table_name}_params") %> - redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %> + redirect_to <%= redirect_resource_name %>, notice: <%= "'#{human_name} was successfully updated.'" %> else render :edit end @@ -59,7 +59,7 @@ class <%= controller_class_name %>Controller < ApplicationController # Only allow a trusted parameter "white list" through. def <%= "#{singular_table_name}_params" %> <%- if attributes_names.empty? -%> - params[:<%= singular_table_name %>] + params.fetch(:<%= singular_table_name %>, {}) <%- else -%> params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>) <%- end -%> diff --git a/railties/lib/rails/generators/rails/system_test/USAGE b/railties/lib/rails/generators/rails/system_test/USAGE new file mode 100644 index 0000000000..f11a99e008 --- /dev/null +++ b/railties/lib/rails/generators/rails/system_test/USAGE @@ -0,0 +1,10 @@ +Description: + Stubs out a new system test. Pass the name of the test, either + CamelCased or under_scored, as an argument. + + This generator invokes the current system tool, which defaults to + TestUnit. + +Example: + `rails generate system_test GeneralStories` creates a GeneralStories + system test in test/system/general_stories_test.rb diff --git a/railties/lib/rails/generators/rails/system_test/system_test_generator.rb b/railties/lib/rails/generators/rails/system_test/system_test_generator.rb new file mode 100644 index 0000000000..7169e1bd3b --- /dev/null +++ b/railties/lib/rails/generators/rails/system_test/system_test_generator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Rails + module Generators + class SystemTestGenerator < NamedBase # :nodoc: + hook_for :system_tests, as: :system + end + end +end diff --git a/railties/lib/rails/generators/rails/task/task_generator.rb b/railties/lib/rails/generators/rails/task/task_generator.rb index 754824ca0c..b7290a7447 100644 --- a/railties/lib/rails/generators/rails/task/task_generator.rb +++ b/railties/lib/rails/generators/rails/task/task_generator.rb @@ -1,12 +1,13 @@ +# frozen_string_literal: true + module Rails module Generators class TaskGenerator < NamedBase # :nodoc: argument :actions, type: :array, default: [], banner: "action action" def create_task_files - template 'task.rb', File.join('lib/tasks', "#{file_name}.rake") + template "task.rb", File.join("lib/tasks", "#{file_name}.rake") end - end end end diff --git a/railties/lib/rails/generators/rails/task/templates/task.rb b/railties/lib/rails/generators/rails/task/templates/task.rb.tt index 1e3ed5f158..1e3ed5f158 100644 --- a/railties/lib/rails/generators/rails/task/templates/task.rb +++ b/railties/lib/rails/generators/rails/task/templates/task.rb.tt diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb index 9c2037783e..5675faff70 100644 --- a/railties/lib/rails/generators/resource_helpers.rb +++ b/railties/lib/rails/generators/resource_helpers.rb @@ -1,12 +1,13 @@ -require 'rails/generators/active_model' -require 'rails/generators/model_helpers' +# frozen_string_literal: true + +require "rails/generators/active_model" +require "rails/generators/model_helpers" module Rails module Generators # Deal with controller names on scaffold and add some helpers to deal with # ActiveModel. module ResourceHelpers # :nodoc: - def self.included(base) #:nodoc: base.include(Rails::Generators::ModelHelpers) base.class_option :model_name, type: :string, desc: "ModelName to be used" @@ -18,14 +19,13 @@ module Rails controller_name = name if options[:model_name] self.name = options[:model_name] - assign_names!(self.name) + assign_names!(name) end assign_controller_names!(controller_name.pluralize) end - protected - + private attr_reader :controller_name, :controller_file_name def controller_class_path @@ -38,25 +38,25 @@ module Rails def assign_controller_names!(name) @controller_name = name - @controller_class_path = name.include?('/') ? name.split('/') : name.split('::') + @controller_class_path = name.include?("/") ? name.split("/") : name.split("::") @controller_class_path.map!(&:underscore) @controller_file_name = @controller_class_path.pop end def controller_file_path - @controller_file_path ||= (controller_class_path + [controller_file_name]).join('/') + @controller_file_path ||= (controller_class_path + [controller_file_name]).join("/") end def controller_class_name - (controller_class_path + [controller_file_name]).map!(&:camelize).join('::') + (controller_class_path + [controller_file_name]).map!(&:camelize).join("::") end def controller_i18n_scope - @controller_i18n_scope ||= controller_file_path.tr('/', '.') + @controller_i18n_scope ||= controller_file_path.tr("/", ".") end # Loads the ORM::Generators::ActiveModel class. This class is responsible - # to tell scaffold entities how to generate an specific method for the + # to tell scaffold entities how to generate a specific method for the # ORM. Check Rails::Generators::ActiveModel for more information. def orm_class @orm_class ||= begin @@ -74,7 +74,7 @@ module Rails end # Initialize ORM::Generators::ActiveModel to access instance methods. - def orm_instance(name=singular_table_name) + def orm_instance(name = singular_table_name) @orm_instance ||= orm_class.new(name) end end diff --git a/railties/lib/rails/generators/test_case.rb b/railties/lib/rails/generators/test_case.rb index 58592b4f8e..5c71bf0be9 100644 --- a/railties/lib/rails/generators/test_case.rb +++ b/railties/lib/rails/generators/test_case.rb @@ -1,8 +1,10 @@ -require 'rails/generators' -require 'rails/generators/testing/behaviour' -require 'rails/generators/testing/setup_and_teardown' -require 'rails/generators/testing/assertions' -require 'fileutils' +# frozen_string_literal: true + +require "rails/generators" +require "rails/generators/testing/behaviour" +require "rails/generators/testing/setup_and_teardown" +require "rails/generators/testing/assertions" +require "fileutils" module Rails module Generators @@ -14,7 +16,7 @@ module Rails # # class AppGeneratorTest < Rails::Generators::TestCase # tests AppGenerator - # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # destination File.expand_path("../tmp", __dir__) # end # # If you want to ensure your destination root is clean before running each test, @@ -22,7 +24,7 @@ module Rails # # class AppGeneratorTest < Rails::Generators::TestCase # tests AppGenerator - # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # destination File.expand_path("../tmp", __dir__) # setup :prepare_destination # end class TestCase < ActiveSupport::TestCase @@ -30,7 +32,6 @@ module Rails include Rails::Generators::Testing::SetupAndTeardown include Rails::Generators::Testing::Assertions include FileUtils - end end end diff --git a/railties/lib/rails/generators/test_unit.rb b/railties/lib/rails/generators/test_unit.rb index fe45c9e15d..1005ac557c 100644 --- a/railties/lib/rails/generators/test_unit.rb +++ b/railties/lib/rails/generators/test_unit.rb @@ -1,4 +1,6 @@ -require 'rails/generators/named_base' +# frozen_string_literal: true + +require "rails/generators/named_base" module TestUnit # :nodoc: module Generators # :nodoc: diff --git a/railties/lib/rails/generators/test_unit/controller/controller_generator.rb b/railties/lib/rails/generators/test_unit/controller/controller_generator.rb index b5aa581769..1a9ac6bf2a 100644 --- a/railties/lib/rails/generators/test_unit/controller/controller_generator.rb +++ b/railties/lib/rails/generators/test_unit/controller/controller_generator.rb @@ -1,4 +1,6 @@ -require 'rails/generators/test_unit' +# frozen_string_literal: true + +require "rails/generators/test_unit" module TestUnit # :nodoc: module Generators # :nodoc: @@ -7,8 +9,8 @@ module TestUnit # :nodoc: check_class_collision suffix: "ControllerTest" def create_test_files - template 'functional_test.rb', - File.join('test/controllers', class_path, "#{file_name}_controller_test.rb") + template "functional_test.rb", + File.join("test/controllers", class_path, "#{file_name}_controller_test.rb") end end end diff --git a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb.tt index 5a8a3ca5e0..ff41fef9e9 100644 --- a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb.tt @@ -1,11 +1,9 @@ require 'test_helper' <% module_namespacing do -%> -class <%= class_name %>ControllerTest < ActionController::TestCase +class <%= class_name %>ControllerTest < ActionDispatch::IntegrationTest <% if mountable_engine? -%> - setup do - @routes = Engine.routes - end + include Engine.routes.url_helpers <% end -%> <% if actions.empty? -%> @@ -15,7 +13,7 @@ class <%= class_name %>ControllerTest < ActionController::TestCase <% else -%> <% actions.each do |action| -%> test "should get <%= action %>" do - get :<%= action %> + get <%= url_helper_prefix %>_<%= action %>_url assert_response :success end diff --git a/railties/lib/rails/generators/test_unit/generator/generator_generator.rb b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb index d7307398ce..19be4f2f51 100644 --- a/railties/lib/rails/generators/test_unit/generator/generator_generator.rb +++ b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb @@ -1,4 +1,6 @@ -require 'rails/generators/test_unit' +# frozen_string_literal: true + +require "rails/generators/test_unit" module TestUnit # :nodoc: module Generators # :nodoc: @@ -9,10 +11,10 @@ module TestUnit # :nodoc: desc: "Namespace generator under lib/generators/name" def create_generator_files - template 'generator_test.rb', File.join('test/lib/generators', class_path, "#{file_name}_generator_test.rb") + template "generator_test.rb", File.join("test/lib/generators", class_path, "#{file_name}_generator_test.rb") end - protected + private def generator_path if options[:namespace] diff --git a/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb b/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb.tt index a7f1fc4fba..a7f1fc4fba 100644 --- a/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb +++ b/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb.tt diff --git a/railties/lib/rails/generators/test_unit/helper/helper_generator.rb b/railties/lib/rails/generators/test_unit/helper/helper_generator.rb index bde4e88915..77308dcf7d 100644 --- a/railties/lib/rails/generators/test_unit/helper/helper_generator.rb +++ b/railties/lib/rails/generators/test_unit/helper/helper_generator.rb @@ -1,4 +1,6 @@ -require 'rails/generators/test_unit' +# frozen_string_literal: true + +require "rails/generators/test_unit" module TestUnit # :nodoc: module Generators # :nodoc: diff --git a/railties/lib/rails/generators/test_unit/integration/integration_generator.rb b/railties/lib/rails/generators/test_unit/integration/integration_generator.rb index e004835bd5..ae307c5cd9 100644 --- a/railties/lib/rails/generators/test_unit/integration/integration_generator.rb +++ b/railties/lib/rails/generators/test_unit/integration/integration_generator.rb @@ -1,4 +1,6 @@ -require 'rails/generators/test_unit' +# frozen_string_literal: true + +require "rails/generators/test_unit" module TestUnit # :nodoc: module Generators # :nodoc: @@ -6,7 +8,7 @@ module TestUnit # :nodoc: check_class_collision suffix: "Test" def create_test_files - template 'integration_test.rb', File.join('test/integration', class_path, "#{file_name}_test.rb") + template "integration_test.rb", File.join("test/integration", class_path, "#{file_name}_test.rb") end end end diff --git a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb.tt index dea7e22196..118e0f1271 100644 --- a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb +++ b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb.tt @@ -1,7 +1,9 @@ require 'test_helper' +<% module_namespacing do -%> class <%= class_name %>Test < ActionDispatch::IntegrationTest # test "the truth" do # assert true # end end +<% end -%> diff --git a/railties/lib/rails/generators/test_unit/job/job_generator.rb b/railties/lib/rails/generators/test_unit/job/job_generator.rb index 566b61ca66..1dae3cb6a5 100644 --- a/railties/lib/rails/generators/test_unit/job/job_generator.rb +++ b/railties/lib/rails/generators/test_unit/job/job_generator.rb @@ -1,13 +1,20 @@ -require 'rails/generators/test_unit' +# frozen_string_literal: true + +require "rails/generators/test_unit" module TestUnit # :nodoc: module Generators # :nodoc: class JobGenerator < Base # :nodoc: - check_class_collision suffix: 'JobTest' + check_class_collision suffix: "JobTest" def create_test_file - template 'unit_test.rb.erb', File.join('test/jobs', class_path, "#{file_name}_job_test.rb") + template "unit_test.rb", File.join("test/jobs", class_path, "#{file_name}_job_test.rb") end + + private + def file_name + @_file_name ||= super.sub(/_job\z/i, "") + end end end end diff --git a/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb b/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.tt index f5351d0ec6..f5351d0ec6 100644 --- a/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb +++ b/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.tt diff --git a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb index 343c8a3949..ab8331f31c 100644 --- a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb @@ -1,4 +1,6 @@ -require 'rails/generators/test_unit' +# frozen_string_literal: true + +require "rails/generators/test_unit" module TestUnit # :nodoc: module Generators # :nodoc: @@ -10,16 +12,16 @@ module TestUnit # :nodoc: end def create_test_files - template "functional_test.rb", File.join('test/mailers', class_path, "#{file_name}_mailer_test.rb") + template "functional_test.rb", File.join("test/mailers", class_path, "#{file_name}_mailer_test.rb") end def create_preview_files - template "preview.rb", File.join('test/mailers/previews', class_path, "#{file_name}_mailer_preview.rb") + template "preview.rb", File.join("test/mailers/previews", class_path, "#{file_name}_mailer_preview.rb") end - protected + private def file_name - @_file_name ||= super.gsub(/\_mailer/i, '') + @_file_name ||= super.sub(/_mailer\z/i, "") end end end diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb.tt index a2f2d30de5..a2f2d30de5 100644 --- a/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/mailer/templates/functional_test.rb.tt diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb.tt index b063cbc47b..b063cbc47b 100644 --- a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb +++ b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb.tt diff --git a/railties/lib/rails/generators/test_unit/model/model_generator.rb b/railties/lib/rails/generators/test_unit/model/model_generator.rb index 086588750e..02d7502592 100644 --- a/railties/lib/rails/generators/test_unit/model/model_generator.rb +++ b/railties/lib/rails/generators/test_unit/model/model_generator.rb @@ -1,9 +1,10 @@ -require 'rails/generators/test_unit' +# frozen_string_literal: true + +require "rails/generators/test_unit" module TestUnit # :nodoc: module Generators # :nodoc: class ModelGenerator < Base # :nodoc: - RESERVED_YAML_KEYWORDS = %w(y yes n no true false on off null) argument :attributes, type: :array, default: [], banner: "field:type field:type" @@ -12,14 +13,14 @@ module TestUnit # :nodoc: check_class_collision suffix: "Test" def create_test_file - template 'unit_test.rb', File.join('test/models', class_path, "#{file_name}_test.rb") + template "unit_test.rb", File.join("test/models", class_path, "#{file_name}_test.rb") end hook_for :fixture_replacement def create_fixture_file if options[:fixture] && options[:fixture_replacement].nil? - template 'fixtures.yml', File.join('test/fixtures', class_path, "#{fixture_file_name}.yml") + template "fixtures.yml", File.join("test/fixtures", class_path, "#{fixture_file_name}.yml") end end diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt index 50ca61a35b..0681780c97 100644 --- a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml +++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt @@ -6,7 +6,7 @@ <%- if attribute.password_digest? -%> password_digest: <%%= BCrypt::Password.create('secret') %> <%- elsif attribute.reference? -%> - <%= yaml_key_value(attribute.column_name.sub(/_id$/, ''), attribute.default) %> + <%= yaml_key_value(attribute.column_name.sub(/_id$/, ''), attribute.default || name) %> <%- else -%> <%= yaml_key_value(attribute.column_name, attribute.default) %> <%- end -%> @@ -17,7 +17,7 @@ <% end -%> <% else -%> -# This model initially had no columns defined. If you add columns to the +# This model initially had no columns defined. If you add columns to the # model remove the '{}' from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # @@ -25,5 +25,5 @@ one: {} # column: value # two: {} -# column: value +# column: value <% end -%> diff --git a/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb b/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb.tt index c9bc7d5b90..c9bc7d5b90 100644 --- a/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb +++ b/railties/lib/rails/generators/test_unit/model/templates/unit_test.rb.tt diff --git a/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb b/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb index b5d4f38444..0657bc2389 100644 --- a/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/test_unit/plugin/plugin_generator.rb @@ -1,4 +1,6 @@ -require 'rails/generators/test_unit' +# frozen_string_literal: true + +require "rails/generators/test_unit" module TestUnit # :nodoc: module Generators # :nodoc: @@ -6,7 +8,7 @@ module TestUnit # :nodoc: check_class_collision suffix: "Test" def create_test_files - directory '.', 'test' + directory ".", "test" end end end diff --git a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb index 0171da7cc7..e2e8b18eab 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb @@ -1,5 +1,7 @@ -require 'rails/generators/test_unit' -require 'rails/generators/resource_helpers' +# frozen_string_literal: true + +require "rails/generators/test_unit" +require "rails/generators/resource_helpers" module TestUnit # :nodoc: module Generators # :nodoc: @@ -11,18 +13,25 @@ module TestUnit # :nodoc: class_option :api, type: :boolean, desc: "Generates API functional tests" + class_option :system_tests, type: :string, + desc: "Skip system test files" + argument :attributes, type: :array, default: [], banner: "field:type field:type" def create_test_files template_file = options.api? ? "api_functional_test.rb" : "functional_test.rb" template template_file, File.join("test/controllers", controller_class_path, "#{controller_file_name}_controller_test.rb") + + if !options.api? && options[:system_tests] + template "system_test.rb", File.join("test/system", class_path, "#{file_name.pluralize}_test.rb") + end end def fixture_name @fixture_name ||= if mountable_engine? - "%s_%s" % [namespaced_path, table_name] + (namespace_dirs + [table_name]).join("_") else table_name end @@ -30,16 +39,20 @@ module TestUnit # :nodoc: private + def attributes_string + attributes_hash.map { |k, v| "#{k}: #{v}" }.join(", ") + end + def attributes_hash - return if attributes_names.empty? + return {} if attributes_names.empty? attributes_names.map do |name| if %w(password password_confirmation).include?(name) && attributes.any?(&:password_digest?) - "#{name}: 'secret'" + ["#{name}", "'secret'"] else - "#{name}: @#{singular_table_name}.#{name}" + ["#{name}", "@#{singular_table_name}.#{name}"] end - end.sort.join(', ') + end.sort.to_h end end end diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb.tt index 896b38bc8f..f21861d8e6 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb.tt @@ -1,37 +1,41 @@ require 'test_helper' <% module_namespacing do -%> -class <%= controller_class_name %>ControllerTest < ActionController::TestCase +class <%= controller_class_name %>ControllerTest < ActionDispatch::IntegrationTest + <%- if mountable_engine? -%> + include Engine.routes.url_helpers + + <%- end -%> setup do - @<%= singular_table_name %> = <%= table_name %>(:one) + @<%= singular_table_name %> = <%= fixture_name %>(:one) end test "should get index" do - get :index + get <%= index_helper %>_url, as: :json assert_response :success end test "should create <%= singular_table_name %>" do assert_difference('<%= class_name %>.count') do - post :create, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> } + post <%= index_helper %>_url, params: { <%= "#{singular_table_name}: { #{attributes_string} }" %> }, as: :json end assert_response 201 end test "should show <%= singular_table_name %>" do - get :show, params: { id: <%= "@#{singular_table_name}" %> } + get <%= show_helper %>, as: :json assert_response :success end test "should update <%= singular_table_name %>" do - patch :update, params: { id: <%= "@#{singular_table_name}" %>, <%= "#{singular_table_name}: { #{attributes_hash} }" %> } + patch <%= show_helper %>, params: { <%= "#{singular_table_name}: { #{attributes_string} }" %> }, as: :json assert_response 200 end test "should destroy <%= singular_table_name %>" do assert_difference('<%= class_name %>.count', -1) do - delete :destroy, params: { id: <%= "@#{singular_table_name}" %> } + delete <%= show_helper %>, as: :json end assert_response 204 diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb deleted file mode 100644 index 50b98b2631..0000000000 --- a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'test_helper' - -<% module_namespacing do -%> -class <%= controller_class_name %>ControllerTest < ActionController::TestCase - setup do - @<%= singular_table_name %> = <%= fixture_name %>(:one) -<% if mountable_engine? -%> - @routes = Engine.routes -<% end -%> - end - - test "should get index" do - get :index - assert_response :success - end - - test "should get new" do - get :new - assert_response :success - end - - test "should create <%= singular_table_name %>" do - assert_difference('<%= class_name %>.count') do - post :create, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> } - end - - assert_redirected_to <%= singular_table_name %>_path(<%= class_name %>.last) - end - - test "should show <%= singular_table_name %>" do - get :show, params: { id: <%= "@#{singular_table_name}" %> } - assert_response :success - end - - test "should get edit" do - get :edit, params: { id: <%= "@#{singular_table_name}" %> } - assert_response :success - end - - test "should update <%= singular_table_name %>" do - patch :update, params: { id: <%= "@#{singular_table_name}" %>, <%= "#{singular_table_name}: { #{attributes_hash} }" %> } - assert_redirected_to <%= singular_table_name %>_path(<%= "@#{singular_table_name}" %>) - end - - test "should destroy <%= singular_table_name %>" do - assert_difference('<%= class_name %>.count', -1) do - delete :destroy, params: { id: <%= "@#{singular_table_name}" %> } - end - - assert_redirected_to <%= index_helper %>_path - end -end -<% end -%> diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb.tt b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb.tt new file mode 100644 index 0000000000..195d60be20 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb.tt @@ -0,0 +1,54 @@ +require 'test_helper' + +<% module_namespacing do -%> +class <%= controller_class_name %>ControllerTest < ActionDispatch::IntegrationTest + <%- if mountable_engine? -%> + include Engine.routes.url_helpers + + <%- end -%> + setup do + @<%= singular_table_name %> = <%= fixture_name %>(:one) + end + + test "should get index" do + get <%= index_helper %>_url + assert_response :success + end + + test "should get new" do + get <%= new_helper %> + assert_response :success + end + + test "should create <%= singular_table_name %>" do + assert_difference('<%= class_name %>.count') do + post <%= index_helper %>_url, params: { <%= "#{singular_table_name}: { #{attributes_string} }" %> } + end + + assert_redirected_to <%= singular_table_name %>_url(<%= class_name %>.last) + end + + test "should show <%= singular_table_name %>" do + get <%= show_helper %> + assert_response :success + end + + test "should get edit" do + get <%= edit_helper %> + assert_response :success + end + + test "should update <%= singular_table_name %>" do + patch <%= show_helper %>, params: { <%= "#{singular_table_name}: { #{attributes_string} }" %> } + assert_redirected_to <%= singular_table_name %>_url(<%= "@#{singular_table_name}" %>) + end + + test "should destroy <%= singular_table_name %>" do + assert_difference('<%= class_name %>.count', -1) do + delete <%= show_helper %> + end + + assert_redirected_to <%= index_helper %>_url + end +end +<% end -%> diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt b/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt new file mode 100644 index 0000000000..15bd7956b6 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt @@ -0,0 +1,49 @@ +require "application_system_test_case" + +<% module_namespacing do -%> +class <%= class_name.pluralize %>Test < ApplicationSystemTestCase + setup do + @<%= singular_table_name %> = <%= fixture_name %>(:one) + end + + test "visiting the index" do + visit <%= plural_table_name %>_url + assert_selector "h1", text: "<%= class_name.pluralize.titleize %>" + end + + test "creating a <%= human_name %>" do + visit <%= plural_table_name %>_url + click_on "New <%= class_name.titleize %>" + + <%- attributes_hash.each do |attr, value| -%> + fill_in "<%= attr.humanize %>", with: <%= value %> + <%- end -%> + click_on "Create <%= human_name %>" + + assert_text "<%= human_name %> was successfully created" + click_on "Back" + end + + test "updating a <%= human_name %>" do + visit <%= plural_table_name %>_url + click_on "Edit", match: :first + + <%- attributes_hash.each do |attr, value| -%> + fill_in "<%= attr.humanize %>", with: <%= value %> + <%- end -%> + click_on "Update <%= human_name %>" + + assert_text "<%= human_name %> was successfully updated" + click_on "Back" + end + + test "destroying a <%= human_name %>" do + visit <%= plural_table_name %>_url + page.accept_confirm do + click_on "Destroy", match: :first + end + + assert_text "<%= human_name %> was successfully destroyed" + end +end +<% end -%> diff --git a/railties/lib/rails/generators/test_unit/system/system_generator.rb b/railties/lib/rails/generators/test_unit/system/system_generator.rb new file mode 100644 index 0000000000..08504d4124 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/system/system_generator.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "rails/generators/test_unit" + +module TestUnit # :nodoc: + module Generators # :nodoc: + class SystemGenerator < Base # :nodoc: + check_class_collision suffix: "Test" + + def create_test_files + if !File.exist?(File.join("test/application_system_test_case.rb")) + template "application_system_test_case.rb", File.join("test", "application_system_test_case.rb") + end + + template "system_test.rb", File.join("test/system", class_path, "#{file_name.pluralize}_test.rb") + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb.tt b/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb.tt new file mode 100644 index 0000000000..d19212abd5 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb.tt @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/railties/lib/rails/generators/test_unit/system/templates/system_test.rb.tt b/railties/lib/rails/generators/test_unit/system/templates/system_test.rb.tt new file mode 100644 index 0000000000..b5ce2ba5c8 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/system/templates/system_test.rb.tt @@ -0,0 +1,9 @@ +require "application_system_test_case" + +class <%= class_name.pluralize %>Test < ApplicationSystemTestCase + # test "visiting the index" do + # visit <%= plural_table_name %>_url + # + # assert_selector "h1", text: "<%= class_name %>" + # end +end diff --git a/railties/lib/rails/generators/testing/assertions.rb b/railties/lib/rails/generators/testing/assertions.rb index 17af6eddfa..c4cff9090b 100644 --- a/railties/lib/rails/generators/testing/assertions.rb +++ b/railties/lib/rails/generators/testing/assertions.rb @@ -1,4 +1,4 @@ -require 'shellwords' +# frozen_string_literal: true module Rails module Generators @@ -31,10 +31,10 @@ module Rails contents.each do |content| case content - when String - assert_equal content, read - when Regexp - assert_match content, read + when String + assert_equal content, read + when Regexp + assert_match content, read end end end @@ -115,7 +115,11 @@ module Rails # # assert_field_default_value :string, "MyString" def assert_field_default_value(attribute_type, value) - assert_equal(value, create_generated_attribute(attribute_type).default) + if value.nil? + assert_nil(create_generated_attribute(attribute_type).default) + else + assert_equal(value, create_generated_attribute(attribute_type).default) + end end end end diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb index c9700e1cd7..ec29ad12ba 100644 --- a/railties/lib/rails/generators/testing/behaviour.rb +++ b/railties/lib/rails/generators/testing/behaviour.rb @@ -1,10 +1,12 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/module/delegation' -require 'active_support/core_ext/hash/reverse_merge' -require 'active_support/core_ext/kernel/reporting' -require 'active_support/testing/stream' -require 'active_support/concern' -require 'rails/generators' +# frozen_string_literal: true + +require "active_support/core_ext/class/attribute" +require "active_support/core_ext/module/delegation" +require "active_support/core_ext/hash/reverse_merge" +require "active_support/core_ext/kernel/reporting" +require "active_support/testing/stream" +require "active_support/concern" +require "rails/generators" module Rails module Generators @@ -14,12 +16,12 @@ module Rails include ActiveSupport::Testing::Stream included do - class_attribute :destination_root, :current_path, :generator_class, :default_arguments - # Generators frequently change the current path using +FileUtils.cd+. # So we need to store the path at file load and revert back to it after each test. - self.current_path = File.expand_path(Dir.pwd) - self.default_arguments = [] + class_attribute :current_path, default: File.expand_path(Dir.pwd) + class_attribute :default_arguments, default: [] + class_attribute :destination_root + class_attribute :generator_class end module ClassMethods @@ -40,7 +42,7 @@ module Rails # Sets the destination of generator files: # - # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # destination File.expand_path("../tmp", __dir__) def destination(path) self.destination_root = path end @@ -51,7 +53,7 @@ module Rails # # class AppGeneratorTest < Rails::Generators::TestCase # tests AppGenerator - # destination File.expand_path("../tmp", File.dirname(__FILE__)) + # destination File.expand_path("../tmp", __dir__) # setup :prepare_destination # # test "database.yml is not created when skipping Active Record" do @@ -62,47 +64,50 @@ module Rails # # You can provide a configuration hash as second argument. This method returns the output # printed by the generator. - def run_generator(args=self.default_arguments, config={}) + def run_generator(args = default_arguments, config = {}) capture(:stdout) do - args += ['--skip-bundle'] unless args.include? '--dev' - self.generator_class.start(args, config.reverse_merge(destination_root: destination_root)) + args += ["--skip-bundle"] unless args.include? "--dev" + args |= ["--skip-bootsnap"] unless args.include? "--no-skip-bootsnap" + args |= ["--skip-webpack-install"] unless args.include? "--no-skip-webpack-install" + + generator_class.start(args, config.reverse_merge(destination_root: destination_root)) end end # Instantiate the generator. - def generator(args=self.default_arguments, options={}, config={}) - @generator ||= self.generator_class.new(args, options, config.reverse_merge(destination_root: destination_root)) + def generator(args = default_arguments, options = {}, config = {}) + @generator ||= generator_class.new(args, options, config.reverse_merge(destination_root: destination_root)) end # Create a Rails::Generators::GeneratedAttribute by supplying the # attribute type and, optionally, the attribute name: # # create_generated_attribute(:string, 'name') - def create_generated_attribute(attribute_type, name = 'test', index = nil) - Rails::Generators::GeneratedAttribute.parse([name, attribute_type, index].compact.join(':')) + def create_generated_attribute(attribute_type, name = "test", index = nil) + Rails::Generators::GeneratedAttribute.parse([name, attribute_type, index].compact.join(":")) end - protected + private - def destination_root_is_set? # :nodoc: + def destination_root_is_set? raise "You need to configure your Rails::Generators::TestCase destination root." unless destination_root end - def ensure_current_path # :nodoc: + def ensure_current_path cd current_path end - def prepare_destination # :nodoc: + # Clears all files and directories in destination. + def prepare_destination # :doc: rm_rf(destination_root) mkdir_p(destination_root) end - def migration_file_name(relative) # :nodoc: + def migration_file_name(relative) absolute = File.expand_path(relative, destination_root) - dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, '') + dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, "") Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first end - end end end diff --git a/railties/lib/rails/generators/testing/setup_and_teardown.rb b/railties/lib/rails/generators/testing/setup_and_teardown.rb index 73102a283f..4374aa5b8c 100644 --- a/railties/lib/rails/generators/testing/setup_and_teardown.rb +++ b/railties/lib/rails/generators/testing/setup_and_teardown.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails module Generators module Testing diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb index 5909446b66..b8173c8d11 100644 --- a/railties/lib/rails/info.rb +++ b/railties/lib/rails/info.rb @@ -1,12 +1,15 @@ +# frozen_string_literal: true + require "cgi" module Rails - # This module helps build the runtime properties used to display in the - # Rails::InfoController responses. Including the active Rails version, Ruby - # version, Rack version, and so on. + # This module helps build the runtime properties that are displayed in + # Rails::InfoController responses. These include the active Rails version, + # Ruby version, Rack version, and so on. module Info - mattr_accessor :properties - class << (@@properties = []) + mattr_accessor :properties, default: [] + + class << @@properties def names map(&:first) end @@ -38,65 +41,69 @@ module Rails alias inspect to_s def to_html - '<table>'.tap do |table| + (+"<table>").tap do |table| properties.each do |(name, value)| table << %(<tr><td class="name">#{CGI.escapeHTML(name.to_s)}</td>) formatted_value = if value.kind_of?(Array) - "<ul>" + value.map { |v| "<li>#{CGI.escapeHTML(v.to_s)}</li>" }.join + "</ul>" - else - CGI.escapeHTML(value.to_s) - end + "<ul>" + value.map { |v| "<li>#{CGI.escapeHTML(v.to_s)}</li>" }.join + "</ul>" + else + CGI.escapeHTML(value.to_s) + end table << %(<td class="value">#{formatted_value}</td></tr>) end - table << '</table>' + table << "</table>" end end + + def to_json + Hash[properties].to_json + end end # The Rails version. - property 'Rails version' do + property "Rails version" do Rails.version.to_s end # The Ruby version and platform, e.g. "2.0.0-p247 (x86_64-darwin12.4.0)". - property 'Ruby version' do + property "Ruby version" do "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})" end # The RubyGems version, if it's installed. - property 'RubyGems version' do + property "RubyGems version" do Gem::RubyGemsVersion end - property 'Rack version' do + property "Rack version" do ::Rack.release end - property 'JavaScript Runtime' do + property "JavaScript Runtime" do ExecJS.runtime.name end - property 'Middleware' do + property "Middleware" do Rails.configuration.middleware.map(&:inspect) end # The application's location on the filesystem. - property 'Application root' do + property "Application root" do File.expand_path(Rails.root) end # The current Rails environment (development, test, or production). - property 'Environment' do + property "Environment" do Rails.env end # The name of the database adapter for the current environment. - property 'Database adapter' do - ActiveRecord::Base.configurations[Rails.env]['adapter'] + property "Database adapter" do + ActiveRecord::Base.configurations[Rails.env]["adapter"] end - property 'Database schema version' do - ActiveRecord::Migrator.current_version rescue nil + property "Database schema version" do + ActiveRecord::Base.connection.migration_context.current_version rescue nil end end end diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb index 778105c5f7..50fe176946 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -1,9 +1,11 @@ -require 'rails/application_controller' -require 'action_dispatch/routing/inspector' +# frozen_string_literal: true + +require "rails/application_controller" +require "action_dispatch/routing/inspector" class Rails::InfoController < Rails::ApplicationController # :nodoc: prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH - layout -> { request.xhr? ? false : 'application' } + layout -> { request.xhr? ? false : "application" } before_action :require_local! @@ -12,8 +14,16 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc: end def properties - @info = Rails::Info.to_html - @page_title = 'Properties' + respond_to do |format| + format.html do + @info = Rails::Info.to_html + @page_title = "Properties" + end + + format.json do + render json: Rails::Info.to_json + end + end end def routes @@ -21,24 +31,24 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc: path = URI.parser.escape path normalized_path = with_leading_slash path render json: { - exact: match_route {|it| it.match normalized_path }, - fuzzy: match_route {|it| it.spec.to_s.match path } + exact: match_route { |it| it.match normalized_path }, + fuzzy: match_route { |it| it.spec.to_s.match path } } else @routes_inspector = ActionDispatch::Routing::RoutesInspector.new(_routes.routes) - @page_title = 'Routes' + @page_title = "Routes" end end private - def match_route - _routes.routes.select {|route| - yield route.path - }.map {|route| route.path.spec.to_s } - end + def match_route + _routes.routes.select { |route| + yield route.path + }.map { |route| route.path.spec.to_s } + end - def with_leading_slash(path) - ('/' + path).squeeze('/') - end + def with_leading_slash(path) + ("/" + path).squeeze("/") + end end diff --git a/railties/lib/rails/initializable.rb b/railties/lib/rails/initializable.rb index 1a0b6d1e1a..5410e17153 100644 --- a/railties/lib/rails/initializable.rb +++ b/railties/lib/rails/initializable.rb @@ -1,4 +1,6 @@ -require 'tsort' +# frozen_string_literal: true + +require "tsort" module Rails module Initializable @@ -34,6 +36,10 @@ module Rails return self if @context Initializer.new(@name, context, @options, &block) end + + def context_class + @context.class + end end class Collection < Array @@ -49,7 +55,7 @@ module Rails end end - def run_initializers(group=:default, *args) + def run_initializers(group = :default, *args) return if instance_variable_defined?(:@ran) initializers.tsort_each do |initializer| initializer.run(*args) if initializer.belongs_to?(group) diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb index 41422a656c..e2d36d7654 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -1,10 +1,16 @@ -require 'rails/application_controller' +# frozen_string_literal: true + +require "rails/application_controller" class Rails::MailersController < Rails::ApplicationController # :nodoc: prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH - before_action :require_local! - before_action :find_preview, only: :preview + before_action :require_local!, unless: :show_previews? + before_action :find_preview, :set_locale, only: :preview + + helper_method :part_query, :locale_query + + content_security_policy(false) def index @previews = ActionMailer::Preview.all @@ -14,25 +20,25 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: def preview if params[:path] == @preview.preview_name @page_title = "Mailer Previews for #{@preview.preview_name}" - render action: 'mailer' + render action: "mailer" else @email_action = File.basename(params[:path]) if @preview.email_exists?(@email_action) - @email = @preview.call(@email_action) + @email = @preview.call(@email_action, params) if params[:part] part_type = Mime::Type.lookup(params[:part]) if part = find_part(part_type) response.content_type = part_type - render text: part.respond_to?(:decoded) ? part.decoded : part + render plain: part.respond_to?(:decoded) ? part.decoded : part else raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{@email_action}" end else - @part = find_preferred_part(request.format, Mime::HTML, Mime::TEXT) - render action: 'email', layout: false, formats: %w[html] + @part = find_preferred_part(request.format, Mime[:html], Mime[:text]) + render action: "email", layout: false, formats: %w[html] end else raise AbstractController::ActionNotFound, "Email '#{@email_action}' not found in #{@preview.name}" @@ -40,11 +46,15 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: end end - protected - def find_preview + private + def show_previews? # :doc: + ActionMailer::Base.show_previews + end + + def find_preview # :doc: candidates = [] - params[:path].to_s.scan(%r{/|$}){ candidates << $` } - preview = candidates.detect{ |candidate| ActionMailer::Preview.exists?(candidate) } + params[:path].to_s.scan(%r{/|$}) { candidates << $` } + preview = candidates.detect { |candidate| ActionMailer::Preview.exists?(candidate) } if preview @preview = ActionMailer::Preview.find(preview) @@ -53,23 +63,35 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: end end - def find_preferred_part(*formats) + def find_preferred_part(*formats) # :doc: formats.each do |format| if part = @email.find_first_mime_type(format) return part end end - if formats.any?{ |f| @email.mime_type == f } + if formats.any? { |f| @email.mime_type == f } @email end end - def find_part(format) + def find_part(format) # :doc: if part = @email.find_first_mime_type(format) part elsif @email.mime_type == format @email end end + + def part_query(mime_type) + request.query_parameters.merge(part: mime_type).to_query + end + + def locale_query(locale) + request.query_parameters.merge(locale: locale).to_query + end + + def set_locale + I18n.locale = params[:locale] || I18n.default_locale + end end diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index ebcaaaba46..8367ac8980 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + module Rails module Paths # This object is an extended hash that behaves as root of the <tt>Rails::Paths</tt> system. # It allows you to collect information about how you want to structure your application - # paths by a Hash like API. It requires you to give a physical path on initialization. + # paths through a Hash-like API. It requires you to give a physical path on initialization. # # root = Root.new "/rails" # root.add "app/controllers", eager_load: true # - # The command above creates a new root object and adds "app/controllers" as a path. + # The above command creates a new root object and adds "app/controllers" as a path. # This means we can get a <tt>Rails::Paths::Path</tt> object back like below: # # path = root["app/controllers"] @@ -30,7 +32,7 @@ module Rails # root["config/routes"].inspect # => ["config/routes.rb"] # # The +add+ method accepts the following options as arguments: - # eager_load, autoload, autoload_once and glob. + # eager_load, autoload, autoload_once, and glob. # # Finally, the +Path+ object also provides a few helpers: # @@ -45,7 +47,6 @@ module Rails attr_accessor :path def initialize(path) - @current = nil @path = path @root = {} end @@ -112,10 +113,11 @@ module Rails attr_accessor :glob def initialize(root, current, paths, options = {}) - @paths = paths - @current = current - @root = root - @glob = options[:glob] + @paths = paths + @current = current + @root = root + @glob = options[:glob] + @exclude = options[:exclude] options[:autoload_once] ? autoload_once! : skip_autoload_once! options[:eager_load] ? eager_load! : skip_eager_load! @@ -123,6 +125,10 @@ module Rails options[:load_path] ? load_path! : skip_load_path! end + def absolute_current # :nodoc: + File.expand_path(@current, @root.path) + end + def children keys = @root.keys.find_all { |k| k.start_with?(@current) && k != @current @@ -175,18 +181,20 @@ module Rails @paths end + def extensions # :nodoc: + $1.split(",") if @glob =~ /\{([\S]+)\}/ + end + # Expands all paths against the root and return all unique values. def expanded raise "You need to set a path root" unless @root.path result = [] - each do |p| - path = File.expand_path(p, @root.path) + each do |path| + path = File.expand_path(path, @root.path) if @glob && File.directory?(path) - Dir.chdir(path) do - result.concat(Dir.glob(@glob).map { |file| File.join path, file }.sort) - end + result.concat files_in(path) else result << path end @@ -198,7 +206,14 @@ module Rails # Returns all expanded paths but only if they exist in the filesystem. def existent - expanded.select { |f| File.exist?(f) } + expanded.select do |f| + does_exist = File.exist?(f) + + if !does_exist && File.symlink?(f) + raise "File #{f.inspect} is a symlink that does not point to a valid file" + end + does_exist + end end def existent_directories @@ -206,6 +221,17 @@ module Rails end alias to_a expanded + + private + + def files_in(path) + Dir.chdir(path) do + files = Dir.glob(@glob) + files -= @exclude if @exclude + files.map! { |file| File.join(path, file) } + files.sort + end + end end end end diff --git a/railties/lib/rails/plugin/test.rb b/railties/lib/rails/plugin/test.rb new file mode 100644 index 0000000000..18b6fd1757 --- /dev/null +++ b/railties/lib/rails/plugin/test.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "rails/test_unit/runner" +require "rails/test_unit/reporter" + +Rails::TestUnitReporter.executable = "bin/test" + +Rails::TestUnit::Runner.parse_options(ARGV) +Rails::TestUnit::Runner.run(ARGV) diff --git a/railties/lib/rails/rack.rb b/railties/lib/rails/rack.rb index a4c4527a72..579fb25cc4 100644 --- a/railties/lib/rails/rack.rb +++ b/railties/lib/rails/rack.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails module Rack autoload :Logger, "rails/rack/logger" diff --git a/railties/lib/rails/rack/debugger.rb b/railties/lib/rails/rack/debugger.rb deleted file mode 100644 index 1fde3db070..0000000000 --- a/railties/lib/rails/rack/debugger.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'active_support/deprecation' - -ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb index 12676b18bc..3a95b55811 100644 --- a/railties/lib/rails/rack/logger.rb +++ b/railties/lib/rails/rack/logger.rb @@ -1,8 +1,10 @@ -require 'active_support/core_ext/time/conversions' -require 'active_support/core_ext/object/blank' -require 'active_support/log_subscriber' -require 'action_dispatch/http/request' -require 'rack/body_proxy' +# frozen_string_literal: true + +require "active_support/core_ext/time/conversions" +require "active_support/core_ext/object/blank" +require "active_support/log_subscriber" +require "action_dispatch/http/request" +require "rack/body_proxy" module Rails module Rack @@ -27,64 +29,52 @@ module Rails end end - protected + private - def call_app(request, env) - # Put some space between requests in development logs. - if development? - logger.debug '' - logger.debug '' + def call_app(request, env) # :doc: + instrumenter = ActiveSupport::Notifications.instrumenter + instrumenter.start "request.action_dispatch", request: request + logger.info { started_request_message(request) } + status, headers, body = @app.call(env) + body = ::Rack::BodyProxy.new(body) { finish(request) } + [status, headers, body] + rescue Exception + finish(request) + raise + ensure + ActiveSupport::LogSubscriber.flush_all! end - instrumenter = ActiveSupport::Notifications.instrumenter - instrumenter.start 'request.action_dispatch', request: request - logger.info { started_request_message(request) } - resp = @app.call(env) - resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) } - resp - rescue Exception - finish(request) - raise - ensure - ActiveSupport::LogSubscriber.flush_all! - end - - # Started GET "/session/new" for 127.0.0.1 at 2012-09-26 14:51:42 -0700 - def started_request_message(request) - 'Started %s "%s" for %s at %s' % [ - request.request_method, - request.filtered_path, - request.ip, - Time.now.to_default_s ] - end + # Started GET "/session/new" for 127.0.0.1 at 2012-09-26 14:51:42 -0700 + def started_request_message(request) # :doc: + 'Started %s "%s" for %s at %s' % [ + request.request_method, + request.filtered_path, + request.remote_ip, + Time.now.to_default_s ] + end - def compute_tags(request) - @taggers.collect do |tag| - case tag - when Proc - tag.call(request) - when Symbol - request.send(tag) - else - tag + def compute_tags(request) # :doc: + @taggers.collect do |tag| + case tag + when Proc + tag.call(request) + when Symbol + request.send(tag) + else + tag + end end end - end - private - - def finish(request) - instrumenter = ActiveSupport::Notifications.instrumenter - instrumenter.finish 'request.action_dispatch', request: request - end - - def development? - Rails.env.development? - end + def finish(request) + instrumenter = ActiveSupport::Notifications.instrumenter + instrumenter.finish "request.action_dispatch", request: request + end - def logger - Rails.logger - end + def logger + Rails.logger + end end end end diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index 8c24d1d56d..a67b90e285 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -1,38 +1,39 @@ -require 'rails/initializable' -require 'rails/configuration' -require 'active_support/inflector' -require 'active_support/core_ext/module/introspection' -require 'active_support/core_ext/module/delegation' +# frozen_string_literal: true + +require "rails/initializable" +require "active_support/inflector" +require "active_support/core_ext/module/introspection" +require "active_support/core_ext/module/delegation" module Rails - # Railtie is the core of the Rails framework and provides several hooks to extend - # Rails and/or modify the initialization process. + # <tt>Rails::Railtie</tt> is the core of the Rails framework and provides + # several hooks to extend Rails and/or modify the initialization process. # - # Every major component of Rails (Action Mailer, Action Controller, - # Action View and Active Record) is a Railtie. Each of - # them is responsible for their own initialization. This makes Rails itself - # absent of any component hooks, allowing other components to be used in - # place of any of the Rails defaults. + # Every major component of Rails (Action Mailer, Action Controller, Active + # Record, etc.) implements a railtie. Each of them is responsible for their + # own initialization. This makes Rails itself absent of any component hooks, + # allowing other components to be used in place of any of the Rails defaults. # - # Developing a Rails extension does _not_ require any implementation of - # Railtie, but if you need to interact with the Rails framework during - # or after boot, then Railtie is needed. + # Developing a Rails extension does _not_ require implementing a railtie, but + # if you need to interact with the Rails framework during or after boot, then + # a railtie is needed. # - # For example, an extension doing any of the following would require Railtie: + # For example, an extension doing any of the following would need a railtie: # # * creating initializers # * configuring a Rails framework for the application, like setting a generator # * adding <tt>config.*</tt> keys to the environment - # * setting up a subscriber with ActiveSupport::Notifications - # * adding rake tasks + # * setting up a subscriber with <tt>ActiveSupport::Notifications</tt> + # * adding Rake tasks # - # == Creating your Railtie + # == Creating a Railtie # - # To extend Rails using Railtie, create a Railtie class which inherits - # from Rails::Railtie within your extension's namespace. This class must be - # loaded during the Rails boot process. + # To extend Rails using a railtie, create a subclass of <tt>Rails::Railtie</tt>. + # This class must be loaded during the Rails boot process, and is conventionally + # called <tt>MyNamespace::Railtie</tt>. # - # The following example demonstrates an extension which can be used with or without Rails. + # The following example demonstrates an extension which can be used with or + # without Rails. # # # lib/my_gem/railtie.rb # module MyGem @@ -45,8 +46,8 @@ module Rails # # == Initializers # - # To add an initialization step from your Railtie to Rails boot process, you just need - # to create an initializer block: + # To add an initialization step to the Rails boot process from your railtie, just + # define the initialization code with the +initializer+ macro: # # class MyRailtie < Rails::Railtie # initializer "my_railtie.configure_rails_initialization" do @@ -55,7 +56,7 @@ module Rails # end # # If specified, the block can also receive the application object, in case you - # need to access some application specific configuration, like middleware: + # need to access some application-specific configuration, like middleware: # # class MyRailtie < Rails::Railtie # initializer "my_railtie.configure_rails_initialization" do |app| @@ -63,54 +64,57 @@ module Rails # end # end # - # Finally, you can also pass <tt>:before</tt> and <tt>:after</tt> as option to initializer, - # in case you want to couple it with a specific step in the initialization process. + # Finally, you can also pass <tt>:before</tt> and <tt>:after</tt> as options to + # +initializer+, in case you want to couple it with a specific step in the + # initialization process. # # == Configuration # - # Inside the Railtie class, you can access a config object which contains configuration - # shared by all railties and the application: + # Railties can access a config object which contains configuration shared by all + # railties and the application: # # class MyRailtie < Rails::Railtie # # Customize the ORM # config.app_generators.orm :my_railtie_orm # # # Add a to_prepare block which is executed once in production - # # and before each request in development + # # and before each request in development. # config.to_prepare do # MyRailtie.setup! # end # end # - # == Loading rake tasks and generators + # == Loading Rake Tasks and Generators # - # If your railtie has rake tasks, you can tell Rails to load them through the method - # rake_tasks: + # If your railtie has Rake tasks, you can tell Rails to load them through the method + # +rake_tasks+: # # class MyRailtie < Rails::Railtie # rake_tasks do - # load "path/to/my_railtie.tasks" + # load 'path/to/my_railtie.tasks' # end # end # # By default, Rails loads generators from your load path. However, if you want to place - # your generators at a different location, you can specify in your Railtie a block which + # your generators at a different location, you can specify in your railtie a block which # will load them during normal generators lookup: # # class MyRailtie < Rails::Railtie # generators do - # require "path/to/my_railtie_generator" + # require 'path/to/my_railtie_generator' # end # end # + # Since filenames on the load path are shared across gems, be sure that files you load + # through a railtie have unique names. + # # == Application and Engine # - # A Rails::Engine is nothing more than a Railtie with some initializers already set. - # And since Rails::Application is an engine, the same configuration described here - # can be used in both. + # An engine is nothing more than a railtie with some initializers already set. And since + # <tt>Rails::Application</tt> is an engine, the same configuration described here can be + # used in both. # # Be sure to look at the documentation of those specific classes for more information. - # class Railtie autoload :Configuration, "rails/railtie/configuration" @@ -133,27 +137,19 @@ module Rails end def rake_tasks(&blk) - @rake_tasks ||= [] - @rake_tasks << blk if blk - @rake_tasks + register_block_for(:rake_tasks, &blk) end def console(&blk) - @load_console ||= [] - @load_console << blk if blk - @load_console + register_block_for(:load_console, &blk) end def runner(&blk) - @load_runner ||= [] - @load_runner << blk if blk - @load_runner + register_block_for(:runner, &blk) end def generators(&blk) - @generators ||= [] - @generators << blk if blk - @generators + register_block_for(:generators, &blk) end def abstract_railtie? @@ -171,10 +167,6 @@ module Rails @instance ||= new end - def respond_to_missing?(*args) - instance.respond_to?(*args) || super - end - # Allows you to configure the railtie. This is the same method seen in # Railtie::Configurable, but this module is no longer required for all # subclasses of Railtie so we provide the class method here. @@ -182,11 +174,15 @@ module Rails instance.configure(&block) end - protected + private def generate_railtie_name(string) ActiveSupport::Inflector.underscore(string).tr("/", "_") end + def respond_to_missing?(name, _) + instance.respond_to?(name) || super + end + # If the class method does not have a method, then send the method call # to the Railtie instance. def method_missing(name, *args, &block) @@ -196,55 +192,69 @@ module Rails super end end + + # receives an instance variable identifier, set the variable value if is + # blank and append given block to value, which will be used later in + # `#each_registered_block(type, &block)` + def register_block_for(type, &blk) + var_name = "@#{type}" + blocks = instance_variable_defined?(var_name) ? instance_variable_get(var_name) : instance_variable_set(var_name, []) + blocks << blk if blk + blocks + end end delegate :railtie_name, to: :class - def initialize + def initialize #:nodoc: if self.class.abstract_railtie? raise "#{self.class.name} is abstract, you cannot instantiate it directly." end end - def configure(&block) + def configure(&block) #:nodoc: instance_eval(&block) end + # This is used to create the <tt>config</tt> object on Railties, an instance of + # Railtie::Configuration, that is used by Railties and Application to store + # related configuration. def config @config ||= Railtie::Configuration.new end - def railtie_namespace - @railtie_namespace ||= self.class.parents.detect { |n| n.respond_to?(:railtie_namespace) } + def railtie_namespace #:nodoc: + @railtie_namespace ||= self.class.module_parents.detect { |n| n.respond_to?(:railtie_namespace) } end protected - def run_console_blocks(app) #:nodoc: - each_registered_block(:console) { |block| block.call(app) } - end + def run_console_blocks(app) #:nodoc: + each_registered_block(:console) { |block| block.call(app) } + end - def run_generators_blocks(app) #:nodoc: - each_registered_block(:generators) { |block| block.call(app) } - end + def run_generators_blocks(app) #:nodoc: + each_registered_block(:generators) { |block| block.call(app) } + end - def run_runner_blocks(app) #:nodoc: - each_registered_block(:runner) { |block| block.call(app) } - end + def run_runner_blocks(app) #:nodoc: + each_registered_block(:runner) { |block| block.call(app) } + end - def run_tasks_blocks(app) #:nodoc: - extend Rake::DSL - each_registered_block(:rake_tasks) { |block| instance_exec(app, &block) } - end + def run_tasks_blocks(app) #:nodoc: + extend Rake::DSL + each_registered_block(:rake_tasks) { |block| instance_exec(app, &block) } + end private - def each_registered_block(type, &block) - klass = self.class - while klass.respond_to?(type) - klass.public_send(type).each(&block) - klass = klass.superclass + # run `&block` in every registered block in `#register_block_for` + def each_registered_block(type, &block) + klass = self.class + while klass.respond_to?(type) + klass.public_send(type).each(&block) + klass = klass.superclass + end end - end end end diff --git a/railties/lib/rails/railtie/configurable.rb b/railties/lib/rails/railtie/configurable.rb index 1572af0b2a..7f42fae10a 100644 --- a/railties/lib/rails/railtie/configurable.rb +++ b/railties/lib/rails/railtie/configurable.rb @@ -1,4 +1,6 @@ -require 'active_support/concern' +# frozen_string_literal: true + +require "active_support/concern" module Rails class Railtie @@ -9,7 +11,7 @@ module Rails delegate :config, to: :instance def inherited(base) - raise "You cannot inherit from a #{self.superclass.name} child" + raise "You cannot inherit from a #{superclass.name} child" end def instance @@ -24,11 +26,11 @@ module Rails class_eval(&block) end - protected + private - def method_missing(*args, &block) - instance.send(*args, &block) - end + def method_missing(*args, &block) + instance.send(*args, &block) + end end end end diff --git a/railties/lib/rails/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb index eb3b2d8ef4..70274b948c 100644 --- a/railties/lib/rails/railtie/configuration.rb +++ b/railties/lib/rails/railtie/configuration.rb @@ -1,4 +1,6 @@ -require 'rails/configuration' +# frozen_string_literal: true + +require "rails/configuration" module Rails class Railtie @@ -53,7 +55,7 @@ module Rails ActiveSupport.on_load(:before_configuration, yield: true, &block) end - # Third configurable block to run. Does not run if +config.cache_classes+ + # Third configurable block to run. Does not run if +config.eager_load+ # set to false. def before_eager_load(&block) ActiveSupport.on_load(:before_eager_load, yield: true, &block) diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb index 67a19d8a94..b2d44d9b8e 100644 --- a/railties/lib/rails/ruby_version_check.rb +++ b/railties/lib/rails/ruby_version_check.rb @@ -1,13 +1,15 @@ -if RUBY_VERSION < '2.2.2' && RUBY_ENGINE == 'ruby' +# frozen_string_literal: true + +if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.4.1") && RUBY_ENGINE == "ruby" desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})" abort <<-end_message - Rails 5 requires Ruby 2.2.2 or newer. + Rails 6 requires Ruby 2.4.1 or newer. You're running #{desc} - Please upgrade to Ruby 2.2.2 or newer to continue. + Please upgrade to Ruby 2.4.1 or newer to continue. end_message end diff --git a/railties/lib/rails/secrets.rb b/railties/lib/rails/secrets.rb new file mode 100644 index 0000000000..747cf31d7a --- /dev/null +++ b/railties/lib/rails/secrets.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require "yaml" +require "active_support/message_encryptor" + +module Rails + # Greatly inspired by Ara T. Howard's magnificent sekrets gem. 😘 + class Secrets # :nodoc: + class MissingKeyError < RuntimeError + def initialize + super(<<-end_of_message.squish) + Missing encryption key to decrypt secrets with. + Ask your team for your master key and put it in ENV["RAILS_MASTER_KEY"] + end_of_message + end + end + + @cipher = "aes-128-gcm" + @root = File # Wonky, but ensures `join` uses the current directory. + + class << self + attr_writer :root + + def parse(paths, env:) + paths.each_with_object(Hash.new) do |path, all_secrets| + require "erb" + + secrets = YAML.load(ERB.new(preprocess(path)).result) || {} + all_secrets.merge!(secrets["shared"].deep_symbolize_keys) if secrets["shared"] + all_secrets.merge!(secrets[env].deep_symbolize_keys) if secrets[env] + end + end + + def key + ENV["RAILS_MASTER_KEY"] || read_key_file || handle_missing_key + end + + def encrypt(data) + encryptor.encrypt_and_sign(data) + end + + def decrypt(data) + encryptor.decrypt_and_verify(data) + end + + def read + decrypt(IO.binread(path)) + end + + def write(contents) + IO.binwrite("#{path}.tmp", encrypt(contents)) + FileUtils.mv("#{path}.tmp", path) + end + + def read_for_editing(&block) + writing(read, &block) + end + + private + def handle_missing_key + raise MissingKeyError + end + + def read_key_file + if File.exist?(key_path) + IO.binread(key_path).strip + end + end + + def key_path + @root.join("config", "secrets.yml.key") + end + + def path + @root.join("config", "secrets.yml.enc").to_s + end + + def preprocess(path) + if path.end_with?(".enc") + decrypt(IO.binread(path)) + else + IO.read(path) + end + end + + def writing(contents) + tmp_file = "#{File.basename(path)}.#{Process.pid}" + tmp_path = File.join(Dir.tmpdir, tmp_file) + IO.binwrite(tmp_path, contents) + + yield tmp_path + + updated_contents = IO.binread(tmp_path) + + write(updated_contents) if updated_contents != contents + ensure + FileUtils.rm(tmp_path) if File.exist?(tmp_path) + end + + def encryptor + @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: @cipher) + end + end + end +end diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index 9b058a1848..d7170e6282 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -1,133 +1,149 @@ -# Implements the logic behind the rake tasks for annotations like -# -# rake notes -# rake notes:optimize -# -# and friends. See <tt>rake -T notes</tt> and <tt>railties/lib/tasks/annotations.rake</tt>. -# -# Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that -# represent the line where the annotation lives, its tag, and its text. Note -# the filename is not stored. -# -# Annotations are looked for in comments and modulus whitespace they have to -# start with the tag optionally followed by a colon. Everything up to the end -# of the line (or closing ERB comment tag) is considered to be their text. -class SourceAnnotationExtractor - class Annotation < Struct.new(:line, :tag, :text) - def self.directories - @@directories ||= %w(app config db lib test) + (ENV['SOURCE_ANNOTATION_DIRECTORIES'] || '').split(',') - end +# frozen_string_literal: true - def self.extensions - @@extensions ||= {} - end +require "active_support/deprecation" - # Registers new Annotations File Extensions - # SourceAnnotationExtractor::Annotation.register_extensions("css", "scss", "sass", "less", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ } - def self.register_extensions(*exts, &block) - extensions[/\.(#{exts.join("|")})$/] = block - end +# Remove this deprecated class in the next minor version +#:nodoc: +SourceAnnotationExtractor = ActiveSupport::Deprecation::DeprecatedConstantProxy. + new("SourceAnnotationExtractor", "Rails::SourceAnnotationExtractor") - register_extensions("builder", "rb", "rake", "yml", "yaml", "ruby") { |tag| /#\s*(#{tag}):?\s*(.*)$/ } - register_extensions("css", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ } - register_extensions("erb") { |tag| /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ } +module Rails + # Implements the logic behind <tt>Rails::Command::NotesCommand</tt>. See <tt>rails notes --help</tt> for usage information. + # + # Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that + # represent the line where the annotation lives, its tag, and its text. Note + # the filename is not stored. + # + # Annotations are looked for in comments and modulus whitespace they have to + # start with the tag optionally followed by a colon. Everything up to the end + # of the line (or closing ERB comment tag) is considered to be their text. + class SourceAnnotationExtractor + class Annotation < Struct.new(:line, :tag, :text) + def self.directories + @@directories ||= %w(app config db lib test) + end - # Returns a representation of the annotation that looks like this: + # Registers additional directories to be included + # Rails::SourceAnnotationExtractor::Annotation.register_directories("spec", "another") + def self.register_directories(*dirs) + directories.push(*dirs) + end + + def self.extensions + @@extensions ||= {} + end + + # Registers new Annotations File Extensions + # Rails::SourceAnnotationExtractor::Annotation.register_extensions("css", "scss", "sass", "less", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ } + def self.register_extensions(*exts, &block) + extensions[/\.(#{exts.join("|")})$/] = block + end + + register_extensions("builder", "rb", "rake", "yml", "yaml", "ruby") { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + register_extensions("css", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ } + register_extensions("erb") { |tag| /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ } + + # Returns a representation of the annotation that looks like this: + # + # [126] [TODO] This algorithm is simple and clearly correct, make it faster. + # + # If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above. + # Otherwise the string contains just line and text. + def to_s(options = {}) + s = +"[#{line.to_s.rjust(options[:indent])}] " + s << "[#{tag}] " if options[:tag] + s << text + end + + # Used in annotations.rake + #:nodoc: + def self.notes_task_deprecation_warning + ActiveSupport::Deprecation.warn("This rake task is deprecated and will be removed in Rails 6.1. \nRefer to `rails notes --help` for more information.\n") + puts "\n" + end + end + + # Prints all annotations with tag +tag+ under the root directories +app+, + # +config+, +db+, +lib+, and +test+ (recursively). + # + # Specific directories can be explicitly set using the <tt>:dirs</tt> key in +options+. + # + # Rails::SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true + # + # If +options+ has a <tt>:tag</tt> flag, it will be passed to each annotation's +to_s+. # - # [126] [TODO] This algorithm is simple and clearly correct, make it faster. + # See <tt>#find_in</tt> for a list of file extensions that will be taken into account. # - # If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above. - # Otherwise the string contains just line and text. - def to_s(options={}) - s = "[#{line.to_s.rjust(options[:indent])}] " - s << "[#{tag}] " if options[:tag] - s << text + # This class method is the single entry point for the `rails notes` command. + def self.enumerate(tag, options = {}) + extractor = new(tag) + dirs = options.delete(:dirs) || Annotation.directories + extractor.display(extractor.find(dirs), options) end - end - # Prints all annotations with tag +tag+ under the root directories +app+, - # +config+, +db+, +lib+, and +test+ (recursively). - # - # Additional directories may be added using a comma-delimited list set using - # <tt>ENV['SOURCE_ANNOTATION_DIRECTORIES']</tt>. - # - # Directories may also be explicitly set using the <tt>:dirs</tt> key in +options+. - # - # SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true - # - # If +options+ has a <tt>:tag</tt> flag, it will be passed to each annotation's +to_s+. - # - # See <tt>#find_in</tt> for a list of file extensions that will be taken into account. - # - # This class method is the single entry point for the rake tasks. - def self.enumerate(tag, options={}) - extractor = new(tag) - dirs = options.delete(:dirs) || Annotation.directories - extractor.display(extractor.find(dirs), options) - end + attr_reader :tag - attr_reader :tag + def initialize(tag) + @tag = tag + end - def initialize(tag) - @tag = tag - end + # Returns a hash that maps filenames under +dirs+ (recursively) to arrays + # with their annotations. + def find(dirs) + dirs.inject({}) { |h, dir| h.update(find_in(dir)) } + end - # Returns a hash that maps filenames under +dirs+ (recursively) to arrays - # with their annotations. - def find(dirs) - dirs.inject({}) { |h, dir| h.update(find_in(dir)) } - end + # Returns a hash that maps filenames under +dir+ (recursively) to arrays + # with their annotations. Files with extensions registered in + # <tt>Rails::SourceAnnotationExtractor::Annotation.extensions</tt> are + # taken into account. Only files with annotations are included. + def find_in(dir) + results = {} - # Returns a hash that maps filenames under +dir+ (recursively) to arrays - # with their annotations. Only files with annotations are included. Files - # with extension +.builder+, +.rb+, +.rake+, +.yml+, +.yaml+, +.ruby+, - # +.css+, +.js+ and +.erb+ are taken into account. - def find_in(dir) - results = {} - - Dir.glob("#{dir}/*") do |item| - next if File.basename(item)[0] == ?. - - if File.directory?(item) - results.update(find_in(item)) - else - extension = Annotation.extensions.detect do |regexp, _block| - regexp.match(item) - end + Dir.glob("#{dir}/*") do |item| + next if File.basename(item)[0] == ?. - if extension - pattern = extension.last.call(tag) - results.update(extract_annotations_from(item, pattern)) if pattern + if File.directory?(item) + results.update(find_in(item)) + else + extension = Annotation.extensions.detect do |regexp, _block| + regexp.match(item) + end + + if extension + pattern = extension.last.call(tag) + results.update(extract_annotations_from(item, pattern)) if pattern + end end end - end - results - end + results + end - # If +file+ is the filename of a file that contains annotations this method returns - # a hash with a single entry that maps +file+ to an array of its annotations. - # Otherwise it returns an empty hash. - def extract_annotations_from(file, pattern) - lineno = 0 - result = File.readlines(file).inject([]) do |list, line| - lineno += 1 - next list unless line =~ pattern - list << Annotation.new(lineno, $1, $2) + # If +file+ is the filename of a file that contains annotations this method returns + # a hash with a single entry that maps +file+ to an array of its annotations. + # Otherwise it returns an empty hash. + def extract_annotations_from(file, pattern) + lineno = 0 + result = File.readlines(file, encoding: Encoding::BINARY).inject([]) do |list, line| + lineno += 1 + next list unless line =~ pattern + list << Annotation.new(lineno, $1, $2) + end + result.empty? ? {} : { file => result } end - result.empty? ? {} : { file => result } - end - # Prints the mapping from filenames to annotations in +results+ ordered by filename. - # The +options+ hash is passed to each annotation's +to_s+. - def display(results, options={}) - options[:indent] = results.flat_map { |f, a| a.map(&:line) }.max.to_s.size - results.keys.sort.each do |file| - puts "#{file}:" - results[file].each do |note| - puts " * #{note.to_s(options)}" + # Prints the mapping from filenames to annotations in +results+ ordered by filename. + # The +options+ hash is passed to each annotation's +to_s+. + def display(results, options = {}) + options[:indent] = results.flat_map { |f, a| a.map(&:line) }.max.to_s.size + results.keys.sort.each do |file| + puts "#{file}:" + results[file].each do |note| + puts " * #{note.to_s(options)}" + end + puts end - puts end end end diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index 2c3d278eca..2f644a20c9 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -1,8 +1,11 @@ -require 'rake' +# frozen_string_literal: true + +require "rake" # Load Rails Rakefile extensions %w( annotations + dev framework initializers log @@ -10,8 +13,10 @@ require 'rake' misc restart routes - statistics tmp -).each do |task| + yarn +).tap { |arr| + arr << "statistics" if Rake.application.current_scope.empty? +}.each do |task| load "rails/tasks/#{task}.rake" end diff --git a/railties/lib/rails/tasks/annotations.rake b/railties/lib/rails/tasks/annotations.rake index 386ecf44be..3a78de418a 100644 --- a/railties/lib/rails/tasks/annotations.rake +++ b/railties/lib/rails/tasks/annotations.rake @@ -1,20 +1,22 @@ -require 'rails/source_annotation_extractor' +# frozen_string_literal: true -desc "Enumerate all annotations (use notes:optimize, :fixme, :todo for focus)" -task :notes do - SourceAnnotationExtractor.enumerate "OPTIMIZE|FIXME|TODO", tag: true +require "rails/source_annotation_extractor" + +task notes: :environment do + Rails::SourceAnnotationExtractor::Annotation.notes_task_deprecation_warning + Rails::Command.invoke :notes end namespace :notes do ["OPTIMIZE", "FIXME", "TODO"].each do |annotation| - # desc "Enumerate all #{annotation} annotations" - task annotation.downcase.intern do - SourceAnnotationExtractor.enumerate annotation + task annotation.downcase.intern => :environment do + Rails::SourceAnnotationExtractor::Annotation.notes_task_deprecation_warning + Rails::Command.invoke :notes, ["--annotations", annotation] end end - desc "Enumerate a custom annotation, specify with ANNOTATION=CUSTOM" - task :custom do - SourceAnnotationExtractor.enumerate ENV['ANNOTATION'] + task custom: :environment do + Rails::SourceAnnotationExtractor::Annotation.notes_task_deprecation_warning + Rails::Command.invoke :notes, ["--annotations", ENV["ANNOTATION"]] end -end
\ No newline at end of file +end diff --git a/railties/lib/rails/tasks/dev.rake b/railties/lib/rails/tasks/dev.rake new file mode 100644 index 0000000000..716fb6a331 --- /dev/null +++ b/railties/lib/rails/tasks/dev.rake @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "rails/command" +require "active_support/deprecation" + +namespace :dev do + task cache: :environment do + ActiveSupport::Deprecation.warn("Using `bin/rake dev:cache` is deprecated and will be removed in Rails 6.1. Use `bin/rails dev:cache` instead.\n") + Rails::Command.invoke "dev:cache" + end +end diff --git a/railties/lib/rails/tasks/engine.rake b/railties/lib/rails/tasks/engine.rake index 16ad1bfc84..8d77904210 100644 --- a/railties/lib/rails/tasks/engine.rake +++ b/railties/lib/rails/tasks/engine.rake @@ -1,11 +1,24 @@ +# frozen_string_literal: true + task "load_app" do namespace :app do load APP_RAKEFILE + + desc "Update some initially generated files" + task update: [ "update:bin" ] + + namespace :update do + require "rails/engine/updater" + # desc "Adds new executables to the engine bin/ directory" + task :bin do + Rails::Engine::Updater.run(:create_bin_files) + end + end end - task :environment => "app:environment" + task environment: "app:environment" - if !defined?(ENGINE_PATH) || !ENGINE_PATH - ENGINE_PATH = find_engine_path(APP_RAKEFILE) + if !defined?(ENGINE_ROOT) || !ENGINE_ROOT + ENGINE_ROOT = find_engine_path(APP_RAKEFILE) end end @@ -26,11 +39,11 @@ namespace :db do desc "Display status of migrations" app_task "migrate:status" - desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all databases in the config)' + desc "Create the database from config/database.yml for the current Rails.env (use db:create:all to create all databases in the config)" app_task "create" app_task "create:all" - desc 'Drops the database for the current Rails.env (use db:drop:all to drop all databases)' + desc "Drops the database for the current Rails.env (use db:drop:all to drop all databases)" app_task "drop" app_task "drop:all" @@ -40,7 +53,7 @@ namespace :db do desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)." app_task "rollback" - desc "Create a db/schema.rb file that can be portably used against any DB supported by AR" + desc "Create a db/schema.rb file that can be portably used against any database supported by Active Record" app_task "schema:dump" desc "Load a schema.rb file into the database" @@ -49,7 +62,7 @@ namespace :db do desc "Load the seed data from db/seeds.rb" app_task "seed" - desc "Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the db first)" + desc "Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)" app_task "setup" desc "Dump the database structure to an SQL file" @@ -57,6 +70,9 @@ namespace :db do desc "Retrieves the current schema version number" app_task "version" + + # desc 'Load the test schema' + app_task "test:prepare" end def find_engine_path(path) @@ -65,7 +81,7 @@ def find_engine_path(path) if Rails::Engine.find(path) path else - find_engine_path(File.expand_path('..', path)) + find_engine_path(File.expand_path("..", path)) end end diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake index 904b9d9ad6..1a3711c446 100644 --- a/railties/lib/rails/tasks/framework.rake +++ b/railties/lib/rails/tasks/framework.rake @@ -1,22 +1,24 @@ -namespace :rails do +# frozen_string_literal: true + +namespace :app do desc "Update configs and some other initially generated files (or use just update:configs or update:bin)" - task update: [ "update:configs", "update:bin" ] + task update: [ "update:configs", "update:bin", "update:upgrade_guide_info" ] desc "Applies the template supplied by LOCATION=(/path/to/template) or URL" task template: :environment do template = ENV["LOCATION"] raise "No LOCATION value given. Please set LOCATION either as path to a file or a URL" if template.blank? template = File.expand_path(template) if template !~ %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} - require 'rails/generators' - require 'rails/generators/rails/app/app_generator' - generator = Rails::Generators::AppGenerator.new [Rails.root], {}, destination_root: Rails.root + require "rails/generators" + require "rails/generators/rails/app/app_generator" + generator = Rails::Generators::AppGenerator.new [Rails.root], {}, { destination_root: Rails.root } generator.apply template, verbose: false end namespace :templates do # desc "Copy all the templates from rails to the application directory for customization. Already existing local copies will be overwritten" task :copy do - generators_lib = File.expand_path("../../generators", __FILE__) + generators_lib = File.expand_path("../generators", __dir__) project_templates = "#{Rails.root}/lib/templates" default_templates = { "erb" => %w{controller mailer scaffold}, @@ -24,45 +26,33 @@ namespace :rails do default_templates.each do |type, names| local_template_type_dir = File.join(project_templates, type) - FileUtils.mkdir_p local_template_type_dir + mkdir_p local_template_type_dir, verbose: false names.each do |name| dst_name = File.join(local_template_type_dir, name) src_name = File.join(generators_lib, type, name, "templates") - FileUtils.cp_r src_name, dst_name + cp_r src_name, dst_name, verbose: false end end end end namespace :update do - class RailsUpdate - def self.invoke_from_app_generator(method) - app_generator.send(method) - end - - def self.app_generator - @app_generator ||= begin - require 'rails/generators' - require 'rails/generators/rails/app/app_generator' - gen = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, - destination_root: Rails.root - File.exist?(Rails.root.join("config", "application.rb")) ? - gen.send(:app_const) : gen.send(:valid_const?) - gen - end - end - end + require "rails/app_updater" - # desc "Update config/boot.rb from your current rails install" + # desc "Update config files from your current rails install" task :configs do - RailsUpdate.invoke_from_app_generator :create_boot_file - RailsUpdate.invoke_from_app_generator :update_config_files + Rails::AppUpdater.invoke_from_app_generator :create_boot_file + Rails::AppUpdater.invoke_from_app_generator :update_config_files end # desc "Adds new executables to the application bin/ directory" task :bin do - RailsUpdate.invoke_from_app_generator :create_bin_files + Rails::AppUpdater.invoke_from_app_generator :update_bin_files + end + + task :upgrade_guide_info do + Rails::AppUpdater.invoke_from_app_generator :display_upgrade_guide_info end end end diff --git a/railties/lib/rails/tasks/initializers.rake b/railties/lib/rails/tasks/initializers.rake index 2968b5cb53..f108517d1d 100644 --- a/railties/lib/rails/tasks/initializers.rake +++ b/railties/lib/rails/tasks/initializers.rake @@ -1,6 +1,9 @@ -desc "Print out all defined initializers in the order they are invoked by Rails." +# frozen_string_literal: true + +require "rails/command" +require "active_support/deprecation" + task initializers: :environment do - Rails.application.initializers.tsort_each do |initializer| - puts initializer.name - end + ActiveSupport::Deprecation.warn("Using `bin/rake initializers` is deprecated and will be removed in Rails 6.1. Use `bin/rails initializers` instead.\n") + Rails::Command.invoke "initializers" end diff --git a/railties/lib/rails/tasks/log.rake b/railties/lib/rails/tasks/log.rake index 877f175ef3..ec56957204 100644 --- a/railties/lib/rails/tasks/log.rake +++ b/railties/lib/rails/tasks/log.rake @@ -1,5 +1,13 @@ +# frozen_string_literal: true + namespace :log do - desc "Truncates all *.log files in log/ to zero bytes (specify which logs with LOGS=test,development)" + ## + # Truncates all/specified log files + # ENV['LOGS'] + # - defaults to all environments log files i.e. 'development,test,production' + # - ENV['LOGS']=all truncates all files i.e. log/*.log + # - ENV['LOGS']='test,development' truncates only specified files + desc "Truncates all/specified *.log files in log/ to zero bytes (specify which logs with LOGS=test,development)" task :clear do log_files.each do |file| clear_log_file(file) @@ -7,17 +15,27 @@ namespace :log do end def log_files - if ENV['LOGS'] - ENV['LOGS'].split(',') - .map { |file| "log/#{file.strip}.log" } - .select { |file| File.exist?(file) } - else + if ENV["LOGS"] == "all" FileList["log/*.log"] + elsif ENV["LOGS"] + log_files_to_truncate(ENV["LOGS"]) + else + log_files_to_truncate(all_environments.join(",")) end end + def log_files_to_truncate(envs) + envs.split(",") + .map { |file| "log/#{file.strip}.log" } + .select { |file| File.exist?(file) } + end + def clear_log_file(file) f = File.open(file, "w") f.close end + + def all_environments + Dir["config/environments/*.rb"].map { |fname| File.basename(fname, ".*") } + end end diff --git a/railties/lib/rails/tasks/middleware.rake b/railties/lib/rails/tasks/middleware.rake index 31e961b483..3a7f86fdcb 100644 --- a/railties/lib/rails/tasks/middleware.rake +++ b/railties/lib/rails/tasks/middleware.rake @@ -1,4 +1,6 @@ -desc 'Prints out your Rack middleware stack' +# frozen_string_literal: true + +desc "Prints out your Rack middleware stack" task middleware: :environment do Rails.configuration.middleware.each do |middleware| puts "use #{middleware.inspect}" diff --git a/railties/lib/rails/tasks/misc.rake b/railties/lib/rails/tasks/misc.rake index 4195106961..e7786aa622 100644 --- a/railties/lib/rails/tasks/misc.rake +++ b/railties/lib/rails/tasks/misc.rake @@ -1,43 +1,62 @@ -desc 'Generate a cryptographically secure secret key (this is typically used to generate a secret for cookie sessions).' +# frozen_string_literal: true + +desc "Generate a cryptographically secure secret key (this is typically used to generate a secret for cookie sessions)." task :secret do - require 'securerandom' + require "securerandom" puts SecureRandom.hex(64) end -desc 'List versions of all Rails frameworks and the environment' +desc "List versions of all Rails frameworks and the environment" task about: :environment do puts Rails::Info end namespace :time do + desc "List all time zones, list by two-letter country code (`rails time:zones[US]`), or list by UTC offset (`rails time:zones[-8]`)" + task :zones, :country_or_offset do |t, args| + zones, offset = ActiveSupport::TimeZone.all, nil + + if country_or_offset = args[:country_or_offset] + begin + zones = ActiveSupport::TimeZone.country_zones(country_or_offset) + rescue TZInfo::InvalidCountryCode + offset = country_or_offset + end + end + + build_time_zone_list zones, offset + end + namespace :zones do - desc 'Displays all time zones, also available: time:zones:us, time:zones:local -- filter with OFFSET parameter, e.g., OFFSET=-6' + # desc 'Displays all time zones, also available: time:zones:us, time:zones:local -- filter with OFFSET parameter, e.g., OFFSET=-6' task :all do - build_time_zone_list(:all) + build_time_zone_list ActiveSupport::TimeZone.all end # desc 'Displays names of US time zones recognized by the Rails TimeZone class, grouped by offset. Results can be filtered with optional OFFSET parameter, e.g., OFFSET=-6' task :us do - build_time_zone_list(:us_zones) + build_time_zone_list ActiveSupport::TimeZone.us_zones end # desc 'Displays names of time zones recognized by the Rails TimeZone class with the same offset as the system local time' task :local do - require 'active_support' - require 'active_support/time' + require "active_support" + require "active_support/time" + jan_offset = Time.now.beginning_of_year.utc_offset jul_offset = Time.now.beginning_of_year.change(month: 7).utc_offset offset = jan_offset < jul_offset ? jan_offset : jul_offset - build_time_zone_list(:all, offset) + + build_time_zone_list(ActiveSupport::TimeZone.all, offset) end # to find UTC -06:00 zones, OFFSET can be set to either -6, -6:00 or 21600 - def build_time_zone_list(method, offset = ENV['OFFSET']) - require 'active_support' - require 'active_support/time' + def build_time_zone_list(zones, offset = ENV["OFFSET"]) + require "active_support" + require "active_support/time" if offset offset = if offset.to_s.match(/(\+|-)?(\d+):(\d+)/) - sign = $1 == '-' ? -1 : 1 + sign = $1 == "-" ? -1 : 1 hours, minutes = $2.to_f, $3.to_f ((hours * 3600) + (minutes.to_f * 60)) * sign elsif offset.to_f.abs <= 13 @@ -47,7 +66,7 @@ namespace :time do end end previous_offset = nil - ActiveSupport::TimeZone.__send__(method).each do |zone| + zones.each do |zone| if offset.nil? || offset == zone.utc_offset puts "\n* UTC #{zone.formatted_offset} *" unless zone.utc_offset == previous_offset puts zone.name diff --git a/railties/lib/rails/tasks/restart.rake b/railties/lib/rails/tasks/restart.rake index f36c86d81b..074e3e89a1 100644 --- a/railties/lib/rails/tasks/restart.rake +++ b/railties/lib/rails/tasks/restart.rake @@ -1,5 +1,9 @@ +# frozen_string_literal: true + desc "Restart app by touching tmp/restart.txt" task :restart do - FileUtils.mkdir_p('tmp') - FileUtils.touch('tmp/restart.txt') + verbose(false) do + mkdir_p "tmp" + touch "tmp/restart.txt" + end end diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake index 1815c2fdc7..21ce900a8c 100644 --- a/railties/lib/rails/tasks/routes.rake +++ b/railties/lib/rails/tasks/routes.rake @@ -1,7 +1,9 @@ -desc 'Print out all defined routes in match order, with names. Target specific controller with CONTROLLER=x.' +# frozen_string_literal: true + +require "rails/command" +require "active_support/deprecation" + task routes: :environment do - all_routes = Rails.application.routes.routes - require 'action_dispatch/routing/inspector' - inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes) - puts inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, ENV['CONTROLLER']) + ActiveSupport::Deprecation.warn("Using `bin/rake routes` is deprecated and will be removed in Rails 6.1. Use `bin/rails routes` instead.\n") + Rails::Command.invoke "routes" end diff --git a/railties/lib/rails/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake index 735c36eb3a..8cacf4a49f 100644 --- a/railties/lib/rails/tasks/statistics.rake +++ b/railties/lib/rails/tasks/statistics.rake @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # While global constants are bad, many 3rd party tools depend on this one (e.g # rspec-rails & cucumber-rails). So a deprecation warning is needed if we want # to remove it. @@ -7,7 +9,9 @@ STATS_DIRECTORIES = [ %w(Jobs app/jobs), %w(Models app/models), %w(Mailers app/mailers), - %w(Javascripts app/assets/javascripts), + %w(Channels app/channels), + %w(JavaScripts app/assets/javascripts), + %w(JavaScript app/javascript), %w(Libraries lib/), %w(APIs app/apis), %w(Controller\ tests test/controllers), @@ -16,12 +20,13 @@ STATS_DIRECTORIES = [ %w(Mailer\ tests test/mailers), %w(Job\ tests test/jobs), %w(Integration\ tests test/integration), + %w(System\ tests test/system), ].collect do |name, dir| [ name, "#{File.dirname(Rake.application.rakefile_location)}/#{dir}" ] end.select { |name, dir| File.directory?(dir) } desc "Report code statistics (KLOCs, etc) from the application or engine" task :stats do - require 'rails/code_statistics' + require "rails/code_statistics" CodeStatistics.new(*STATS_DIRECTORIES).to_s end diff --git a/railties/lib/rails/tasks/tmp.rake b/railties/lib/rails/tasks/tmp.rake index 9162ef234a..7340b41ee4 100644 --- a/railties/lib/rails/tasks/tmp.rake +++ b/railties/lib/rails/tasks/tmp.rake @@ -1,13 +1,13 @@ +# frozen_string_literal: true + namespace :tmp do - desc "Clear cache and socket files from tmp/ (narrow w/ tmp:cache:clear, tmp:sockets:clear)" - task clear: ["tmp:cache:clear", "tmp:sockets:clear"] + desc "Clear cache, socket and screenshot files from tmp/ (narrow w/ tmp:cache:clear, tmp:sockets:clear, tmp:screenshots:clear)" + task clear: ["tmp:cache:clear", "tmp:sockets:clear", "tmp:screenshots:clear"] - tmp_dirs = [ 'tmp/cache', - 'tmp/sockets', - 'tmp/pids', - 'tmp/cache/assets/development', - 'tmp/cache/assets/test', - 'tmp/cache/assets/production' ] + tmp_dirs = [ "tmp/cache", + "tmp/sockets", + "tmp/pids", + "tmp/cache/assets" ] tmp_dirs.each { |d| directory d } @@ -17,21 +17,28 @@ namespace :tmp do namespace :cache do # desc "Clears all files and directories in tmp/cache" task :clear do - FileUtils.rm_rf(Dir['tmp/cache/[^.]*']) + rm_rf Dir["tmp/cache/[^.]*"], verbose: false end end namespace :sockets do # desc "Clears all files in tmp/sockets" task :clear do - FileUtils.rm(Dir['tmp/sockets/[^.]*']) + rm Dir["tmp/sockets/[^.]*"], verbose: false end end namespace :pids do # desc "Clears all files in tmp/pids" task :clear do - FileUtils.rm(Dir['tmp/pids/[^.]*']) + rm Dir["tmp/pids/[^.]*"], verbose: false + end + end + + namespace :screenshots do + # desc "Clears all files in tmp/screenshots" + task :clear do + rm Dir["tmp/screenshots/[^.]*"], verbose: false end end end diff --git a/railties/lib/rails/tasks/yarn.rake b/railties/lib/rails/tasks/yarn.rake new file mode 100644 index 0000000000..4fb8586b69 --- /dev/null +++ b/railties/lib/rails/tasks/yarn.rake @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +namespace :yarn do + desc "Install all JavaScript dependencies as specified via Yarn" + task :install do + # Install only production deps when for not usual envs. + valid_node_envs = %w[test development production] + node_env = ENV.fetch("NODE_ENV") do + rails_env = ENV["RAILS_ENV"] + valid_node_envs.include?(rails_env) ? rails_env : "production" + end + system({ "NODE_ENV" => node_env }, "#{Rails.root}/bin/yarn install --no-progress --frozen-lockfile") + end +end + +# Run Yarn prior to Sprockets assets precompilation, so dependencies are available for use. +if Rake::Task.task_defined?("assets:precompile") + Rake::Task["assets:precompile"].enhance [ "yarn:install" ] +end diff --git a/railties/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb index bb6e49966d..e46364ba8a 100644 --- a/railties/lib/rails/templates/rails/mailers/email.html.erb +++ b/railties/lib/rails/templates/rails/mailers/email.html.erb @@ -88,15 +88,32 @@ <% unless @email.attachments.nil? || @email.attachments.empty? %> <dt>Attachments:</dt> <dd> - <%= @email.attachments.map { |a| a.respond_to?(:original_filename) ? a.original_filename : a.filename }.join(', ') %> + <% @email.attachments.each do |a| %> + <% filename = a.respond_to?(:original_filename) ? a.original_filename : a.filename %> + <%= link_to filename, "data:application/octet-stream;charset=utf-8;base64,#{Base64.encode64(a.body.to_s)}", download: filename %> + <% end %> </dd> <% end %> + <dt>Format:</dt> <% if @email.multipart? %> <dd> - <select onchange="document.getElementsByName('messageBody')[0].src=this.options[this.selectedIndex].value;"> - <option <%= request.format == Mime::HTML ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option> - <option <%= request.format == Mime::TEXT ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option> + <select id="part" onchange="refreshBody(false);"> + <option <%= request.format == Mime[:html] ? 'selected' : '' %> value="<%= part_query('text/html') %>">View as HTML email</option> + <option <%= request.format == Mime[:text] ? 'selected' : '' %> value="<%= part_query('text/plain') %>">View as plain-text email</option> + </select> + </dd> + <% else %> + <dd id="mime_type" data-mime-type="<%= part_query(@email.mime_type) %>"><%= @email.mime_type == 'text/html' ? 'HTML email' : 'plain-text email' %></dd> + <% end %> + + <% if I18n.available_locales.count > 1 %> + <dt>Locale:</dt> + <dd> + <select id="locale" onchange="refreshBody(true);"> + <% I18n.available_locales.each do |locale| %> + <option <%= I18n.locale == locale ? 'selected' : '' %> value="<%= locale_query(locale) %>"><%= locale %></option> + <% end %> </select> </dd> <% end %> @@ -104,7 +121,7 @@ </header> <% if @part && @part.mime_type %> - <iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe> + <iframe seamless name="messageBody" src="?<%= part_query(@part.mime_type) %>"></iframe> <% else %> <p> You are trying to preview an email that does not have any content. @@ -112,5 +129,34 @@ </p> <% end %> +<script> + function refreshBody(reload) { + var part_select = document.querySelector('select#part'); + var locale_select = document.querySelector('select#locale'); + var iframe = document.getElementsByName('messageBody')[0]; + var part_param = part_select ? + part_select.options[part_select.selectedIndex].value : + document.querySelector('#mime_type').dataset.mimeType; + var locale_param = locale_select ? locale_select.options[locale_select.selectedIndex].value : null; + var fresh_location; + if (locale_param) { + fresh_location = '?' + part_param + '&' + locale_param; + } else { + fresh_location = '?' + part_param; + } + iframe.contentWindow.location = fresh_location; + + var url = location.pathname.replace(/\.(txt|html)$/, ''); + var format = /html/.test(part_param) ? '.html' : '.txt'; + var state_to_replace = locale_param ? (url + format + '?' + locale_param) : (url + format); + + if (reload) { + location.href = state_to_replace; + } else if (history.replaceState) { + window.history.replaceState({}, '', state_to_replace); + } + } +</script> + </body> </html> diff --git a/railties/lib/rails/templates/rails/welcome/index.html.erb b/railties/lib/rails/templates/rails/welcome/index.html.erb index acf04af416..b6823457c0 100644 --- a/railties/lib/rails/templates/rails/welcome/index.html.erb +++ b/railties/lib/rails/templates/rails/welcome/index.html.erb @@ -1,268 +1,74 @@ <!DOCTYPE html> <html> - <head> - <title>Ruby on Rails: Welcome aboard</title> - <style media="screen"> - body { - margin: 0; - margin-bottom: 25px; - padding: 0; - background-color: #f0f0f0; - font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana"; - font-size: 13px; - color: #333; - } - - h1 { - font-size: 28px; - color: #000; - } - - a { - color: #03c; - } - - a:hover { - background-color: #03c; - color: white; - text-decoration: none; - } - - #page { - background-color: #f0f0f0; - width: 750px; - margin: 0; - margin-left: auto; - margin-right: auto; - } - - #content { - float: left; - background-color: white; - border: 3px solid #aaa; - border-top: none; - padding: 25px; - width: 500px; - } - - #sidebar { - float: right; - width: 175px; - } - - #footer { - clear: both; - } - - #header, #about, #getting-started { - padding-left: 75px; - padding-right: 30px; - } - - #header { - background-image: url(data:image/png;base64,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); - background-repeat: no-repeat; - background-position: top left; - height: 64px; - } - - #header h1, - #header h2 { - margin: 0; - } - - #header h2 { - color: #888; - font-weight: normal; - font-size: 16px; - } - - #about h3 { - margin: 0; - margin-bottom: 10px; - font-size: 14px; - } - - #about-content { - background-color: #ffd; - border: 1px solid #fc0; - margin-left: -55px; - margin-right: -10px; - } - - #about-content table { - margin-top: 10px; - margin-bottom: 10px; - font-size: 11px; - border-collapse: collapse; - } - - #about-content td { - padding: 10px; - padding-top: 3px; - padding-bottom: 3px; - } - - #about-content td.name { - font-weight: bold; - vertical-align: top; - color: #555; - } - - #about-content td.value { - color: #000; - } - - #about-content ul { - padding: 0; - list-style-type: none; - } - - #about-content.failure { - background-color: #fcc; - border: 1px solid #f00; - } - - #about-content.failure p { - margin: 0; - padding: 10px; - } - - #getting-started { - border-top: 1px solid #ccc; - margin-top: 25px; - padding-top: 15px; - } - - #getting-started h1 { - margin: 0; - font-size: 20px; - } - - #getting-started h2 { - margin: 0; - font-size: 14px; - font-weight: normal; - color: #333; - margin-bottom: 25px; - } - - #getting-started ol { - margin-left: 0; - padding-left: 0; - } - - #getting-started li { - font-size: 18px; - color: #888; - margin-bottom: 25px; - } - - #getting-started li h2 { - margin: 0; - font-weight: normal; - font-size: 18px; - color: #333; - } - - #getting-started li p { - color: #555; - font-size: 13px; - } - - #sidebar ul { - margin-left: 0; - padding-left: 0; - } - - #sidebar ul h3 { - margin-top: 25px; - font-size: 16px; - padding-bottom: 10px; - border-bottom: 1px solid #ccc; - } - - #sidebar li { - list-style-type: none; - } - - #sidebar ul.links li { - margin-bottom: 5px; - } - - .filename { - font-style: italic; - } - </style> - <script> - function about() { - var info = document.getElementById('about-content'), - xhr; - - if (info.innerHTML === '') { - xhr = new XMLHttpRequest(); - xhr.open("GET", "/rails/info/properties", false); - xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); - xhr.send(""); - info.innerHTML = xhr.responseText; - } - - info.style.display = info.style.display === 'none' ? 'block' : 'none'; - } - </script> - </head> - <body> - <div id="page"> - <div id="sidebar"> - <ul id="sidebar-items"> - <li> - <h3>Browse the documentation</h3> - <ul class="links"> - <li><a href="http://guides.rubyonrails.org/">Rails Guides</a></li> - <li><a href="http://api.rubyonrails.org/">Rails API</a></li> - <li><a href="http://www.ruby-doc.org/core/">Ruby core</a></li> - <li><a href="http://www.ruby-doc.org/stdlib/">Ruby standard library</a></li> - </ul> - </li> - </ul> - </div> - - <div id="content"> - <div id="header"> - <h1>Welcome aboard</h1> - <h2>You’re riding Ruby on Rails!</h2> - </div> - - <div id="about"> - <h3><a href="/rails/info/properties" onclick="about(); return false">About your application’s environment</a></h3> - <div id="about-content" style="display: none"></div> - </div> - - <div id="getting-started"> - <h1>Getting started</h1> - <h2>Here’s how to get rolling:</h2> - - <ol> - <li> - <h2>Use <code>bin/rails generate</code> to create your models and controllers</h2> - <p>To see all available options, run it without parameters.</p> - </li> - - <li> - <h2>Set up a root route to replace this page</h2> - <p>You're seeing this page because you're running in development mode and you haven't set a root route yet.</p> - <p>Routes are set up in <span class="filename">config/routes.rb</span>.</p> - </li> - - <li> - <h2>Configure your database</h2> - <p>If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p> - </li> - </ol> - </div> - </div> - - <div id="footer"> </div> - </div> - </body> +<head> + <title>Ruby on Rails</title> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width"> + <style type="text/css" media="screen" charset="utf-8"> + body { + font-family: Georgia, sans-serif; + line-height: 2rem; + font-size: 1.3rem; + background-color: white; + margin: 0; + padding: 0; + color: #000; + } + + h1 { + font-weight: normal; + line-height: 2.8rem; + font-size: 2.5rem; + letter-spacing: -1px; + color: black; + } + + p { font-family: monospace; } + + .container { + max-width: 960px; + margin: 0 auto 40px; + overflow: hidden; + } + + section { + margin: 0 auto 2rem; + padding: 1rem 0 0; + text-align: center; + } + + @media only screen and (max-width: 500px) { + h1 { font-size: 2rem; } + + .version { font-size: 1.1rem; } + } + + .welcome { + width: 600px; + max-width: 100%; + height: auto; + } + </style> +</head> + +<body> + <div class="container"> + <section> + <p> + <a href="http://rubyonrails.org"> + <img width="130" height="46" alt="Ruby on Rails" border="0" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCA0MDAgMTQwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCA0MDAgMTQwIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxzdHlsZT4uYXtmaWxsOiNjMDA7fTwvc3R5bGU+DQo8dGl0bGU+cmFpbHMtbG9nbzwvdGl0bGU+DQo8Zz4NCgk8cGF0aCBjbGFzcz0iYSIgZD0iTTM0Ni42LDEyMS41djE4LjFjMCwwLDIzLjQsMCwzMi43LDBjNi43LDAsMTguMi00LjksMTguNi0xOC42YzAtMC42LDAtNi41LDAtN2MwLTExLjctOS42LTE4LjYtMTguNi0xOC42DQoJCWMtNC4yLDAtMTYuMywwLTE2LjMsMFY4N2wzMi4zLDBWNjguOGMwLDAtMjIuMiwwLTMxLDBjLTgsMC0xOC43LDYuNi0xOC43LDE4LjljMCwxLjIsMCw1LjIsMCw2LjNjMCwxMi4zLDEwLjYsMTguNiwxOC43LDE4LjYNCgkJYzIyLjUsMC4xLTUuNCwwLDE1LjQsMGMwLDguOCwwLDguOCwwLDguOCIvPg0KCTxwYXRoIGNsYXNzPSJhIiBkPSJNMTcxLjQsMTE3LjFjMCwwLDE3LjUtMS41LDE3LjUtMjQuMXMtMjEuMi0yNC43LTIxLjItMjQuN2gtMzguMnY3MS4zaDE5LjJ2LTE3LjJsMTYuNiwxNy4yaDI4LjQNCgkJTDE3MS40LDExNy4xeiBNMTY0LDEwMi41aC0xNS4zVjg2LjJoMTUuNGMwLDAsNC4zLDEuNiw0LjMsOC4xUzE2NCwxMDIuNSwxNjQsMTAyLjV6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik0yMzYuMyw2OC44Yy00LjksMC01LjYsMC0xOS41LDBjLTEzLjksMC0xOC42LDEyLjYtMTguNiwxOC42YzAsMTMsMCw1Mi4yLDAsNTIuMmgxOS41di0xMi41SDIzNnYxMi41aDE4LjkNCgkJYzAsMCwwLTM4LjUsMC01Mi4yQzI1NC45LDcyLjIsMjQxLjEsNjguOCwyMzYuMyw2OC44eiBNMjM2LDEwNi45aC0xOC40Vjg5LjZjMCwwLDAtMy45LDYuMS0zLjljNS42LDAsMSwwLDYuNywwDQoJCWM1LjQsMCw1LjUsMy45LDUuNSwzLjlWMTA2Ljl6Ii8+DQoJPHJlY3QgeD0iMjYzLjgiIHk9IjY4LjgiIGNsYXNzPSJhIiB3aWR0aD0iMjAuMyIgaGVpZ2h0PSI3MC44Ii8+DQoJPHBvbHlnb24gY2xhc3M9ImEiIHBvaW50cz0iMzEyLjYsMTIxLjMgMzEyLjYsNjguOCAyOTIuNCw2OC44IDI5Mi40LDEyMS4zIDI5Mi40LDEzOS42IDMxMi42LDEzOS42IDMzOS45LDEzOS42IDMzOS45LDEyMS4zIAkNCgkJIi8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik05LDEzOS42aDc5YzAsMC0xNS4xLTY4LjksMzQuOS05Ni44YzEwLjktNS4zLDQ1LjYtMjUuMSwxMDIuNCwxNi45YzEuOC0xLjUsMy41LTIuNywzLjUtMi43DQoJCVMxNzYuOCw1LjEsMTE4LjksMTAuOUM4OS44LDEzLjUsNTQsNDAsMzMsNzVTOSwxMzkuNiw5LDEzOS42eiIvPg0KCTxwYXRoIGNsYXNzPSJhIiBkPSJNOSwxMzkuNmg3OWMwLDAtMTUuMS02OC45LDM0LjktOTYuOGMxMC45LTUuMyw0NS42LTI1LjEsMTAyLjQsMTYuOWMxLjgtMS41LDMuNS0yLjcsMy41LTIuNw0KCQlTMTc2LjgsNS4xLDExOC45LDEwLjlDODkuOCwxMy41LDU0LDQwLDMzLDc1UzksMTM5LjYsOSwxMzkuNnoiLz4NCgk8cGF0aCBjbGFzcz0iYSIgZD0iTTksMTM5LjZoNzljMCwwLTE1LjEtNjguOSwzNC45LTk2LjhjMTAuOS01LjMsNDUuNi0yNS4xLDEwMi40LDE2LjljMS44LTEuNSwzLjUtMi43LDMuNS0yLjcNCgkJUzE3Ni44LDUuMSwxMTguOSwxMC45Qzg5LjcsMTMuNSw1My45LDQwLDMyLjksNzVTOSwxMzkuNiw5LDEzOS42eiIvPg0KCTxwYXRoIGNsYXNzPSJhIiBkPSJNMTczLjYsMTYuNWwwLjQtNi43Yy0wLjktMC41LTMuNC0xLjctOS43LTMuNWwtMC40LDYuNkMxNjcuMiwxNCwxNzAuNCwxNS4yLDE3My42LDE2LjV6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik0xNjQuMSwzNy43bC0wLjQsNi4zYzMuMywwLjEsNi42LDAuNSw5LjksMS4ybDAuNC02LjJDMTcwLjYsMzguMywxNjcuMywzNy45LDE2NC4xLDM3Ljd6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik0xMjcuMSw2LjVjMC4zLDAsMC43LDAsMSwwbC0yLTYuMWMtMy4xLDAtNi4zLDAuMi05LjYsMC42bDEuOSw1LjlDMTIxLjMsNi42LDEyNC4yLDYuNSwxMjcuMSw2LjV6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik0xMzEuOSw0My4zbDIuMyw2LjljMi45LTEuNCw1LjgtMi42LDguNy0zLjVsLTIuMi02LjZDMTM3LjMsNDEuMSwxMzQuNCw0Mi4yLDEzMS45LDQzLjN6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik04Ni41LDE3TDgyLDEwLjFjLTIuNSwxLjMtNS4xLDIuNy03LjgsNC4zbDQuNiw3QzgxLjQsMTkuOCw4My45LDE4LjMsODYuNSwxN3oiLz4NCgk8cGF0aCBjbGFzcz0iYSIgZD0iTTEwNyw2Mmw0LjgsNy4yYzEuNy0yLjUsMy43LTQuOCw1LjktNy4xbC00LjUtNi44QzExMC45LDU3LjQsMTA4LjgsNTkuNywxMDcsNjJ6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik05Mi41LDk0LjJsOC4xLDYuNGMwLjQtMy45LDEuMS03LjgsMi4xLTExLjdsLTcuMi01LjdDOTQuMiw4Ni45LDkzLjMsOTAuNiw5Mi41LDk0LjJ6Ii8+DQoJPHBhdGggY2xhc3M9ImEiIGQ9Ik00OC43LDQ2LjdsLTcuMS02LjJjLTIuNiwyLjUtNS4xLDUtNy40LDcuNWw3LjcsNi42QzQ0LDUxLjksNDYuMyw0OS4yLDQ4LjcsNDYuN3oiLz4NCgk8cGF0aCBjbGFzcz0iYSIgZD0iTTE4LjUsOTEuNEw3LDg3LjJjLTEuOSw0LjMtNCw5LjMtNSwxMmwxMS41LDQuMkMxNC44LDEwMCwxNi45LDk1LjEsMTguNSw5MS40eiIvPg0KCTxwYXRoIGNsYXNzPSJhIiBkPSJNOTEsMTE5LjZjMC4yLDUuMywwLjcsOS42LDEuMiwxMi42bDEyLDQuM2MtMC45LTMuOS0xLjgtOC4zLTIuNC0xM0w5MSwxMTkuNnoiLz4NCjwvZz4NCjwvc3ZnPg0K" /> + </a> + </p> + + <h1>Yay! You’re on Rails!</h1> + + <img alt="Welcome" class="welcome" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABIIAAAMGCAIAAAABcbh7AAAAA3NCSVQICAjb4U/gAAAAinpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjaVY7RDcMwCET/PUVGwIAPGKeKEqkbdPxAnNbq+4DTyXqmHZ/32baiEzcd5giAEg0NfmVwmghRZ+q1c06eLT0Tr7oJz4BwI10P9em/DIHjNDXDwI6d086HsHjNFJWV6oxYEudbmd/+94T7th/tAkOgLCrTUorzAAAKCGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjExNTQiCiAgIGV4aWY6UGl4ZWxZRGltZW5zaW9uPSI3NzQiCiAgIHRpZmY6SW1hZ2VXaWR0aD0iMTE1NCIKICAgdGlmZjpJbWFnZUhlaWdodD0iNzc0IgogICB0aWZmOk9yaWVudGF0aW9uPSIxIi8+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg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" /> + + <p class="version"> + <strong>Rails version:</strong> <%= Rails.version %><br /> + <strong>Ruby version:</strong> <%= RUBY_VERSION %> (<%= RUBY_PLATFORM %>) + </p> + </section> + </div> +</body> </html> diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 828039dc43..4e3ec184be 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -1,41 +1,49 @@ +# frozen_string_literal: true + # Make double-sure the RAILS_ENV is not set to production, # so fixtures aren't loaded into that environment abort("Abort testing: Your Rails environment is running in production mode!") if Rails.env.production? -require "rails/test_unit/minitest_plugin" -require 'active_support/test_case' -require 'action_controller' -require 'action_controller/test_case' -require 'action_dispatch/testing/integration' -require 'rails/generators/test_case' +require "active_support/test_case" +require "action_controller" +require "action_controller/test_case" +require "action_dispatch/testing/integration" +require "rails/generators/test_case" -require 'active_support/testing/autorun' +require "active_support/testing/autorun" if defined?(ActiveRecord::Base) - ActiveRecord::Migration.maintain_test_schema! + begin + ActiveRecord::Migration.maintain_test_schema! + rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 + end - class ActiveSupport::TestCase + ActiveSupport.on_load(:active_support_test_case) do + include ActiveRecord::TestDatabases include ActiveRecord::TestFixtures + self.fixture_path = "#{Rails.root}/test/fixtures/" - self.file_fixture_path = self.fixture_path + "files" + self.file_fixture_path = fixture_path + "files" end - ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path - - def create_fixtures(*fixture_set_names, &block) - FixtureSet.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, {}, &block) + ActiveSupport.on_load(:action_dispatch_integration_test) do + self.fixture_path = ActiveSupport::TestCase.fixture_path end end -class ActionController::TestCase - def before_setup +# :enddoc: + +ActiveSupport.on_load(:action_controller_test_case) do + def before_setup # :nodoc: @routes = Rails.application.routes super end end -class ActionDispatch::IntegrationTest - def before_setup +ActiveSupport.on_load(:action_dispatch_integration_test) do + def before_setup # :nodoc: @routes = Rails.application.routes super end diff --git a/railties/lib/rails/test_unit/line_filtering.rb b/railties/lib/rails/test_unit/line_filtering.rb new file mode 100644 index 0000000000..f8ca77fe4a --- /dev/null +++ b/railties/lib/rails/test_unit/line_filtering.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "rails/test_unit/runner" + +module Rails + module LineFiltering # :nodoc: + def run(reporter, options = {}) + options[:filter] = Rails::TestUnit::Runner.compose_filter(self, options[:filter]) + + super + end + end +end diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb deleted file mode 100644 index ab71298509..0000000000 --- a/railties/lib/rails/test_unit/minitest_plugin.rb +++ /dev/null @@ -1,51 +0,0 @@ -require "active_support/core_ext/module/attribute_accessors" -require "rails/test_unit/reporter" -require "rails/test_unit/test_requirer" - -module Minitest - def self.plugin_rails_options(opts, options) - opts.separator "" - opts.separator "Usage: bin/rails test [options] [files or directories]" - opts.separator "You can run a single test by appending a line number to a filename:" - opts.separator "" - opts.separator " bin/rails test test/models/user_test.rb:27" - opts.separator "" - opts.separator "You can run multiple files and directories at the same time:" - opts.separator "" - opts.separator " bin/rails test test/controllers test/integration/login_test.rb" - opts.separator "" - - opts.separator "Rails options:" - opts.on("-e", "--environment [ENV]", - "Run tests in the ENV environment") do |env| - options[:environment] = env.strip - end - - opts.on("-b", "--backtrace", - "Show the complete backtrace") do - options[:full_backtrace] = true - end - - options[:patterns] = opts.order! - end - - def self.plugin_rails_init(options) - self.run_with_rails_extension = true - - ENV["RAILS_ENV"] = options[:environment] || "test" - - ::Rails::TestRequirer.require_files options[:patterns] unless run_with_autorun - - unless options[:full_backtrace] || ENV["BACKTRACE"] - # Plugin can run without Rails loaded, check before filtering. - Minitest.backtrace_filter = ::Rails.backtrace_cleaner if ::Rails.respond_to?(:backtrace_cleaner) - end - - self.reporter << ::Rails::TestUnitReporter.new(options[:io], options) - end - - mattr_accessor(:run_with_autorun) { false } - mattr_accessor(:run_with_rails_extension) { false } -end - -Minitest.extensions << 'rails' diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb index 75180ff978..42b6daa3d1 100644 --- a/railties/lib/rails/test_unit/railtie.rb +++ b/railties/lib/rails/test_unit/railtie.rb @@ -1,5 +1,9 @@ +# frozen_string_literal: true + +require "rails/test_unit/line_filtering" + if defined?(Rake.application) && Rake.application.top_level_tasks.grep(/^(default$|test(:|$))/).any? - ENV['RAILS_ENV'] ||= 'test' + ENV["RAILS_ENV"] ||= Rake.application.options.show_tasks ? "development" : "test" end module Rails @@ -9,6 +13,13 @@ module Rails fixture_replacement: nil c.integration_tool :test_unit + c.system_tests :test_unit + end + + initializer "test_unit.line_filtering" do + ActiveSupport.on_load(:active_support_test_case) { + ActiveSupport::TestCase.extend Rails::LineFiltering + } end rake_tasks do diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb index 09b8675cf8..417933836b 100644 --- a/railties/lib/rails/test_unit/reporter.rb +++ b/railties/lib/rails/test_unit/reporter.rb @@ -1,13 +1,37 @@ +# frozen_string_literal: true + require "active_support/core_ext/class/attribute" require "minitest" module Rails class TestUnitReporter < Minitest::StatisticsReporter - class_attribute :executable - self.executable = "bin/rails test" + class_attribute :executable, default: "rails test" + + def record(result) + super + + if options[:verbose] + io.puts color_output(format_line(result), by: result) + else + io.print color_output(result.result_code, by: result) + end + + if output_inline? && result.failure && (!result.skipped? || options[:verbose]) + io.puts + io.puts + io.puts color_output(result, by: result) + io.puts + io.puts format_rerun_snippet(result) + io.puts + end + + if fail_fast? && result.failure && !result.skipped? + raise Interrupt + end + end def report - return if filtered_results.empty? + return if output_inline? || filtered_results.empty? io.puts io.puts "Failed tests:" io.puts @@ -15,10 +39,7 @@ module Rails end def aggregated_results # :nodoc: - filtered_results.map do |result| - location, line = result.method(result.name).source_location - "#{self.executable} #{relative_path_for(location)}:#{line}" - end.join "\n" + filtered_results.map { |result| format_rerun_snippet(result) }.join "\n" end def filtered_results @@ -30,7 +51,60 @@ module Rails end def relative_path_for(file) - file.sub(/^#{Rails.root}\/?/, '') + file.sub(/^#{app_root}\/?/, "") end + + private + def output_inline? + options[:output_inline] + end + + def fail_fast? + options[:fail_fast] + end + + def format_line(result) + klass = result.respond_to?(:klass) ? result.klass : result.class + "%s#%s = %.2f s = %s" % [klass, result.name, result.time, result.result_code] + end + + def format_rerun_snippet(result) + location, line = if result.respond_to?(:source_location) + result.source_location + else + result.method(result.name).source_location + end + + "#{executable} #{relative_path_for(location)}:#{line}" + end + + def app_root + @app_root ||= + if defined?(ENGINE_ROOT) + ENGINE_ROOT + elsif Rails.respond_to?(:root) + Rails.root + end + end + + def colored_output? + options[:color] && io.respond_to?(:tty?) && io.tty? + end + + codes = { red: 31, green: 32, yellow: 33 } + COLOR_BY_RESULT_CODE = { + "." => codes[:green], + "E" => codes[:red], + "F" => codes[:red], + "S" => codes[:yellow] + } + + def color_output(string, by:) + if colored_output? + "\e[#{COLOR_BY_RESULT_CODE[by.result_code]}m#{string}\e[0m" + else + string + end + end end end diff --git a/railties/lib/rails/test_unit/runner.rb b/railties/lib/rails/test_unit/runner.rb new file mode 100644 index 0000000000..d38952bb30 --- /dev/null +++ b/railties/lib/rails/test_unit/runner.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require "shellwords" +require "method_source" +require "rake/file_list" +require "active_support/core_ext/module/attribute_accessors" + +module Rails + module TestUnit + class Runner + mattr_reader :filters, default: [] + + class << self + def attach_before_load_options(opts) + opts.on("--warnings", "-w", "Run with Ruby warnings enabled") { } + opts.on("-e", "--environment ENV", "Run tests in the ENV environment") { } + end + + def parse_options(argv) + # Perform manual parsing and cleanup since option parser raises on unknown options. + env_index = argv.index("--environment") || argv.index("-e") + if env_index + argv.delete_at(env_index) + environment = argv.delete_at(env_index).strip + end + ENV["RAILS_ENV"] = environment || "test" + + w_index = argv.index("--warnings") || argv.index("-w") + $VERBOSE = argv.delete_at(w_index) if w_index + end + + def rake_run(argv = []) + ARGV.replace Shellwords.split(ENV["TESTOPTS"] || "") + + run(argv) + end + + def run(argv = []) + load_tests(argv) + + require "active_support/testing/autorun" + end + + def load_tests(argv) + patterns = extract_filters(argv) + + tests = Rake::FileList[patterns.any? ? patterns : "test/**/*_test.rb"] + tests.exclude("test/system/**/*") if patterns.empty? + + tests.to_a.each { |path| require File.expand_path(path) } + end + + def compose_filter(runnable, filter) + if filters.any? { |_, lines| lines.any? } + CompositeFilter.new(runnable, filter, filters) + else + filter + end + end + + private + def extract_filters(argv) + # Extract absolute and relative paths but skip -n /.*/ regexp filters. + argv.select { |arg| arg =~ %r%^/?\w+/% && !arg.end_with?("/") }.map do |path| + case + when /(:\d+)+$/.match?(path) + file, *lines = path.split(":") + filters << [ file, lines ] + file + when Dir.exist?(path) + "#{path}/**/*_test.rb" + else + filters << [ path, [] ] + path + end + end + end + end + end + + class CompositeFilter # :nodoc: + attr_reader :named_filter + + def initialize(runnable, filter, patterns) + @runnable = runnable + @named_filter = derive_named_filter(filter) + @filters = [ @named_filter, *derive_line_filters(patterns) ].compact + end + + # minitest uses === to find matching filters. + def ===(method) + @filters.any? { |filter| filter === method } + end + + private + def derive_named_filter(filter) + if filter.respond_to?(:named_filter) + filter.named_filter + elsif filter =~ %r%/(.*)/% # Regexp filtering copied from minitest. + Regexp.new $1 + elsif filter.is_a?(String) + filter + end + end + + def derive_line_filters(patterns) + patterns.flat_map do |file, lines| + if lines.empty? + Filter.new(@runnable, file, nil) if file + else + lines.map { |line| Filter.new(@runnable, file, line) } + end + end + end + end + + class Filter # :nodoc: + def initialize(runnable, file, line) + @runnable, @file = runnable, File.expand_path(file) + @line = line.to_i if line + end + + def ===(method) + return unless @runnable.method_defined?(method) + + if @line + test_file, test_range = definition_for(@runnable.instance_method(method)) + test_file == @file && test_range.include?(@line) + else + @runnable.instance_method(method).source_location.first == @file + end + end + + private + def definition_for(method) + file, start_line = method.source_location + end_line = method.source.count("\n") + start_line - 1 + + return file, start_line..end_line + end + end + end +end diff --git a/railties/lib/rails/test_unit/test_requirer.rb b/railties/lib/rails/test_unit/test_requirer.rb deleted file mode 100644 index 84c2256729..0000000000 --- a/railties/lib/rails/test_unit/test_requirer.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'active_support/core_ext/object/blank' -require 'rake/file_list' - -module Rails - class TestRequirer # :nodoc: - class << self - def require_files(patterns) - patterns = expand_patterns(patterns) - - Rake::FileList[patterns.compact.presence || 'test/**/*_test.rb'].to_a.each do |file| - require File.expand_path(file) - end - end - - private - def expand_patterns(patterns) - patterns.map do |arg| - arg = arg.gsub(/:(\d+)?$/, '') - if Dir.exist?(arg) - "#{arg}/**/*_test.rb" - elsif File.file?(arg) - arg - end - end - end - end - end -end diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index dda492f974..32ac27a135 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -1,13 +1,20 @@ -gem 'minitest' -require 'minitest' -require 'rails/test_unit/minitest_plugin' +# frozen_string_literal: true + +gem "minitest" +require "minitest" +require "rails/test_unit/runner" task default: :test -desc "Runs all tests in test folder" +desc "Runs all tests in test folder except system ones" task :test do $: << "test" - Minitest.run(['test']) + + if ENV.key?("TEST") + Rails::TestUnit::Runner.rake_run([ENV["TEST"]]) + else + Rails::TestUnit::Runner.rake_run + end end namespace :test do @@ -16,30 +23,36 @@ namespace :test do # If used with Active Record, this task runs before the database schema is synchronized. end - task :run => %w[test] + task run: %w[test] desc "Run tests quickly, but also reset db" - task :db => %w[db:test:prepare test] + task db: %w[db:test:prepare test] ["models", "helpers", "controllers", "mailers", "integration", "jobs"].each do |name| task name => "test:prepare" do $: << "test" - Minitest.run(["test/#{name}"]) + Rails::TestUnit::Runner.rake_run(["test/#{name}"]) end end - task :generators => "test:prepare" do + task generators: "test:prepare" do + $: << "test" + Rails::TestUnit::Runner.rake_run(["test/lib/generators"]) + end + + task units: "test:prepare" do $: << "test" - Minitest.run(["test/lib/generators"]) + Rails::TestUnit::Runner.rake_run(["test/models", "test/helpers", "test/unit"]) end - task :units => "test:prepare" do + task functionals: "test:prepare" do $: << "test" - Minitest.run(["test/models", "test/helpers", "test/unit"]) + Rails::TestUnit::Runner.rake_run(["test/controllers", "test/mailers", "test/functional"]) end - task :functionals => "test:prepare" do + desc "Run system tests only" + task system: "test:prepare" do $: << "test" - Minitest.run(["test/controllers", "test/mailers", "test/functional"]) + Rails::TestUnit::Runner.rake_run(["test/system"]) end end diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb index df351c4238..ba6763a572 100644 --- a/railties/lib/rails/version.rb +++ b/railties/lib/rails/version.rb @@ -1,4 +1,6 @@ -require_relative 'gem_version' +# frozen_string_literal: true + +require_relative "gem_version" module Rails # Returns the version of the currently loaded Rails as a string. diff --git a/railties/lib/rails/welcome_controller.rb b/railties/lib/rails/welcome_controller.rb index de9cd18b01..5b84b57679 100644 --- a/railties/lib/rails/welcome_controller.rb +++ b/railties/lib/rails/welcome_controller.rb @@ -1,4 +1,6 @@ -require 'rails/application_controller' +# frozen_string_literal: true + +require "rails/application_controller" class Rails::WelcomeController < Rails::ApplicationController # :nodoc: layout false diff --git a/railties/railties.gemspec b/railties/railties.gemspec index afe1603448..4e4a504c97 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -1,34 +1,44 @@ -version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip +# frozen_string_literal: true + +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY - s.name = 'railties' + s.name = "railties" s.version = version - s.summary = 'Tools for creating, working with, and running Rails applications.' - s.description = 'Rails internals: application bootup, plugins, generators, and rake tasks.' + s.summary = "Tools for creating, working with, and running Rails applications." + s.description = "Rails internals: application bootup, plugins, generators, and rake tasks." + + s.required_ruby_version = ">= 2.4.1" + + s.license = "MIT" - s.required_ruby_version = '>= 2.2.2' + s.author = "David Heinemeier Hansson" + s.email = "david@loudthinking.com" + s.homepage = "http://rubyonrails.org" - s.license = 'MIT' + s.files = Dir["CHANGELOG.md", "README.rdoc", "MIT-LICENSE", "RDOC_MAIN.rdoc", "exe/**/*", "lib/**/{*,.[a-z]*}"] + s.require_path = "lib" - s.author = 'David Heinemeier Hansson' - s.email = 'david@loudthinking.com' - s.homepage = 'http://www.rubyonrails.org' + s.bindir = "exe" + s.executables = ["rails"] - s.files = Dir['CHANGELOG.md', 'README.rdoc', 'RDOC_MAIN.rdoc', 'exe/**/*', 'lib/**/{*,.[a-z]*}'] - s.require_path = 'lib' + s.rdoc_options << "--exclude" << "." - s.bindir = 'exe' - s.executables = ['rails'] + s.metadata = { + "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/railties", + "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/railties/CHANGELOG.md" + } - s.rdoc_options << '--exclude' << '.' + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves - s.add_dependency 'activesupport', version - s.add_dependency 'actionpack', version + s.add_dependency "activesupport", version + s.add_dependency "actionpack", version - s.add_dependency 'rake', '>= 0.8.7' - s.add_dependency 'thor', '>= 0.18.1', '< 2.0' - s.add_dependency 'method_source' + s.add_dependency "rake", ">= 0.8.7" + s.add_dependency "thor", ">= 0.19.0", "< 2.0" + s.add_dependency "method_source" - s.add_development_dependency 'actionview', version + s.add_development_dependency "actionview", version end diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index 794d180e5d..9c866263f0 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -1,33 +1,34 @@ -ENV["RAILS_ENV"] ||= "test" +# frozen_string_literal: true -require File.expand_path("../../../load_paths", __FILE__) +ENV["RAILS_ENV"] ||= "test" -require 'stringio' -require 'active_support/testing/autorun' -require 'active_support/testing/stream' -require 'fileutils' +require "stringio" +require "active_support/testing/autorun" +require "active_support/testing/stream" +require "fileutils" -require 'active_support' -require 'action_controller' -require 'action_view' -require 'rails/all' +require "active_support" +require "action_controller" +require "action_view" +require "rails/all" module TestApp class Application < Rails::Application - config.root = File.dirname(__FILE__) - secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + config.root = __dir__ end end -# Skips the current run on Rubinius using Minitest::Assertions#skip -def rubinius_skip(message = '') - skip message if RUBY_ENGINE == 'rbx' -end -# Skips the current run on JRuby using Minitest::Assertions#skip -def jruby_skip(message = '') - skip message if defined?(JRUBY_VERSION) -end - class ActiveSupport::TestCase include ActiveSupport::Testing::Stream + + private + # Skips the current run on Rubinius using Minitest::Assertions#skip + def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + + # Skips the current run on JRuby using Minitest::Assertions#skip + def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/railties/test/app_loader_test.rb b/railties/test/app_loader_test.rb index 5946c8fd4c..0deb1a76df 100644 --- a/railties/test/app_loader_test.rb +++ b/railties/test/app_loader_test.rb @@ -1,23 +1,25 @@ -require 'tmpdir' -require 'abstract_unit' -require 'rails/app_loader' +# frozen_string_literal: true + +require "tmpdir" +require "abstract_unit" +require "rails/app_loader" class AppLoaderTest < ActiveSupport::TestCase def loader @loader ||= Class.new do extend Rails::AppLoader - def self.exec_arguments - @exec_arguments - end + class << self + attr_accessor :exec_arguments - def self.exec(*args) - @exec_arguments = args + def exec(*args) + self.exec_arguments = args + end end end end - def write(filename, contents=nil) + def write(filename, contents = nil) FileUtils.mkdir_p(File.dirname(filename)) File.write(filename, contents) end @@ -27,27 +29,27 @@ class AppLoaderTest < ActiveSupport::TestCase end setup do - @tmp = Dir.mktmpdir('railties-rails-loader-test-suite') + @tmp = Dir.mktmpdir("railties-rails-loader-test-suite") @cwd = Dir.pwd Dir.chdir(@tmp) end - ['bin', 'script'].each do |script_dir| + ["bin", "script"].each do |script_dir| exe = "#{script_dir}/rails" test "is not in a Rails application if #{exe} is not found in the current or parent directories" do def loader.find_executables; end - assert !loader.exec_app + assert_not loader.exec_app end test "is not in a Rails application if #{exe} exists but is a folder" do FileUtils.mkdir_p(exe) - assert !loader.exec_app + assert_not loader.exec_app end - ['APP_PATH', 'ENGINE_PATH'].each do |keyword| + ["APP_PATH", "ENGINE_PATH"].each do |keyword| test "is in a Rails application if #{exe} exists and contains #{keyword}" do write exe, keyword @@ -59,14 +61,14 @@ class AppLoaderTest < ActiveSupport::TestCase test "is not in a Rails application if #{exe} exists but doesn't contain #{keyword}" do write exe - assert !loader.exec_app + assert_not loader.exec_app end test "is in a Rails application if parent directory has #{exe} containing #{keyword} and chdirs to the root directory" do write "foo/bar/#{exe}" write "foo/#{exe}", keyword - Dir.chdir('foo/bar') + Dir.chdir("foo/bar") loader.exec_app @@ -74,7 +76,7 @@ class AppLoaderTest < ActiveSupport::TestCase # Compare the realpath in case either of them has symlinks. # - # This happens in particular in Mac OS X, where @tmp starts + # This happens in particular in macOS, where @tmp starts # with "/var", and Dir.pwd with "/private/var", due to a # default system symlink var -> private/var. assert_equal File.realpath("#@tmp/foo"), File.realpath(Dir.pwd) diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb index 36ab8109a7..3e0f31860b 100644 --- a/railties/test/application/asset_debugging_test.rb +++ b/railties/test/application/asset_debugging_test.rb @@ -1,5 +1,7 @@ -require 'isolation/abstract_unit' -require 'rack/test' +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rack/test" module ApplicationTests class AssetDebuggingTest < ActiveSupport::TestCase @@ -25,8 +27,6 @@ module ApplicationTests RUBY ENV["RAILS_ENV"] = "production" - - boot_rails end def teardown @@ -36,14 +36,15 @@ module ApplicationTests test "assets are concatenated when debug is off and compile is off either if debug_assets param is provided" do # config.assets.debug and config.assets.compile are false for production environment ENV["RAILS_ENV"] = "production" - output = Dir.chdir(app_path){ `bin/rake assets:precompile --trace 2>&1` } - assert $?.success?, output - require "#{app_path}/config/environment" + rails "assets:precompile", "--trace" + + # Load app env + app "production" class ::PostsController < ActionController::Base ; end # the debug_assets params isn't used if compile is off - get '/posts?debug_assets=true' + get "/posts?debug_assets=true" assert_match(/<script src="\/assets\/application-([0-z]+)\.js"><\/script>/, last_response.body) assert_no_match(/<script src="\/assets\/xmlhr-([0-z]+)\.js"><\/script>/, last_response.body) end @@ -51,14 +52,112 @@ module ApplicationTests test "assets aren't concatenated when compile is true is on and debug_assets params is true" do add_to_env_config "production", "config.assets.compile = true" - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" + # Load app env + app "production" class ::PostsController < ActionController::Base ; end - get '/posts?debug_assets=true' + get "/posts?debug_assets=true" assert_match(/<script src="\/assets\/application(\.self)?-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) assert_match(/<script src="\/assets\/xmlhr(\.self)?-([0-z]+)\.js\?body=1"><\/script>/, last_response.body) end + + test "public path and tag methods are not over-written by the asset pipeline" do + contents = "doesnotexist" + cases = { + asset_path: %r{/#{contents}}, + image_path: %r{/images/#{contents}}, + video_path: %r{/videos/#{contents}}, + audio_path: %r{/audios/#{contents}}, + font_path: %r{/fonts/#{contents}}, + javascript_path: %r{/javascripts/#{contents}}, + stylesheet_path: %r{/stylesheets/#{contents}}, + image_tag: %r{<img src="/images/#{contents}"}, + favicon_link_tag: %r{<link rel="shortcut icon" type="image/x-icon" href="/images/#{contents}" />}, + stylesheet_link_tag: %r{<link rel="stylesheet" media="screen" href="/stylesheets/#{contents}.css" />}, + javascript_include_tag: %r{<script src="/javascripts/#{contents}.js">}, + audio_tag: %r{<audio src="/audios/#{contents}"></audio>}, + video_tag: %r{<video src="/videos/#{contents}"></video>}, + image_submit_tag: %r{<input type="image" src="/images/#{contents}" />} + } + + cases.each do |(view_method, tag_match)| + app_file "app/views/posts/index.html.erb", "<%= #{view_method} '#{contents}', skip_pipeline: true %>" + + app "development" + + class ::PostsController < ActionController::Base ; end + + get "/posts?debug_assets=true" + + body = last_response.body + assert_match(tag_match, body, "Expected `#{view_method}` to produce a match to #{tag_match}, but did not: #{body}") + end + end + + test "public url methods are not over-written by the asset pipeline" do + contents = "doesnotexist" + cases = { + asset_url: %r{http://example.org/#{contents}}, + image_url: %r{http://example.org/images/#{contents}}, + video_url: %r{http://example.org/videos/#{contents}}, + audio_url: %r{http://example.org/audios/#{contents}}, + font_url: %r{http://example.org/fonts/#{contents}}, + javascript_url: %r{http://example.org/javascripts/#{contents}}, + stylesheet_url: %r{http://example.org/stylesheets/#{contents}}, + } + + cases.each do |(view_method, tag_match)| + app_file "app/views/posts/index.html.erb", "<%= #{view_method} '#{contents}', skip_pipeline: true %>" + + app "development" + + class ::PostsController < ActionController::Base ; end + + get "/posts?debug_assets=true" + + body = last_response.body + assert_match(tag_match, body, "Expected `#{view_method}` to produce a match to #{tag_match}, but did not: #{body}") + end + end + + test "{ skip_pipeline: true } does not use the asset pipeline" do + cases = { + /\/assets\/application-.*.\.js/ => {}, + /application.js/ => { skip_pipeline: true }, + } + cases.each do |(tag_match, options_hash)| + app_file "app/views/posts/index.html.erb", "<%= asset_path('application.js', #{options_hash}) %>" + + app "development" + + class ::PostsController < ActionController::Base ; end + + get "/posts?debug_assets=true" + + body = last_response.body.strip + assert_match(tag_match, body, "Expected `asset_path` with `#{options_hash}` to produce a match to #{tag_match}, but did not: #{body}") + end + end + + test "public_compute_asset_path does not use the asset pipeline" do + cases = { + compute_asset_path: /\/assets\/application-.*.\.js/, + public_compute_asset_path: /application.js/, + } + + cases.each do |(view_method, tag_match)| + app_file "app/views/posts/index.html.erb", "<%= #{ view_method } 'application.js' %>" + + app "development" + + class ::PostsController < ActionController::Base ; end + + get "/posts?debug_assets=true" + + body = last_response.body.strip + assert_match(tag_match, body, "Expected `#{view_method}` to produce a match to #{ tag_match }, but did not: #{ body }") + end + end end end diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 6952472ff5..46ee0d670e 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -1,6 +1,8 @@ -require 'isolation/abstract_unit' -require 'rack/test' -require 'active_support/json' +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rack/test" +require "active_support/json" module ApplicationTests class AssetsTest < ActiveSupport::TestCase @@ -9,7 +11,6 @@ module ApplicationTests def setup build_app(initializers: true) - boot_rails end def teardown @@ -17,33 +18,43 @@ module ApplicationTests end def precompile!(env = nil) - quietly do - precompile_task = "bin/rake assets:precompile #{env} --trace 2>&1" - output = Dir.chdir(app_path) { %x[ #{precompile_task} ] } - assert $?.success?, output - output + with_env env.to_h do + quietly do + precompile_task = "bin/rails assets:precompile --trace 2>&1" + output = Dir.chdir(app_path) { %x[ #{precompile_task} ] } + assert $?.success?, output + output + end end end + def with_env(env) + env.each { |k, v| ENV[k.to_s] = v } + yield + ensure + env.each_key { |k| ENV.delete k.to_s } + end + def clean_assets! quietly do - assert Dir.chdir(app_path) { system('bin/rake assets:clobber') } + assert Dir.chdir(app_path) { system("bin/rails assets:clobber") } end end def assert_file_exists(filename) - assert Dir[filename].first, "missing #{filename}" + globbed = Dir[filename] + assert globbed.one?, "Found #{globbed.size} files matching #{filename}. All files in the directory: #{Dir.entries(File.dirname(filename)).inspect}" end def assert_no_file_exists(filename) - assert !File.exist?(filename), "#{filename} does exist" + assert_not File.exist?(filename), "#{filename} does exist" end test "assets routes have higher priority" do app_file "app/assets/images/rails.png", "notactuallyapng" app_file "app/assets/javascripts/demo.js.erb", "a = <%= image_path('rails.png').inspect %>;" - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get '*path', to: lambda { |env| [200, { "Content-Type" => "text/html" }, ["Not an asset"]] } end @@ -57,24 +68,11 @@ module ApplicationTests assert_equal 'a = "/assets/rails.png";', last_response.body.strip end - test "assets do not require compressors until it is used" do - app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();" - add_to_env_config "production", "config.assets.compile = true" - - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" - - assert !defined?(Uglifier) - get "/assets/demo.js" - assert_match "alert()", last_response.body - assert defined?(Uglifier) - end - test "precompile creates the file, gives it the original asset's content and run in production as default" do + app_file "app/assets/config/manifest.js", "//= link_tree ../javascripts" app_file "app/assets/javascripts/application.js", "alert();" app_file "app/assets/javascripts/foo/application.js", "alert();" - ENV["RAILS_ENV"] = nil precompile! files = Dir["#{app_path}/public/assets/application-*.js"] @@ -86,6 +84,7 @@ module ApplicationTests end def test_precompile_does_not_hit_the_database + app_file "app/assets/config/manifest.js", "//= link_tree ../javascripts" app_file "app/assets/javascripts/application.js", "alert();" app_file "app/assets/javascripts/foo/application.js", "alert();" app_file "app/controllers/users_controller.rb", <<-eoruby @@ -95,10 +94,9 @@ module ApplicationTests class User < ActiveRecord::Base; raise 'should not be reached'; end eoruby - ENV['RAILS_ENV'] = 'production' - ENV['DATABASE_URL'] = 'postgresql://baduser:badpass@127.0.0.1/dbname' - - precompile! + precompile! \ + RAILS_ENV: "production", + DATABASE_URL: "postgresql://baduser:badpass@127.0.0.1/dbname" files = Dir["#{app_path}/public/assets/application-*.js"] files << Dir["#{app_path}/public/assets/foo/application-*.js"].first @@ -106,9 +104,6 @@ module ApplicationTests assert_not_nil file, "Expected application.js asset to be generated, but none found" assert_equal "alert();".strip, File.read(file).strip end - ensure - ENV.delete 'RAILS_ENV' - ENV.delete 'DATABASE_URL' end test "precompile application.js and application.css and all other non JS/CSS files" do @@ -163,40 +158,64 @@ module ApplicationTests precompile! + assert_file_exists("#{app_path}/public/assets/something/index-*.js") + end + + test "precompile use assets defined in app env config" do + add_to_env_config "production", 'config.assets.precompile = [ "something.js" ]' + app_file "app/assets/javascripts/something.js.erb", "alert();" + + precompile! RAILS_ENV: "production" + assert_file_exists("#{app_path}/public/assets/something-*.js") end - test 'precompile use assets defined in app env config' do - add_to_env_config 'production', 'config.assets.precompile = [ "something.js" ]' + test "sprockets cache is not shared between environments" do + app_file "app/assets/images/rails.png", "notactuallyapng" + app_file "app/assets/stylesheets/application.css.erb", "body { background: '<%= asset_path('rails.png') %>'; }" + add_to_env_config "production", 'config.assets.prefix = "production_assets"' - app_file 'app/assets/javascripts/something.js.erb', 'alert();' + precompile! - precompile! 'RAILS_ENV=production' + assert_file_exists("#{app_path}/public/assets/application-*.css") - assert_file_exists("#{app_path}/public/assets/something-*.js") + file = Dir["#{app_path}/public/assets/application-*.css"].first + assert_match(/assets\/rails-([0-z]+)\.png/, File.read(file)) + + precompile! RAILS_ENV: "production" + + assert_file_exists("#{app_path}/public/production_assets/application-*.css") + + file = Dir["#{app_path}/public/production_assets/application-*.css"].first + assert_match(/production_assets\/rails-([0-z]+)\.png/, File.read(file)) end - test 'precompile use assets defined in app config and reassigned in app env config' do - add_to_config 'config.assets.precompile = [ "something.js" ]' - add_to_env_config 'production', 'config.assets.precompile += [ "another.js" ]' + test "precompile use assets defined in app config and reassigned in app env config" do + add_to_config 'config.assets.precompile = [ "something_manifest.js" ]' + add_to_env_config "production", 'config.assets.precompile += [ "another_manifest.js" ]' + + app_file "app/assets/config/something_manifest.js", "//= link something.js" + app_file "app/assets/config/another_manifest.js", "//= link another.js" - app_file 'app/assets/javascripts/something.js.erb', 'alert();' - app_file 'app/assets/javascripts/another.js.erb', 'alert();' + app_file "app/assets/javascripts/something.js.erb", "alert();" + app_file "app/assets/javascripts/another.js.erb", "alert();" - precompile! 'RAILS_ENV=production' + precompile! RAILS_ENV: "production" + assert_file_exists("#{app_path}/public/assets/something_manifest-*.js") assert_file_exists("#{app_path}/public/assets/something-*.js") + assert_file_exists("#{app_path}/public/assets/another_manifest-*.js") assert_file_exists("#{app_path}/public/assets/another-*.js") end - test "asset pipeline should use a Sprockets::Index when config.assets.digest is true" do + test "asset pipeline should use a Sprockets::CachedEnvironment when config.assets.digest is true" do add_to_config "config.action_controller.perform_caching = false" add_to_env_config "production", "config.assets.compile = true" - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" + # Load app env + app "production" - assert_equal Sprockets::Index, Rails.application.assets.class + assert_equal Sprockets::CachedEnvironment, Rails.application.assets.class end test "precompile creates a manifest file with all the assets listed" do @@ -205,8 +224,8 @@ module ApplicationTests app_file "app/assets/javascripts/application.js", "alert();" precompile! - manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first + manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) assert_match(/application-([0-z]+)\.js/, assets["assets"]["application.js"]) assert_match(/application-([0-z]+)\.css/, assets["assets"]["application.css"]) @@ -225,30 +244,29 @@ module ApplicationTests test "assets do not require any assets group gem when manifest file is present" do app_file "app/assets/javascripts/application.js", "alert();" - add_to_env_config "production", "config.serve_static_files = true" + add_to_env_config "production", "config.public_file_server.enabled = true" - ENV["RAILS_ENV"] = "production" - precompile! + precompile! RAILS_ENV: "production" manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) asset_path = assets["assets"]["application.js"] - require "#{app_path}/config/environment" + # Load app env + app "production" # Checking if Uglifier is defined we can know if Sprockets was reached or not - assert !defined?(Uglifier) + assert_not defined?(Uglifier) get "/assets/#{asset_path}" assert_match "alert()", last_response.body - assert !defined?(Uglifier) + assert_not defined?(Uglifier) end - test "precompile properly refers files referenced with asset_path and runs in the provided RAILS_ENV" do + test "precompile properly refers files referenced with asset_path" do app_file "app/assets/images/rails.png", "notactuallyapng" - app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>" - add_to_env_config "test", "config.assets.digest = true" + app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }" - precompile!('RAILS_ENV=test') + precompile! file = Dir["#{app_path}/public/assets/application-*.css"].first assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file)) @@ -257,10 +275,9 @@ module ApplicationTests test "precompile shouldn't use the digests present in manifest.json" do app_file "app/assets/images/rails.png", "notactuallyapng" - app_file "app/assets/stylesheets/application.css.erb", "p { url: <%= asset_path('rails.png') %> }" + app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }" - ENV["RAILS_ENV"] = "production" - precompile! + precompile! RAILS_ENV: "production" manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first assets = ActiveSupport::JSON.decode(File.read(manifest)) @@ -269,17 +286,16 @@ module ApplicationTests app_file "app/assets/images/rails.png", "p { url: change }" precompile! - assets = ActiveSupport::JSON.decode(File.read(manifest)) + assets = ActiveSupport::JSON.decode(File.read(manifest)) assert_not_equal asset_path, assets["assets"]["application.css"] end - test "precompile appends the md5 hash to files referenced with asset_path and run in production with digest true" do + test "precompile appends the MD5 hash to files referenced with asset_path and run in production with digest true" do app_file "app/assets/images/rails.png", "notactuallyapng" - app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>" + app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }" - ENV["RAILS_ENV"] = "production" - precompile! + precompile! RAILS_ENV: "production" file = Dir["#{app_path}/public/assets/application-*.css"].first assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file)) @@ -288,7 +304,8 @@ module ApplicationTests test "precompile should handle utf8 filenames" do filename = "レイルズ.png" app_file "app/assets/images/#{filename}", "not an image really" - add_to_config "config.assets.precompile = [ /\.png$/, /application.(css|js)$/ ]" + app_file "app/assets/config/manifest.js", "//= link_tree ../images" + add_to_config "config.assets.precompile = %w(manifest.js)" precompile! @@ -296,7 +313,8 @@ module ApplicationTests assets = ActiveSupport::JSON.decode(File.read(manifest)) assert asset_path = assets["assets"].find { |(k, _)| k && k =~ /.png/ }[1] - require "#{app_path}/config/environment" + # Load app env + app "development" get "/assets/#{URI.parser.escape(asset_path)}" assert_match "not an image really", last_response.body @@ -310,8 +328,7 @@ module ApplicationTests clean_assets! - files = Dir["#{app_path}/public/assets/**/*", "#{app_path}/tmp/cache/assets/development/*", - "#{app_path}/tmp/cache/assets/test/*", "#{app_path}/tmp/cache/assets/production/*"] + files = Dir["#{app_path}/public/assets/**/*"] assert_equal 0, files.length, "Expected no assets, but found #{files.join(', ')}" end @@ -319,8 +336,8 @@ module ApplicationTests app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();" add_to_config "config.assets.compile = false" - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" + # Load app env + app "production" get "/assets/demo.js" assert_equal 404, last_response.status @@ -337,21 +354,22 @@ module ApplicationTests add_to_env_config "development", "config.assets.digest = false" - require "#{app_path}/config/environment" + # Load app env + app "development" class ::OmgController < ActionController::Base def index flash[:cool_story] = true - render text: "ok" + render plain: "ok" end end get "/omg" - assert_equal 'ok', last_response.body + assert_equal "ok", last_response.body get "/assets/demo.js" assert_match "alert()", last_response.body - assert_equal nil, last_response.headers["Set-Cookie"] + assert_nil last_response.headers["Set-Cookie"] end test "files in any assets/ directories are not added to Sprockets" do @@ -363,7 +381,8 @@ module ApplicationTests add_to_env_config "development", "config.assets.digest = false" - require "#{app_path}/config/environment" + # Load app env + app "development" get "/assets/demo.js" assert_match "alert();", last_response.body @@ -374,15 +393,15 @@ module ApplicationTests app_with_assets_in_view # config.assets.debug and config.assets.compile are false for production environment - ENV["RAILS_ENV"] = "production" - precompile! + precompile! RAILS_ENV: "production" - require "#{app_path}/config/environment" + # Load app env + app "production" class ::PostsController < ActionController::Base ; end # the debug_assets params isn't used if compile is off - get '/posts?debug_assets=true' + get "/posts?debug_assets=true" assert_match(/<script src="\/assets\/application-([0-z]+)\.js"><\/script>/, last_response.body) assert_no_match(/<script src="\/assets\/xmlhr-([0-z]+)\.js"><\/script>/, last_response.body) end @@ -393,7 +412,8 @@ module ApplicationTests app_file "app/assets/javascripts/xmlhr.js.erb", "<%= Post.name %>" precompile! - assert_equal "Post;\n", File.read(Dir["#{app_path}/public/assets/application-*.js"].first) + + assert_match(/Post;/, File.read(Dir["#{app_path}/public/assets/application-*.js"].first)) end test "initialization on the assets group should set assets_dir" do @@ -409,13 +429,13 @@ module ApplicationTests end test "digested assets are not mistakenly removed" do - app_file "app/assets/application.js", "alert();" + app_file "app/assets/application.css", "div { font-weight: bold }" add_to_config "config.assets.compile = true" precompile! - files = Dir["#{app_path}/public/assets/application-*.js"] - assert_equal 1, files.length, "Expected digested application.js asset to be generated, but none found" + files = Dir["#{app_path}/public/assets/application-*.css"] + assert_equal 1, files.length, "Expected digested application.css asset to be generated, but none found" end test "digested assets are removed from configured path" do @@ -434,12 +454,15 @@ module ApplicationTests app_with_assets_in_view add_to_config "config.asset_host = 'example.com'" add_to_env_config "development", "config.assets.digest = false" - require "#{app_path}/config/environment" + + # Load app env + app "development" + class ::PostsController < ActionController::Base; end - get '/posts', {}, {'HTTPS'=>'off'} + get "/posts", {}, { "HTTPS" => "off" } assert_match('src="http://example.com/assets/application.self.js', last_response.body) - get '/posts', {}, {'HTTPS'=>'on'} + get "/posts", {}, { "HTTPS" => "on" } assert_match('src="https://example.com/assets/application.self.js', last_response.body) end @@ -449,6 +472,7 @@ module ApplicationTests add_to_config "config.assets.precompile = %w{rails.png image_loader.js}" add_to_config "config.asset_host = 'example.com'" add_to_env_config "development", "config.assets.digest = false" + precompile! assert_match "src='//example.com/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/image_loader-*.js"].first) @@ -460,6 +484,7 @@ module ApplicationTests app_file "app/assets/javascripts/app.js.erb", "var src='<%= image_path('rails.png') %>';" add_to_config "config.assets.precompile = %w{rails.png app.js}" add_to_env_config "development", "config.assets.digest = false" + precompile! assert_match "src='/sub/uri/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/app-*.js"].first) @@ -467,16 +492,16 @@ module ApplicationTests private - def app_with_assets_in_view - app_file "app/assets/javascripts/application.js", "//= require_tree ." - app_file "app/assets/javascripts/xmlhr.js", "function f1() { alert(); }" - app_file "app/views/posts/index.html.erb", "<%= javascript_include_tag 'application' %>" + def app_with_assets_in_view + app_file "app/assets/javascripts/application.js", "//= require_tree ." + app_file "app/assets/javascripts/xmlhr.js", "function f1() { alert(); }" + app_file "app/views/posts/index.html.erb", "<%= javascript_include_tag 'application' %>" - app_file "config/routes.rb", <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get '/posts', :to => "posts#index" end RUBY - end + end end end diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb index 1bdced02e9..a952d2466b 100644 --- a/railties/test/application/bin_setup_test.rb +++ b/railties/test/application/bin_setup_test.rb @@ -1,4 +1,6 @@ -require 'isolation/abstract_unit' +# frozen_string_literal: true + +require "isolation/abstract_unit" module ApplicationTests class BinSetupTest < ActiveSupport::TestCase @@ -14,39 +16,49 @@ module ApplicationTests def test_bin_setup Dir.chdir(app_path) do - app_file 'db/schema.rb', <<-RUBY + app_file "db/schema.rb", <<-RUBY ActiveRecord::Schema.define(version: 20140423102712) do create_table(:articles) {} end RUBY - list_tables = lambda { `bin/rails runner 'p ActiveRecord::Base.connection.tables'`.strip } - File.write("log/my.log", "zomg!") + list_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip } + File.write("log/test.log", "zomg!") - assert_equal '[]', list_tables.call - assert_equal 5, File.size("log/my.log") + assert_equal "[]", list_tables.call + assert_equal 5, File.size("log/test.log") assert_not File.exist?("tmp/restart.txt") `bin/setup 2>&1` - assert_equal 0, File.size("log/my.log") - assert_equal '["articles", "schema_migrations"]', list_tables.call + assert_equal 0, File.size("log/test.log") + assert_equal '["articles", "schema_migrations", "ar_internal_metadata"]', list_tables.call assert File.exist?("tmp/restart.txt") end end def test_bin_setup_output Dir.chdir(app_path) do - app_file 'db/schema.rb', "" + app_file "db/schema.rb", "" output = `bin/setup 2>&1` - assert_equal(<<-OUTPUT, output) -== Installing dependencies == -The Gemfile's dependencies are satisfied -== Preparing database == + # Ignore line that's only output by Bundler < 1.14 + output.sub!(/^Resolving dependencies\.\.\.\n/, "") + # Suppress Bundler platform warnings from output + output.gsub!(/^The dependency .* will be unused .*\.\n/, "") + # Ignore warnings such as `Psych.safe_load is deprecated` + output.gsub!(/^warning:\s.*\n/, "") + + assert_equal(<<~OUTPUT, output) + == Installing dependencies == + The Gemfile's dependencies are satisfied + + == Preparing database == + Created database 'db/development.sqlite3' + Created database 'db/test.sqlite3' -== Removing old logs and tempfiles == + == Removing old logs and tempfiles == -== Restarting application server == + == Restarting application server == OUTPUT end end diff --git a/railties/test/application/configuration/custom_test.rb b/railties/test/application/configuration/custom_test.rb index 28b3b2f2d6..608bc2fbe3 100644 --- a/railties/test/application/configuration/custom_test.rb +++ b/railties/test/application/configuration/custom_test.rb @@ -1,20 +1,20 @@ -require 'isolation/abstract_unit' +# frozen_string_literal: true + +require "isolation/abstract_unit" module ApplicationTests module ConfigurationTests class CustomTest < ActiveSupport::TestCase def setup build_app - boot_rails FileUtils.rm_rf("#{app_path}/config/environments") end def teardown teardown_app - FileUtils.rm_rf(new_app) if File.directory?(new_app) end - test 'access custom configuration point' do + test "access custom configuration point" do add_to_config <<-RUBY config.x.payment_processing.schedule = :daily config.x.payment_processing.retries = 3 @@ -29,23 +29,16 @@ module ApplicationTests assert_equal 3, x.payment_processing.retries assert_equal true, x.super_debugger assert_equal false, x.hyper_debugger - assert_equal nil, x.nil_debugger + assert_nil x.nil_debugger assert_nil x.i_do_not_exist.zomg + + # test that custom configuration responds to all messages + assert_respond_to x, :i_do_not_exist + assert_kind_of Method, x.method(:i_do_not_exist) + assert_kind_of ActiveSupport::OrderedOptions, x.i_do_not_exist end private - def new_app - File.expand_path("#{app_path}/../new_app") - end - - def copy_app - FileUtils.cp_r(app_path, new_app) - end - - def app - @app ||= Rails.application - end - def require_environment require "#{app_path}/config/environment" end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index f677a7c42a..b8a0434432 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -1,6 +1,9 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" -require 'rack/test' -require 'env_helpers' +require "rack/test" +require "env_helpers" +require "set" class ::MyMailInterceptor def self.delivering_email(email); email; end @@ -34,14 +37,21 @@ module ApplicationTests FileUtils.cp_r(app_path, new_app) end - def app - @app ||= Rails.application + def app(env = "development") + @app ||= begin + ENV["RAILS_ENV"] = env + + require "#{app_path}/config/environment" + + Rails.application + ensure + ENV.delete "RAILS_ENV" + end end def setup build_app - boot_rails - supress_default_config + suppress_default_config end def teardown @@ -49,7 +59,7 @@ module ApplicationTests FileUtils.rm_rf(new_app) if File.directory?(new_app) end - def supress_default_config + def suppress_default_config FileUtils.mv("#{app_path}/config/environments", "#{app_path}/config/__environments__") end @@ -64,12 +74,42 @@ module ApplicationTests switch_env "RAILS_ENV", nil do Rails.env = "development" assert_equal "development", Rails.env - assert_nil ENV['RAILS_ENV'] + assert_nil ENV["RAILS_ENV"] + end + end + + test "Rails.env falls back to development if RAILS_ENV is blank and RACK_ENV is nil" do + with_rails_env("") do + assert_equal "development", Rails.env + end + end + + test "Rails.env falls back to development if RACK_ENV is blank and RAILS_ENV is nil" do + with_rack_env("") do + assert_equal "development", Rails.env + end + end + + test "By default logs tags are not set in development" do + restore_default_config + + with_rails_env "development" do + app "development" + assert_predicate Rails.application.config.log_tags, :blank? + end + end + + test "By default logs are tagged with :request_id in production" do + restore_default_config + + with_rails_env "production" do + app "production" + assert_equal [:request_id], Rails.application.config.log_tags end end test "lib dir is on LOAD_PATH during config" do - app_file 'lib/my_logger.rb', <<-RUBY + app_file "lib/my_logger.rb", <<-RUBY require "logger" class MyLogger < ::Logger end @@ -78,8 +118,22 @@ module ApplicationTests require 'my_logger' config.logger = MyLogger.new STDOUT RUBY - require "#{app_path}/config/environment" - assert_equal 'MyLogger', Rails.application.config.logger.class.name + + app "development" + + assert_equal "MyLogger", Rails.application.config.logger.class.name + end + + test "raises an error if cache does not support recyclable cache keys" do + build_app(initializers: true) + add_to_env_config "production", "config.cache_store = Class.new {}.new" + add_to_env_config "production", "config.active_record.cache_versioning = true" + + error = assert_raise(RuntimeError) do + app "production" + end + + assert_match(/You're using a cache/, error.message) end test "a renders exception on pending migration" do @@ -89,15 +143,15 @@ module ApplicationTests config.action_dispatch.show_exceptions = true RUBY - app_file 'db/migrate/20140708012246_create_user.rb', <<-RUBY - class CreateUser < ActiveRecord::Migration + app_file "db/migrate/20140708012246_create_user.rb", <<-RUBY + class CreateUser < ActiveRecord::Migration::Current def change create_table :users end end RUBY - require "#{app_path}/config/environment" + app "development" ActiveRecord::Migrator.migrations_paths = ["#{app_path}/db/migrate"] @@ -126,31 +180,29 @@ module ApplicationTests end test "Rails.application is nil until app is initialized" do - require 'rails' + require "rails" assert_nil Rails.application - require "#{app_path}/config/environment" + app "development" assert_equal AppTemplate::Application.instance, Rails.application end test "Rails.application responds to all instance methods" do - require "#{app_path}/config/environment" - assert_respond_to Rails.application, :routes_reloader + app "development" assert_equal Rails.application.routes_reloader, AppTemplate::Application.routes_reloader end test "Rails::Application responds to paths" do - require "#{app_path}/config/environment" - assert_respond_to AppTemplate::Application, :paths + app "development" assert_equal ["#{app_path}/app/views"], AppTemplate::Application.paths["app/views"].expanded end test "the application root is set correctly" do - require "#{app_path}/config/environment" + app "development" assert_equal Pathname.new(app_path), Rails.application.root end test "the application root can be seen from the application singleton" do - require "#{app_path}/config/environment" + app "development" assert_equal Pathname.new(app_path), AppTemplate::Application.root end @@ -162,7 +214,8 @@ module ApplicationTests use_frameworks [] - require "#{app_path}/config/environment" + app "development" + assert_equal Pathname.new(new_app), Rails.application.root end @@ -172,7 +225,7 @@ module ApplicationTests use_frameworks [] Dir.chdir("#{app_path}") do - require "#{app_path}/config/environment" + app "development" assert_equal Pathname.new("#{app_path}"), Rails.application.root end end @@ -181,7 +234,9 @@ module ApplicationTests add_to_config <<-RUBY config.root = "#{app_path}" RUBY - require "#{app_path}/config/environment" + + app "development" + assert_instance_of Pathname, Rails.root end @@ -189,26 +244,139 @@ module ApplicationTests add_to_config <<-RUBY config.paths["public"] = "somewhere" RUBY - require "#{app_path}/config/environment" + + app "development" + assert_instance_of Pathname, Rails.public_path end + test "does not eager load controller actions in development" do + app_file "app/controllers/posts_controller.rb", <<-RUBY + class PostsController < ActionController::Base + def index;end + def show;end + end + RUBY + + app "development" + + assert_nil PostsController.instance_variable_get(:@action_methods) + end + + test "eager loads controller actions in production" do + app_file "app/controllers/posts_controller.rb", <<-RUBY + class PostsController < ActionController::Base + def index;end + def show;end + end + RUBY + + add_to_config <<-RUBY + config.eager_load = true + config.cache_classes = true + RUBY + + app "production" + + assert_equal %w(index show).to_set, PostsController.instance_variable_get(:@action_methods) + end + + test "does not eager load mailer actions in development" do + app_file "app/mailers/posts_mailer.rb", <<-RUBY + class PostsMailer < ActionMailer::Base + def noop_email;end + end + RUBY + + app "development" + + assert_nil PostsMailer.instance_variable_get(:@action_methods) + end + + test "eager loads mailer actions in production" do + app_file "app/mailers/posts_mailer.rb", <<-RUBY + class PostsMailer < ActionMailer::Base + def noop_email;end + end + RUBY + + add_to_config <<-RUBY + config.eager_load = true + config.cache_classes = true + RUBY + + app "production" + + assert_equal %w(noop_email).to_set, PostsMailer.instance_variable_get(:@action_methods) + end + + test "does not eager load attribute methods in development" do + app_file "app/models/post.rb", <<-RUBY + class Post < ActiveRecord::Base + end + RUBY + + app_file "config/initializers/active_record.rb", <<-RUBY + ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define(version: 1) do + create_table :posts do |t| + t.string :title + end + end + RUBY + + app "development" + + assert_not_includes Post.instance_methods, :title + end + + test "eager loads attribute methods in production" do + app_file "app/models/post.rb", <<-RUBY + class Post < ActiveRecord::Base + end + RUBY + + app_file "config/initializers/active_record.rb", <<-RUBY + ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define(version: 1) do + create_table :posts do |t| + t.string :title + end + end + RUBY + + add_to_config <<-RUBY + config.eager_load = true + config.cache_classes = true + RUBY + + app "production" + + assert_includes Post.instance_methods, :title + end + test "initialize an eager loaded, cache classes app" do add_to_config <<-RUBY config.eager_load = true config.cache_classes = true RUBY - require "#{app_path}/config/application" - assert Rails.application.initialize! + app "development" + + assert_equal :require, ActiveSupport::Dependencies.mechanism end test "application is always added to eager_load namespaces" do - require "#{app_path}/config/application" + app "development" assert_includes Rails.application.config.eager_load_namespaces, AppTemplate::Application end test "the application can be eager loaded even when there are no frameworks" do + FileUtils.rm_rf("#{app_path}/app/jobs/application_job.rb") + FileUtils.rm_rf("#{app_path}/app/models/application_record.rb") + FileUtils.rm_rf("#{app_path}/app/mailers/application_mailer.rb") FileUtils.rm_rf("#{app_path}/config/environments") add_to_config <<-RUBY config.eager_load = true @@ -218,7 +386,7 @@ module ApplicationTests use_frameworks [] assert_nothing_raised do - require "#{app_path}/config/application" + app "development" end end @@ -230,18 +398,18 @@ module ApplicationTests RUBY assert_nothing_raised do - require "#{app_path}/config/application" + app "development" end end test "filter_parameters should be able to set via config.filter_parameters in an initializer" do - app_file 'config/initializers/filter_parameters_logging.rb', <<-RUBY + app_file "config/initializers/filter_parameters_logging.rb", <<-RUBY Rails.application.config.filter_parameters += [ :password, :foo, 'bar' ] RUBY - require "#{app_path}/config/environment" + app "development" - assert_equal [:password, :foo, 'bar'], Rails.application.env_config['action_dispatch.parameter_filter'] + assert_equal [:password, :foo, "bar"], Rails.application.env_config["action_dispatch.parameter_filter"] end test "config.to_prepare is forwarded to ActionDispatch" do @@ -253,9 +421,9 @@ module ApplicationTests end RUBY - assert !$prepared + assert_not $prepared - require "#{app_path}/config/environment" + app "development" get "/" assert $prepared @@ -267,7 +435,7 @@ module ApplicationTests end test "skipping config.encoding still results in 'utf-8' as the default" do - require "#{app_path}/config/application" + app "development" assert_utf8 end @@ -276,7 +444,7 @@ module ApplicationTests config.encoding = "utf-8" RUBY - require "#{app_path}/config/application" + app "development" assert_utf8 end @@ -285,133 +453,173 @@ module ApplicationTests config.paths["public"] = "somewhere" RUBY - require "#{app_path}/config/application" + app "development" assert_equal Pathname.new(app_path).join("somewhere"), Rails.public_path end - test "In production mode, config.serve_static_files is off by default" do + test "In production mode, config.public_file_server.enabled is off by default" do restore_default_config with_rails_env "production" do - require "#{app_path}/config/environment" - assert_not app.config.serve_static_files + app "production" + assert_not app.config.public_file_server.enabled end end - test "In production mode, config.serve_static_files is enabled when RAILS_SERVE_STATIC_FILES is set" do + test "In production mode, config.public_file_server.enabled is enabled when RAILS_SERVE_STATIC_FILES is set" do restore_default_config with_rails_env "production" do switch_env "RAILS_SERVE_STATIC_FILES", "1" do - require "#{app_path}/config/environment" - assert app.config.serve_static_files + app "production" + assert app.config.public_file_server.enabled + end + end + end + + test "In production mode, STDOUT logging is enabled when RAILS_LOG_TO_STDOUT is set" do + restore_default_config + + with_rails_env "production" do + switch_env "RAILS_LOG_TO_STDOUT", "1" do + app "production" + assert ActiveSupport::Logger.logger_outputs_to?(app.config.logger, STDOUT) end end end - test "In production mode, config.serve_static_files is disabled when RAILS_SERVE_STATIC_FILES is blank" do + test "In production mode, config.public_file_server.enabled is disabled when RAILS_SERVE_STATIC_FILES is blank" do restore_default_config with_rails_env "production" do switch_env "RAILS_SERVE_STATIC_FILES", " " do - require "#{app_path}/config/environment" - assert_not app.config.serve_static_files + app "production" + assert_not app.config.public_file_server.enabled end end end test "Use key_generator when secret_key_base is set" do make_basic_app do |application| - application.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + application.secrets.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" application.config.session_store :disabled end class ::OmgController < ActionController::Base def index cookies.signed[:some_key] = "some_value" - render text: cookies[:some_key] + render plain: cookies[:some_key] end end get "/" - secret = app.key_generator.generate_key('signed cookie') + secret = app.key_generator.generate_key("signed cookie") verifier = ActiveSupport::MessageVerifier.new(secret) - assert_equal 'some_value', verifier.verify(last_response.body) + assert_equal "some_value", verifier.verify(last_response.body) end test "application verifier can be used in the entire application" do make_basic_app do |application| - application.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + application.secrets.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" application.config.session_store :disabled end message = app.message_verifier(:sensitive_value).generate("some_value") - assert_equal 'some_value', Rails.application.message_verifier(:sensitive_value).verify(message) + assert_equal "some_value", Rails.application.message_verifier(:sensitive_value).verify(message) - secret = app.key_generator.generate_key('sensitive_value') + secret = app.key_generator.generate_key("sensitive_value") verifier = ActiveSupport::MessageVerifier.new(secret) - assert_equal 'some_value', verifier.verify(message) + assert_equal "some_value", verifier.verify(message) end test "application message verifier can be used when the key_generator is ActiveSupport::LegacyKeyGenerator" do - app_file 'config/initializers/secret_token.rb', <<-RUBY + app_file "config/initializers/secret_token.rb", <<-RUBY + Rails.application.credentials.secret_key_base = nil Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33" RUBY - app_file 'config/secrets.yml', <<-YAML - development: - secret_key_base: - YAML - require "#{app_path}/config/environment" + app "production" - assert_equal app.env_config['action_dispatch.key_generator'], Rails.application.key_generator - assert_equal app.env_config['action_dispatch.key_generator'].class, ActiveSupport::LegacyKeyGenerator + assert_kind_of ActiveSupport::LegacyKeyGenerator, Rails.application.key_generator message = app.message_verifier(:sensitive_value).generate("some_value") - assert_equal 'some_value', Rails.application.message_verifier(:sensitive_value).verify(message) + assert_equal "some_value", Rails.application.message_verifier(:sensitive_value).verify(message) end - test "warns when secrets.secret_key_base is blank and config.secret_token is set" do - app_file 'config/initializers/secret_token.rb', <<-RUBY + test "config.secret_token is deprecated" do + app_file "config/initializers/secret_token.rb", <<-RUBY Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33" RUBY - app_file 'config/secrets.yml', <<-YAML - development: - secret_key_base: + + app "production" + + assert_deprecated(/secret_token/) do + app.secrets + end + end + + test "secrets.secret_token is deprecated" do + app_file "config/secrets.yml", <<-YAML + production: + secret_token: "b3c631c314c0bbca50c1b2843150fe33" YAML - require "#{app_path}/config/environment" - assert_deprecated(/You didn't set `secret_key_base`./) do - app.env_config + app "production" + + assert_deprecated(/secret_token/) do + app.secrets + end + end + + + test "raises when secret_key_base is blank" do + app_file "config/initializers/secret_token.rb", <<-RUBY + Rails.application.credentials.secret_key_base = nil + RUBY + + error = assert_raise(ArgumentError) do + app "production" + end + assert_match(/Missing `secret_key_base`./, error.message) + end + + test "raise when secret_key_base is not a type of string" do + add_to_config <<-RUBY + Rails.application.credentials.secret_key_base = 123 + RUBY + + assert_raise(ArgumentError) do + app "production" end end test "prefer secrets.secret_token over config.secret_token" do - app_file 'config/initializers/secret_token.rb', <<-RUBY + app_file "config/initializers/secret_token.rb", <<-RUBY Rails.application.config.secret_token = "" RUBY - app_file 'config/secrets.yml', <<-YAML + app_file "config/secrets.yml", <<-YAML development: secret_token: 3b7cd727ee24e8444053437c36cc66c3 YAML - require "#{app_path}/config/environment" - assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_token + app "development" + + assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secrets.secret_token end test "application verifier can build different verifiers" do make_basic_app do |application| - application.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33' + application.credentials.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" application.config.session_store :disabled end default_verifier = app.message_verifier(:sensitive_value) text_verifier = app.message_verifier(:text) - message = text_verifier.generate('some_value') + message = text_verifier.generate("some_value") - assert_equal 'some_value', text_verifier.verify(message) + assert_equal "some_value", text_verifier.verify(message) assert_raises ActiveSupport::MessageVerifier::InvalidSignature do default_verifier.verify(message) end @@ -421,104 +629,151 @@ module ApplicationTests end test "secrets.secret_key_base is used when config/secrets.yml is present" do - app_file 'config/secrets.yml', <<-YAML + app_file "config/secrets.yml", <<-YAML development: secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 YAML - require "#{app_path}/config/environment" - assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base + app "development" + assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secrets.secret_key_base + assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secret_key_base end test "secret_key_base is copied from config to secrets when not set" do remove_file "config/secrets.yml" - app_file 'config/initializers/secret_token.rb', <<-RUBY + app_file "config/initializers/secret_token.rb", <<-RUBY Rails.application.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c3" RUBY - require "#{app_path}/config/environment" - assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base + app "development" + assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secrets.secret_key_base end test "config.secret_token over-writes a blank secrets.secret_token" do - app_file 'config/initializers/secret_token.rb', <<-RUBY + app_file "config/initializers/secret_token.rb", <<-RUBY Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33" RUBY - app_file 'config/secrets.yml', <<-YAML + app_file "config/secrets.yml", <<-YAML development: secret_key_base: secret_token: YAML - require "#{app_path}/config/environment" - assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.secrets.secret_token - assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token + app "development" + + assert_equal "b3c631c314c0bbca50c1b2843150fe33", app.secrets.secret_token + assert_equal "b3c631c314c0bbca50c1b2843150fe33", app.config.secret_token end test "custom secrets saved in config/secrets.yml are loaded in app secrets" do - app_file 'config/secrets.yml', <<-YAML + app_file "config/secrets.yml", <<-YAML development: secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 aws_access_key_id: myamazonaccesskeyid aws_secret_access_key: myamazonsecretaccesskey YAML - require "#{app_path}/config/environment" - assert_equal 'myamazonaccesskeyid', app.secrets.aws_access_key_id - assert_equal 'myamazonsecretaccesskey', app.secrets.aws_secret_access_key + app "development" + + assert_equal "myamazonaccesskeyid", app.secrets.aws_access_key_id + assert_equal "myamazonsecretaccesskey", app.secrets.aws_secret_access_key + end + + test "shared secrets saved in config/secrets.yml are loaded in app secrets" do + app_file "config/secrets.yml", <<-YAML + shared: + api_key: 3b7cd727 + YAML + + app "development" + + assert_equal "3b7cd727", app.secrets.api_key + end + + test "shared secrets will yield to environment specific secrets" do + app_file "config/secrets.yml", <<-YAML + shared: + api_key: 3b7cd727 + + development: + api_key: abc12345 + YAML + + app "development" + + assert_equal "abc12345", app.secrets.api_key end test "blank config/secrets.yml does not crash the loading process" do - app_file 'config/secrets.yml', <<-YAML + app_file "config/secrets.yml", <<-YAML YAML - require "#{app_path}/config/environment" + + app "development" assert_nil app.secrets.not_defined end test "config.secret_key_base over-writes a blank secrets.secret_key_base" do - app_file 'config/initializers/secret_token.rb', <<-RUBY + app_file "config/initializers/secret_token.rb", <<-RUBY Rails.application.config.secret_key_base = "iaminallyoursecretkeybase" RUBY - app_file 'config/secrets.yml', <<-YAML + app_file "config/secrets.yml", <<-YAML development: secret_key_base: YAML - require "#{app_path}/config/environment" + + app "development" assert_equal "iaminallyoursecretkeybase", app.secrets.secret_key_base end test "uses ActiveSupport::LegacyKeyGenerator as app.key_generator when secrets.secret_key_base is blank" do - app_file 'config/initializers/secret_token.rb', <<-RUBY + app_file "config/initializers/secret_token.rb", <<-RUBY + Rails.application.credentials.secret_key_base = nil Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33" RUBY - app_file 'config/secrets.yml', <<-YAML - development: - secret_key_base: - YAML - require "#{app_path}/config/environment" - assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token - assert_equal nil, app.secrets.secret_key_base - assert_equal app.key_generator.class, ActiveSupport::LegacyKeyGenerator + app "production" + + assert_equal "b3c631c314c0bbca50c1b2843150fe33", app.config.secret_token + assert_nil app.credentials.secret_key_base + assert_kind_of ActiveSupport::LegacyKeyGenerator, app.key_generator end - test "uses ActiveSupport::LegacyKeyGenerator with config.secret_token as app.key_generator when secrets.secret_key_base is blank" do - app_file 'config/initializers/secret_token.rb', <<-RUBY - Rails.application.config.secret_token = "" - RUBY - app_file 'config/secrets.yml', <<-YAML + test "that nested keys are symbolized the same as parents for hashes more than one level deep" do + app_file "config/secrets.yml", <<-YAML development: - secret_key_base: + smtp_settings: + address: "smtp.example.com" + user_name: "postmaster@example.com" + password: "697361616320736c6f616e2028656c6f7265737429" YAML - require "#{app_path}/config/environment" - assert_equal '', app.config.secret_token - assert_equal nil, app.secrets.secret_key_base - assert_raise ArgumentError, /\AA secret is required/ do - app.key_generator + app "development" + + assert_equal "697361616320736c6f616e2028656c6f7265737429", app.secrets.smtp_settings[:password] + end + + test "require_master_key aborts app boot when missing key" do + skip "can't run without fork" unless Process.respond_to?(:fork) + + remove_file "config/master.key" + add_to_config "config.require_master_key = true" + + error = capture(:stderr) do + Process.wait(Process.fork { app "development" }) end + + assert_equal 1, $?.exitstatus + assert_match(/Missing.*RAILS_MASTER_KEY/, error) + end + + test "credentials does not raise error when require_master_key is false and master key does not exist" do + remove_file "config/master.key" + add_to_config "config.require_master_key = false" + app "development" + + assert_not app.credentials.secret_key_base end test "protect from forgery is the default in a new app" do @@ -535,8 +790,7 @@ module ApplicationTests end test "default form builder specified as a string" do - - app_file 'config/initializers/form_builder.rb', <<-RUBY + app_file "config/initializers/form_builder.rb", <<-RUBY class CustomFormBuilder < ActionView::Helpers::FormBuilder def text_field(attribute, *args) label(attribute) + super(attribute, *args) @@ -545,15 +799,14 @@ module ApplicationTests Rails.configuration.action_view.default_form_builder = "CustomFormBuilder" RUBY - app_file 'app/models/post.rb', <<-RUBY + app_file "app/models/post.rb", <<-RUBY class Post include ActiveModel::Model attr_accessor :name end RUBY - - app_file 'app/controllers/posts_controller.rb', <<-RUBY + app_file "app/controllers/posts_controller.rb", <<-RUBY class PostsController < ApplicationController def index render inline: "<%= begin; form_for(Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>" @@ -567,14 +820,136 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app "development" get "/posts" assert_match(/label/, last_response.body) end + test "form_with can be configured with form_with_generates_ids" do + app_file "config/initializers/form_builder.rb", <<-RUBY + Rails.configuration.action_view.form_with_generates_ids = false + RUBY + + app_file "app/models/post.rb", <<-RUBY + class Post + include ActiveModel::Model + attr_accessor :name + end + RUBY + + app_file "app/controllers/posts_controller.rb", <<-RUBY + class PostsController < ApplicationController + def index + render inline: "<%= begin; form_with(model: Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>" + end + end + RUBY + + add_to_config <<-RUBY + routes.prepend do + resources :posts + end + RUBY + + app "development" + + get "/posts" + + assert_no_match(/id=('|")post_name('|")/, last_response.body) + end + + test "form_with outputs ids by default" do + app_file "app/models/post.rb", <<-RUBY + class Post + include ActiveModel::Model + attr_accessor :name + end + RUBY + + app_file "app/controllers/posts_controller.rb", <<-RUBY + class PostsController < ApplicationController + def index + render inline: "<%= begin; form_with(model: Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>" + end + end + RUBY + + add_to_config <<-RUBY + routes.prepend do + resources :posts + end + RUBY + + app "development" + + get "/posts" + + assert_match(/id=('|")post_name('|")/, last_response.body) + end + + test "form_with can be configured with form_with_generates_remote_forms" do + app_file "config/initializers/form_builder.rb", <<-RUBY + Rails.configuration.action_view.form_with_generates_remote_forms = false + RUBY + + app_file "app/models/post.rb", <<-RUBY + class Post + include ActiveModel::Model + attr_accessor :name + end + RUBY + + app_file "app/controllers/posts_controller.rb", <<-RUBY + class PostsController < ApplicationController + def index + render inline: "<%= begin; form_with(model: Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>" + end + end + RUBY + + add_to_config <<-RUBY + routes.prepend do + resources :posts + end + RUBY + + app "development" + + get "/posts" + assert_no_match(/data-remote/, last_response.body) + end + + test "form_with generates remote forms by default" do + app_file "app/models/post.rb", <<-RUBY + class Post + include ActiveModel::Model + attr_accessor :name + end + RUBY + + app_file "app/controllers/posts_controller.rb", <<-RUBY + class PostsController < ApplicationController + def index + render inline: "<%= begin; form_with(model: Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>" + end + end + RUBY + + add_to_config <<-RUBY + routes.prepend do + resources :posts + end + RUBY + + app "development" + + get "/posts" + assert_match(/data-remote/, last_response.body) + end + test "default method for update can be changed" do - app_file 'app/models/post.rb', <<-RUBY + app_file "app/models/post.rb", <<-RUBY class Post include ActiveModel::Model def to_key; [1]; end @@ -584,19 +959,19 @@ module ApplicationTests token = "cf50faa3fe97702ca1ae" - app_file 'app/controllers/posts_controller.rb', <<-RUBY + app_file "app/controllers/posts_controller.rb", <<-RUBY class PostsController < ApplicationController def show render inline: "<%= begin; form_for(Post.new) {}; rescue => e; e.to_s; end %>" end def update - render text: "update" + render plain: "update" end private - def form_authenticity_token; token; end # stub the authenticy token + def form_authenticity_token(*args); token; end # stub the authenticity token end RUBY @@ -606,9 +981,9 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app "development" - params = {authenticity_token: token} + params = { authenticity_token: token } get "/posts/1" assert_match(/patch/, last_response.body) @@ -628,7 +1003,7 @@ module ApplicationTests test "request forgery token param can be changed" do make_basic_app do |application| - application.config.action_controller.request_forgery_protection_token = '_xsrf_token_here' + application.config.action_controller.request_forgery_protection_token = "_xsrf_token_here" end class ::OmgController < ActionController::Base @@ -659,12 +1034,12 @@ module ApplicationTests config.action_mailer.interceptors = MyMailInterceptor RUBY - require "#{app_path}/config/environment" - require "mail" + app "development" + require "mail" _ = ActionMailer::Base - assert_equal [::MyMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors") + assert_equal [::MyMailInterceptor], ::Mail.class_variable_get(:@@delivery_interceptors) end test "registers multiple interceptors with ActionMailer" do @@ -672,12 +1047,12 @@ module ApplicationTests config.action_mailer.interceptors = [MyMailInterceptor, "MyOtherMailInterceptor"] RUBY - require "#{app_path}/config/environment" - require "mail" + app "development" + require "mail" _ = ActionMailer::Base - assert_equal [::MyMailInterceptor, ::MyOtherMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors") + assert_equal [::MyMailInterceptor, ::MyOtherMailInterceptor], ::Mail.class_variable_get(:@@delivery_interceptors) end test "registers preview interceptors with ActionMailer" do @@ -685,9 +1060,9 @@ module ApplicationTests config.action_mailer.preview_interceptors = MyPreviewMailInterceptor RUBY - require "#{app_path}/config/environment" - require "mail" + app "development" + require "mail" _ = ActionMailer::Base assert_equal [ActionMailer::InlinePreviewInterceptor, ::MyPreviewMailInterceptor], ActionMailer::Base.preview_interceptors @@ -698,22 +1073,22 @@ module ApplicationTests config.action_mailer.preview_interceptors = [MyPreviewMailInterceptor, "MyOtherPreviewMailInterceptor"] RUBY - require "#{app_path}/config/environment" - require "mail" + app "development" + require "mail" _ = ActionMailer::Base assert_equal [ActionMailer::InlinePreviewInterceptor, MyPreviewMailInterceptor, MyOtherPreviewMailInterceptor], ActionMailer::Base.preview_interceptors end test "default preview interceptor can be removed" do - app_file 'config/initializers/preview_interceptors.rb', <<-RUBY + app_file "config/initializers/preview_interceptors.rb", <<-RUBY ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor) RUBY - require "#{app_path}/config/environment" - require "mail" + app "development" + require "mail" _ = ActionMailer::Base assert_equal [], ActionMailer::Base.preview_interceptors @@ -724,12 +1099,12 @@ module ApplicationTests config.action_mailer.observers = MyMailObserver RUBY - require "#{app_path}/config/environment" - require "mail" + app "development" + require "mail" _ = ActionMailer::Base - assert_equal [::MyMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers") + assert_equal [::MyMailObserver], ::Mail.class_variable_get(:@@delivery_notification_observers) end test "registers multiple observers with ActionMailer" do @@ -737,12 +1112,12 @@ module ApplicationTests config.action_mailer.observers = [MyMailObserver, "MyOtherMailObserver"] RUBY - require "#{app_path}/config/environment" - require "mail" + app "development" + require "mail" _ = ActionMailer::Base - assert_equal [::MyMailObserver, ::MyOtherMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers") + assert_equal [::MyMailObserver, ::MyOtherMailObserver], ::Mail.class_variable_get(:@@delivery_notification_observers) end test "allows setting the queue name for the ActionMailer::DeliveryJob" do @@ -750,12 +1125,12 @@ module ApplicationTests config.action_mailer.deliver_later_queue_name = 'test_default' RUBY - require "#{app_path}/config/environment" - require "mail" + app "development" + require "mail" _ = ActionMailer::Base - assert_equal 'test_default', ActionMailer::Base.send(:class_variable_get, "@@deliver_later_queue_name") + assert_equal "test_default", ActionMailer::Base.class_variable_get(:@@deliver_later_queue_name) end test "valid timezone is setup correctly" do @@ -764,7 +1139,7 @@ module ApplicationTests config.time_zone = "Wellington" RUBY - require "#{app_path}/config/environment" + app "development" assert_equal "Wellington", Rails.application.config.time_zone end @@ -776,7 +1151,7 @@ module ApplicationTests RUBY assert_raise(ArgumentError) do - require "#{app_path}/config/environment" + app "development" end end @@ -786,7 +1161,7 @@ module ApplicationTests config.beginning_of_week = :wednesday RUBY - require "#{app_path}/config/environment" + app "development" assert_equal :wednesday, Rails.application.config.beginning_of_week end @@ -798,22 +1173,24 @@ module ApplicationTests RUBY assert_raise(ArgumentError) do - require "#{app_path}/config/environment" + app "development" end end test "config.action_view.cache_template_loading with cache_classes default" do add_to_config "config.cache_classes = true" - require "#{app_path}/config/environment" - require 'action_view/base' + + app "development" + require "action_view/base" assert_equal true, ActionView::Resolver.caching? end test "config.action_view.cache_template_loading without cache_classes default" do add_to_config "config.cache_classes = false" - require "#{app_path}/config/environment" - require 'action_view/base' + + app "development" + require "action_view/base" assert_equal false, ActionView::Resolver.caching? end @@ -823,8 +1200,9 @@ module ApplicationTests config.cache_classes = true config.action_view.cache_template_loading = false RUBY - require "#{app_path}/config/environment" - require 'action_view/base' + + app "development" + require "action_view/base" assert_equal false, ActionView::Resolver.caching? end @@ -834,8 +1212,9 @@ module ApplicationTests config.cache_classes = false config.action_view.cache_template_loading = true RUBY - require "#{app_path}/config/environment" - require 'action_view/base' + + app "development" + require "action_view/base" assert_equal true, ActionView::Resolver.caching? end @@ -845,11 +1224,11 @@ module ApplicationTests add_to_env_config "development", "config.cache_classes = false" # These requires are to emulate an engine loading Action View before the application - require 'action_view' - require 'action_view/railtie' - require 'action_view/base' + require "action_view" + require "action_view/railtie" + require "action_view/base" - require "#{app_path}/config/environment" + app "development" assert_equal false, ActionView::Resolver.caching? end @@ -861,20 +1240,20 @@ module ApplicationTests class ::OmgController < ActionController::Base def index - render text: env["action_dispatch.show_exceptions"] + render plain: request.env["action_dispatch.show_exceptions"] end end get "/" - assert_equal 'true', last_response.body + assert_equal "true", last_response.body end test "config.action_controller.wrap_parameters is set in ActionController::Base" do - app_file 'config/initializers/wrap_parameters.rb', <<-RUBY + app_file "config/initializers/wrap_parameters.rb", <<-RUBY ActionController::Base.wrap_parameters format: [:json] RUBY - app_file 'app/models/post.rb', <<-RUBY + app_file "app/models/post.rb", <<-RUBY class Post def self.attribute_names %w(title) @@ -882,16 +1261,16 @@ module ApplicationTests end RUBY - app_file 'app/controllers/application_controller.rb', <<-RUBY + app_file "app/controllers/application_controller.rb", <<-RUBY class ApplicationController < ActionController::Base protect_from_forgery with: :reset_session # as we are testing API here end RUBY - app_file 'app/controllers/posts_controller.rb', <<-RUBY + app_file "app/controllers/posts_controller.rb", <<-RUBY class PostsController < ApplicationController def create - render text: params[:post].inspect + render plain: params[:post].inspect end end RUBY @@ -902,17 +1281,17 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app "development" post "/posts.json", '{ "title": "foo", "name": "bar" }', "CONTENT_TYPE" => "application/json" - assert_equal '{"title"=>"foo"}', last_response.body + assert_equal '<ActionController::Parameters {"title"=>"foo"} permitted: false>', last_response.body end test "config.action_controller.permit_all_parameters = true" do - app_file 'app/controllers/posts_controller.rb', <<-RUBY + app_file "app/controllers/posts_controller.rb", <<-RUBY class PostsController < ActionController::Base def create - render text: params[:post].permitted? ? "permitted" : "forbidden" + render plain: params[:post].permitted? ? "permitted" : "forbidden" end end RUBY @@ -924,17 +1303,17 @@ module ApplicationTests config.action_controller.permit_all_parameters = true RUBY - require "#{app_path}/config/environment" + app "development" - post "/posts", {post: {"title" =>"zomg"}} - assert_equal 'permitted', last_response.body + post "/posts", post: { "title" => "zomg" } + assert_equal "permitted", last_response.body end test "config.action_controller.action_on_unpermitted_parameters = :raise" do - app_file 'app/controllers/posts_controller.rb', <<-RUBY + app_file "app/controllers/posts_controller.rb", <<-RUBY class PostsController < ActionController::Base def create - render text: params.require(:post).permit(:name) + render plain: params.require(:post).permit(:name) end end RUBY @@ -946,16 +1325,23 @@ module ApplicationTests config.action_controller.action_on_unpermitted_parameters = :raise RUBY - require "#{app_path}/config/environment" + app "development" + + force_lazy_load_hooks { ActionController::Base } + force_lazy_load_hooks { ActionController::API } assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters - post "/posts", {post: {"title" =>"zomg"}} + post "/posts", post: { "title" => "zomg" } assert_match "We're sorry, but something went wrong", last_response.body end test "config.action_controller.always_permitted_parameters are: controller, action by default" do - require "#{app_path}/config/environment" + app "development" + + force_lazy_load_hooks { ActionController::Base } + force_lazy_load_hooks { ActionController::API } + assert_equal %w(controller action), ActionController::Parameters.always_permitted_parameters end @@ -963,15 +1349,20 @@ module ApplicationTests add_to_config <<-RUBY config.action_controller.always_permitted_parameters = %w( controller action format ) RUBY - require "#{app_path}/config/environment" + + app "development" + + force_lazy_load_hooks { ActionController::Base } + force_lazy_load_hooks { ActionController::API } + assert_equal %w( controller action format ), ActionController::Parameters.always_permitted_parameters end - test "config.action_controller.always_permitted_parameters = ['controller','action','format'] does not raise exeception" do - app_file 'app/controllers/posts_controller.rb', <<-RUBY + test "config.action_controller.always_permitted_parameters = ['controller','action','format'] does not raise exception" do + app_file "app/controllers/posts_controller.rb", <<-RUBY class PostsController < ActionController::Base def create - render text: params.permit(post: [:title]) + render plain: params.permit(post: [:title]) end end RUBY @@ -984,38 +1375,87 @@ module ApplicationTests config.action_controller.action_on_unpermitted_parameters = :raise RUBY - require "#{app_path}/config/environment" + app "development" + + force_lazy_load_hooks { ActionController::Base } + force_lazy_load_hooks { ActionController::API } assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters - post "/posts", {post: {"title" =>"zomg"}, format: "json"} + post "/posts", post: { "title" => "zomg" }, format: "json" assert_equal 200, last_response.status end - test "config.action_controller.action_on_unpermitted_parameters is :log by default on development" do - ENV["RAILS_ENV"] = "development" + test "config.action_controller.action_on_unpermitted_parameters is :log by default in development" do + app "development" - require "#{app_path}/config/environment" + force_lazy_load_hooks { ActionController::Base } + force_lazy_load_hooks { ActionController::API } assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters end - test "config.action_controller.action_on_unpermitted_parameters is :log by default on test" do - ENV["RAILS_ENV"] = "test" + test "config.action_controller.action_on_unpermitted_parameters is :log by default in test" do + app "test" - require "#{app_path}/config/environment" + force_lazy_load_hooks { ActionController::Base } + force_lazy_load_hooks { ActionController::API } assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters end - test "config.action_controller.action_on_unpermitted_parameters is false by default on production" do - ENV["RAILS_ENV"] = "production" + test "config.action_controller.action_on_unpermitted_parameters is false by default in production" do + app "production" - require "#{app_path}/config/environment" + force_lazy_load_hooks { ActionController::Base } + force_lazy_load_hooks { ActionController::API } assert_equal false, ActionController::Parameters.action_on_unpermitted_parameters end + test "config.action_controller.default_protect_from_forgery is true by default" do + app "development" + + assert_equal true, ActionController::Base.default_protect_from_forgery + assert_includes ActionController::Base.__callbacks[:process_action].map(&:filter), :verify_authenticity_token + end + + test "config.action_controller.permit_all_parameters can be configured in an initializer" do + app_file "config/initializers/permit_all_parameters.rb", <<-RUBY + Rails.application.config.action_controller.permit_all_parameters = true + RUBY + + app "development" + + force_lazy_load_hooks { ActionController::Base } + force_lazy_load_hooks { ActionController::API } + assert_equal true, ActionController::Parameters.permit_all_parameters + end + + test "config.action_controller.always_permitted_parameters can be configured in an initializer" do + app_file "config/initializers/always_permitted_parameters.rb", <<-RUBY + Rails.application.config.action_controller.always_permitted_parameters = [] + RUBY + + app "development" + + force_lazy_load_hooks { ActionController::Base } + force_lazy_load_hooks { ActionController::API } + assert_equal [], ActionController::Parameters.always_permitted_parameters + end + + test "config.action_controller.action_on_unpermitted_parameters can be configured in an initializer" do + app_file "config/initializers/action_on_unpermitted_parameters.rb", <<-RUBY + Rails.application.config.action_controller.action_on_unpermitted_parameters = :raise + RUBY + + app "development" + + force_lazy_load_hooks { ActionController::Base } + force_lazy_load_hooks { ActionController::API } + assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters + end + test "config.action_dispatch.ignore_accept_header" do make_basic_app do |application| application.config.action_dispatch.ignore_accept_header = true @@ -1024,28 +1464,27 @@ module ApplicationTests class ::OmgController < ActionController::Base def index respond_to do |format| - format.html { render text: "HTML" } - format.xml { render text: "XML" } + format.html { render plain: "HTML" } + format.xml { render plain: "XML" } end end end - get "/", {}, "HTTP_ACCEPT" => "application/xml" - assert_equal 'HTML', last_response.body + get "/", {}, { "HTTP_ACCEPT" => "application/xml" } + assert_equal "HTML", last_response.body - get "/", { format: :xml }, "HTTP_ACCEPT" => "application/xml" - assert_equal 'XML', last_response.body + get "/", { format: :xml }, { "HTTP_ACCEPT" => "application/xml" } + assert_equal "XML", last_response.body end - test "Rails.application#env_config exists and include some existing parameters" do + test "Rails.application#env_config exists and includes some existing parameters" do make_basic_app - assert_respond_to app, :env_config - assert_equal app.env_config['action_dispatch.parameter_filter'], app.config.filter_parameters - assert_equal app.env_config['action_dispatch.show_exceptions'], app.config.action_dispatch.show_exceptions - assert_equal app.env_config['action_dispatch.logger'], Rails.logger - assert_equal app.env_config['action_dispatch.backtrace_cleaner'], Rails.backtrace_cleaner - assert_equal app.env_config['action_dispatch.key_generator'], Rails.application.key_generator + assert_equal app.env_config["action_dispatch.parameter_filter"], app.config.filter_parameters + assert_equal app.env_config["action_dispatch.show_exceptions"], app.config.action_dispatch.show_exceptions + assert_equal app.env_config["action_dispatch.logger"], Rails.logger + assert_equal app.env_config["action_dispatch.backtrace_cleaner"], Rails.backtrace_cleaner + assert_equal app.env_config["action_dispatch.key_generator"], Rails.application.key_generator end test "config.colorize_logging default is true" do @@ -1065,11 +1504,28 @@ module ApplicationTests end test "config.session_store with :active_record_store without activerecord-session_store gem" do - assert_raise RuntimeError, /activerecord-session_store/ do + e = assert_raise RuntimeError do make_basic_app do |application| application.config.session_store :active_record_store end end + assert_match(/activerecord-session_store/, e.message) + end + + test "default session store initializer does not overwrite the user defined session store even if it is disabled" do + make_basic_app do |application| + application.config.session_store :disabled + end + + assert_nil app.config.session_store + end + + test "default session store initializer sets session store to cookie store" do + session_options = { key: "_myapp_session", cookie_only: true } + make_basic_app + + assert_equal ActionDispatch::Session::CookieStore, app.config.session_store + assert_equal session_options, app.config.session_options end test "config.log_level with custom logger" do @@ -1083,27 +1539,30 @@ module ApplicationTests test "respond_to? accepts include_private" do make_basic_app - assert_not Rails.configuration.respond_to?(:method_missing) + assert_not_respond_to Rails.configuration, :method_missing assert Rails.configuration.respond_to?(:method_missing, true) end test "config.active_record.dump_schema_after_migration is false on production" do build_app - ENV["RAILS_ENV"] = "production" - require "#{app_path}/config/environment" + app "production" assert_not ActiveRecord::Base.dump_schema_after_migration end - test "config.active_record.dump_schema_after_migration is true by default on development" do - ENV["RAILS_ENV"] = "development" - - require "#{app_path}/config/environment" + test "config.active_record.dump_schema_after_migration is true by default in development" do + app "development" assert ActiveRecord::Base.dump_schema_after_migration end + test "config.active_record.verbose_query_logs is false by default in development" do + app "development" + + assert_not ActiveRecord::Base.verbose_query_logs + end + test "config.annotations wrapping SourceAnnotationExtractor::Annotation class" do make_basic_app do |application| application.config.annotations.register_extensions("coffee") do |tag| @@ -1111,7 +1570,7 @@ module ApplicationTests end end - assert_not_nil SourceAnnotationExtractor::Annotation.extensions[/\.(coffee)$/] + assert_not_nil Rails::SourceAnnotationExtractor::Annotation.extensions[/\.(coffee)$/] end test "rake_tasks block works at instance level" do @@ -1125,12 +1584,12 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app "development" assert_not Rails.configuration.ran_block - require 'rake' - require 'rake/testtask' - require 'rdoc/task' + require "rake" + require "rake/testtask" + require "rdoc/task" Rails.application.load_tasks assert Rails.configuration.ran_block @@ -1147,7 +1606,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app "development" assert_not Rails.configuration.ran_block Rails.application.load_generators @@ -1165,7 +1624,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app "development" assert_not Rails.configuration.ran_block Rails.application.load_console @@ -1183,7 +1642,7 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app "development" assert_not Rails.configuration.ran_block Rails.application.load_runner @@ -1191,7 +1650,7 @@ module ApplicationTests end test "loading the first existing database configuration available" do - app_file 'config/environments/development.rb', <<-RUBY + app_file "config/environments/development.rb", <<-RUBY Rails.application.configure do config.paths.add 'config/database', with: 'config/nonexistent.yml' @@ -1199,57 +1658,159 @@ module ApplicationTests end RUBY - require "#{app_path}/config/environment" + app "development" assert_kind_of Hash, Rails.application.config.database_configuration end - test 'raises with proper error message if no database configuration found' do + test "autoload paths do not include asset paths" do + app "development" + ActiveSupport::Dependencies.autoload_paths.each do |path| + assert_not_operator path, :ends_with?, "app/assets" + assert_not_operator path, :ends_with?, "app/javascript" + end + end + + test "raises with proper error message if no database configuration found" do FileUtils.rm("#{app_path}/config/database.yml") - require "#{app_path}/config/environment" err = assert_raises RuntimeError do + app "development" Rails.application.config.database_configuration end - assert_match 'config/database', err.message + assert_match "config/database", err.message end - test 'config.action_mailer.show_previews defaults to true in development' do - Rails.env = "development" - require "#{app_path}/config/environment" + test "loads database.yml using shared keys" do + app_file "config/database.yml", <<-YAML + shared: + username: bobby + adapter: sqlite3 + + development: + database: 'dev_db' + YAML + + app "development" + + ar_config = Rails.application.config.database_configuration + assert_equal "sqlite3", ar_config["development"]["adapter"] + assert_equal "bobby", ar_config["development"]["username"] + assert_equal "dev_db", ar_config["development"]["database"] + end + + test "loads database.yml using shared keys for undefined environments" do + app_file "config/database.yml", <<-YAML + shared: + username: bobby + adapter: sqlite3 + database: 'dev_db' + YAML + + app "development" + + ar_config = Rails.application.config.database_configuration + assert_equal "sqlite3", ar_config["development"]["adapter"] + assert_equal "bobby", ar_config["development"]["username"] + assert_equal "dev_db", ar_config["development"]["database"] + end + + test "config.action_mailer.show_previews defaults to true in development" do + app "development" assert Rails.application.config.action_mailer.show_previews end - test 'config.action_mailer.show_previews defaults to false in production' do - Rails.env = "production" - require "#{app_path}/config/environment" + test "config.action_mailer.show_previews defaults to false in production" do + app "production" assert_equal false, Rails.application.config.action_mailer.show_previews end - test 'config.action_mailer.show_previews can be set in the configuration file' do - Rails.env = "production" + test "config.action_mailer.show_previews can be set in the configuration file" do add_to_config <<-RUBY config.action_mailer.show_previews = true RUBY - require "#{app_path}/config/environment" + + app "production" assert_equal true, Rails.application.config.action_mailer.show_previews end test "config_for loads custom configuration from yaml files" do - app_file 'config/custom.yml', <<-RUBY + app_file "config/custom.yml", <<-RUBY development: - key: 'custom key' + foo: 'bar' RUBY add_to_config <<-RUBY config.my_custom_config = config_for('custom') RUBY - require "#{app_path}/config/environment" + app "development" + + assert_equal "bar", Rails.application.config.my_custom_config["foo"] + end + + test "config_for loads custom configuration from yaml accessible as symbol" do + app_file "config/custom.yml", <<-RUBY + development: + foo: 'bar' + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + + app "development" + + assert_equal "bar", Rails.application.config.my_custom_config[:foo] + end + + test "config_for loads custom configuration from yaml accessible as method" do + app_file "config/custom.yml", <<-RUBY + development: + foo: 'bar' + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + + app "development" + + assert_equal "bar", Rails.application.config.my_custom_config.foo + end + + test "config_for loads nested custom configuration from yaml as symbol keys" do + app_file "config/custom.yml", <<-RUBY + development: + foo: + bar: + baz: 1 + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + + app "development" + + assert_equal 1, Rails.application.config.my_custom_config.foo[:bar][:baz] + end + + test "config_for uses the Pathname object if it is provided" do + app_file "config/custom.yml", <<-RUBY + development: + key: 'custom key' + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for(Pathname.new(Rails.root.join("config/custom.yml"))) + RUBY + + app "development" - assert_equal 'custom key', Rails.application.config.my_custom_config['key'] + assert_equal "custom key", Rails.application.config.my_custom_config["key"] end test "config_for raises an exception if the file does not exist" do @@ -1258,14 +1819,14 @@ module ApplicationTests RUBY exception = assert_raises(RuntimeError) do - require "#{app_path}/config/environment" + app "development" end assert_equal "Could not load configuration. No such file - #{app_path}/config/custom.yml", exception.message end test "config_for without the environment configured returns an empty hash" do - app_file 'config/custom.yml', <<-RUBY + app_file "config/custom.yml", <<-RUBY test: key: 'custom key' RUBY @@ -1273,25 +1834,71 @@ module ApplicationTests add_to_config <<-RUBY config.my_custom_config = config_for('custom') RUBY - require "#{app_path}/config/environment" + + app "development" assert_equal({}, Rails.application.config.my_custom_config) end test "config_for with empty file returns an empty hash" do - app_file 'config/custom.yml', <<-RUBY + app_file "config/custom.yml", <<-RUBY RUBY add_to_config <<-RUBY config.my_custom_config = config_for('custom') RUBY - require "#{app_path}/config/environment" + + app "development" assert_equal({}, Rails.application.config.my_custom_config) end + test "default SQLite3Adapter.represent_boolean_as_integer for 5.1 is false" do + remove_from_config '.*config\.load_defaults.*\n' + + app_file "app/models/post.rb", <<-RUBY + class Post < ActiveRecord::Base + end + RUBY + + app "development" + force_lazy_load_hooks { Post } + + assert_not ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer + end + + test "default SQLite3Adapter.represent_boolean_as_integer for new installs is true" do + app_file "app/models/post.rb", <<-RUBY + class Post < ActiveRecord::Base + end + RUBY + + app "development" + force_lazy_load_hooks { Post } + + assert ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer + end + + test "represent_boolean_as_integer should be able to set via config.active_record.sqlite3.represent_boolean_as_integer" do + remove_from_config '.*config\.load_defaults.*\n' + + app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY + Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true + RUBY + + app_file "app/models/post.rb", <<-RUBY + class Post < ActiveRecord::Base + end + RUBY + + app "development" + force_lazy_load_hooks { Post } + + assert ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer + end + test "config_for containing ERB tags should evaluate" do - app_file 'config/custom.yml', <<-RUBY + app_file "config/custom.yml", <<-RUBY development: key: <%= 'custom key' %> RUBY @@ -1299,13 +1906,14 @@ module ApplicationTests add_to_config <<-RUBY config.my_custom_config = config_for('custom') RUBY - require "#{app_path}/config/environment" - assert_equal 'custom key', Rails.application.config.my_custom_config['key'] + app "development" + + assert_equal "custom key", Rails.application.config.my_custom_config["key"] end - test "config_for with syntax error show a more descritive exception" do - app_file 'config/custom.yml', <<-RUBY + test "config_for with syntax error show a more descriptive exception" do + app_file "config/custom.yml", <<-RUBY development: key: foo: RUBY @@ -1315,10 +1923,224 @@ module ApplicationTests RUBY exception = assert_raises(RuntimeError) do - require "#{app_path}/config/environment" + app "development" + end + + assert_match "YAML syntax error occurred while parsing", exception.message + end + + test "config_for allows overriding the environment" do + app_file "config/custom.yml", <<-RUBY + test: + key: 'walrus' + production: + key: 'unicorn' + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom', env: 'production') + RUBY + require "#{app_path}/config/environment" + + assert_equal "unicorn", Rails.application.config.my_custom_config["key"] + end + + test "api_only is false by default" do + app "development" + assert_not Rails.application.config.api_only + end + + test "api_only generator config is set when api_only is set" do + add_to_config <<-RUBY + config.api_only = true + RUBY + app "development" + + Rails.application.load_generators + assert Rails.configuration.api_only + end + + test "debug_exception_response_format is :api by default if api_only is enabled" do + add_to_config <<-RUBY + config.api_only = true + RUBY + app "development" + + assert_equal :api, Rails.configuration.debug_exception_response_format + end + + test "debug_exception_response_format can be overridden" do + add_to_config <<-RUBY + config.api_only = true + RUBY + + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + config.debug_exception_response_format = :default + end + RUBY + + app "development" + + assert_equal :default, Rails.configuration.debug_exception_response_format + end + + test "controller force_ssl declaration can be used even if session_store is disabled" do + make_basic_app do |application| + application.config.session_store :disabled end - assert_match 'YAML syntax error occurred while parsing', exception.message + class ::OmgController < ActionController::Base + force_ssl + + def index + render plain: "Yay! You're on Rails!" + end + end + + get "/" + + assert_equal 301, last_response.status + assert_equal "https://example.org/", last_response.location end + + test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption is true by default for new apps" do + app "development" + + assert_equal true, ActiveSupport::MessageEncryptor.use_authenticated_message_encryption + end + + test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption is false by default for upgraded apps" do + remove_from_config '.*config\.load_defaults.*\n' + + app "development" + + assert_equal false, ActiveSupport::MessageEncryptor.use_authenticated_message_encryption + end + + test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption can be configured via config.active_support.use_authenticated_message_encryption" do + remove_from_config '.*config\.load_defaults.*\n' + + app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY + Rails.application.config.active_support.use_authenticated_message_encryption = true + RUBY + + app "development" + + assert_equal true, ActiveSupport::MessageEncryptor.use_authenticated_message_encryption + end + + test "ActiveSupport::Digest.hash_digest_class is Digest::SHA1 by default for new apps" do + app "development" + + assert_equal Digest::SHA1, ActiveSupport::Digest.hash_digest_class + end + + test "ActiveSupport::Digest.hash_digest_class is Digest::MD5 by default for upgraded apps" do + remove_from_config '.*config\.load_defaults.*\n' + + app "development" + + assert_equal Digest::MD5, ActiveSupport::Digest.hash_digest_class + end + + test "ActiveSupport::Digest.hash_digest_class can be configured via config.active_support.use_sha1_digests" do + remove_from_config '.*config\.load_defaults.*\n' + + app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY + Rails.application.config.active_support.use_sha1_digests = true + RUBY + + app "development" + + assert_equal Digest::SHA1, ActiveSupport::Digest.hash_digest_class + end + + test "custom serializers should be able to set via config.active_job.custom_serializers in an initializer" do + class ::DummySerializer < ActiveJob::Serializers::ObjectSerializer; end + + app_file "config/initializers/custom_serializers.rb", <<-RUBY + Rails.application.config.active_job.custom_serializers << DummySerializer + RUBY + + app "development" + + assert_includes ActiveJob::Serializers.serializers, DummySerializer + end + + test "ActionView::Helpers::FormTagHelper.default_enforce_utf8 is false by default" do + app "development" + assert_equal false, ActionView::Helpers::FormTagHelper.default_enforce_utf8 + end + + test "ActionView::Helpers::FormTagHelper.default_enforce_utf8 is true in an upgraded app" do + remove_from_config '.*config\.load_defaults.*\n' + add_to_config 'config.load_defaults "5.2"' + + app "development" + + assert_equal true, ActionView::Helpers::FormTagHelper.default_enforce_utf8 + end + + test "ActionView::Helpers::FormTagHelper.default_enforce_utf8 can be configured via config.action_view.default_enforce_utf8" do + remove_from_config '.*config\.load_defaults.*\n' + + app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY + Rails.application.config.action_view.default_enforce_utf8 = true + RUBY + + app "development" + + assert_equal true, ActionView::Helpers::FormTagHelper.default_enforce_utf8 + end + + test "ActionView::Template.finalize_compiled_template_methods is true by default" do + app "test" + assert_equal true, ActionView::Template.finalize_compiled_template_methods + end + + test "ActionView::Template.finalize_compiled_template_methods can be configured via config.action_view.finalize_compiled_template_methods" do + app_file "config/environments/test.rb", <<-RUBY + Rails.application.configure do + config.action_view.finalize_compiled_template_methods = false + end + RUBY + + app "test" + + assert_equal false, ActionView::Template.finalize_compiled_template_methods + end + + test "ActiveRecord::Base.filter_attributes should equal to filter_parameters" do + app_file "config/initializers/filter_parameters_logging.rb", <<-RUBY + Rails.application.config.filter_parameters += [ :password, :credit_card_number ] + RUBY + app "development" + assert_equal [ :password, :credit_card_number ], Rails.application.config.filter_parameters + assert_equal [ :password, :credit_card_number ], ActiveRecord::Base.filter_attributes + end + + test "ActiveStorage.routes_prefix can be configured via config.active_storage.routes_prefix" do + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + config.active_storage.routes_prefix = '/files' + end + RUBY + + output = rails("routes", "-g", "active_storage") + assert_equal <<~MESSAGE, output + Prefix Verb URI Pattern Controller#Action + rails_service_blob GET /files/blobs/:signed_id/*filename(.:format) active_storage/blobs#show + rails_blob_representation GET /files/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show + rails_disk_service GET /files/disk/:encoded_key/*filename(.:format) active_storage/disk#show + update_rails_disk_service PUT /files/disk/:encoded_token(.:format) active_storage/disk#update + rails_direct_uploads POST /files/direct_uploads(.:format) active_storage/direct_uploads#create + MESSAGE + end + + private + def force_lazy_load_hooks + yield # Tasty clarifying sugar, homie! We only need to reference a constant to load it. + end end end diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index 7bf123d12b..29f8b1e3d9 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -1,11 +1,13 @@ -require 'isolation/abstract_unit' +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "console_helpers" class ConsoleTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation def setup build_app - boot_rails end def teardown @@ -30,7 +32,7 @@ class ConsoleTest < ActiveSupport::TestCase end def test_app_can_access_path_helper_method - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#index' end @@ -38,7 +40,7 @@ class ConsoleTest < ActiveSupport::TestCase load_environment console_session = irb_context.app - assert_equal '/foo', console_session.foo_path + assert_equal "/foo", console_session.foo_path end def test_new_session_should_return_integration_session @@ -52,12 +54,11 @@ class ConsoleTest < ActiveSupport::TestCase a = b = c = nil # TODO: These should be defined on the initializer - ActionDispatch::Reloader.to_cleanup { a = b = c = 1 } - ActionDispatch::Reloader.to_cleanup { b = c = 2 } - ActionDispatch::Reloader.to_prepare { c = 3 } + ActiveSupport::Reloader.to_complete { a = b = c = 1 } + ActiveSupport::Reloader.to_complete { b = c = 2 } + ActiveSupport::Reloader.to_prepare { c = 3 } - # Hide Reloading... output - silence_stream(STDOUT) { irb_context.reload! } + irb_context.reload!(false) assert_equal 1, a assert_equal 2, b @@ -72,7 +73,7 @@ class ConsoleTest < ActiveSupport::TestCase MODEL load_environment - assert User.new.respond_to?(:name) + assert_respond_to User.new, :name app_file "app/models/user.rb", <<-MODEL class User @@ -80,9 +81,9 @@ class ConsoleTest < ActiveSupport::TestCase end MODEL - assert !User.new.respond_to?(:age) - silence_stream(STDOUT) { irb_context.reload! } - assert User.new.respond_to?(:age) + assert_not_respond_to User.new, :age + irb_context.reload!(false) + assert_respond_to User.new, :age end def test_access_to_helpers @@ -90,76 +91,68 @@ class ConsoleTest < ActiveSupport::TestCase helper = irb_context.helper assert_not_nil helper assert_instance_of ActionView::Base, helper - assert_equal 'Once upon a time in a world...', - helper.truncate('Once upon a time in a world far far away') + assert_equal "Once upon a time in a world...", + helper.truncate("Once upon a time in a world far far away") end end -begin - require "pty" -rescue LoadError -end - class FullStackConsoleTest < ActiveSupport::TestCase + include ConsoleHelpers + def setup - skip "PTY unavailable" unless defined?(PTY) && PTY.respond_to?(:open) + skip "PTY unavailable" unless available_pty? build_app - app_file 'app/models/post.rb', <<-CODE + app_file "app/models/post.rb", <<-CODE class Post < ActiveRecord::Base end CODE system "#{app_path}/bin/rails runner 'Post.connection.create_table :posts'" - @master, @slave = PTY.open + @primary, @replica = PTY.open end def teardown teardown_app end - def assert_output(expected, timeout = 1) - timeout = Time.now + timeout - - output = "" - until output.include?(expected) || Time.now > timeout - if IO.select([@master], [], [], 0.1) - output << @master.read(1) - end - end - - assert output.include?(expected), "#{expected.inspect} expected, but got:\n\n#{output}" - end - def write_prompt(command, expected_output = nil) - @master.puts command - assert_output command - assert_output expected_output if expected_output - assert_output "> " + @primary.puts command + assert_output command, @primary + assert_output expected_output, @primary if expected_output + assert_output "> ", @primary end - def spawn_console + def spawn_console(options) Process.spawn( - "#{app_path}/bin/rails console --sandbox", - in: @slave, out: @slave, err: @slave + "#{app_path}/bin/rails console #{options}", + in: @replica, out: @replica, err: @replica ) - assert_output "> ", 30 + assert_output "> ", @primary, 30 end def test_sandbox - spawn_console + spawn_console("--sandbox") write_prompt "Post.count", "=> 0" write_prompt "Post.create" write_prompt "Post.count", "=> 1" - @master.puts "quit" + @primary.puts "quit" - spawn_console + spawn_console("--sandbox") write_prompt "Post.count", "=> 0" write_prompt "Post.transaction { Post.create; raise }" write_prompt "Post.count", "=> 0" - @master.puts "quit" + @primary.puts "quit" + end + + def test_environment_option_and_irb_option + spawn_console("test -- --verbose") + + write_prompt "a = 1", "a = 1" + write_prompt "puts Rails.env", "puts Rails.env\r\ntest" + @primary.puts "quit" end end diff --git a/railties/test/application/content_security_policy_test.rb b/railties/test/application/content_security_policy_test.rb new file mode 100644 index 0000000000..0d28df16f8 --- /dev/null +++ b/railties/test/application/content_security_policy_test.rb @@ -0,0 +1,223 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rack/test" + +module ApplicationTests + class ContentSecurityPolicyTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include Rack::Test::Methods + + def setup + build_app + end + + def teardown + teardown_app + end + + test "default content security policy is nil" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + def index + render html: "<h1>Welcome to Rails!</h1>" + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_nil last_response.headers["Content-Security-Policy"] + end + + test "empty content security policy is generated" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + def index + render html: "<h1>Welcome to Rails!</h1>" + end + end + RUBY + + app_file "config/initializers/content_security_policy.rb", <<-RUBY + Rails.application.config.content_security_policy do |p| + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_policy "" + end + + test "global content security policy in an initializer" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + def index + render html: "<h1>Welcome to Rails!</h1>" + end + end + RUBY + + app_file "config/initializers/content_security_policy.rb", <<-RUBY + Rails.application.config.content_security_policy do |p| + p.default_src :self, :https + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_policy "default-src 'self' https:" + end + + test "global report only content security policy in an initializer" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + def index + render html: "<h1>Welcome to Rails!</h1>" + end + end + RUBY + + app_file "config/initializers/content_security_policy.rb", <<-RUBY + Rails.application.config.content_security_policy do |p| + p.default_src :self, :https + end + + Rails.application.config.content_security_policy_report_only = true + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_policy "default-src 'self' https:", report_only: true + end + + test "override content security policy in a controller" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + content_security_policy do |p| + p.default_src "https://example.com" + end + + def index + render html: "<h1>Welcome to Rails!</h1>" + end + end + RUBY + + app_file "config/initializers/content_security_policy.rb", <<-RUBY + Rails.application.config.content_security_policy do |p| + p.default_src :self, :https + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_policy "default-src https://example.com" + end + + test "override content security policy to report only in a controller" do + controller :pages, <<-RUBY + class PagesController < ApplicationController + content_security_policy_report_only + + def index + render html: "<h1>Welcome to Rails!</h1>" + end + end + RUBY + + app_file "config/initializers/content_security_policy.rb", <<-RUBY + Rails.application.config.content_security_policy do |p| + p.default_src :self, :https + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + root to: "pages#index" + end + RUBY + + app("development") + + get "/" + assert_policy "default-src 'self' https:", report_only: true + end + + test "global content security policy added to rack app" do + app_file "config/initializers/content_security_policy.rb", <<-RUBY + Rails.application.config.content_security_policy do |p| + p.default_src :self, :https + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + + app = ->(env) { + [200, { "Content-Type" => "text/html" }, ["<p>Hello, World!</p>"]] + } + + root to: app + end + RUBY + + app("development") + + get "/" + assert_policy "default-src 'self' https:" + end + + private + + def assert_policy(expected, report_only: false) + assert_equal 200, last_response.status + + if report_only + expected_header = "Content-Security-Policy-Report-Only" + unexpected_header = "Content-Security-Policy" + else + expected_header = "Content-Security-Policy" + unexpected_header = "Content-Security-Policy-Report-Only" + end + + assert_nil last_response.headers[unexpected_header] + assert_equal expected, last_response.headers[expected_header] + end + end +end diff --git a/railties/test/application/current_attributes_integration_test.rb b/railties/test/application/current_attributes_integration_test.rb new file mode 100644 index 0000000000..146e96facc --- /dev/null +++ b/railties/test/application/current_attributes_integration_test.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rack/test" + +class CurrentAttributesIntegrationTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include Rack::Test::Methods + + setup do + build_app + + app_file "app/models/current.rb", <<-RUBY + class Current < ActiveSupport::CurrentAttributes + attribute :customer + + resets { Time.zone = "UTC" } + + def customer=(customer) + super + Time.zone = customer.try(:time_zone) + end + end + RUBY + + app_file "app/models/customer.rb", <<-RUBY + class Customer < Struct.new(:name) + def time_zone + "Copenhagen" + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get "/customers/:action", controller: :customers + end + RUBY + + app_file "app/controllers/customers_controller.rb", <<-RUBY + class CustomersController < ApplicationController + layout false + + def set_current_customer + Current.customer = Customer.new("david") + render :index + end + + def set_no_customer + render :index + end + end + RUBY + + app_file "app/views/customers/index.html.erb", <<-RUBY + <%= Current.customer.try(:name) || 'noone' %>,<%= Time.zone.name %> + RUBY + + require "#{app_path}/config/environment" + end + + teardown :teardown_app + + test "current customer is assigned and cleared" do + get "/customers/set_current_customer" + assert_equal 200, last_response.status + assert_match(/david,Copenhagen/, last_response.body) + + get "/customers/set_no_customer" + assert_equal 200, last_response.status + assert_match(/noone,UTC/, last_response.body) + end + + test "resets after execution" do + assert_nil Current.customer + assert_equal "UTC", Time.zone.name + + Rails.application.executor.wrap do + Current.customer = Customer.new("david") + + assert_equal "david", Current.customer.name + assert_equal "Copenhagen", Time.zone.name + end + + assert_nil Current.customer + assert_equal "UTC", Time.zone.name + end +end diff --git a/railties/test/application/dbconsole_test.rb b/railties/test/application/dbconsole_test.rb new file mode 100644 index 0000000000..8c03fe4ac6 --- /dev/null +++ b/railties/test/application/dbconsole_test.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "console_helpers" + +module ApplicationTests + class DBConsoleTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include ConsoleHelpers + + def setup + skip "PTY unavailable" unless available_pty? + + build_app + end + + def teardown + teardown_app + end + + def test_use_value_defined_in_environment_file_in_database_yml + app_file "config/database.yml", <<-YAML + development: + database: <%= Rails.application.config.database %> + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + YAML + + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + config.database = "db/development.sqlite3" + end + RUBY + + primary, replica = PTY.open + spawn_dbconsole(replica) + assert_output("sqlite>", primary) + ensure + primary.puts ".exit" + end + + def test_respect_environment_option + app_file "config/database.yml", <<-YAML + default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + + development: + <<: *default + database: db/development.sqlite3 + + production: + <<: *default + database: db/production.sqlite3 + YAML + + primary, replica = PTY.open + spawn_dbconsole(replica, "-e production") + assert_output("sqlite>", primary) + + primary.puts "pragma database_list;" + assert_output("production.sqlite3", primary) + ensure + primary.puts ".exit" + end + + private + def spawn_dbconsole(fd, options = nil) + Process.spawn("#{app_path}/bin/rails dbconsole #{options}", in: fd, out: fd, err: fd) + end + end +end diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb index 84cc6e120b..e5e557d204 100644 --- a/railties/test/application/generators_test.rb +++ b/railties/test/application/generators_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" module ApplicationTests @@ -6,7 +8,6 @@ module ApplicationTests def setup build_app - boot_rails end def teardown @@ -30,7 +31,7 @@ module ApplicationTests end test "allow running plugin new generator inside Rails app directory" do - FileUtils.cd(rails_root){ `ruby bin/rails plugin new vendor/plugins/bukkits` } + rails "plugin", "new", "vendor/plugins/bukkits" assert File.exist?(File.join(rails_root, "vendor/plugins/bukkits/test/dummy/config/application.rb")) end @@ -114,7 +115,7 @@ module ApplicationTests test "generators with string and hash for options should generate symbol keys" do with_bare_config do |c| c.generators do |g| - g.orm 'data_mapper', migration: false + g.orm "data_mapper", migration: false end expected = { @@ -135,10 +136,10 @@ module ApplicationTests require "#{app_path}/config/environment" Rails.application.load_generators - assert Rails::Generators.hidden_namespaces.include?("assets") - assert Rails::Generators.hidden_namespaces.include?("helper") - assert Rails::Generators.hidden_namespaces.include?("js") - assert Rails::Generators.hidden_namespaces.include?("css") + assert_includes Rails::Generators.hidden_namespaces, "assets" + assert_includes Rails::Generators.hidden_namespaces, "helper" + assert_includes Rails::Generators.hidden_namespaces, "js" + assert_includes Rails::Generators.hidden_namespaces, "css" assert Rails::Generators.options[:rails][:api] assert_equal false, Rails::Generators.options[:rails][:assets] assert_equal false, Rails::Generators.options[:rails][:helper] @@ -160,5 +161,42 @@ module ApplicationTests assert Rails::Generators.options[:rails][:helper] assert_equal :my_template, Rails::Generators.options[:rails][:template_engine] end + + test "api only generator generate mailer views" do + add_to_config <<-RUBY + config.api_only = true + RUBY + + rails "generate", "mailer", "notifier", "foo" + assert File.exist?(File.join(rails_root, "app/views/notifier_mailer/foo.text.erb")) + assert File.exist?(File.join(rails_root, "app/views/notifier_mailer/foo.html.erb")) + end + + test "ARGV is mutated as expected" do + require "#{app_path}/config/environment" + require "rails/command" + Rails::Command.const_set("APP_PATH", "rails/all") + + FileUtils.cd(rails_root) do + ARGV = ["mailer", "notifier", "foo"] + Rails::Command.const_set("ARGV", ARGV) + quietly { Rails::Command.invoke :generate, ARGV } + + assert_equal ["notifier", "foo"], ARGV + end + + Rails::Command.send(:remove_const, "APP_PATH") + end + + test "help does not show hidden namespaces and hidden commands" do + FileUtils.cd(rails_root) do + output = rails("generate", "--help") + assert_no_match "active_record:migration", output + assert_no_match "credentials", output + + output = rails("destroy", "--help") + assert_no_match "active_record:migration", output + end + end end end diff --git a/railties/test/application/help_test.rb b/railties/test/application/help_test.rb new file mode 100644 index 0000000000..f728fc3b85 --- /dev/null +++ b/railties/test/application/help_test.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" + +class HelpTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test "command works" do + output = rails("help") + assert_match "The most common rails commands are", output + end + + test "short-cut alias works" do + output = rails("-h") + assert_match "The most common rails commands are", output + end +end diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 7bba910b9e..1530ea82d6 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" -require 'set' module ApplicationTests class FrameworksTest < ActiveSupport::TestCase @@ -7,7 +8,6 @@ module ApplicationTests def setup build_app - boot_rails FileUtils.rm_rf "#{app_path}/config/environments" end @@ -17,7 +17,7 @@ module ApplicationTests # AC & AM test "set load paths set only if action controller or action mailer are in use" do - assert_nothing_raised NameError do + assert_nothing_raised do add_to_config <<-RUBY config.root = "#{app_path}" RUBY @@ -50,6 +50,17 @@ module ApplicationTests assert_equal "test.rails", ActionMailer::Base.default_url_options[:host] end + test "Default to HTTPS for ActionMailer URLs when force_ssl is on" do + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + config.force_ssl = true + end + RUBY + + require "#{app_path}/config/environment" + assert_equal "https", ActionMailer::Base.default_url_options[:protocol] + end + test "includes url helpers as action methods" do app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do @@ -119,7 +130,7 @@ module ApplicationTests end RUBY - require 'rack/test' + require "rack/test" extend Rack::Test::Methods get "/foo/included_helpers" @@ -151,10 +162,10 @@ module ApplicationTests end RUBY - require 'rack/test' + require "rack/test" extend Rack::Test::Methods - get 'omg/show' + get "omg/show" assert_equal '{"omg":"omg"}', last_response.body end @@ -166,7 +177,7 @@ module ApplicationTests end test "assignment config.encoding to default_charset" do - charset = 'Shift_JIS' + charset = "Shift_JIS" add_to_config "config.encoding = '#{charset}'" require "#{app_path}/config/environment" assert_equal charset, ActionDispatch::Response.default_charset @@ -176,7 +187,7 @@ module ApplicationTests test "if there's no config.active_support.bare, all of ActiveSupport is required" do use_frameworks [] require "#{app_path}/config/environment" - assert_nothing_raised { [1,2,3].sample } + assert_nothing_raised { [1, 2, 3].sample } end test "config.active_support.bare does not require all of ActiveSupport" do @@ -194,41 +205,35 @@ module ApplicationTests test "active_record extensions are applied to ActiveRecord" do add_to_config "config.active_record.table_name_prefix = 'tbl_'" require "#{app_path}/config/environment" - assert_equal 'tbl_', ActiveRecord::Base.table_name_prefix + assert_equal "tbl_", ActiveRecord::Base.table_name_prefix end test "database middleware doesn't initialize when activerecord is not in frameworks" do use_frameworks [] require "#{app_path}/config/environment" - assert_nil defined?(ActiveRecord::Base) + assert !defined?(ActiveRecord::Base) || ActiveRecord.autoload?(:Base) end test "use schema cache dump" do - Dir.chdir(app_path) do - `rails generate model post title:string; - bin/rake db:migrate db:schema:cache:dump` - end + rails %w(generate model post title:string) + rails %w(db:migrate db:schema:cache:dump) require "#{app_path}/config/environment" ActiveRecord::Base.connection.drop_table("posts") # force drop posts table for test. - assert ActiveRecord::Base.connection.schema_cache.tables("posts") + assert ActiveRecord::Base.connection.schema_cache.data_sources("posts") end test "expire schema cache dump" do - Dir.chdir(app_path) do - `rails generate model post title:string; - bin/rake db:migrate db:schema:cache:dump db:rollback` - end - silence_warnings { - require "#{app_path}/config/environment" - assert !ActiveRecord::Base.connection.schema_cache.tables("posts") - } + rails %w(generate model post title:string) + rails %w(db:migrate db:schema:cache:dump db:rollback) + require "#{app_path}/config/environment" + assert_not ActiveRecord::Base.connection.schema_cache.data_sources("posts") end test "active record establish_connection uses Rails.env if DATABASE_URL is not set" do begin require "#{app_path}/config/environment" orig_database_url = ENV.delete("DATABASE_URL") - orig_rails_env, Rails.env = Rails.env, 'development' + orig_rails_env, Rails.env = Rails.env, "development" ActiveRecord::Base.establish_connection assert ActiveRecord::Base.connection assert_match(/#{ActiveRecord::Base.configurations[Rails.env]['database']}/, ActiveRecord::Base.connection_config[:database]) @@ -243,7 +248,7 @@ module ApplicationTests begin require "#{app_path}/config/environment" orig_database_url = ENV.delete("DATABASE_URL") - orig_rails_env, Rails.env = Rails.env, 'development' + orig_rails_env, Rails.env = Rails.env, "development" database_url_db_name = "db/database_url_db.sqlite3" ENV["DATABASE_URL"] = "sqlite3:#{database_url_db_name}" ActiveRecord::Base.establish_connection @@ -255,5 +260,13 @@ module ApplicationTests Rails.env = orig_rails_env if orig_rails_env end end + + test "connections checked out during initialization are returned to the pool" do + app_file "config/initializers/active_record.rb", <<-RUBY + ActiveRecord::Base.connection + RUBY + require "#{app_path}/config/environment" + assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection? + end end end diff --git a/railties/test/application/initializers/hooks_test.rb b/railties/test/application/initializers/hooks_test.rb index b2cea0a8e1..1e130c2f9e 100644 --- a/railties/test/application/initializers/hooks_test.rb +++ b/railties/test/application/initializers/hooks_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" module ApplicationTests @@ -6,7 +8,6 @@ module ApplicationTests def setup build_app - boot_rails FileUtils.rm_rf "#{app_path}/config/environments" end @@ -32,7 +33,7 @@ module ApplicationTests RUBY require "#{app_path}/config/environment" - assert_equal [1,2,3], $initialization_callbacks + assert_equal [1, 2, 3], $initialization_callbacks end test "hooks block works correctly with eager_load" do @@ -47,7 +48,7 @@ module ApplicationTests RUBY require "#{app_path}/config/environment" - assert_equal [1,2,3,4], $initialization_callbacks + assert_equal [1, 2, 3, 4], $initialization_callbacks end test "after_initialize runs after frameworks have been initialized" do @@ -58,7 +59,7 @@ module ApplicationTests require "#{app_path}/config/environment" assert $activerecord_configurations - assert $activerecord_configurations['development'] + assert $activerecord_configurations["development"] end test "after_initialize happens after to_prepare in development" do diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb index 9ee54796a4..8058052771 100644 --- a/railties/test/application/initializers/i18n_test.rb +++ b/railties/test/application/initializers/i18n_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" module ApplicationTests @@ -6,7 +8,6 @@ module ApplicationTests def setup build_app - boot_rails FileUtils.rm_rf "#{app_path}/config/environments" require "rails/all" end @@ -31,7 +32,7 @@ module ApplicationTests end def assert_no_fallbacks - assert !I18n.backend.class.included_modules.include?(I18n::Backend::Fallbacks) + assert_not_includes I18n.backend.class.included_modules, I18n::Backend::Fallbacks end # Locales @@ -63,8 +64,8 @@ module ApplicationTests "#{app_path}/config/locales/en.yml", "#{app_path}/config/another_locale.yml" ], Rails.application.config.i18n.load_path - assert I18n.load_path.include?("#{app_path}/config/locales/en.yml") - assert I18n.load_path.include?("#{app_path}/config/another_locale.yml") + assert_includes I18n.load_path, "#{app_path}/config/locales/en.yml" + assert_includes I18n.load_path, "#{app_path}/config/another_locale.yml" end test "load_path is populated before eager loaded models" do @@ -77,19 +78,19 @@ en: foo: "1" YAML - app_file 'app/models/foo.rb', <<-RUBY + app_file "app/models/foo.rb", <<-RUBY class Foo < ActiveRecord::Base @foo = I18n.t(:foo) end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get '/i18n', :to => lambda { |env| [200, {}, [Foo.instance_variable_get('@foo')]] } end RUBY - require 'rack/test' + require "rack/test" extend Rack::Test::Methods load_app @@ -107,13 +108,13 @@ en: foo: "1" YAML - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get '/i18n', :to => lambda { |env| [200, {}, [I18n.t(:foo)]] } end RUBY - require 'rack/test' + require "rack/test" extend Rack::Test::Methods load_app @@ -132,6 +133,79 @@ en: assert_equal "2", last_response.body end + test "new locale files are loaded" do + add_to_config <<-RUBY + config.cache_classes = false + RUBY + + app_file "config/locales/en.yml", <<-YAML +en: + foo: "1" + YAML + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/i18n', :to => lambda { |env| [200, {}, [I18n.t(:foo)]] } + end + RUBY + + require "rack/test" + extend Rack::Test::Methods + load_app + + get "/i18n" + assert_equal "1", last_response.body + + # Wait a full second so we have time for changes to propagate + sleep(1) + + remove_file "config/locales/en.yml" + app_file "config/locales/custom.en.yml", <<-YAML +en: + foo: "2" + YAML + + get "/i18n" + assert_equal "2", last_response.body + end + + test "I18n.load_path is reloaded" do + add_to_config <<-RUBY + config.cache_classes = false + RUBY + + app_file "config/locales/en.yml", <<-YAML +en: + foo: "1" + YAML + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/i18n', :to => lambda { |env| [200, {}, [I18n.load_path.inspect]] } + end + RUBY + + require "rack/test" + extend Rack::Test::Methods + load_app + + get "/i18n" + + assert_match "en.yml", last_response.body + + # Wait a full second so we have time for changes to propagate + sleep(1) + + app_file "config/locales/fr.yml", <<-YAML +fr: + foo: "2" + YAML + + get "/i18n" + assert_match "fr.yml", last_response.body + assert_match "en.yml", last_response.body + end + # Fallbacks test "not using config.i18n.fallbacks does not initialize I18n.fallbacks" do I18n.backend = Class.new(I18n::Backend::Simple).new @@ -142,7 +216,7 @@ en: test "config.i18n.fallbacks = true initializes I18n.fallbacks with default settings" do I18n::Railtie.config.i18n.fallbacks = true load_app - assert I18n.backend.class.included_modules.include?(I18n::Backend::Fallbacks) + assert_includes I18n.backend.class.included_modules, I18n::Backend::Fallbacks assert_fallbacks de: [:de, :en] end @@ -150,7 +224,7 @@ en: I18n::Railtie.config.i18n.fallbacks = true I18n::Railtie.config.i18n.backend = Class.new(I18n::Backend::Simple).new load_app - assert I18n.backend.class.included_modules.include?(I18n::Backend::Fallbacks) + assert_includes I18n.backend.class.included_modules, I18n::Backend::Fallbacks assert_fallbacks de: [:de, :en] end @@ -161,7 +235,7 @@ en: end test "config.i18n.fallbacks.map = { :ca => :'es-ES' } initializes fallbacks with a mapping ca => es-ES" do - I18n::Railtie.config.i18n.fallbacks.map = { :ca => :'es-ES' } + I18n::Railtie.config.i18n.fallbacks.map = { ca: :'es-ES' } load_app assert_fallbacks ca: [:ca, :"es-ES", :es, :en] end @@ -172,18 +246,24 @@ en: assert_fallbacks de: [:de, :'en-US', :en] end - test "[shortcut] config.i18n.fallbacks = [{ :ca => :'es-ES' }] initializes fallbacks with a mapping de-AT => de-DE" do - I18n::Railtie.config.i18n.fallbacks.map = { :ca => :'es-ES' } + test "[shortcut] config.i18n.fallbacks = [{ :ca => :'es-ES' }] initializes fallbacks with a mapping ca => es-ES" do + I18n::Railtie.config.i18n.fallbacks = [{ ca: :'es-ES' }] load_app assert_fallbacks ca: [:ca, :"es-ES", :es, :en] end test "[shortcut] config.i18n.fallbacks = [:'en-US', { :ca => :'es-ES' }] initializes fallbacks with the given arguments" do - I18n::Railtie.config.i18n.fallbacks = [:'en-US', { :ca => :'es-ES' }] + I18n::Railtie.config.i18n.fallbacks = [:'en-US', { ca: :'es-ES' }] load_app assert_fallbacks ca: [:ca, :"es-ES", :es, :'en-US', :en] end + test "[shortcut] config.i18n.fallbacks = { ca: :en } initializes fallbacks with a mapping ca => :en" do + I18n::Railtie.config.i18n.fallbacks = { ca: :en } + load_app + assert_fallbacks ca: [:ca, :en] + end + test "disable config.i18n.enforce_available_locales" do add_to_config <<-RUBY config.i18n.enforce_available_locales = false diff --git a/railties/test/application/initializers/load_path_test.rb b/railties/test/application/initializers/load_path_test.rb index cd05956356..78cd4776d6 100644 --- a/railties/test/application/initializers/load_path_test.rb +++ b/railties/test/application/initializers/load_path_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" module ApplicationTests @@ -6,7 +8,6 @@ module ApplicationTests def setup build_app - boot_rails FileUtils.rm_rf "#{app_path}/config/environments" end @@ -20,7 +21,7 @@ module ApplicationTests RUBY require "#{app_path}/config/environment" - assert $:.include?("#{app_path}/app/models") + assert_includes $:, "#{app_path}/app/models" end test "initializing an application allows to load code on lib path inside application class definition" do @@ -37,7 +38,7 @@ module ApplicationTests require "#{app_path}/config/environment" end - assert $:.include?("#{app_path}/lib") + assert_includes $:, "#{app_path}/lib" end test "initializing an application eager load any path under app" do diff --git a/railties/test/application/initializers/notifications_test.rb b/railties/test/application/initializers/notifications_test.rb index 95655b74cf..c65c955734 100644 --- a/railties/test/application/initializers/notifications_test.rb +++ b/railties/test/application/initializers/notifications_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" module ApplicationTests @@ -6,7 +8,6 @@ module ApplicationTests def setup build_app - boot_rails end def teardown @@ -31,6 +32,7 @@ module ApplicationTests logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new ActiveRecord::Base.logger = logger + ActiveRecord::Base.verbose_query_logs = false # Mimic Active Record notifications instrument "sql.active_record", name: "SQL", sql: "SHOW tables" @@ -40,17 +42,17 @@ module ApplicationTests assert_match(/SHOW tables/, logger.logged(:debug).last) end - test 'rails load_config_initializer event is instrumented' do - app_file 'config/initializers/foo.rb', '' + test "rails load_config_initializer event is instrumented" do + app_file "config/initializers/foo.rb", "" events = [] callback = ->(*_) { events << _ } - ActiveSupport::Notifications.subscribed(callback, 'load_config_initializer.railties') do + ActiveSupport::Notifications.subscribed(callback, "load_config_initializer.railties") do app end assert_equal %w[load_config_initializer.railties], events.map(&:first) - assert_includes events.first.last[:initializer], 'config/initializers/foo.rb' + assert_includes events.first.last[:initializer], "config/initializers/foo.rb" end end end diff --git a/railties/test/application/integration_test_case_test.rb b/railties/test/application/integration_test_case_test.rb new file mode 100644 index 0000000000..c08761092b --- /dev/null +++ b/railties/test/application/integration_test_case_test.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "env_helpers" + +module ApplicationTests + class IntegrationTestCaseTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation, EnvHelpers + + setup do + build_app + end + + teardown do + teardown_app + end + + test "resets Action Mailer test deliveries" do + rails "generate", "mailer", "BaseMailer", "welcome" + + app_file "test/integration/mailer_integration_test.rb", <<-RUBY + require 'test_helper' + + class MailerIntegrationTest < ActionDispatch::IntegrationTest + setup do + @old_delivery_method = ActionMailer::Base.delivery_method + ActionMailer::Base.delivery_method = :test + end + + teardown do + ActionMailer::Base.delivery_method = @old_delivery_method + end + + 2.times do |i| + define_method "test_resets_deliveries_\#{i}" do + BaseMailer.welcome.deliver_now + assert_equal 1, ActionMailer::Base.deliveries.count + end + end + end + RUBY + + with_rails_env("test") { rails("db:migrate") } + output = rails("test") + assert_match(/0 failures, 0 errors/, output) + end + end + + class IntegrationTestDefaultApp < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation, EnvHelpers + + setup do + build_app + end + + teardown do + teardown_app + end + + test "app method of integration tests returns test_app by default" do + app_file "test/integration/default_app_test.rb", <<-RUBY + require 'test_helper' + + class DefaultAppIntegrationTest < ActionDispatch::IntegrationTest + def test_app_returns_action_dispatch_test_app_by_default + assert_equal ActionDispatch.test_app, app + end + end + RUBY + + with_rails_env("test") { rails("db:migrate") } + output = rails("test") + assert_match(/0 failures, 0 errors/, output) + end + end +end diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index 1027bca2c1..bfa66770bd 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -1,11 +1,12 @@ -require 'isolation/abstract_unit' +# frozen_string_literal: true + +require "isolation/abstract_unit" class LoadingTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation def setup build_app - boot_rails end def teardown @@ -26,11 +27,11 @@ class LoadingTest < ActiveSupport::TestCase require "#{rails_root}/config/environment" setup_ar! - p = Post.create(title: 'omg') + p = Post.create(title: "omg") assert_equal 1, Post.count - assert_equal 'omg', p.title + assert_equal "omg", p.title p = Post.first - assert_equal 'omg', p.title + assert_equal "omg", p.title end test "concerns in app are autoloaded" do @@ -56,10 +57,10 @@ class LoadingTest < ActiveSupport::TestCase require "#{rails_root}/config/environment" - assert_nothing_raised(NameError) { Trackable } - assert_nothing_raised(NameError) { EmailLoggable } - assert_nothing_raised(NameError) { Orderable } - assert_nothing_raised(NameError) { Matchable } + assert_nothing_raised { Trackable } + assert_nothing_raised { EmailLoggable } + assert_nothing_raised { Orderable } + assert_nothing_raised { Matchable } end test "models without table do not panic on scope definitions when loaded" do @@ -103,24 +104,24 @@ class LoadingTest < ActiveSupport::TestCase end MODEL - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get '/load', to: lambda { |env| [200, {}, Post.all] } get '/unload', to: lambda { |env| [200, {}, []] } end RUBY - require 'rack/test' + require "rack/test" extend Rack::Test::Methods require "#{rails_root}/config/environment" setup_ar! - assert_equal [ActiveRecord::SchemaMigration], ActiveRecord::Base.descendants + assert_equal [ActiveStorage::Blob, ActiveStorage::Attachment, ActiveRecord::SchemaMigration, ActiveRecord::InternalMetadata].collect(&:to_s).sort, ActiveRecord::Base.descendants.collect(&:to_s).sort get "/load" - assert_equal [ActiveRecord::SchemaMigration, Post], ActiveRecord::Base.descendants + assert_equal [ActiveStorage::Blob, ActiveStorage::Attachment, ActiveRecord::SchemaMigration, ActiveRecord::InternalMetadata, Post].collect(&:to_s).sort, ActiveRecord::Base.descendants.collect(&:to_s).sort get "/unload" - assert_equal [ActiveRecord::SchemaMigration], ActiveRecord::Base.descendants + assert_equal [ActiveStorage::Blob, ActiveStorage::Attachment, ActiveRecord::SchemaMigration, ActiveRecord::InternalMetadata].collect(&:to_s).sort, ActiveRecord::Base.descendants.collect(&:to_s).sort end test "initialize cant be called twice" do @@ -133,7 +134,7 @@ class LoadingTest < ActiveSupport::TestCase config.cache_classes = false RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get '/c', to: lambda { |env| [200, {"Content-Type" => "text/plain"}, [User.counter.to_s]] } end @@ -145,7 +146,7 @@ class LoadingTest < ActiveSupport::TestCase end MODEL - require 'rack/test' + require "rack/test" extend Rack::Test::Methods require "#{rails_root}/config/environment" @@ -169,10 +170,12 @@ class LoadingTest < ActiveSupport::TestCase config.file_watcher = Class.new do def initialize(*); end def updated?; false; end + def execute; end + def execute_if_updated; false; end end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get '/c', to: lambda { |env| [200, {"Content-Type" => "text/plain"}, [User.counter.to_s]] } end @@ -184,7 +187,7 @@ class LoadingTest < ActiveSupport::TestCase end MODEL - require 'rack/test' + require "rack/test" extend Rack::Test::Methods require "#{rails_root}/config/environment" @@ -207,7 +210,7 @@ class LoadingTest < ActiveSupport::TestCase config.cache_classes = false RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY $counter ||= 0 Rails.application.routes.draw do get '/c', to: lambda { |env| User.name; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] } @@ -220,7 +223,7 @@ class LoadingTest < ActiveSupport::TestCase end MODEL - require 'rack/test' + require "rack/test" extend Rack::Test::Methods require "#{rails_root}/config/environment" @@ -239,7 +242,7 @@ class LoadingTest < ActiveSupport::TestCase config.cache_classes = false RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY $counter ||= 1 $counter *= 2 Rails.application.routes.draw do @@ -253,7 +256,7 @@ class LoadingTest < ActiveSupport::TestCase end MODEL - require 'rack/test' + require "rack/test" extend Rack::Test::Methods require "#{rails_root}/config/environment" @@ -272,7 +275,7 @@ class LoadingTest < ActiveSupport::TestCase config.cache_classes = false RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get '/title', to: lambda { |env| [200, {"Content-Type" => "text/plain"}, [Post.new.title]] } get '/body', to: lambda { |env| [200, {"Content-Type" => "text/plain"}, [Post.new.body]] } @@ -284,11 +287,11 @@ class LoadingTest < ActiveSupport::TestCase end MODEL - require 'rack/test' + require "rack/test" extend Rack::Test::Methods app_file "db/migrate/1_create_posts.rb", <<-MIGRATION - class CreatePosts < ActiveRecord::Migration + class CreatePosts < ActiveRecord::Migration::Current def change create_table :posts do |t| t.string :title, default: "TITLE" @@ -297,21 +300,21 @@ class LoadingTest < ActiveSupport::TestCase end MIGRATION - Dir.chdir(app_path) { `rake db:migrate`} + rails("db:migrate") require "#{rails_root}/config/environment" get "/title" assert_equal "TITLE", last_response.body app_file "db/migrate/2_add_body_to_posts.rb", <<-MIGRATION - class AddBodyToPosts < ActiveRecord::Migration + class AddBodyToPosts < ActiveRecord::Migration::Current def change add_column :posts, :body, :text, default: "BODY" end end MIGRATION - Dir.chdir(app_path) { `rake db:migrate` } + rails("db:migrate") get "/body" assert_equal "BODY", last_response.body @@ -339,32 +342,118 @@ class LoadingTest < ActiveSupport::TestCase require "#{rails_root}/config/environment" - require 'rack/test' + require "rack/test" extend Rack::Test::Methods - get '/omg/show' - assert_equal 'OK', last_response.body + get "/omg/show" + assert_equal "OK", last_response.body end def test_initialize_can_be_called_at_any_time require "#{app_path}/config/application" - assert !Rails.initialized? - assert !Rails.application.initialized? + assert_not_predicate Rails, :initialized? + assert_not_predicate Rails.application, :initialized? Rails.initialize! - assert Rails.initialized? - assert Rails.application.initialized? + assert_predicate Rails, :initialized? + assert_predicate Rails.application, :initialized? end - protected - - def setup_ar! - ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") - ActiveRecord::Migration.verbose = false - ActiveRecord::Schema.define(version: 1) do - create_table :posts do |t| - t.string :title + test "frameworks aren't loaded during initialization" do + app_file "config/initializers/raise_when_frameworks_load.rb", <<-RUBY + %i(action_controller action_mailer active_job active_record).each do |framework| + ActiveSupport.on_load(framework) { raise "\#{framework} loaded!" } end + RUBY + + assert_nothing_raised do + require "#{app_path}/config/environment" end end + + test "active record query cache hooks are installed before first request in production" do + app_file "app/controllers/omg_controller.rb", <<-RUBY + begin + class OmgController < ActionController::Metal + ActiveSupport.run_load_hooks(:action_controller, self) + def show + if ActiveRecord::Base.connection.query_cache_enabled + self.response_body = ["Query cache is enabled."] + else + self.response_body = ["Expected ActiveRecord::Base.connection.query_cache_enabled to be true"] + end + end + end + rescue => e + puts "Error loading metal: \#{e.class} \#{e.message}" + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get "/:controller(/:action)" + end + RUBY + + boot_app "production" + + require "rack/test" + extend Rack::Test::Methods + + get "/omg/show" + assert_equal "Query cache is enabled.", last_response.body + end + + test "active record query cache hooks are installed before first request in development" do + app_file "app/controllers/omg_controller.rb", <<-RUBY + begin + class OmgController < ActionController::Metal + ActiveSupport.run_load_hooks(:action_controller, self) + def show + if ActiveRecord::Base.connection.query_cache_enabled + self.response_body = ["Query cache is enabled."] + else + self.response_body = ["Expected ActiveRecord::Base.connection.query_cache_enabled to be true"] + end + end + end + rescue => e + puts "Error loading metal: \#{e.class} \#{e.message}" + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get "/:controller(/:action)" + end + RUBY + + boot_app "development" + + require "rack/test" + extend Rack::Test::Methods + + get "/omg/show" + assert_equal "Query cache is enabled.", last_response.body + end + + private + + def setup_ar! + ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define(version: 1) do + create_table :posts do |t| + t.string :title + end + end + end + + def boot_app(env = "development") + ENV["RAILS_ENV"] = env + + require "#{app_path}/config/environment" + ensure + ENV.delete "RAILS_ENV" + end end diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb index e462d2c15e..ba186bda44 100644 --- a/railties/test/application/mailer_previews_test.rb +++ b/railties/test/application/mailer_previews_test.rb @@ -1,6 +1,8 @@ -require 'isolation/abstract_unit' -require 'rack/test' -require 'base64' +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rack/test" +require "base64" module ApplicationTests class MailerPreviewsTest < ActiveSupport::TestCase @@ -9,7 +11,6 @@ module ApplicationTests def setup build_app - boot_rails end def teardown @@ -28,10 +29,10 @@ module ApplicationTests assert_equal 404, last_response.status end - test "/rails/mailers is accessible with correct configuraiton" do + test "/rails/mailers is accessible with correct configuration" do add_to_config "config.action_mailer.show_previews = true" app("production") - get "/rails/mailers" + get "/rails/mailers", {}, { "REMOTE_ADDR" => "4.2.42.42" } assert_equal 200, last_response.status end @@ -54,7 +55,7 @@ module ApplicationTests end test "mailer previews are loaded from the default preview_path" do - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -64,11 +65,11 @@ module ApplicationTests end RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -76,7 +77,7 @@ module ApplicationTests end RUBY - app('development') + app("development") get "/rails/mailers" assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body @@ -86,7 +87,7 @@ module ApplicationTests test "mailer previews are loaded from a custom preview_path" do add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'" - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -96,11 +97,11 @@ module ApplicationTests end RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - app_file 'lib/mailer_previews/notifier_preview.rb', <<-RUBY + app_file "lib/mailer_previews/notifier_preview.rb", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -108,7 +109,7 @@ module ApplicationTests end RUBY - app('development') + app("development") get "/rails/mailers" assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body @@ -116,12 +117,12 @@ module ApplicationTests end test "mailer previews are reloaded across requests" do - app('development') + app("development") get "/rails/mailers" assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -131,11 +132,11 @@ module ApplicationTests end RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -146,7 +147,7 @@ module ApplicationTests get "/rails/mailers" assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body - remove_file 'test/mailers/previews/notifier_preview.rb' + remove_file "test/mailers/previews/notifier_preview.rb" sleep(1) get "/rails/mailers" @@ -154,7 +155,7 @@ module ApplicationTests end test "mailer preview actions are added and removed" do - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -164,11 +165,11 @@ module ApplicationTests end RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -176,14 +177,14 @@ module ApplicationTests end RUBY - app('development') + app("development") get "/rails/mailers" assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body assert_no_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -197,15 +198,15 @@ module ApplicationTests end RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - text_template 'notifier/bar', <<-RUBY + text_template "notifier/bar", <<-RUBY Goodbye, World! RUBY - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -224,7 +225,7 @@ module ApplicationTests assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body assert_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -234,9 +235,9 @@ module ApplicationTests end RUBY - remove_file 'app/views/notifier/bar.text.erb' + remove_file "app/views/notifier/bar.text.erb" - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -255,12 +256,12 @@ module ApplicationTests test "mailer previews are reloaded from a custom preview_path" do add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'" - app('development') + app("development") get "/rails/mailers" assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -270,11 +271,11 @@ module ApplicationTests end RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - app_file 'lib/mailer_previews/notifier_preview.rb', <<-RUBY + app_file "lib/mailer_previews/notifier_preview.rb", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -285,7 +286,7 @@ module ApplicationTests get "/rails/mailers" assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body - remove_file 'lib/mailer_previews/notifier_preview.rb' + remove_file "lib/mailer_previews/notifier_preview.rb" sleep(1) get "/rails/mailers" @@ -293,14 +294,14 @@ module ApplicationTests end test "mailer preview not found" do - app('development') + app("development") get "/rails/mailers/notifier" - assert last_response.not_found? + assert_predicate last_response, :not_found? assert_match "Mailer preview 'notifier' not found", last_response.body end test "mailer preview email not found" do - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -310,11 +311,11 @@ module ApplicationTests end RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -322,15 +323,15 @@ module ApplicationTests end RUBY - app('development') + app("development") get "/rails/mailers/notifier/bar" - assert last_response.not_found? + assert_predicate last_response, :not_found? assert_match "Email 'bar' not found in NotifierPreview", last_response.body end test "mailer preview NullMail" do - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -340,7 +341,7 @@ module ApplicationTests end RUBY - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -348,7 +349,7 @@ module ApplicationTests end RUBY - app('development') + app("development") get "/rails/mailers/notifier/foo" assert_match "You are trying to preview an email that does not have any content.", last_response.body @@ -356,7 +357,7 @@ module ApplicationTests end test "mailer preview email part not found" do - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -366,11 +367,11 @@ module ApplicationTests end RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -378,15 +379,15 @@ module ApplicationTests end RUBY - app('development') + app("development") get "/rails/mailers/notifier/foo?part=text%2Fhtml" - assert last_response.not_found? + assert_predicate last_response, :not_found? assert_match "Email part 'text/html' not found in NotifierPreview#foo", last_response.body end test "message header uses full display names" do - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "Ruby on Rails <core@rubyonrails.org>" @@ -397,11 +398,11 @@ module ApplicationTests end RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -409,7 +410,7 @@ module ApplicationTests end RUBY - app('development') + app("development") get "/rails/mailers/notifier/foo" assert_equal 200, last_response.status @@ -419,7 +420,51 @@ module ApplicationTests end test "part menu selects correct option" do - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + html_template "notifier/foo", <<-RUBY + <p>Hello, World!</p> + RUBY + + text_template "notifier/foo", <<-RUBY + Hello, World! + RUBY + + mailer_preview "notifier", <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app("development") + + get "/rails/mailers/notifier/foo.html" + assert_equal 200, last_response.status + assert_match '<option selected value="part=text%2Fhtml">View as HTML email</option>', last_response.body + + get "/rails/mailers/notifier/foo.txt" + assert_equal 200, last_response.status + assert_match '<option selected value="part=text%2Fplain">View as plain-text email</option>', last_response.body + end + + test "locale menu selects correct option" do + app_file "config/initializers/available_locales.rb", <<-RUBY + Rails.application.configure do + config.i18n.available_locales = %i[en ja] + end + RUBY + + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -429,15 +474,15 @@ module ApplicationTests end RUBY - html_template 'notifier/foo', <<-RUBY + html_template "notifier/foo", <<-RUBY <p>Hello, World!</p> RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -445,19 +490,31 @@ module ApplicationTests end RUBY - app('development') + app("development") get "/rails/mailers/notifier/foo.html" assert_equal 200, last_response.status - assert_match '<option selected value="?part=text%2Fhtml">View as HTML email</option>', last_response.body + assert_match '<option selected value="locale=en">en', last_response.body + assert_match '<option value="locale=ja">ja', last_response.body + + get "/rails/mailers/notifier/foo.html?locale=ja" + assert_equal 200, last_response.status + assert_match '<option value="locale=en">en', last_response.body + assert_match '<option selected value="locale=ja">ja', last_response.body get "/rails/mailers/notifier/foo.txt" assert_equal 200, last_response.status - assert_match '<option selected value="?part=text%2Fplain">View as plain-text email</option>', last_response.body + assert_match '<option selected value="locale=en">en', last_response.body + assert_match '<option value="locale=ja">ja', last_response.body + + get "/rails/mailers/notifier/foo.txt?locale=ja" + assert_equal 200, last_response.status + assert_match '<option value="locale=en">en', last_response.body + assert_match '<option selected value="locale=ja">ja', last_response.body end test "mailer previews create correct links when loaded on a subdirectory" do - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -467,11 +524,11 @@ module ApplicationTests end RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -479,17 +536,68 @@ module ApplicationTests end RUBY - app('development') + app("development") - get "/rails/mailers", {}, 'SCRIPT_NAME' => '/my_app' + get "/rails/mailers", {}, { "SCRIPT_NAME" => "/my_app" } assert_match '<h3><a href="/my_app/rails/mailers/notifier">Notifier</a></h3>', last_response.body assert_match '<li><a href="/my_app/rails/mailers/notifier/foo">foo</a></li>', last_response.body end + test "mailer preview receives query params" do + mailer "notifier", <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo(name) + @name = name + mail to: "to@example.org" + end + end + RUBY + + html_template "notifier/foo", <<-RUBY + <p>Hello, <%= @name %>!</p> + RUBY + + text_template "notifier/foo", <<-RUBY + Hello, <%= @name %>! + RUBY + + mailer_preview "notifier", <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo(params[:name] || "World") + end + end + RUBY + + app("development") + + get "/rails/mailers/notifier/foo.txt" + assert_equal 200, last_response.status + assert_match '<iframe seamless name="messageBody" src="?part=text%2Fplain">', last_response.body + assert_match '<option selected value="part=text%2Fplain">', last_response.body + assert_match '<option value="part=text%2Fhtml">', last_response.body + + get "/rails/mailers/notifier/foo?part=text%2Fplain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + + get "/rails/mailers/notifier/foo.html?name=Ruby" + assert_equal 200, last_response.status + assert_match '<iframe seamless name="messageBody" src="?name=Ruby&part=text%2Fhtml">', last_response.body + assert_match '<option selected value="name=Ruby&part=text%2Fhtml">', last_response.body + assert_match '<option value="name=Ruby&part=text%2Fplain">', last_response.body + + get "/rails/mailers/notifier/foo?name=Ruby&part=text%2Fhtml" + assert_equal 200, last_response.status + assert_match %r[<p>Hello, Ruby!</p>], last_response.body + end + test "plain text mailer preview with attachment" do image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo=" - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -500,11 +608,11 @@ module ApplicationTests end RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -512,7 +620,7 @@ module ApplicationTests end RUBY - app('development') + app("development") get "/rails/mailers/notifier/foo" assert_equal 200, last_response.status @@ -526,7 +634,7 @@ module ApplicationTests test "multipart mailer preview with attachment" do image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo=" - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -537,15 +645,15 @@ module ApplicationTests end RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - html_template 'notifier/foo', <<-RUBY + html_template "notifier/foo", <<-RUBY <p>Hello, World!</p> RUBY - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -553,7 +661,7 @@ module ApplicationTests end RUBY - app('development') + app("development") get "/rails/mailers/notifier/foo" assert_equal 200, last_response.status @@ -571,7 +679,7 @@ module ApplicationTests test "multipart mailer preview with inline attachment" do image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo=" - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -582,16 +690,16 @@ module ApplicationTests end RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - html_template 'notifier/foo', <<-RUBY + html_template "notifier/foo", <<-RUBY <p>Hello, World!</p> <%= image_tag attachments['pixel.png'].url %> RUBY - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -599,7 +707,7 @@ module ApplicationTests end RUBY - app('development') + app("development") get "/rails/mailers/notifier/foo" assert_equal 200, last_response.status @@ -616,7 +724,7 @@ module ApplicationTests end test "multipart mailer preview with attached email" do - mailer 'notifier', <<-RUBY + mailer "notifier", <<-RUBY class Notifier < ActionMailer::Base default from: "from@example.com" @@ -641,15 +749,15 @@ module ApplicationTests end RUBY - text_template 'notifier/foo', <<-RUBY + text_template "notifier/foo", <<-RUBY Hello, World! RUBY - html_template 'notifier/foo', <<-RUBY + html_template "notifier/foo", <<-RUBY <p>Hello, World!</p> RUBY - mailer_preview 'notifier', <<-RUBY + mailer_preview "notifier", <<-RUBY class NotifierPreview < ActionMailer::Preview def foo Notifier.foo @@ -657,7 +765,7 @@ module ApplicationTests end RUBY - app('development') + app("development") get "/rails/mailers/notifier/foo" assert_equal 200, last_response.status @@ -672,6 +780,40 @@ module ApplicationTests assert_match %r[<p>Hello, World!</p>], last_response.body end + test "multipart mailer preview with empty parts" do + mailer "notifier", <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template "notifier/foo", <<-RUBY + RUBY + + html_template "notifier/foo", <<-RUBY + RUBY + + mailer_preview "notifier", <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app("development") + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + + get "/rails/mailers/notifier/foo?part=text/html" + assert_equal 200, last_response.status + end + private def build_app super @@ -695,7 +837,7 @@ module ApplicationTests end def image_file(name, contents) - app_file("public/images/#{name}", Base64.strict_decode64(contents), 'wb') + app_file("public/images/#{name}", Base64.strict_decode64(contents), "wb") end end end diff --git a/railties/test/application/middleware/cache_test.rb b/railties/test/application/middleware/cache_test.rb index c951dabd6c..3768d8ce2d 100644 --- a/railties/test/application/middleware/cache_test.rb +++ b/railties/test/application/middleware/cache_test.rb @@ -1,4 +1,6 @@ -require 'isolation/abstract_unit' +# frozen_string_literal: true + +require "isolation/abstract_unit" module ApplicationTests class CacheTest < ActiveSupport::TestCase @@ -6,8 +8,7 @@ module ApplicationTests def setup build_app - boot_rails - require 'rack/test' + require "rack/test" extend Rack::Test::Methods end @@ -20,7 +21,7 @@ module ApplicationTests class ExpiresController < ApplicationController def expires_header expires_in 10, public: !params[:private] - render text: SecureRandom.hex(16) + render plain: SecureRandom.hex(16) end def expires_etag @@ -33,18 +34,18 @@ module ApplicationTests end def keeps_if_modified_since - render :text => request.headers['If-Modified-Since'] + render plain: request.headers['If-Modified-Since'] end private def render_conditionally(headers) if stale?(headers.merge(public: !params[:private])) - render text: SecureRandom.hex(16) + render plain: SecureRandom.hex(16) end end end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller(/:action)' end @@ -55,7 +56,7 @@ module ApplicationTests simple_controller expected = "Wed, 30 May 1984 19:43:31 GMT" - get "/expires/keeps_if_modified_since", {}, "HTTP_IF_MODIFIED_SINCE" => expected + get "/expires/keeps_if_modified_since", {}, { "HTTP_IF_MODIFIED_SINCE" => expected } assert_equal 200, last_response.status assert_equal expected, last_response.body, "cache should have kept If-Modified-Since" @@ -66,12 +67,12 @@ module ApplicationTests app("development") get "/expires/expires_header" - assert_nil last_response.headers['X-Rack-Cache'] + assert_nil last_response.headers["X-Rack-Cache"] body = last_response.body get "/expires/expires_header" - assert_nil last_response.headers['X-Rack-Cache'] + assert_nil last_response.headers["X-Rack-Cache"] assert_not_equal body, last_response.body end @@ -118,12 +119,12 @@ module ApplicationTests assert_equal "miss, store", last_response.headers["X-Rack-Cache"] assert_equal "public", last_response.headers["Cache-Control"] - body = last_response.body etag = last_response.headers["ETag"] - get "/expires/expires_etag", {}, "If-None-Match" => etag + get "/expires/expires_etag", {}, { "HTTP_IF_NONE_MATCH" => etag } assert_equal "stale, valid, store", last_response.headers["X-Rack-Cache"] - assert_equal body, last_response.body + assert_equal 304, last_response.status + assert_equal "", last_response.body end def test_cache_works_with_etags_private @@ -138,8 +139,8 @@ module ApplicationTests body = last_response.body etag = last_response.headers["ETag"] - get "/expires/expires_etag", {private: true}, "If-None-Match" => etag - assert_equal "miss", last_response.headers["X-Rack-Cache"] + get "/expires/expires_etag", { private: true }, { "HTTP_IF_NONE_MATCH" => etag } + assert_equal "miss", last_response.headers["X-Rack-Cache"] assert_not_equal body, last_response.body end @@ -152,12 +153,12 @@ module ApplicationTests assert_equal "miss, store", last_response.headers["X-Rack-Cache"] assert_equal "public", last_response.headers["Cache-Control"] - body = last_response.body last = last_response.headers["Last-Modified"] - get "/expires/expires_last_modified", {}, "If-Modified-Since" => last + get "/expires/expires_last_modified", {}, { "HTTP_IF_MODIFIED_SINCE" => last } assert_equal "stale, valid, store", last_response.headers["X-Rack-Cache"] - assert_equal body, last_response.body + assert_equal 304, last_response.status + assert_equal "", last_response.body end def test_cache_works_with_last_modified_private @@ -172,8 +173,8 @@ module ApplicationTests body = last_response.body last = last_response.headers["Last-Modified"] - get "/expires/expires_last_modified", {private: true}, "If-Modified-Since" => last - assert_equal "miss", last_response.headers["X-Rack-Cache"] + get "/expires/expires_last_modified", { private: true }, { "HTTP_IF_MODIFIED_SINCE" => last } + assert_equal "miss", last_response.headers["X-Rack-Cache"] assert_not_equal body, last_response.body end end diff --git a/railties/test/application/middleware/cookies_test.rb b/railties/test/application/middleware/cookies_test.rb index bbb7627be9..fe48ef3f03 100644 --- a/railties/test/application/middleware/cookies_test.rb +++ b/railties/test/application/middleware/cookies_test.rb @@ -1,8 +1,12 @@ -require 'isolation/abstract_unit' +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rack/test" module ApplicationTests class CookiesTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation + include Rack::Test::Methods def new_app File.expand_path("#{app_path}/../new_app") @@ -10,38 +14,180 @@ module ApplicationTests def setup build_app - boot_rails FileUtils.rm_rf("#{app_path}/config/environments") end + def app + Rails.application + end + def teardown teardown_app FileUtils.rm_rf(new_app) if File.directory?(new_app) end - test 'always_write_cookie is true by default in development' do - require 'rails' - Rails.env = 'development' + test "always_write_cookie is true by default in development" do + require "rails" + Rails.env = "development" require "#{app_path}/config/environment" assert_equal true, ActionDispatch::Cookies::CookieJar.always_write_cookie end - test 'always_write_cookie is false by default in production' do - require 'rails' - Rails.env = 'production' + test "always_write_cookie is false by default in production" do + require "rails" + Rails.env = "production" require "#{app_path}/config/environment" assert_equal false, ActionDispatch::Cookies::CookieJar.always_write_cookie end - test 'always_write_cookie can be overridden' do + test "always_write_cookie can be overridden" do add_to_config <<-RUBY config.action_dispatch.always_write_cookie = false RUBY - require 'rails' - Rails.env = 'development' + require "rails" + Rails.env = "development" require "#{app_path}/config/environment" assert_equal false, ActionDispatch::Cookies::CookieJar.always_write_cookie end + + test "signed cookies with SHA512 digest and rotated out SHA256 and SHA1 digests" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get ':controller(/:action)' + post ':controller(/:action)' + end + RUBY + + controller :foo, <<-RUBY + class FooController < ActionController::Base + protect_from_forgery with: :null_session + + def write_raw_cookie_sha1 + cookies[:signed_cookie] = TestVerifiers.sha1.generate("signed cookie") + head :ok + end + + def write_raw_cookie_sha256 + cookies[:signed_cookie] = TestVerifiers.sha256.generate("signed cookie") + head :ok + end + + def read_signed + render plain: cookies.signed[:signed_cookie].inspect + end + + def read_raw_cookie + render plain: cookies[:signed_cookie] + end + end + RUBY + + add_to_config <<-RUBY + sha1_secret = Rails.application.key_generator.generate_key("sha1") + sha256_secret = Rails.application.key_generator.generate_key("sha256") + + ::TestVerifiers = Class.new do + class_attribute :sha1, default: ActiveSupport::MessageVerifier.new(sha1_secret, digest: "SHA1") + class_attribute :sha256, default: ActiveSupport::MessageVerifier.new(sha256_secret, digest: "SHA256") + end + + config.action_dispatch.signed_cookie_digest = "SHA512" + config.action_dispatch.signed_cookie_salt = "sha512 salt" + + config.action_dispatch.cookies_rotations.tap do |cookies| + cookies.rotate :signed, sha1_secret, digest: "SHA1" + cookies.rotate :signed, sha256_secret, digest: "SHA256" + end + RUBY + + require "#{app_path}/config/environment" + + verifier_sha512 = ActiveSupport::MessageVerifier.new(app.key_generator.generate_key("sha512 salt"), digest: :SHA512) + + get "/foo/write_raw_cookie_sha1" + get "/foo/read_signed" + assert_equal "signed cookie".inspect, last_response.body + + get "/foo/read_raw_cookie" + assert_equal "signed cookie", verifier_sha512.verify(last_response.body, purpose: "cookie.signed_cookie") + + get "/foo/write_raw_cookie_sha256" + get "/foo/read_signed" + assert_equal "signed cookie".inspect, last_response.body + + get "/foo/read_raw_cookie" + assert_equal "signed cookie", verifier_sha512.verify(last_response.body, purpose: "cookie.signed_cookie") + end + + test "encrypted cookies rotating multiple encryption keys" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get ':controller(/:action)' + post ':controller(/:action)' + end + RUBY + + controller :foo, <<-RUBY + class FooController < ActionController::Base + protect_from_forgery with: :null_session + + def write_raw_cookie_one + cookies[:encrypted_cookie] = TestEncryptors.first_gcm.encrypt_and_sign("encrypted cookie") + head :ok + end + + def write_raw_cookie_two + cookies[:encrypted_cookie] = TestEncryptors.second_gcm.encrypt_and_sign("encrypted cookie") + head :ok + end + + def read_encrypted + render plain: cookies.encrypted[:encrypted_cookie].inspect + end + + def read_raw_cookie + render plain: cookies[:encrypted_cookie] + end + end + RUBY + + add_to_config <<-RUBY + first_secret = Rails.application.key_generator.generate_key("first", 32) + second_secret = Rails.application.key_generator.generate_key("second", 32) + + ::TestEncryptors = Class.new do + class_attribute :first_gcm, default: ActiveSupport::MessageEncryptor.new(first_secret, cipher: "aes-256-gcm") + class_attribute :second_gcm, default: ActiveSupport::MessageEncryptor.new(second_secret, cipher: "aes-256-gcm") + end + + config.action_dispatch.use_authenticated_cookie_encryption = true + config.action_dispatch.encrypted_cookie_cipher = "aes-256-gcm" + config.action_dispatch.authenticated_encrypted_cookie_salt = "salt" + + config.action_dispatch.cookies_rotations.tap do |cookies| + cookies.rotate :encrypted, first_secret + cookies.rotate :encrypted, second_secret + end + RUBY + + require "#{app_path}/config/environment" + + encryptor = ActiveSupport::MessageEncryptor.new(app.key_generator.generate_key("salt", 32), cipher: "aes-256-gcm") + + get "/foo/write_raw_cookie_one" + get "/foo/read_encrypted" + assert_equal "encrypted cookie".inspect, last_response.body + + get "/foo/read_raw_cookie" + assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body, purpose: "cookie.encrypted_cookie") + + get "/foo/write_raw_cookie_two" + get "/foo/read_encrypted" + assert_equal "encrypted cookie".inspect, last_response.body + + get "/foo/read_raw_cookie" + assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body, purpose: "cookie.encrypted_cookie") + end end end diff --git a/railties/test/application/middleware/exceptions_test.rb b/railties/test/application/middleware/exceptions_test.rb index 4906f9a1e8..2d659ade8d 100644 --- a/railties/test/application/middleware/exceptions_test.rb +++ b/railties/test/application/middleware/exceptions_test.rb @@ -1,5 +1,7 @@ -require 'isolation/abstract_unit' -require 'rack/test' +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rack/test" module ApplicationTests class MiddlewareExceptionsTest < ActiveSupport::TestCase @@ -8,7 +10,6 @@ module ApplicationTests def setup build_app - boot_rails end def teardown @@ -48,7 +49,7 @@ module ApplicationTests test "uses custom exceptions app" do add_to_config <<-RUBY config.exceptions_app = lambda do |env| - [404, { "Content-Type" => "text/plain" }, ["YOU FAILED BRO"]] + [404, { "Content-Type" => "text/plain" }, ["YOU FAILED"]] end RUBY @@ -56,7 +57,7 @@ module ApplicationTests get "/foo" assert_equal 404, last_response.status - assert_equal "YOU FAILED BRO", last_response.body + assert_equal "YOU FAILED", last_response.body end test "url generation error when action_dispatch.show_exceptions is set raises an exception" do @@ -67,10 +68,10 @@ module ApplicationTests end end RUBY - + app.config.action_dispatch.show_exceptions = true - get '/foo' + get "/foo" assert_equal 500, last_response.status end @@ -78,15 +79,15 @@ module ApplicationTests app.config.action_dispatch.show_exceptions = false assert_raise(ActionController::RoutingError) do - get '/foo' + get "/foo" end end test "unspecified route when action_dispatch.show_exceptions is set shows 404" do app.config.action_dispatch.show_exceptions = true - assert_nothing_raised(ActionController::RoutingError) do - get '/foo' + assert_nothing_raised do + get "/foo" assert_match "The page you were looking for doesn't exist.", last_response.body end end @@ -95,12 +96,26 @@ module ApplicationTests app.config.action_dispatch.show_exceptions = true app.config.consider_all_requests_local = true - assert_nothing_raised(ActionController::RoutingError) do - get '/foo' + assert_nothing_raised do + get "/foo" assert_match "No route matches", last_response.body end end + test "routing to a nonexistent controller when action_dispatch.show_exceptions and consider_all_requests_local are set shows diagnostics" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + resources :articles + end + RUBY + + app.config.action_dispatch.show_exceptions = true + app.config.consider_all_requests_local = true + + get "/articles" + assert_match "<title>Action Controller: Exception caught</title>", last_response.body + end + test "displays diagnostics message when exception raised in template that contains UTF-8" do controller :foo, <<-RUBY class FooController < ActionController::Base @@ -112,12 +127,12 @@ module ApplicationTests app.config.action_dispatch.show_exceptions = true app.config.consider_all_requests_local = true - app_file 'app/views/foo/index.html.erb', <<-ERB + app_file "app/views/foo/index.html.erb", <<-ERB <% raise 'boooom' %> ✓測試テスト시험 ERB - get '/foo', :utf8 => '✓' + get "/foo", utf8: "✓" assert_match(/boooom/, last_response.body) assert_match(/測試テスト시험/, last_response.body) end diff --git a/railties/test/application/middleware/remote_ip_test.rb b/railties/test/application/middleware/remote_ip_test.rb index 97d5b5c698..83cf8a27f7 100644 --- a/railties/test/application/middleware/remote_ip_test.rb +++ b/railties/test/application/middleware/remote_ip_test.rb @@ -1,6 +1,8 @@ -require 'ipaddr' -require 'isolation/abstract_unit' -require 'active_support/key_generator' +# frozen_string_literal: true + +require "ipaddr" +require "isolation/abstract_unit" +require "active_support/key_generator" module ApplicationTests class RemoteIpTest < ActiveSupport::TestCase @@ -9,8 +11,8 @@ module ApplicationTests def remote_ip(env = {}) remote_ip = nil env = Rack::MockRequest.env_for("/").merge(env).merge!( - 'action_dispatch.show_exceptions' => false, - 'action_dispatch.key_generator' => ActiveSupport::LegacyKeyGenerator.new('b3c631c314c0bbca50c1b2843150fe33') + "action_dispatch.show_exceptions" => false, + "action_dispatch.key_generator" => ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33") ) endpoint = Proc.new do |e| @@ -36,10 +38,10 @@ module ApplicationTests test "works with both headers individually" do make_basic_app - assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do + assert_nothing_raised do assert_equal "1.1.1.1", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1") end - assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do + assert_nothing_raised do assert_equal "1.1.1.2", remote_ip("HTTP_CLIENT_IP" => "1.1.1.2") end end @@ -49,7 +51,7 @@ module ApplicationTests app.config.action_dispatch.ip_spoofing_check = false end - assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do + assert_nothing_raised do assert_equal "1.1.1.1", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1", "HTTP_CLIENT_IP" => "1.1.1.2") end end @@ -69,7 +71,7 @@ module ApplicationTests test "the user can set trusted proxies with an IPAddr argument" do make_basic_app do |app| - app.config.action_dispatch.trusted_proxies = IPAddr.new('4.2.42.0/24') + app.config.action_dispatch.trusted_proxies = IPAddr.new("4.2.42.0/24") end assert_equal "1.1.1.1", remote_ip("REMOTE_ADDR" => "1.1.1.1", "HTTP_X_FORWARDED_FOR" => "10.0.0.0,4.2.42.42") diff --git a/railties/test/application/middleware/sendfile_test.rb b/railties/test/application/middleware/sendfile_test.rb index dc96480d6d..818ad61c64 100644 --- a/railties/test/application/middleware/sendfile_test.rb +++ b/railties/test/application/middleware/sendfile_test.rb @@ -1,4 +1,6 @@ -require 'isolation/abstract_unit' +# frozen_string_literal: true + +require "isolation/abstract_unit" module ApplicationTests class SendfileTest < ActiveSupport::TestCase @@ -6,7 +8,6 @@ module ApplicationTests def setup build_app - boot_rails FileUtils.rm_rf "#{app_path}/config/environments" end @@ -14,10 +15,6 @@ module ApplicationTests teardown_app end - def app - @app ||= Rails.application - end - define_method :simple_controller do class ::OmgController < ActionController::Base def index @@ -32,7 +29,7 @@ module ApplicationTests simple_controller get "/" - assert !last_response.headers["X-Sendfile"] + assert_not last_response.headers["X-Sendfile"] assert_equal File.read(__FILE__), last_response.body end @@ -49,7 +46,7 @@ module ApplicationTests test "config.action_dispatch.x_sendfile_header is sent to Rack::Sendfile" do make_basic_app do |app| - app.config.action_dispatch.x_sendfile_header = 'X-Lighttpd-Send-File' + app.config.action_dispatch.x_sendfile_header = "X-Lighttpd-Send-File" end simple_controller @@ -60,8 +57,8 @@ module ApplicationTests test "files handled by ActionDispatch::Static are handled by Rack::Sendfile" do make_basic_app do |app| - app.config.action_dispatch.x_sendfile_header = 'X-Sendfile' - app.config.serve_static_files = true + app.config.action_dispatch.x_sendfile_header = "X-Sendfile" + app.config.public_file_server.enabled = true app.paths["public"] = File.join(rails_root, "public") end diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index 25eadfc387..b25e56b625 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -1,5 +1,7 @@ -require 'isolation/abstract_unit' -require 'rack/test' +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rack/test" module ApplicationTests class MiddlewareSessionTest < ActiveSupport::TestCase @@ -8,7 +10,6 @@ module ApplicationTests def setup build_app - boot_rails FileUtils.rm_rf "#{app_path}/config/environments" end @@ -20,12 +21,19 @@ module ApplicationTests @app ||= Rails.application end - test "config.force_ssl sets cookie to secure only" do + test "config.force_ssl sets cookie to secure only by default" do add_to_config "config.force_ssl = true" require "#{app_path}/config/environment" assert app.config.session_options[:secure], "Expected session to be marked as secure" end + test "config.force_ssl doesn't set cookie to secure only when changed from default" do + add_to_config "config.force_ssl = true" + add_to_config "config.ssl_options = { secure_cookies: false }" + require "#{app_path}/config/environment" + assert_not app.config.session_options[:secure] + end + test "session is not loaded if it's not used" do make_basic_app @@ -43,11 +51,11 @@ module ApplicationTests get "/" assert last_request.env["HTTP_COOKIE"] - assert !last_response.headers["Set-Cookie"] + assert_not last_response.headers["Set-Cookie"] end test "session is empty and isn't saved on unverified request when using :null_session protect method" do - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller(/:action)' post ':controller(/:action)' @@ -64,7 +72,7 @@ module ApplicationTests end def read_session - render text: session[:foo].inspect + render plain: session[:foo].inspect end end RUBY @@ -75,20 +83,20 @@ module ApplicationTests require "#{app_path}/config/environment" - get '/foo/write_session' - get '/foo/read_session' - assert_equal '1', last_response.body + get "/foo/write_session" + get "/foo/read_session" + assert_equal "1", last_response.body - post '/foo/read_session' # Read session using POST request without CSRF token - assert_equal 'nil', last_response.body # Stored value shouldn't be accessible + post "/foo/read_session" # Read session using POST request without CSRF token + assert_equal "nil", last_response.body # Stored value shouldn't be accessible - post '/foo/write_session' # Write session using POST request without CSRF token - get '/foo/read_session' # Session shouldn't be changed - assert_equal '1', last_response.body + post "/foo/write_session" # Write session using POST request without CSRF token + get "/foo/read_session" # Session shouldn't be changed + assert_equal "1", last_response.body end test "cookie jar is empty and isn't saved on unverified request when using :null_session protect method" do - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller(/:action)' post ':controller(/:action)' @@ -105,7 +113,7 @@ module ApplicationTests end def read_cookie - render text: cookies[:foo].inspect + render plain: cookies[:foo].inspect end end RUBY @@ -116,20 +124,20 @@ module ApplicationTests require "#{app_path}/config/environment" - get '/foo/write_cookie' - get '/foo/read_cookie' + get "/foo/write_cookie" + get "/foo/read_cookie" assert_equal '"1"', last_response.body - post '/foo/read_cookie' # Read cookie using POST request without CSRF token - assert_equal 'nil', last_response.body # Stored value shouldn't be accessible + post "/foo/read_cookie" # Read cookie using POST request without CSRF token + assert_equal "nil", last_response.body # Stored value shouldn't be accessible - post '/foo/write_cookie' # Write cookie using POST request without CSRF token - get '/foo/read_cookie' # Cookie shouldn't be changed + post "/foo/write_cookie" # Write cookie using POST request without CSRF token + get "/foo/read_cookie" # Cookie shouldn't be changed assert_equal '"1"', last_response.body end test "session using encrypted cookie store" do - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller(/:action)' end @@ -143,38 +151,43 @@ module ApplicationTests end def read_session - render text: session[:foo] + render plain: session[:foo] end def read_encrypted_cookie - render text: cookies.encrypted[:_myapp_session]['foo'] + render plain: cookies.encrypted[:_myapp_session]['foo'] end def read_raw_cookie - render text: cookies[:_myapp_session] + render plain: cookies[:_myapp_session] end end RUBY + add_to_config <<-RUBY + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true + RUBY + require "#{app_path}/config/environment" - get '/foo/write_session' - get '/foo/read_session' - assert_equal '1', last_response.body + get "/foo/write_session" + get "/foo/read_session" + assert_equal "1", last_response.body - get '/foo/read_encrypted_cookie' - assert_equal '1', last_response.body + get "/foo/read_encrypted_cookie" + assert_equal "1", last_response.body - secret = app.key_generator.generate_key('encrypted cookie') - sign_secret = app.key_generator.generate_key('signed encrypted cookie') - encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) - get '/foo/read_raw_cookie' - assert_equal 1, encryptor.decrypt_and_verify(last_response.body)['foo'] + get "/foo/read_raw_cookie" + assert_equal 1, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"] end test "session upgrading signature to encryption cookie store works the same way as encrypted cookie store" do - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller(/:action)' end @@ -188,42 +201,45 @@ module ApplicationTests end def read_session - render text: session[:foo] + render plain: session[:foo] end def read_encrypted_cookie - render text: cookies.encrypted[:_myapp_session]['foo'] + render plain: cookies.encrypted[:_myapp_session]['foo'] end def read_raw_cookie - render text: cookies[:_myapp_session] + render plain: cookies[:_myapp_session] end end RUBY add_to_config <<-RUBY secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" + + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true RUBY require "#{app_path}/config/environment" - get '/foo/write_session' - get '/foo/read_session' - assert_equal '1', last_response.body + get "/foo/write_session" + get "/foo/read_session" + assert_equal "1", last_response.body - get '/foo/read_encrypted_cookie' - assert_equal '1', last_response.body + get "/foo/read_encrypted_cookie" + assert_equal "1", last_response.body - secret = app.key_generator.generate_key('encrypted cookie') - sign_secret = app.key_generator.generate_key('signed encrypted cookie') - encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) - get '/foo/read_raw_cookie' - assert_equal 1, encryptor.decrypt_and_verify(last_response.body)['foo'] + get "/foo/read_raw_cookie" + assert_equal 1, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"] end test "session upgrading signature to encryption cookie store upgrades session to encrypted mode" do - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller(/:action)' end @@ -243,46 +259,119 @@ module ApplicationTests end def read_session - render text: session[:foo] + render plain: session[:foo] end def read_encrypted_cookie - render text: cookies.encrypted[:_myapp_session]['foo'] + render plain: cookies.encrypted[:_myapp_session]['foo'] end def read_raw_cookie - render text: cookies[:_myapp_session] + render plain: cookies[:_myapp_session] end end RUBY add_to_config <<-RUBY secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" + + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true RUBY require "#{app_path}/config/environment" - get '/foo/write_raw_session' - get '/foo/read_session' - assert_equal '1', last_response.body + get "/foo/write_raw_session" + get "/foo/read_session" + assert_equal "1", last_response.body + + get "/foo/write_session" + get "/foo/read_session" + assert_equal "2", last_response.body + + get "/foo/read_encrypted_cookie" + assert_equal "2", last_response.body + + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) + + get "/foo/read_raw_cookie" + assert_equal 2, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"] + end + + test "session upgrading from AES-CBC-HMAC encryption to AES-GCM encryption" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get ':controller(/:action)' + end + RUBY + + controller :foo, <<-RUBY + class FooController < ActionController::Base + def write_raw_session + # AES-256-CBC with SHA1 HMAC + # {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1} + cookies[:_myapp_session] = "TlgrdS85aUpDd1R2cDlPWlR6K0FJeGExckwySjZ2Z0pkR3d2QnRObGxZT25aalJWYWVvbFVLcHF4d0VQVDdSaFF2QjFPbG9MVjJzeWp3YjcyRUlKUUU2ZlR4bXlSNG9ZUkJPRUtld0E3dVU9LS0xNDZXbGpRZ3NjdW43N2haUEZJSUNRPT0=--3639b5ce54c09495cfeaae928cd5634e0c4b2e96" + head :ok + end + + def write_session + session[:foo] = session[:foo] + 1 + head :ok + end + + def read_session + render plain: session[:foo] + end + + def read_encrypted_cookie + render plain: cookies.encrypted[:_myapp_session]['foo'] + end + + def read_raw_cookie + render plain: cookies[:_myapp_session] + end + end + RUBY + + add_to_config <<-RUBY + # Use a static key + Rails.application.credentials.secret_key_base = "known key base" - get '/foo/write_session' - get '/foo/read_session' - assert_equal '2', last_response.body + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true + RUBY + + begin + old_rails_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "production" + + require "#{app_path}/config/environment" + + get "/foo/write_raw_session" + get "/foo/read_session" + assert_equal "1", last_response.body + + get "/foo/write_session" + get "/foo/read_session" + assert_equal "2", last_response.body - get '/foo/read_encrypted_cookie' - assert_equal '2', last_response.body + get "/foo/read_encrypted_cookie" + assert_equal "2", last_response.body - secret = app.key_generator.generate_key('encrypted cookie') - sign_secret = app.key_generator.generate_key('signed encrypted cookie') - encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) - get '/foo/read_raw_cookie' - assert_equal 2, encryptor.decrypt_and_verify(last_response.body)['foo'] + get "/foo/read_raw_cookie" + assert_equal 2, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"] + ensure + ENV["RAILS_ENV"] = old_rails_env + end end test "session upgrading legacy signed cookies to new signed cookies" do - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller(/:action)' end @@ -302,41 +391,81 @@ module ApplicationTests end def read_session - render text: session[:foo] + render plain: session[:foo] end def read_signed_cookie - render text: cookies.signed[:_myapp_session]['foo'] + render plain: cookies.signed[:_myapp_session]['foo'] end def read_raw_cookie - render text: cookies[:_myapp_session] + render plain: cookies[:_myapp_session] end end RUBY add_to_config <<-RUBY secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" - secrets.secret_key_base = nil + Rails.application.credentials.secret_key_base = nil RUBY - require "#{app_path}/config/environment" + begin + old_rails_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "production" + + require "#{app_path}/config/environment" + + get "/foo/write_raw_session" + get "/foo/read_session" + assert_equal "1", last_response.body - get '/foo/write_raw_session' - get '/foo/read_session' - assert_equal '1', last_response.body + get "/foo/write_session" + get "/foo/read_session" + assert_equal "2", last_response.body - get '/foo/write_session' - get '/foo/read_session' - assert_equal '2', last_response.body + get "/foo/read_signed_cookie" + assert_equal "2", last_response.body - get '/foo/read_signed_cookie' - assert_equal '2', last_response.body + verifier = ActiveSupport::MessageVerifier.new(app.secrets.secret_token) - verifier = ActiveSupport::MessageVerifier.new(app.secrets.secret_token) + get "/foo/read_raw_cookie" + assert_equal 2, verifier.verify(last_response.body, purpose: "cookie._myapp_session")["foo"] + ensure + ENV["RAILS_ENV"] = old_rails_env + end + end + + test "calling reset_session on request does not trigger an error for API apps" do + add_to_config "config.api_only = true" + + controller :test, <<-RUBY + class TestController < ApplicationController + def dump_flash + request.reset_session + render plain: 'It worked!' + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/dump_flash' => "test#dump_flash" + end + RUBY + + require "#{app_path}/config/environment" + + get "/dump_flash" - get '/foo/read_raw_cookie' - assert_equal 2, verifier.verify(last_response.body)['foo'] + assert_equal 200, last_response.status + assert_equal "It worked!", last_response.body + + assert_not_includes Rails.application.middleware, ActionDispatch::Flash + end + + test "cookie_only is set to true even if user tries to overwrite it" do + add_to_config "config.session_store :cookie_store, key: '_myapp_session', cookie_only: false" + require "#{app_path}/config/environment" + assert app.config.session_options[:cookie_only], "Expected cookie_only to be set to true" end end end diff --git a/railties/test/application/middleware/static_test.rb b/railties/test/application/middleware/static_test.rb index 1a46cd3568..0977042cfe 100644 --- a/railties/test/application/middleware/static_test.rb +++ b/railties/test/application/middleware/static_test.rb @@ -1,5 +1,7 @@ -require 'isolation/abstract_unit' -require 'rack/test' +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rack/test" module ApplicationTests class MiddlewareStaticTest < ActiveSupport::TestCase @@ -18,32 +20,49 @@ module ApplicationTests # Regression test to #8907 # See https://github.com/rails/rails/commit/9cc82b77196d21a5c7021f6dca59ab9b2b158a45#commitcomment-2416514 test "doesn't set Cache-Control header when it is nil" do - app_file "public/foo.html", 'static' + app_file "public/foo.html", "static" + + require "#{app_path}/config/environment" + + get "foo" + + assert_not last_response.headers.has_key?("Cache-Control"), "Cache-Control should not be set" + end + + test "headers for static files are configurable" do + app_file "public/about.html", "static" + add_to_config <<-CONFIG + config.public_file_server.headers = { + "Access-Control-Allow-Origin" => "http://rubyonrails.org", + "Cache-Control" => "public, max-age=60" + } + CONFIG require "#{app_path}/config/environment" - get 'foo' + get "/about.html" - assert_not last_response.headers.has_key?('Cache-Control'), "Cache-Control should not be set" + assert_equal "http://rubyonrails.org", last_response.headers["Access-Control-Allow-Origin"] + assert_equal "public, max-age=60", last_response.headers["Cache-Control"] end - test "static_index defaults to 'index'" do + test "public_file_server.index_name defaults to 'index'" do app_file "public/index.html", "/index.html" - + require "#{app_path}/config/environment" - get '/' - + get "/" + assert_equal "/index.html\n", last_response.body end - test "static_index configurable" do + test "public_file_server.index_name configurable" do app_file "public/other-index.html", "/other-index.html" - add_to_config "config.static_index = 'other-index'" - + add_to_config "config.public_file_server.index_name = 'other-index'" + require "#{app_path}/config/environment" - get '/' + get "/" assert_equal "/other-index.html\n", last_response.body end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index d298e8d632..631f5bac7f 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -1,4 +1,6 @@ -require 'isolation/abstract_unit' +# frozen_string_literal: true + +require "isolation/abstract_unit" module ApplicationTests class MiddlewareTest < ActiveSupport::TestCase @@ -6,7 +8,6 @@ module ApplicationTests def setup build_app - boot_rails FileUtils.rm_rf "#{app_path}/config/environments" end @@ -24,29 +25,29 @@ module ApplicationTests boot! assert_equal [ + "Webpacker::DevServerProxy", "Rack::Sendfile", "ActionDispatch::Static", - "ActionDispatch::LoadInterlock", + "ActionDispatch::Executor", "ActiveSupport::Cache::Strategy::LocalCache", "Rack::Runtime", "Rack::MethodOverride", "ActionDispatch::RequestId", - "Rails::Rack::Logger", # must come after Rack::MethodOverride to properly log overridden methods + "ActionDispatch::RemoteIp", + "Rails::Rack::Logger", "ActionDispatch::ShowExceptions", "ActionDispatch::DebugExceptions", - "ActionDispatch::RemoteIp", "ActionDispatch::Reloader", "ActionDispatch::Callbacks", "ActiveRecord::Migration::CheckPending", - "ActiveRecord::ConnectionAdapters::ConnectionManagement", - "ActiveRecord::QueryCache", "ActionDispatch::Cookies", "ActionDispatch::Session::CookieStore", "ActionDispatch::Flash", - "ActionDispatch::ParamsParser", + "ActionDispatch::ContentSecurityPolicy::Middleware", "Rack::Head", "Rack::ConditionalGet", - "Rack::ETag" + "Rack::ETag", + "Rack::TempfileReaper" ], middleware end @@ -56,31 +57,60 @@ module ApplicationTests boot! assert_equal [ + "Webpacker::DevServerProxy", "Rack::Sendfile", "ActionDispatch::Static", - "ActionDispatch::LoadInterlock", + "ActionDispatch::Executor", "ActiveSupport::Cache::Strategy::LocalCache", "Rack::Runtime", "ActionDispatch::RequestId", - "Rails::Rack::Logger", # must come after Rack::MethodOverride to properly log overridden methods + "ActionDispatch::RemoteIp", + "Rails::Rack::Logger", "ActionDispatch::ShowExceptions", "ActionDispatch::DebugExceptions", - "ActionDispatch::RemoteIp", "ActionDispatch::Reloader", "ActionDispatch::Callbacks", - "ActiveRecord::ConnectionAdapters::ConnectionManagement", - "ActiveRecord::QueryCache", - "ActionDispatch::ParamsParser", "Rack::Head", "Rack::ConditionalGet", "Rack::ETag" ], middleware end + test "middleware dependencies" do + boot! + + # The following array-of-arrays describes dependencies between + # middlewares: the first item in each list depends on the + # remaining items (and therefore must occur later in the + # middleware stack). + + dependencies = [ + # Logger needs a fully "corrected" request environment + %w(Rails::Rack::Logger Rack::MethodOverride ActionDispatch::RequestId ActionDispatch::RemoteIp), + + # Serving public/ doesn't invoke user code, so it should skip + # locks etc + %w(ActionDispatch::Executor ActionDispatch::Static), + + # Errors during reload must be reported + %w(ActionDispatch::Reloader ActionDispatch::ShowExceptions ActionDispatch::DebugExceptions), + + # Outright dependencies + %w(ActionDispatch::Static Rack::Sendfile), + %w(ActionDispatch::Flash ActionDispatch::Session::CookieStore), + %w(ActionDispatch::Session::CookieStore ActionDispatch::Cookies), + ] + + require "tsort" + sorted = TSort.tsort((middleware | dependencies.flatten).method(:each), + lambda { |n, &b| dependencies.each { |m, *ds| ds.each(&b) if m == n } }) + assert_equal sorted, middleware + end + test "Rack::Cache is not included by default" do boot! - assert !middleware.include?("Rack::Cache"), "Rack::Cache is not included in the default stack unless you set config.action_dispatch.rack_cache" + assert_not_includes middleware, "Rack::Cache", "Rack::Cache is not included in the default stack unless you set config.action_dispatch.rack_cache" end test "Rack::Cache is present when action_dispatch.rack_cache is set" do @@ -88,7 +118,7 @@ module ApplicationTests boot! - assert middleware.include?("Rack::Cache") + assert_includes middleware, "Rack::Cache" end test "ActiveRecord::Migration::CheckPending is present when active_record.migration_error is set to :page_load" do @@ -96,43 +126,32 @@ module ApplicationTests boot! - assert middleware.include?("ActiveRecord::Migration::CheckPending") + assert_includes middleware, "ActiveRecord::Migration::CheckPending" end test "ActionDispatch::SSL is present when force_ssl is set" do add_to_config "config.force_ssl = true" boot! - assert middleware.include?("ActionDispatch::SSL") + assert_includes middleware, "ActionDispatch::SSL" end test "ActionDispatch::SSL is configured with options when given" do add_to_config "config.force_ssl = true" - add_to_config "config.ssl_options = { host: 'example.com' }" + add_to_config "config.ssl_options = { redirect: { host: 'example.com' } }" boot! - assert_equal [{host: 'example.com'}], Rails.application.middleware.first.args + assert_equal [{ redirect: { host: "example.com" } }], Rails.application.middleware[1].args end test "removing Active Record omits its middleware" do use_frameworks [] boot! - assert !middleware.include?("ActiveRecord::ConnectionAdapters::ConnectionManagement") - assert !middleware.include?("ActiveRecord::QueryCache") - assert !middleware.include?("ActiveRecord::Migration::CheckPending") + assert_not_includes middleware, "ActiveRecord::Migration::CheckPending" end - test "includes interlock if cache_classes is set but eager_load is not" do - add_to_config "config.cache_classes = true" + test "includes executor" do boot! - assert_not_includes middleware, "Rack::Lock" - assert_includes middleware, "ActionDispatch::LoadInterlock" - end - - test "includes interlock if cache_classes is off" do - add_to_config "config.cache_classes = false" - boot! - assert_not_includes middleware, "Rack::Lock" - assert_includes middleware, "ActionDispatch::LoadInterlock" + assert_includes middleware, "ActionDispatch::Executor" end test "does not include lock if cache_classes is set and so is eager_load" do @@ -140,40 +159,37 @@ module ApplicationTests add_to_config "config.eager_load = true" boot! assert_not_includes middleware, "Rack::Lock" - assert_not_includes middleware, "ActionDispatch::LoadInterlock" end test "does not include lock if allow_concurrency is set to :unsafe" do add_to_config "config.allow_concurrency = :unsafe" boot! assert_not_includes middleware, "Rack::Lock" - assert_not_includes middleware, "ActionDispatch::LoadInterlock" end test "includes lock if allow_concurrency is disabled" do add_to_config "config.allow_concurrency = false" boot! assert_includes middleware, "Rack::Lock" - assert_not_includes middleware, "ActionDispatch::LoadInterlock" end - test "removes static asset server if serve_static_files is disabled" do - add_to_config "config.serve_static_files = false" + test "removes static asset server if public_file_server.enabled is disabled" do + add_to_config "config.public_file_server.enabled = false" boot! - assert !middleware.include?("ActionDispatch::Static") + assert_not_includes middleware, "ActionDispatch::Static" end test "can delete a middleware from the stack" do add_to_config "config.middleware.delete ActionDispatch::Static" boot! - assert !middleware.include?("ActionDispatch::Static") + assert_not_includes middleware, "ActionDispatch::Static" end test "can delete a middleware from the stack even if insert_before is added after delete" do add_to_config "config.middleware.delete Rack::Runtime" add_to_config "config.middleware.insert_before(Rack::Runtime, Rack::Config)" boot! - assert middleware.include?("Rack::Config") + assert_includes middleware, "Rack::Config" assert_not middleware.include?("Rack::Runtime") end @@ -181,21 +197,21 @@ module ApplicationTests add_to_config "config.middleware.delete Rack::Runtime" add_to_config "config.middleware.insert_after(Rack::Runtime, Rack::Config)" boot! - assert middleware.include?("Rack::Config") + assert_includes middleware, "Rack::Config" assert_not middleware.include?("Rack::Runtime") end test "includes exceptions middlewares even if action_dispatch.show_exceptions is disabled" do add_to_config "config.action_dispatch.show_exceptions = false" boot! - assert middleware.include?("ActionDispatch::ShowExceptions") - assert middleware.include?("ActionDispatch::DebugExceptions") + assert_includes middleware, "ActionDispatch::ShowExceptions" + assert_includes middleware, "ActionDispatch::DebugExceptions" end test "removes ActionDispatch::Reloader if cache_classes is true" do add_to_config "config.cache_classes = true" boot! - assert !middleware.include?("ActionDispatch::Reloader") + assert_not_includes middleware, "ActionDispatch::Reloader" end test "use middleware" do @@ -208,35 +224,36 @@ module ApplicationTests test "insert middleware after" do add_to_config "config.middleware.insert_after Rack::Sendfile, Rack::Config" boot! - assert_equal "Rack::Config", middleware.second + assert_equal "Rack::Config", middleware.third end - test 'unshift middleware' do - add_to_config 'config.middleware.unshift Rack::Config' + test "unshift middleware" do + add_to_config "config.middleware.unshift Rack::Config" boot! - assert_equal 'Rack::Config', middleware.first + assert_equal "Rack::Config", middleware.second end test "Rails.cache does not respond to middleware" do add_to_config "config.cache_store = :memory_store" boot! - assert_equal "Rack::Runtime", middleware.fourth + assert_equal "Rack::Runtime", middleware.fifth end test "Rails.cache does respond to middleware" do boot! - assert_equal "Rack::Runtime", middleware.fifth + assert_equal "ActiveSupport::Cache::Strategy::LocalCache", middleware.fifth + assert_equal "Rack::Runtime", middleware[5] end test "insert middleware before" do add_to_config "config.middleware.insert_before Rack::Sendfile, Rack::Config" boot! - assert_equal "Rack::Config", middleware.first + assert_equal "Rack::Config", middleware.second end test "can't change middleware after it's built" do boot! - assert_raise RuntimeError do + assert_raise frozen_error_class do app.config.middleware.use Rack::Config end end @@ -248,35 +265,35 @@ module ApplicationTests class ::OmgController < ActionController::Base def index if params[:nothing] - render text: "" + render plain: "" else - render text: "OMG" + render plain: "OMG" end end end - etag = "W/" + "5af83e3196bf99f440f31f2e1a6c9afe".inspect + etag = "W/" + "c00862d1c6c1cf7c1b49388306e7b3c1".inspect get "/" assert_equal 200, last_response.status assert_equal "OMG", last_response.body - assert_equal "text/html; charset=utf-8", last_response.headers["Content-Type"] + assert_equal "text/plain; charset=utf-8", last_response.headers["Content-Type"] assert_equal "max-age=0, private, must-revalidate", last_response.headers["Cache-Control"] assert_equal etag, last_response.headers["Etag"] - get "/", {}, "HTTP_IF_NONE_MATCH" => etag + get "/", {}, { "HTTP_IF_NONE_MATCH" => etag } assert_equal 304, last_response.status assert_equal "", last_response.body - assert_equal nil, last_response.headers["Content-Type"] + assert_nil last_response.headers["Content-Type"] assert_equal "max-age=0, private, must-revalidate", last_response.headers["Cache-Control"] assert_equal etag, last_response.headers["Etag"] get "/?nothing=true" assert_equal 200, last_response.status assert_equal "", last_response.body - assert_equal "text/html; charset=utf-8", last_response.headers["Content-Type"] + assert_equal "text/plain; charset=utf-8", last_response.headers["Content-Type"] assert_equal "no-cache", last_response.headers["Cache-Control"] - assert_equal nil, last_response.headers["Etag"] + assert_nil last_response.headers["Etag"] end test "ORIGINAL_FULLPATH is passed to env" do diff --git a/railties/test/application/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb index f2770a9cb4..d6c81c1fe2 100644 --- a/railties/test/application/multiple_applications_test.rb +++ b/railties/test/application/multiple_applications_test.rb @@ -1,4 +1,6 @@ -require 'isolation/abstract_unit' +# frozen_string_literal: true + +require "isolation/abstract_unit" module ApplicationTests class MultipleApplicationsTest < ActiveSupport::TestCase @@ -6,9 +8,8 @@ module ApplicationTests def setup build_app(initializers: true) - boot_rails require "#{rails_root}/config/environment" - Rails.application.config.some_setting = 'something_or_other' + Rails.application.config.some_setting = "something_or_other" end def teardown @@ -88,9 +89,9 @@ module ApplicationTests require "#{app_path}/config/environment" assert_equal 0, run_count, "The count should stay at zero without any calls to the rake tasks" - require 'rake' - require 'rake/testtask' - require 'rdoc/task' + require "rake" + require "rake/testtask" + require "rdoc/task" Rails.application.load_tasks assert_equal 2, run_count, "Calling a rake task should result in two increments to the count" end diff --git a/railties/test/application/paths_test.rb b/railties/test/application/paths_test.rb index 4029984ce9..28a9206daa 100644 --- a/railties/test/application/paths_test.rb +++ b/railties/test/application/paths_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" module ApplicationTests @@ -6,7 +8,6 @@ module ApplicationTests def setup build_app - boot_rails FileUtils.rm_rf("#{app_path}/config/environments") app_file "config/environments/development.rb", "" add_to_config <<-RUBY @@ -36,7 +37,7 @@ module ApplicationTests end def assert_not_in_load_path(*path) - assert !$:.any? { |p| File.expand_path(p) == root(*path) }, "Load path includes '#{root(*path)}'. They are:\n-----\n #{$:.join("\n")}\n-----" + assert_not $:.any? { |p| File.expand_path(p) == root(*path) }, "Load path includes '#{root(*path)}'. They are:\n-----\n #{$:.join("\n")}\n-----" end test "booting up Rails yields a valid paths object" do @@ -56,9 +57,9 @@ module ApplicationTests test "booting up Rails yields a list of paths that are eager" do eager_load = @paths.eager_load - assert eager_load.include?(root("app/controllers")) - assert eager_load.include?(root("app/helpers")) - assert eager_load.include?(root("app/models")) + assert_includes eager_load, root("app/controllers") + assert_includes eager_load, root("app/helpers") + assert_includes eager_load, root("app/models") end test "environments has a glob equal to the current environment" do diff --git a/railties/test/application/per_request_digest_cache_test.rb b/railties/test/application/per_request_digest_cache_test.rb new file mode 100644 index 0000000000..ab055c7648 --- /dev/null +++ b/railties/test/application/per_request_digest_cache_test.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rack/test" +require "minitest/mock" + +require "action_view" + +class PerRequestDigestCacheTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include Rack::Test::Methods + + setup do + build_app + add_to_config "config.consider_all_requests_local = true" + + app_file "app/models/customer.rb", <<-RUBY + class Customer < Struct.new(:name, :id) + extend ActiveModel::Naming + include ActiveModel::Conversion + + def cache_key + [ name, id ].join("/") + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + resources :customers, only: :index + end + RUBY + + app_file "app/controllers/customers_controller.rb", <<-RUBY + class CustomersController < ApplicationController + self.perform_caching = true + + def index + render [ Customer.new('david', 1), Customer.new('dingus', 2) ] + end + end + RUBY + + app_file "app/views/customers/_customer.html.erb", <<-RUBY + <% cache customer do %> + <%= customer.name %> + <% end %> + RUBY + + require "#{app_path}/config/environment" + end + + teardown :teardown_app + + test "digests are reused when rendering the same template twice" do + get "/customers" + assert_equal 200, last_response.status + + values = ActionView::LookupContext::DetailsKey.digest_caches.first.values + assert_equal [ "effc8928d0b33535c8a21d24ec617161" ], values + assert_equal %w(david dingus), last_response.body.split.map(&:strip) + end + + test "template digests are cleared before a request" do + assert_called(ActionView::LookupContext::DetailsKey, :clear) do + get "/customers" + assert_equal 200, last_response.status + end + end +end diff --git a/railties/test/application/rack/logger_test.rb b/railties/test/application/rack/logger_test.rb index 0082ec9cd2..ea425d5fa5 100644 --- a/railties/test/application/rack/logger_test.rb +++ b/railties/test/application/rack/logger_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" require "active_support/log_subscriber/test_helper" require "rack/test" @@ -41,16 +43,22 @@ module ApplicationTests end test "logger logs HTTP verb override" do - post "/", _method: 'put' + post "/", _method: "put" wait assert_match 'Started PUT "/"', logs end test "logger logs HEAD requests" do - post "/", _method: 'head' + post "/", _method: "head" wait assert_match 'Started HEAD "/"', logs end + + test "logger logs correct remote IP address" do + get "/", {}, { "REMOTE_ADDR" => "127.0.0.1", "HTTP_X_FORWARDED_FOR" => "1.2.3.4" } + wait + assert_match 'Started GET "/" for 1.2.3.4', logs + end end end end diff --git a/railties/test/application/rackup_test.rb b/railties/test/application/rackup_test.rb index 49ac9fc66c..383f18a7da 100644 --- a/railties/test/application/rackup_test.rb +++ b/railties/test/application/rackup_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" module ApplicationTests @@ -12,14 +14,13 @@ module ApplicationTests def setup build_app - boot_rails end def teardown teardown_app end - test "rails app is present" do + test "Rails app is present" do assert File.exist?(app_path("config")) end @@ -37,7 +38,7 @@ module ApplicationTests test "the config object is available on the application object" do rackup - assert_equal 'UTC', Rails.application.config.time_zone + assert_equal "UTC", Rails.application.config.time_zone end end end diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index e7beab8b5e..039987ac8c 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" -require "active_support/core_ext/string/strip" module ApplicationTests module RakeTests @@ -8,7 +9,6 @@ module ApplicationTests def setup build_app - boot_rails FileUtils.rm_rf("#{app_path}/config/environments") end @@ -21,50 +21,136 @@ module ApplicationTests end def set_database_url - ENV['DATABASE_URL'] = "sqlite3:#{database_url_db_name}" + ENV["DATABASE_URL"] = "sqlite3:#{database_url_db_name}" # ensure it's using the DATABASE_URL FileUtils.rm_rf("#{app_path}/config/database.yml") end - def db_create_and_drop(expected_database) + def db_create_and_drop(expected_database, environment_loaded: true) Dir.chdir(app_path) do - output = `bin/rake db:create` - assert_empty output + output = rails("db:create") + assert_match(/Created database/, output) assert File.exist?(expected_database) - assert_equal expected_database, ActiveRecord::Base.connection_config[:database] - output = `bin/rake db:drop` - assert_empty output - assert !File.exist?(expected_database) + yield if block_given? + assert_equal expected_database, ActiveRecord::Base.connection_config[:database] if environment_loaded + output = rails("db:drop") + assert_match(/Dropped database/, output) + assert_not File.exist?(expected_database) end end - test 'db:create and db:drop without database url' do + test "db:create and db:drop without database url" do require "#{app_path}/config/environment" - db_create_and_drop ActiveRecord::Base.configurations[Rails.env]['database'] + db_create_and_drop ActiveRecord::Base.configurations[Rails.env]["database"] end - test 'db:create and db:drop with database url' do + test "db:create and db:drop with database url" do require "#{app_path}/config/environment" set_database_url db_create_and_drop database_url_db_name end - def db_migrate_and_status(expected_database) + test "db:create and db:drop respect environment setting" do + app_file "config/database.yml", <<-YAML + development: + database: db/development.sqlite3 + adapter: sqlite3 + YAML + + app_file "config/environments/development.rb", <<-RUBY + Rails.application.configure do + config.read_encrypted_secrets = true + end + RUBY + + app_file "lib/tasks/check_env.rake", <<-RUBY + Rake::Task["db:create"].enhance do + File.write("tmp/config_value", Rails.application.config.read_encrypted_secrets) + end + RUBY + + db_create_and_drop("db/development.sqlite3", environment_loaded: false) do + assert File.exist?("tmp/config_value") + assert_equal "true", File.read("tmp/config_value") + end + end + + def with_database_existing Dir.chdir(app_path) do - `bin/rails generate model book title:string; - bin/rake db:migrate` - output = `bin/rake db:migrate:status` - assert_match(%r{database:\s+\S*#{Regexp.escape(expected_database)}}, output) - assert_match(/up\s+\d{14}\s+Create books/, output) + set_database_url + rails "db:create" + yield + rails "db:drop" + end + end + + test "db:create failure because database exists" do + with_database_existing do + output = rails("db:create") + assert_match(/already exists/, output) + end + end + + def with_bad_permissions + Dir.chdir(app_path) do + set_database_url + FileUtils.chmod("-w", "db") + yield + FileUtils.chmod("+w", "db") + end + end + + test "db:create failure because bad permissions" do + with_bad_permissions do + output = rails("db:create", allow_failure: true) + assert_match("Couldn't create '#{database_url_db_name}' database. Please check your configuration.", output) + assert_equal 1, $?.exitstatus end end - test 'db:migrate and db:migrate:status without database_url' do + test "db:create works when schema cache exists and database does not exist" do + use_postgresql + + begin + rails %w(db:create db:migrate db:schema:cache:dump) + + rails "db:drop" + rails "db:create" + assert_equal 0, $?.exitstatus + ensure + rails "db:drop" rescue nil + end + end + + test "db:drop failure because database does not exist" do + output = rails("db:drop:_unsafe", "--trace") + assert_match(/does not exist/, output) + end + + test "db:drop failure because bad permissions" do + with_database_existing do + with_bad_permissions do + output = rails("db:drop", allow_failure: true) + assert_match(/Couldn't drop/, output) + assert_equal 1, $?.exitstatus + end + end + end + + def db_migrate_and_status(expected_database) + rails "generate", "model", "book", "title:string" + rails "db:migrate" + output = rails("db:migrate:status") + assert_match(%r{database:\s+\S*#{Regexp.escape(expected_database)}}, output) + assert_match(/up\s+\d{14}\s+Create books/, output) + end + + test "db:migrate and db:migrate:status without database_url" do require "#{app_path}/config/environment" - db_migrate_and_status ActiveRecord::Base.configurations[Rails.env]['database'] + db_migrate_and_status ActiveRecord::Base.configurations[Rails.env]["database"] end - test 'db:migrate and db:migrate:status with database_url' do + test "db:migrate and db:migrate:status with database_url" do require "#{app_path}/config/environment" set_database_url db_migrate_and_status database_url_db_name @@ -72,170 +158,176 @@ module ApplicationTests def db_schema_dump Dir.chdir(app_path) do - `bin/rails generate model book title:string; - bin/rake db:migrate db:schema:dump` + rails "generate", "model", "book", "title:string" + rails "db:migrate", "db:schema:dump" schema_dump = File.read("db/schema.rb") assert_match(/create_table \"books\"/, schema_dump) end end - test 'db:schema:dump without database_url' do + test "db:schema:dump without database_url" do db_schema_dump end - test 'db:schema:dump with database_url' do + test "db:schema:dump with database_url" do set_database_url db_schema_dump end def db_fixtures_load(expected_database) Dir.chdir(app_path) do - `bin/rails generate model book title:string; - bin/rake db:migrate db:fixtures:load` + rails "generate", "model", "book", "title:string" + rails "db:migrate", "db:fixtures:load" assert_match expected_database, ActiveRecord::Base.connection_config[:database] require "#{app_path}/app/models/book" assert_equal 2, Book.count end end - test 'db:fixtures:load without database_url' do + test "db:fixtures:load without database_url" do require "#{app_path}/config/environment" - db_fixtures_load ActiveRecord::Base.configurations[Rails.env]['database'] + db_fixtures_load ActiveRecord::Base.configurations[Rails.env]["database"] end - test 'db:fixtures:load with database_url' do + test "db:fixtures:load with database_url" do require "#{app_path}/config/environment" set_database_url db_fixtures_load database_url_db_name end - test 'db:fixtures:load with namespaced fixture' do + test "db:fixtures:load with namespaced fixture" do require "#{app_path}/config/environment" - Dir.chdir(app_path) do - `bin/rails generate model admin::book title:string; - bin/rake db:migrate db:fixtures:load` - require "#{app_path}/app/models/admin/book" - assert_equal 2, Admin::Book.count - end + + rails "generate", "model", "admin::book", "title:string" + rails "db:migrate", "db:fixtures:load" + require "#{app_path}/app/models/admin/book" + assert_equal 2, Admin::Book.count end def db_structure_dump_and_load(expected_database) Dir.chdir(app_path) do - `bin/rails generate model book title:string; - bin/rake db:migrate db:structure:dump` + rails "generate", "model", "book", "title:string" + rails "db:migrate", "db:structure:dump" structure_dump = File.read("db/structure.sql") - assert_match(/CREATE TABLE \"books\"/, structure_dump) - `bin/rake environment db:drop db:structure:load` + assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"books\"/, structure_dump) + rails "environment", "db:drop", "db:structure:load" assert_match expected_database, ActiveRecord::Base.connection_config[:database] require "#{app_path}/app/models/book" - #if structure is not loaded correctly, exception would be raised + # if structure is not loaded correctly, exception would be raised assert_equal 0, Book.count end end - test 'db:structure:dump and db:structure:load without database_url' do + test "db:structure:dump and db:structure:load without database_url" do require "#{app_path}/config/environment" - db_structure_dump_and_load ActiveRecord::Base.configurations[Rails.env]['database'] + db_structure_dump_and_load ActiveRecord::Base.configurations[Rails.env]["database"] end - test 'db:structure:dump and db:structure:load with database_url' do + test "db:structure:dump and db:structure:load with database_url" do require "#{app_path}/config/environment" set_database_url db_structure_dump_and_load database_url_db_name end - test 'db:structure:dump does not dump schema information when no migrations are used' do - Dir.chdir(app_path) do - # create table without migrations - `bin/rails runner 'ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }'` + test "db:structure:dump and db:structure:load set ar_internal_metadata" do + require "#{app_path}/config/environment" + db_structure_dump_and_load ActiveRecord::Base.configurations[Rails.env]["database"] - stderr_output = capture(:stderr) { `bin/rake db:structure:dump` } - assert_empty stderr_output - structure_dump = File.read("db/structure.sql") - assert_match(/CREATE TABLE \"posts\"/, structure_dump) - end + assert_equal "test", rails("runner", "-e", "test", "puts ActiveRecord::InternalMetadata[:environment]").strip + assert_equal "development", rails("runner", "puts ActiveRecord::InternalMetadata[:environment]").strip end - test 'db:schema:load and db:structure:load do not purge the existing database' do - Dir.chdir(app_path) do - `bin/rails runner 'ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }'` + test "db:structure:dump does not dump schema information when no migrations are used" do + # create table without migrations + rails "runner", "ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }" - app_file 'db/schema.rb', <<-RUBY - ActiveRecord::Schema.define(version: 20140423102712) do - create_table(:comments) {} - end - RUBY + stderr_output = capture(:stderr) { rails("db:structure:dump", stderr: true, allow_failure: true) } + assert_empty stderr_output + structure_dump = File.read("#{app_path}/db/structure.sql") + assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"posts\"/, structure_dump) + end - list_tables = lambda { `bin/rails runner 'p ActiveRecord::Base.connection.tables'`.strip } + test "db:schema:load and db:structure:load do not purge the existing database" do + rails "runner", "ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }" - assert_equal '["posts"]', list_tables[] - `bin/rake db:schema:load` - assert_equal '["posts", "comments", "schema_migrations"]', list_tables[] + app_file "db/schema.rb", <<-RUBY + ActiveRecord::Schema.define(version: 20140423102712) do + create_table(:comments) {} + end + RUBY - app_file 'db/structure.sql', <<-SQL - CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255)); - SQL + list_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip } - `bin/rake db:structure:load` - assert_equal '["posts", "comments", "schema_migrations", "users"]', list_tables[] - end + assert_equal '["posts"]', list_tables[] + rails "db:schema:load" + assert_equal '["posts", "comments", "schema_migrations", "ar_internal_metadata"]', list_tables[] + + app_file "db/structure.sql", <<-SQL + CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255)); + SQL + + rails "db:structure:load" + assert_equal '["posts", "comments", "schema_migrations", "ar_internal_metadata", "users"]', list_tables[] end test "db:schema:load with inflections" do - Dir.chdir(app_path) do - app_file 'config/initializers/inflection.rb', <<-RUBY - ActiveSupport::Inflector.inflections do |inflect| - inflect.irregular 'goose', 'geese' - end - RUBY - app_file 'config/initializers/primary_key_table_name.rb', <<-RUBY - ActiveRecord::Base.primary_key_prefix_type = :table_name - RUBY - app_file 'db/schema.rb', <<-RUBY - ActiveRecord::Schema.define(version: 20140423102712) do - create_table("goose".pluralize) do |t| - t.string :name - end + app_file "config/initializers/inflection.rb", <<-RUBY + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular 'goose', 'geese' + end + RUBY + app_file "config/initializers/primary_key_table_name.rb", <<-RUBY + ActiveRecord::Base.primary_key_prefix_type = :table_name + RUBY + app_file "db/schema.rb", <<-RUBY + ActiveRecord::Schema.define(version: 20140423102712) do + create_table("goose".pluralize) do |t| + t.string :name end - RUBY + end + RUBY - `bin/rake db:schema:load` + rails "db:schema:load" - tables = `bin/rails runner 'p ActiveRecord::Base.connection.tables'`.strip - assert_match(/"geese"/, tables) + tables = rails("runner", "p ActiveRecord::Base.connection.tables").strip + assert_match(/"geese"/, tables) - columns = `bin/rails runner 'p ActiveRecord::Base.connection.columns("geese").map(&:name)'`.strip - assert_equal columns, '["gooseid", "name"]' - end + columns = rails("runner", "p ActiveRecord::Base.connection.columns('geese').map(&:name)").strip + assert_equal columns, '["gooseid", "name"]' + end + + test "db:schema:load fails if schema.rb doesn't exist yet" do + stderr_output = capture(:stderr) { rails("db:schema:load", stderr: true, allow_failure: true) } + assert_match(/Run `rails db:migrate` to create it/, stderr_output) end def db_test_load_structure Dir.chdir(app_path) do - `bin/rails generate model book title:string; - bin/rake db:migrate db:structure:dump db:test:load_structure` + rails "generate", "model", "book", "title:string" + rails "db:migrate", "db:structure:dump", "db:test:load_structure" ActiveRecord::Base.configurations = Rails.application.config.database_configuration ActiveRecord::Base.establish_connection :test require "#{app_path}/app/models/book" - #if structure is not loaded correctly, exception would be raised + # if structure is not loaded correctly, exception would be raised assert_equal 0, Book.count - assert_match ActiveRecord::Base.configurations['test']['database'], + assert_match ActiveRecord::Base.configurations["test"]["database"], ActiveRecord::Base.connection_config[:database] end end - test 'db:test:load_structure without database_url' do + test "db:test:load_structure without database_url" do require "#{app_path}/config/environment" db_test_load_structure end - test 'db:setup loads schema and seeds database' do + test "db:setup loads schema and seeds database" do begin @old_rails_env = ENV["RAILS_ENV"] @old_rack_env = ENV["RACK_ENV"] ENV.delete "RAILS_ENV" ENV.delete "RACK_ENV" - app_file 'db/schema.rb', <<-RUBY + app_file "db/schema.rb", <<-RUBY ActiveRecord::Schema.define(version: "1") do create_table :users do |t| t.string :name @@ -243,19 +335,56 @@ module ApplicationTests end RUBY - app_file 'db/seeds.rb', <<-RUBY + app_file "db/seeds.rb", <<-RUBY puts ActiveRecord::Base.connection_config[:database] RUBY - Dir.chdir(app_path) do - database_path = `bin/rake db:setup` - assert_equal "development.sqlite3", File.basename(database_path.strip) - end + database_path = rails("db:setup") + assert_equal "development.sqlite3", File.basename(database_path.strip) ensure ENV["RAILS_ENV"] = @old_rails_env ENV["RACK_ENV"] = @old_rack_env end end + + test "db:setup sets ar_internal_metadata" do + app_file "db/schema.rb", "" + rails "db:setup" + + test_environment = lambda { rails("runner", "-e", "test", "puts ActiveRecord::InternalMetadata[:environment]").strip } + development_environment = lambda { rails("runner", "puts ActiveRecord::InternalMetadata[:environment]").strip } + + assert_equal "test", test_environment.call + assert_equal "development", development_environment.call + + app_file "db/structure.sql", "" + app_file "config/initializers/enable_sql_schema_format.rb", <<-RUBY + Rails.application.config.active_record.schema_format = :sql + RUBY + + rails "db:setup" + + assert_equal "test", test_environment.call + assert_equal "development", development_environment.call + end + + test "db:test:prepare sets test ar_internal_metadata" do + app_file "db/schema.rb", "" + rails "db:test:prepare" + + test_environment = lambda { rails("runner", "-e", "test", "puts ActiveRecord::InternalMetadata[:environment]").strip } + + assert_equal "test", test_environment.call + + app_file "db/structure.sql", "" + app_file "config/initializers/enable_sql_schema_format.rb", <<-RUBY + Rails.application.config.active_record.schema_format = :sql + RUBY + + rails "db:test:prepare" + + assert_equal "test", test_environment.call + end end end end diff --git a/railties/test/application/rake/dev_test.rb b/railties/test/application/rake/dev_test.rb new file mode 100644 index 0000000000..a87f453075 --- /dev/null +++ b/railties/test/application/rake/dev_test.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class RakeDevTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + add_to_env_config("development", "config.active_support.deprecation = :stderr") + end + + def teardown + teardown_app + end + + test "dev:cache creates file and outputs message" do + Dir.chdir(app_path) do + stderr = capture(:stderr) do + output = run_rake_dev_cache + assert File.exist?("tmp/caching-dev.txt") + assert_match(/Development mode is now being cached/, output) + end + assert_match(/DEPRECATION WARNING: Using `bin\/rake dev:cache` is deprecated and will be removed in Rails 6.1/, stderr) + end + end + + test "dev:cache deletes file and outputs message" do + Dir.chdir(app_path) do + stderr = capture(:stderr) do + run_rake_dev_cache # Create caching file. + output = run_rake_dev_cache # Delete caching file. + assert_not File.exist?("tmp/caching-dev.txt") + assert_match(/Development mode is no longer being cached/, output) + end + assert_match(/DEPRECATION WARNING: Using `bin\/rake dev:cache` is deprecated and will be removed in Rails 6.1/, stderr) + end + end + + test "dev:cache touches tmp/restart.txt" do + Dir.chdir(app_path) do + stderr = capture(:stderr) do + run_rake_dev_cache + assert File.exist?("tmp/restart.txt") + + prev_mtime = File.mtime("tmp/restart.txt") + run_rake_dev_cache + curr_mtime = File.mtime("tmp/restart.txt") + assert_not_equal prev_mtime, curr_mtime + end + assert_match(/DEPRECATION WARNING: Using `bin\/rake dev:cache` is deprecated and will be removed in Rails 6.1/, stderr) + end + end + + private + def run_rake_dev_cache + `bin/rake dev:cache` + end + end + end +end diff --git a/railties/test/application/rake/framework_test.rb b/railties/test/application/rake/framework_test.rb index ec57af79f6..644b1924b5 100644 --- a/railties/test/application/rake/framework_test.rb +++ b/railties/test/application/rake/framework_test.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" -require "active_support/core_ext/string/strip" module ApplicationTests module RakeTests @@ -8,7 +9,6 @@ module ApplicationTests def setup build_app - boot_rails FileUtils.rm_rf("#{app_path}/config/environments") end @@ -17,14 +17,14 @@ module ApplicationTests end def load_tasks - require 'rake' - require 'rdoc/task' - require 'rake/testtask' + require "rake" + require "rdoc/task" + require "rake/testtask" Rails.application.load_tasks end - test 'requiring the rake task should not define method .app_generator on Object' do + test "requiring the rake task should not define method .app_generator on Object" do require "#{app_path}/config/environment" load_tasks @@ -34,7 +34,7 @@ module ApplicationTests end end - test 'requiring the rake task should not define method .invoke_from_app_generator on Object' do + test "requiring the rake task should not define method .invoke_from_app_generator on Object" do require "#{app_path}/config/environment" load_tasks diff --git a/railties/test/application/rake/initializers_test.rb b/railties/test/application/rake/initializers_test.rb new file mode 100644 index 0000000000..8de4967021 --- /dev/null +++ b/railties/test/application/rake/initializers_test.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class RakeInitializersTest < ActiveSupport::TestCase + setup :build_app + teardown :teardown_app + + test "`rake initializers` prints out defined initializers invoked by Rails" do + capture(:stderr) do + initial_output = run_rake_initializers + initial_output_length = initial_output.split("\n").length + + assert_operator initial_output_length, :>, 0 + assert_not initial_output.include?("set_added_test_module") + + add_to_config <<-RUBY + initializer(:set_added_test_module) { } + RUBY + + final_output = run_rake_initializers + final_output_length = final_output.split("\n").length + + assert_equal 1, (final_output_length - initial_output_length) + assert final_output.include?("set_added_test_module") + end + end + + test "`rake initializers` outputs a deprecation warning" do + add_to_env_config("development", "config.active_support.deprecation = :stderr") + + stderr = capture(:stderr) { run_rake_initializers } + assert_match(/DEPRECATION WARNING: Using `bin\/rake initializers` is deprecated and will be removed in Rails 6.1/, stderr) + end + + private + def run_rake_initializers + Dir.chdir(app_path) { `bin/rake initializers` } + end + end + end +end diff --git a/railties/test/application/rake/log_test.rb b/railties/test/application/rake/log_test.rb new file mode 100644 index 0000000000..678f26db26 --- /dev/null +++ b/railties/test/application/rake/log_test.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class LogTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test "log:clear clear all environments log files by default" do + Dir.chdir(app_path) do + File.open("config/environments/staging.rb", "w") + + File.write("log/staging.log", "staging") + File.write("log/test.log", "test") + File.write("log/dummy.log", "dummy") + + rails "log:clear" + + assert_equal 0, File.size("log/test.log") + assert_equal 0, File.size("log/staging.log") + assert_equal 5, File.size("log/dummy.log") + end + end + end + end +end diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb index 2d8bd7c571..47c5ac105a 100644 --- a/railties/test/application/rake/migrations_test.rb +++ b/railties/test/application/rake/migrations_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" module ApplicationTests @@ -5,7 +7,6 @@ module ApplicationTests class RakeMigrationsTest < ActiveSupport::TestCase def setup build_app - boot_rails FileUtils.rm_rf("#{app_path}/config/environments") end @@ -13,189 +14,441 @@ module ApplicationTests teardown_app end - test 'running migrations with given scope' do - Dir.chdir(app_path) do - `bin/rails generate model user username:string password:string` + test "running migrations with given scope" do + rails "generate", "model", "user", "username:string", "password:string" - app_file "db/migrate/01_a_migration.bukkits.rb", <<-MIGRATION - class AMigration < ActiveRecord::Migration - end - MIGRATION + app_file "db/migrate/01_a_migration.bukkits.rb", <<-MIGRATION + class AMigration < ActiveRecord::Migration::Current + end + MIGRATION - output = `bin/rake db:migrate SCOPE=bukkits` - assert_no_match(/create_table\(:users\)/, output) - assert_no_match(/CreateUsers/, output) - assert_no_match(/add_column\(:users, :email, :string\)/, output) + output = rails("db:migrate", "SCOPE=bukkits") + assert_no_match(/create_table\(:users\)/, output) + assert_no_match(/CreateUsers/, output) + assert_no_match(/add_column\(:users, :email, :string\)/, output) - assert_match(/AMigration: migrated/, output) + assert_match(/AMigration: migrated/, output) - output = `bin/rake db:migrate SCOPE=bukkits VERSION=0` - assert_no_match(/drop_table\(:users\)/, output) - assert_no_match(/CreateUsers/, output) - assert_no_match(/remove_column\(:users, :email\)/, output) + # run all the migrations to test scope for down + output = rails("db:migrate") + assert_match(/CreateUsers: migrated/, output) - assert_match(/AMigration: reverted/, output) - end + output = rails("db:migrate", "SCOPE=bukkits", "VERSION=0") + assert_no_match(/drop_table\(:users\)/, output) + assert_no_match(/CreateUsers/, output) + assert_no_match(/remove_column\(:users, :email\)/, output) + + assert_match(/AMigration: reverted/, output) + + output = rails("db:migrate", "VERSION=0") + + assert_match(/CreateUsers: reverted/, output) end - test 'model and migration generator with change syntax' do - Dir.chdir(app_path) do - `bin/rails generate model user username:string password:string; - bin/rails generate migration add_email_to_users email:string` - - output = `bin/rake db:migrate` - assert_match(/create_table\(:users\)/, output) - assert_match(/CreateUsers: migrated/, output) - assert_match(/add_column\(:users, :email, :string\)/, output) - assert_match(/AddEmailToUsers: migrated/, output) - - output = `bin/rake db:rollback STEP=2` - assert_match(/drop_table\(:users\)/, output) - assert_match(/CreateUsers: reverted/, output) - assert_match(/remove_column\(:users, :email, :string\)/, output) - assert_match(/AddEmailToUsers: reverted/, output) - end + test "version outputs current version" do + app_file "db/migrate/01_one_migration.rb", <<-MIGRATION + class OneMigration < ActiveRecord::Migration::Current + end + MIGRATION + + rails "db:migrate" + + output = rails("db:version") + assert_match(/Current version: 1/, output) end - test 'migration status when schema migrations table is not present' do - output = Dir.chdir(app_path){ `bin/rake db:migrate:status 2>&1` } + test "migrate with specified VERSION in different formats" do + app_file "db/migrate/01_one_migration.rb", <<-MIGRATION + class OneMigration < ActiveRecord::Migration::Current + end + MIGRATION + + app_file "db/migrate/02_two_migration.rb", <<-MIGRATION + class TwoMigration < ActiveRecord::Migration::Current + end + MIGRATION + + app_file "db/migrate/03_three_migration.rb", <<-MIGRATION + class ThreeMigration < ActiveRecord::Migration::Current + end + MIGRATION + + rails "db:migrate" + + output = rails("db:migrate:status") + assert_match(/up\s+001\s+One migration/, output) + assert_match(/up\s+002\s+Two migration/, output) + assert_match(/up\s+003\s+Three migration/, output) + + rails "db:migrate", "VERSION=01_one_migration.rb" + output = rails("db:migrate:status") + assert_match(/up\s+001\s+One migration/, output) + assert_match(/down\s+002\s+Two migration/, output) + assert_match(/down\s+003\s+Three migration/, output) + + rails "db:migrate", "VERSION=3" + output = rails("db:migrate:status") + assert_match(/up\s+001\s+One migration/, output) + assert_match(/up\s+002\s+Two migration/, output) + assert_match(/up\s+003\s+Three migration/, output) + + rails "db:migrate", "VERSION=001" + output = rails("db:migrate:status") + assert_match(/up\s+001\s+One migration/, output) + assert_match(/down\s+002\s+Two migration/, output) + assert_match(/down\s+003\s+Three migration/, output) + end + + test "migration with empty version" do + app_file "db/migrate/01_one_migration.rb", <<-MIGRATION + class OneMigration < ActiveRecord::Migration::Current + end + MIGRATION + + app_file "db/migrate/02_two_migration.rb", <<-MIGRATION + class TwoMigration < ActiveRecord::Migration::Current + end + MIGRATION + + rails("db:migrate", "VERSION=") + + output = rails("db:migrate:status") + assert_match(/up\s+001\s+One migration/, output) + assert_match(/up\s+002\s+Two migration/, output) + + output = rails("db:migrate:redo", "VERSION=", allow_failure: true) + assert_match(/Empty VERSION provided/, output) + + output = rails("db:migrate:up", "VERSION=", allow_failure: true) + assert_match(/VERSION is required/, output) + + output = rails("db:migrate:up", allow_failure: true) + assert_match(/VERSION is required/, output) + + output = rails("db:migrate:down", "VERSION=", allow_failure: true) + assert_match(/VERSION is required - To go down one migration, use db:rollback/, output) + + output = rails("db:migrate:down", allow_failure: true) + assert_match(/VERSION is required - To go down one migration, use db:rollback/, output) + + output = rails("db:migrate:status") + assert_match(/up\s+001\s+One migration/, output) + assert_match(/up\s+002\s+Two migration/, output) + end + + test "migration with 0 version" do + app_file "db/migrate/01_one_migration.rb", <<-MIGRATION + class OneMigration < ActiveRecord::Migration::Current + end + MIGRATION + + app_file "db/migrate/02_two_migration.rb", <<-MIGRATION + class TwoMigration < ActiveRecord::Migration::Current + end + MIGRATION + + rails "db:migrate" + + output = rails("db:migrate:status") + assert_match(/up\s+001\s+One migration/, output) + assert_match(/up\s+002\s+Two migration/, output) + + rails "db:migrate", "VERSION=0" + + output = rails("db:migrate:status") + assert_match(/down\s+001\s+One migration/, output) + assert_match(/down\s+002\s+Two migration/, output) + end + + test "model and migration generator with change syntax" do + rails "generate", "model", "user", "username:string", "password:string" + rails "generate", "migration", "add_email_to_users", "email:string" + + output = rails("db:migrate") + assert_match(/create_table\(:users\)/, output) + assert_match(/CreateUsers: migrated/, output) + assert_match(/add_column\(:users, :email, :string\)/, output) + assert_match(/AddEmailToUsers: migrated/, output) + + output = rails("db:rollback", "STEP=2") + assert_match(/drop_table\(:users\)/, output) + assert_match(/CreateUsers: reverted/, output) + assert_match(/remove_column\(:users, :email, :string\)/, output) + assert_match(/AddEmailToUsers: reverted/, output) + end + + test "migration status when schema migrations table is not present" do + output = rails("db:migrate:status", allow_failure: true) assert_equal "Schema migrations table does not exist yet.\n", output end - test 'test migration status' do + test "migration status" do + rails "generate", "model", "user", "username:string", "password:string" + rails "generate", "migration", "add_email_to_users", "email:string" + rails "db:migrate" + + output = rails("db:migrate:status") + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + + rails "db:rollback", "STEP=1" + output = rails("db:migrate:status") + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/down\s+\d{14}\s+Add email to users/, output) + end + + test "migration status without timestamps" do + add_to_config("config.active_record.timestamped_migrations = false") + + rails "generate", "model", "user", "username:string", "password:string" + rails "generate", "migration", "add_email_to_users", "email:string" + rails "db:migrate" + + output = rails("db:migrate:status") + + assert_match(/up\s+\d{3,}\s+Create users/, output) + assert_match(/up\s+\d{3,}\s+Add email to users/, output) + + rails "db:rollback", "STEP=1" + output = rails("db:migrate:status") + + assert_match(/up\s+\d{3,}\s+Create users/, output) + assert_match(/down\s+\d{3,}\s+Add email to users/, output) + end + + test "migration status after rollback and redo" do + rails "generate", "model", "user", "username:string", "password:string" + rails "generate", "migration", "add_email_to_users", "email:string" + rails "db:migrate" + + output = rails("db:migrate:status") + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + + rails "db:rollback", "STEP=2" + output = rails("db:migrate:status") + + assert_match(/down\s+\d{14}\s+Create users/, output) + assert_match(/down\s+\d{14}\s+Add email to users/, output) + + rails "db:migrate:redo" + output = rails("db:migrate:status") + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + end + + test "migration status after rollback and forward" do + rails "generate", "model", "user", "username:string", "password:string" + rails "generate", "migration", "add_email_to_users", "email:string" + rails "db:migrate" + + output = rails("db:migrate:status") + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + + rails "db:rollback", "STEP=2" + output = rails("db:migrate:status") + + assert_match(/down\s+\d{14}\s+Create users/, output) + assert_match(/down\s+\d{14}\s+Add email to users/, output) + + rails "db:forward", "STEP=2" + output = rails("db:migrate:status") + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + end + + test "raise error on any move when current migration does not exist" do Dir.chdir(app_path) do - `bin/rails generate model user username:string password:string; - bin/rails generate migration add_email_to_users email:string; - bin/rake db:migrate` + rails "generate", "model", "user", "username:string", "password:string" + rails "generate", "migration", "add_email_to_users", "email:string" + rails "db:migrate" + `rm db/migrate/*email*.rb` - output = `bin/rake db:migrate:status` + output = rails("db:migrate:status") + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output) + + output = rails("db:rollback", allow_failure: true) + assert_match(/rails aborted!/, output) + assert_match(/ActiveRecord::UnknownMigrationVersionError:/, output) + assert_match(/No migration with version number\s\d{14}\./, output) + output = rails("db:migrate:status") assert_match(/up\s+\d{14}\s+Create users/, output) - assert_match(/up\s+\d{14}\s+Add email to users/, output) + assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output) - `bin/rake db:rollback STEP=1` - output = `bin/rake db:migrate:status` + output = rails("db:forward", allow_failure: true) + assert_match(/rails aborted!/, output) + assert_match(/ActiveRecord::UnknownMigrationVersionError:/, output) + assert_match(/No migration with version number\s\d{14}\./, output) + output = rails("db:migrate:status") assert_match(/up\s+\d{14}\s+Create users/, output) - assert_match(/down\s+\d{14}\s+Add email to users/, output) + assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output) end end - test 'migration status without timestamps' do - add_to_config('config.active_record.timestamped_migrations = false') + test "raise error on any move when target migration does not exist" do + app_file "db/migrate/01_one_migration.rb", <<-MIGRATION + class OneMigration < ActiveRecord::Migration::Current + end + MIGRATION - Dir.chdir(app_path) do - `bin/rails generate model user username:string password:string; - bin/rails generate migration add_email_to_users email:string; - bin/rake db:migrate` + app_file "db/migrate/02_two_migration.rb", <<-MIGRATION + class TwoMigration < ActiveRecord::Migration::Current + end + MIGRATION - output = `bin/rake db:migrate:status` + rails "db:migrate" - assert_match(/up\s+\d{3,}\s+Create users/, output) - assert_match(/up\s+\d{3,}\s+Add email to users/, output) + output = rails("db:migrate:status") + assert_match(/up\s+001\s+One migration/, output) + assert_match(/up\s+002\s+Two migration/, output) - `bin/rake db:rollback STEP=1` - output = `bin/rake db:migrate:status` + output = rails("db:migrate", "VERSION=3", allow_failure: true) + assert_match(/rails aborted!/, output) + assert_match(/ActiveRecord::UnknownMigrationVersionError:/, output) + assert_match(/No migration with version number 3/, output) - assert_match(/up\s+\d{3,}\s+Create users/, output) - assert_match(/down\s+\d{3,}\s+Add email to users/, output) - end + output = rails("db:migrate:status") + assert_match(/up\s+001\s+One migration/, output) + assert_match(/up\s+002\s+Two migration/, output) end - test 'test migration status after rollback and redo' do - Dir.chdir(app_path) do - `bin/rails generate model user username:string password:string; - bin/rails generate migration add_email_to_users email:string; - bin/rake db:migrate` + test "raise error on any move when VERSION has invalid format" do + output = rails("db:migrate", "VERSION=unknown", allow_failure: true) + assert_match(/rails aborted!/, output) + assert_match(/Invalid format of target version/, output) - output = `bin/rake db:migrate:status` + output = rails("db:migrate", "VERSION=0.1.11", allow_failure: true) + assert_match(/rails aborted!/, output) + assert_match(/Invalid format of target version/, output) - assert_match(/up\s+\d{14}\s+Create users/, output) - assert_match(/up\s+\d{14}\s+Add email to users/, output) + output = rails("db:migrate", "VERSION=1.1.11", allow_failure: true) + assert_match(/rails aborted!/, output) + assert_match(/Invalid format of target version/, output) - `bin/rake db:rollback STEP=2` - output = `bin/rake db:migrate:status` + output = rails("db:migrate", "VERSION='0 '", allow_failure: true) + assert_match(/rails aborted!/, output) + assert_match(/Invalid format of target version/, output) - assert_match(/down\s+\d{14}\s+Create users/, output) - assert_match(/down\s+\d{14}\s+Add email to users/, output) + output = rails("db:migrate", "VERSION=1.", allow_failure: true) + assert_match(/rails aborted!/, output) + assert_match(/Invalid format of target version/, output) - `bin/rake db:migrate:redo` - output = `bin/rake db:migrate:status` + output = rails("db:migrate", "VERSION=1_", allow_failure: true) + assert_match(/rails aborted!/, output) + assert_match(/Invalid format of target version/, output) - assert_match(/up\s+\d{14}\s+Create users/, output) - assert_match(/up\s+\d{14}\s+Add email to users/, output) - end + output = rails("db:migrate", "VERSION=1_name", allow_failure: true) + assert_match(/rails aborted!/, output) + assert_match(/Invalid format of target version/, output) + + output = rails("db:migrate:redo", "VERSION=unknown", allow_failure: true) + assert_match(/rails aborted!/, output) + assert_match(/Invalid format of target version/, output) + + output = rails("db:migrate:up", "VERSION=unknown", allow_failure: true) + assert_match(/rails aborted!/, output) + assert_match(/Invalid format of target version/, output) + + output = rails("db:migrate:down", "VERSION=unknown", allow_failure: true) + assert_match(/rails aborted!/, output) + assert_match(/Invalid format of target version/, output) end - test 'migration status after rollback and redo without timestamps' do - add_to_config('config.active_record.timestamped_migrations = false') + test "migration status after rollback and redo without timestamps" do + add_to_config("config.active_record.timestamped_migrations = false") - Dir.chdir(app_path) do - `bin/rails generate model user username:string password:string; - bin/rails generate migration add_email_to_users email:string; - bin/rake db:migrate` + rails "generate", "model", "user", "username:string", "password:string" + rails "generate", "migration", "add_email_to_users", "email:string" + rails "db:migrate" - output = `bin/rake db:migrate:status` + output = rails("db:migrate:status") - assert_match(/up\s+\d{3,}\s+Create users/, output) - assert_match(/up\s+\d{3,}\s+Add email to users/, output) + assert_match(/up\s+\d{3,}\s+Create users/, output) + assert_match(/up\s+\d{3,}\s+Add email to users/, output) - `bin/rake db:rollback STEP=2` - output = `bin/rake db:migrate:status` + rails "db:rollback", "STEP=2" + output = rails("db:migrate:status") - assert_match(/down\s+\d{3,}\s+Create users/, output) - assert_match(/down\s+\d{3,}\s+Add email to users/, output) + assert_match(/down\s+\d{3,}\s+Create users/, output) + assert_match(/down\s+\d{3,}\s+Add email to users/, output) - `bin/rake db:migrate:redo` - output = `bin/rake db:migrate:status` + rails "db:migrate:redo" + output = rails("db:migrate:status") - assert_match(/up\s+\d{3,}\s+Create users/, output) - assert_match(/up\s+\d{3,}\s+Add email to users/, output) - end + assert_match(/up\s+\d{3,}\s+Create users/, output) + assert_match(/up\s+\d{3,}\s+Add email to users/, output) + end + + test "running migrations with not timestamp head migration files" do + app_file "db/migrate/1_one_migration.rb", <<-MIGRATION + class OneMigration < ActiveRecord::Migration::Current + end + MIGRATION + + app_file "db/migrate/02_two_migration.rb", <<-MIGRATION + class TwoMigration < ActiveRecord::Migration::Current + end + MIGRATION + + rails "db:migrate" + + output = rails("db:migrate:status") + + assert_match(/up\s+001\s+One migration/, output) + assert_match(/up\s+002\s+Two migration/, output) end - test 'schema generation when dump_schema_after_migration is set' do - add_to_config('config.active_record.dump_schema_after_migration = false') + test "schema generation when dump_schema_after_migration is set" do + add_to_config("config.active_record.dump_schema_after_migration = false") Dir.chdir(app_path) do - `bin/rails generate model book title:string` - output = `bin/rails generate model author name:string` + rails "generate", "model", "book", "title:string" + output = rails("generate", "model", "author", "name:string") version = output =~ %r{[^/]+db/migrate/(\d+)_create_authors\.rb} && $1 - `bin/rake db:migrate db:rollback db:forward db:migrate:up db:migrate:down VERSION=#{version}` - assert !File.exist?("db/schema.rb"), "should not dump schema when configured not to" + rails "db:migrate", "db:rollback", "db:forward", "db:migrate:up", "db:migrate:down", "VERSION=#{version}" + assert_not File.exist?("db/schema.rb"), "should not dump schema when configured not to" end - add_to_config('config.active_record.dump_schema_after_migration = true') + add_to_config("config.active_record.dump_schema_after_migration = true") Dir.chdir(app_path) do - `bin/rails generate model reviews book_id:integer` - `bin/rake db:migrate` + rails "generate", "model", "reviews", "book_id:integer" + rails "db:migrate" structure_dump = File.read("db/schema.rb") assert_match(/create_table "reviews"/, structure_dump) end end - test 'default schema generation after migration' do + test "default schema generation after migration" do Dir.chdir(app_path) do - `bin/rails generate model book title:string; - bin/rake db:migrate` + rails "generate", "model", "book", "title:string" + rails "db:migrate" structure_dump = File.read("db/schema.rb") assert_match(/create_table "books"/, structure_dump) end end - test 'test migration status migrated file is deleted' do + test "migration status migrated file is deleted" do Dir.chdir(app_path) do - `bin/rails generate model user username:string password:string; - bin/rails generate migration add_email_to_users email:string; - bin/rake db:migrate - rm db/migrate/*email*.rb` + rails "generate", "model", "user", "username:string", "password:string" + rails "generate", "migration", "add_email_to_users", "email:string" + rails "db:migrate" + `rm db/migrate/*email*.rb` - output = `bin/rake db:migrate:status` - File.write('test.txt', output) + output = rails("db:migrate:status") assert_match(/up\s+\d{14}\s+Create users/, output) assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output) diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb new file mode 100644 index 0000000000..ef99365e75 --- /dev/null +++ b/railties/test/application/rake/multi_dbs_test.rb @@ -0,0 +1,230 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class RakeMultiDbsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app(multi_db: true) + FileUtils.rm_rf("#{app_path}/config/environments") + end + + def teardown + teardown_app + end + + def db_create_and_drop(namespace, expected_database) + Dir.chdir(app_path) do + output = rails("db:create") + assert_match(/Created database/, output) + assert_match_namespace(namespace, output) + assert_no_match(/already exists/, output) + assert File.exist?(expected_database) + + + output = rails("db:drop") + assert_match(/Dropped database/, output) + assert_match_namespace(namespace, output) + assert_no_match(/does not exist/, output) + assert_not File.exist?(expected_database) + end + end + + def db_create_and_drop_namespace(namespace, expected_database) + Dir.chdir(app_path) do + output = rails("db:create:#{namespace}") + assert_match(/Created database/, output) + assert_match_namespace(namespace, output) + assert File.exist?(expected_database) + + output = rails("db:drop:#{namespace}") + assert_match(/Dropped database/, output) + assert_match_namespace(namespace, output) + assert_not File.exist?(expected_database) + end + end + + def assert_match_namespace(namespace, output) + if namespace == "primary" + assert_match(/#{Rails.env}.sqlite3/, output) + else + assert_match(/#{Rails.env}_#{namespace}.sqlite3/, output) + end + end + + def db_migrate_and_migrate_status + Dir.chdir(app_path) do + generate_models_for_animals + rails "db:migrate" + output = rails "db:migrate:status" + assert_match(/up \d+ Create books/, output) + assert_match(/up \d+ Create dogs/, output) + end + end + + def db_migrate_and_schema_cache_dump + Dir.chdir(app_path) do + generate_models_for_animals + rails "db:migrate" + rails "db:schema:cache:dump" + assert File.exist?("db/schema_cache.yml") + assert File.exist?("db/animals_schema_cache.yml") + end + end + + def db_migrate_and_schema_cache_dump_and_schema_cache_clear + Dir.chdir(app_path) do + generate_models_for_animals + rails "db:migrate" + rails "db:schema:cache:dump" + rails "db:schema:cache:clear" + assert_not File.exist?("db/schema_cache.yml") + assert_not File.exist?("db/animals_schema_cache.yml") + end + end + + def db_migrate_and_schema_dump_and_load(format) + Dir.chdir(app_path) do + generate_models_for_animals + rails "db:migrate", "db:#{format}:dump" + + if format == "schema" + schema_dump = File.read("db/#{format}.rb") + schema_dump_animals = File.read("db/animals_#{format}.rb") + assert_match(/create_table \"books\"/, schema_dump) + assert_match(/create_table \"dogs\"/, schema_dump_animals) + else + schema_dump = File.read("db/#{format}.sql") + schema_dump_animals = File.read("db/animals_#{format}.sql") + assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"books\"/, schema_dump) + assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"dogs\"/, schema_dump_animals) + end + + rails "db:#{format}:load" + + ar_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip } + animals_tables = lambda { rails("runner", "p AnimalsBase.connection.tables").strip } + + assert_equal '["schema_migrations", "ar_internal_metadata", "books"]', ar_tables[] + assert_equal '["schema_migrations", "ar_internal_metadata", "dogs"]', animals_tables[] + end + end + + def db_migrate_namespaced(namespace) + Dir.chdir(app_path) do + generate_models_for_animals + output = rails("db:migrate:#{namespace}") + if namespace == "primary" + assert_match(/CreateBooks: migrated/, output) + else + assert_match(/CreateDogs: migrated/, output) + end + end + end + + def db_migrate_status_namespaced(namespace) + Dir.chdir(app_path) do + generate_models_for_animals + output = rails("db:migrate:status:#{namespace}") + if namespace == "primary" + assert_match(/up \d+ Create books/, output) + else + assert_match(/up \d+ Create dogs/, output) + end + end + end + + def write_models_for_animals + # make a directory for the animals migration + FileUtils.mkdir_p("#{app_path}/db/animals_migrate") + # move the dogs migration if it unless it already lives there + FileUtils.mv(Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first, "db/animals_migrate/") unless Dir.glob("#{app_path}/db/animals_migrate/**/*dogs.rb").first + # delete the dogs migration if it's still present in the + # migrate folder. This is necessary because sometimes + # the code isn't fast enough and an extra migration gets made + FileUtils.rm(Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first) if Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first + + # change the base of the dog model + app_path("/app/models/dog.rb") do |file_name| + file = File.read("#{app_path}/app/models/dog.rb") + file.sub!(/ApplicationRecord/, "AnimalsBase") + File.write(file_name, file) + end + + # create the base model for dog to inherit from + File.open("#{app_path}/app/models/animals_base.rb", "w") do |file| + file.write(<<~EOS) + class AnimalsBase < ActiveRecord::Base + self.abstract_class = true + + establish_connection :animals + end + EOS + end + end + + def generate_models_for_animals + rails "generate", "model", "book", "title:string" + rails "generate", "model", "dog", "name:string" + write_models_for_animals + end + + test "db:create and db:drop works on all databases for env" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + db_create_and_drop db_config.spec_name, db_config.config["database"] + end + end + + test "db:create:namespace and db:drop:namespace works on specified databases" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + db_create_and_drop_namespace db_config.spec_name, db_config.config["database"] + end + end + + test "db:migrate and db:schema:dump and db:schema:load works on all databases" do + require "#{app_path}/config/environment" + db_migrate_and_schema_dump_and_load "schema" + end + + test "db:migrate and db:structure:dump and db:structure:load works on all databases" do + require "#{app_path}/config/environment" + db_migrate_and_schema_dump_and_load "structure" + end + + test "db:migrate:namespace works" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + db_migrate_namespaced db_config.spec_name + end + end + + test "db:migrate:status works on all databases" do + require "#{app_path}/config/environment" + db_migrate_and_migrate_status + end + + test "db:migrate:status:namespace works" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + db_migrate_namespaced db_config.spec_name + db_migrate_status_namespaced db_config.spec_name + end + end + + test "db:schema:cache:dump works on all databases" do + require "#{app_path}/config/environment" + db_migrate_and_schema_cache_dump + end + + test "db:schema:cache:clear works on all databases" do + require "#{app_path}/config/environment" + db_migrate_and_schema_cache_dump_and_schema_cache_clear + end + end + end +end diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb index d6f5fee5c3..60802ef7c4 100644 --- a/railties/test/application/rake/notes_test.rb +++ b/railties/test/application/rake/notes_test.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" -require 'rails/source_annotation_extractor' +require "rails/source_annotation_extractor" module ApplicationTests module RakeTests @@ -8,6 +10,7 @@ module ApplicationTests def setup build_app + add_to_env_config("development", "config.active_support.deprecation = :stderr") require "rails/all" super end @@ -17,33 +20,36 @@ module ApplicationTests teardown_app end - test 'notes finds notes for certain file_types' do + test "notes finds notes for certain file_types" do app_file "app/views/home/index.html.erb", "<% # TODO: note in erb %>" app_file "app/assets/javascripts/application.js", "// TODO: note in js" app_file "app/assets/stylesheets/application.css", "// TODO: note in css" app_file "app/controllers/application_controller.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in ruby" app_file "lib/tasks/task.rake", "# TODO: note in rake" - app_file 'app/views/home/index.html.builder', '# TODO: note in builder' - app_file 'config/locales/en.yml', '# TODO: note in yml' - app_file 'config/locales/en.yaml', '# TODO: note in yaml' + app_file "app/views/home/index.html.builder", "# TODO: note in builder" + app_file "config/locales/en.yml", "# TODO: note in yml" + app_file "config/locales/en.yaml", "# TODO: note in yaml" app_file "app/views/home/index.ruby", "# TODO: note in ruby" - run_rake_notes do |output, lines| - assert_match(/note in erb/, output) - assert_match(/note in js/, output) - assert_match(/note in css/, output) - assert_match(/note in rake/, output) - assert_match(/note in builder/, output) - assert_match(/note in yml/, output) - assert_match(/note in yaml/, output) - assert_match(/note in ruby/, output) - - assert_equal 9, lines.size - assert_equal [4], lines.map(&:size).uniq + stderr = capture(:stderr) do + run_rake_notes do |output, lines| + assert_match(/note in erb/, output) + assert_match(/note in js/, output) + assert_match(/note in css/, output) + assert_match(/note in rake/, output) + assert_match(/note in builder/, output) + assert_match(/note in yml/, output) + assert_match(/note in yaml/, output) + assert_match(/note in ruby/, output) + + assert_equal 9, lines.size + assert_equal [4], lines.map(&:size).uniq + end end + assert_match(/DEPRECATION WARNING: This rake task is deprecated and will be removed in Rails 6.1/, stderr) end - test 'notes finds notes in default directories' do + test "notes finds notes in default directories" do app_file "app/controllers/some_controller.rb", "# TODO: note in app directory" app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory" app_file "db/some_seeds.rb", "# TODO: note in db directory" @@ -52,20 +58,23 @@ module ApplicationTests app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory" - run_rake_notes do |output, lines| - assert_match(/note in app directory/, output) - assert_match(/note in config directory/, output) - assert_match(/note in db directory/, output) - assert_match(/note in lib directory/, output) - assert_match(/note in test directory/, output) - assert_no_match(/note in some_other directory/, output) - - assert_equal 5, lines.size - assert_equal [4], lines.map(&:size).uniq + stderr = capture(:stderr) do + run_rake_notes do |output, lines| + assert_match(/note in app directory/, output) + assert_match(/note in config directory/, output) + assert_match(/note in db directory/, output) + assert_match(/note in lib directory/, output) + assert_match(/note in test directory/, output) + assert_no_match(/note in some_other directory/, output) + + assert_equal 5, lines.size + assert_equal [4], lines.map(&:size).uniq + end end + assert_match(/DEPRECATION WARNING: This rake task is deprecated and will be removed in Rails 6.1/, stderr) end - test 'notes finds notes in custom directories' do + test "notes finds notes in custom directories" do app_file "app/controllers/some_controller.rb", "# TODO: note in app directory" app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory" app_file "db/some_seeds.rb", "# TODO: note in db directory" @@ -74,23 +83,28 @@ module ApplicationTests app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory" - run_rake_notes "SOURCE_ANNOTATION_DIRECTORIES='some_other_dir' bin/rake notes" do |output, lines| - assert_match(/note in app directory/, output) - assert_match(/note in config directory/, output) - assert_match(/note in db directory/, output) - assert_match(/note in lib directory/, output) - assert_match(/note in test directory/, output) + stderr = capture(:stderr) do + run_rake_notes "SOURCE_ANNOTATION_DIRECTORIES='some_other_dir' bin/rake notes" do |output, lines| + assert_match(/note in app directory/, output) + assert_match(/note in config directory/, output) + assert_match(/note in db directory/, output) + assert_match(/note in lib directory/, output) + assert_match(/note in test directory/, output) - assert_match(/note in some_other directory/, output) + assert_match(/note in some_other directory/, output) - assert_equal 6, lines.size - assert_equal [4], lines.map(&:size).uniq + assert_equal 6, lines.size + assert_equal [4], lines.map(&:size).uniq + end end + assert_match(/DEPRECATION WARNING: This rake task is deprecated and will be removed in Rails 6.1/, stderr) + assert_match(/DEPRECATION WARNING: `SOURCE_ANNOTATION_DIRECTORIES` is deprecated and will be removed in Rails 6.1/, stderr) end - test 'custom rake task finds specific notes in specific directories' do + test "custom rake task finds specific notes in specific directories" do app_file "app/controllers/some_controller.rb", "# TODO: note in app directory" - app_file "lib/some_file.rb", "# OPTIMIZE: note in lib directory\n" << "# FIXME: note in lib directory" + app_file "lib/some_file.rb", "# OPTIMIZE: note in lib directory\n" \ + "# FIXME: note in lib directory" app_file "test/some_test.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in test directory" app_file "lib/tasks/notes_custom.rake", <<-EOS @@ -98,11 +112,11 @@ module ApplicationTests task :notes_custom do tags = 'TODO|FIXME' opts = { dirs: %w(lib test), tag: true } - SourceAnnotationExtractor.enumerate(tags, opts) + Rails::SourceAnnotationExtractor.enumerate(tags, opts) end EOS - run_rake_notes "bin/rake notes_custom" do |output, lines| + run_rake_notes "bin/rails notes_custom" do |output, lines| assert_match(/\[FIXME\] note in lib directory/, output) assert_match(/\[TODO\] note in test directory/, output) assert_no_match(/OPTIMIZE/, output) @@ -113,44 +127,48 @@ module ApplicationTests end end - test 'register a new extension' do + test "register a new extension" do + add_to_config "config.assets.precompile = []" add_to_config %q{ config.annotations.register_extensions("scss", "sass") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } } app_file "app/assets/stylesheets/application.css.scss", "// TODO: note in scss" app_file "app/assets/stylesheets/application.css.sass", "// TODO: note in sass" - run_rake_notes do |output, lines| - assert_match(/note in scss/, output) - assert_match(/note in sass/, output) - assert_equal 2, lines.size + stderr = capture(:stderr) do + run_rake_notes do |output, lines| + assert_match(/note in scss/, output) + assert_match(/note in sass/, output) + assert_equal 2, lines.size + end end + assert_match(/DEPRECATION WARNING: This rake task is deprecated and will be removed in Rails 6.1/, stderr) end - private + test "register additional directories" do + app_file "spec/spec_helper.rb", "# TODO: note in spec" + app_file "spec/models/user_spec.rb", "# TODO: note in model spec" + add_to_config ' config.annotations.register_directories("spec") ' - def run_rake_notes(command = 'bin/rake notes') - boot_rails - load_tasks - - Dir.chdir(app_path) do - output = `#{command}` - lines = output.scan(/\[([0-9\s]+)\]\s/).flatten - - yield output, lines + stderr = capture(:stderr) do + run_rake_notes do |output, lines| + assert_match(/note in spec/, output) + assert_match(/note in model spec/, output) + assert_equal 2, lines.size + end end + assert_match(/DEPRECATION WARNING: This rake task is deprecated and will be removed in Rails 6.1/, stderr) end - def load_tasks - require 'rake' - require 'rdoc/task' - require 'rake/testtask' + private - Rails.application.load_tasks - end + def run_rake_notes(command = "bin/rake notes") + Dir.chdir(app_path) do + output = `#{command}` - def boot_rails - super - require "#{app_path}/config/environment" - end + lines = output.scan(/\[([0-9\s]+)\]\s/).flatten + + yield output, lines + end + end end end end diff --git a/railties/test/application/rake/restart_test.rb b/railties/test/application/rake/restart_test.rb index 4cae199e6b..8614560bf2 100644 --- a/railties/test/application/rake/restart_test.rb +++ b/railties/test/application/rake/restart_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" module ApplicationTests @@ -7,31 +9,30 @@ module ApplicationTests def setup build_app - boot_rails end def teardown teardown_app end - test 'rake restart touches tmp/restart.txt' do + test "rails restart touches tmp/restart.txt" do Dir.chdir(app_path) do - `rake restart` + rails "restart" assert File.exist?("tmp/restart.txt") prev_mtime = File.mtime("tmp/restart.txt") sleep(1) - `rake restart` + rails "restart" curr_mtime = File.mtime("tmp/restart.txt") assert_not_equal prev_mtime, curr_mtime end end - test 'rake restart should work even if tmp folder does not exist' do + test "rails restart should work even if tmp folder does not exist" do Dir.chdir(app_path) do - FileUtils.remove_dir('tmp') - `rake restart` - assert File.exist?('tmp/restart.txt') + FileUtils.remove_dir("tmp") + rails "restart" + assert File.exist?("tmp/restart.txt") end end end diff --git a/railties/test/application/rake/routes_test.rb b/railties/test/application/rake/routes_test.rb new file mode 100644 index 0000000000..2c23ff4679 --- /dev/null +++ b/railties/test/application/rake/routes_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class RakeRoutesTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + setup :build_app + teardown :teardown_app + + test "`rake routes` outputs routes" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/cart', to: 'cart#show' + end + RUBY + + assert_equal <<~MESSAGE, run_rake_routes + Prefix Verb URI Pattern Controller#Action + cart GET /cart(.:format) cart#show + rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show +rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show + rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show +update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update + rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create + MESSAGE + end + + test "`rake routes` outputs a deprecation warning" do + add_to_env_config("development", "config.active_support.deprecation = :stderr") + + stderr = capture(:stderr) { run_rake_routes } + assert_match(/DEPRECATION WARNING: Using `bin\/rake routes` is deprecated and will be removed in Rails 6.1/, stderr) + end + + private + def run_rake_routes + Dir.chdir(app_path) { `bin/rake routes` } + end + end + end +end diff --git a/railties/test/application/rake/tmp_test.rb b/railties/test/application/rake/tmp_test.rb new file mode 100644 index 0000000000..048fd7adcc --- /dev/null +++ b/railties/test/application/rake/tmp_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class TmpTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test "tmp:clear clear cache, socket and screenshot files" do + Dir.chdir(app_path) do + FileUtils.mkdir_p("tmp/cache") + FileUtils.touch("tmp/cache/cache_file") + + FileUtils.mkdir_p("tmp/sockets") + FileUtils.touch("tmp/sockets/socket_file") + + FileUtils.mkdir_p("tmp/screenshots") + FileUtils.touch("tmp/screenshots/fail.png") + + rails "tmp:clear" + + assert_not File.exist?("tmp/cache/cache_file") + assert_not File.exist?("tmp/sockets/socket_file") + assert_not File.exist?("tmp/screenshots/fail.png") + end + end + + test "tmp:clear should work if folder missing" do + FileUtils.remove_dir("#{app_path}/tmp") + rails "tmp:clear" + end + end + end +end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index a040dd4cf6..c38b91ef03 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -1,14 +1,14 @@ -# coding:utf-8 +# frozen_string_literal: true + require "isolation/abstract_unit" -require "active_support/core_ext/string/strip" +require "env_helpers" module ApplicationTests class RakeTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Isolation + include ActiveSupport::Testing::Isolation, EnvHelpers def setup build_app - boot_rails end def teardown @@ -25,6 +25,26 @@ module ApplicationTests assert $task_loaded end + test "task is protected when previous migration was production" do + with_rails_env "production" do + rails "generate", "model", "product", "name:string" + rails "db:create", "db:migrate" + output = rails("db:test:prepare", allow_failure: true) + + assert_match(/ActiveRecord::ProtectedEnvironmentError/, output) + end + end + + def test_not_protected_when_previous_migration_was_not_production + with_rails_env "test" do + rails "generate", "model", "product", "name:string" + rails "db:create", "db:migrate" + output = rails("db:test:prepare", "test") + + assert_no_match(/ActiveRecord::ProtectedEnvironmentError/, output) + end + end + def test_environment_is_required_in_rake_tasks app_file "config/environment.rb", <<-RUBY SuperMiddleware = Struct.new(:app) @@ -36,7 +56,7 @@ module ApplicationTests Rails.application.initialize! RUBY - assert_match("SuperMiddleware", Dir.chdir(app_path){ `bin/rake middleware` }) + assert_match("SuperMiddleware", rails("middleware")) end def test_initializers_are_executed_in_rake_tasks @@ -51,7 +71,7 @@ module ApplicationTests end RUBY - output = Dir.chdir(app_path){ `bin/rake do_nothing` } + output = rails("do_nothing") assert_match "Doing something...", output end @@ -64,7 +84,7 @@ module ApplicationTests end RUBY - app_file 'app/models/hello.rb', <<-RUBY + app_file "app/models/hello.rb", <<-RUBY class Hello def world puts 'Hello world' @@ -72,74 +92,34 @@ module ApplicationTests end RUBY - output = Dir.chdir(app_path) { `bin/rake do_nothing` } - assert_match 'Hello world', output + output = rails("do_nothing") + assert_match "Hello world", output end def test_should_not_eager_load_model_for_rake add_to_config <<-RUBY rake_tasks do task do_nothing: :environment do + puts 'There is nothing' end end RUBY - add_to_env_config 'production', <<-RUBY + add_to_env_config "production", <<-RUBY config.eager_load = true RUBY - app_file 'app/models/hello.rb', <<-RUBY + app_file "app/models/hello.rb", <<-RUBY raise 'should not be pre-required for rake even eager_load=true' RUBY - Dir.chdir(app_path) do - assert system('bin/rake do_nothing RAILS_ENV=production'), - 'should not be pre-required for rake even eager_load=true' - end + output = rails("do_nothing", "RAILS_ENV=production") + assert_match "There is nothing", output end def test_code_statistics_sanity - assert_match "Code LOC: 7 Test LOC: 0 Code to Test Ratio: 1:0.0", - Dir.chdir(app_path){ `bin/rake stats` } - end - - def test_rake_routes_calls_the_route_inspector - app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - get '/cart', to: 'cart#show' - end - RUBY - - output = Dir.chdir(app_path){ `bin/rake routes` } - assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output - end - - def test_rake_routes_with_controller_environment - app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - get '/cart', to: 'cart#show' - get '/basketball', to: 'basketball#index' - end - RUBY - - ENV['CONTROLLER'] = 'cart' - output = Dir.chdir(app_path){ `bin/rake routes` } - assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output - end - - def test_rake_routes_displays_message_when_no_routes_are_defined - app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - end - RUBY - - assert_equal <<-MESSAGE.strip_heredoc, Dir.chdir(app_path){ `bin/rake routes` } - You don't have any routes defined! - - Please add some routes in config/routes.rb. - - For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html. - MESSAGE + assert_match "Code LOC: 32 Test LOC: 0 Code to Test Ratio: 1:0.0", + rails("stats") end def test_logger_is_flushed_when_exiting_production_rake_tasks @@ -151,46 +131,39 @@ module ApplicationTests end RUBY - output = Dir.chdir(app_path){ `bin/rake log_something RAILS_ENV=production && cat log/production.log` } - assert_match "Sample log message", output + rails "log_something", "RAILS_ENV=production" + assert_match "Sample log message", File.read("#{app_path}/log/production.log") end def test_loading_specific_fixtures - Dir.chdir(app_path) do - `bin/rails generate model user username:string password:string; - bin/rails generate model product name:string; - bin/rake db:migrate` - end + rails "generate", "model", "user", "username:string", "password:string" + rails "generate", "model", "product", "name:string" + rails "db:migrate" require "#{rails_root}/config/environment" # loading a specific fixture - errormsg = Dir.chdir(app_path) { `bin/rake db:fixtures:load FIXTURES=products` } - assert $?.success?, errormsg + rails "db:fixtures:load", "FIXTURES=products" assert_equal 2, ::AppTemplate::Application::Product.count assert_equal 0, ::AppTemplate::Application::User.count end def test_loading_only_yml_fixtures - Dir.chdir(app_path) do - `bin/rake db:migrate` - end + rails "db:migrate" app_file "test/fixtures/products.csv", "" require "#{rails_root}/config/environment" - errormsg = Dir.chdir(app_path) { `bin/rake db:fixtures:load` } - assert $?.success?, errormsg + rails "db:fixtures:load" end def test_scaffold_tests_pass_by_default - output = Dir.chdir(app_path) do - `bin/rails generate scaffold user username:string password:string; - bin/rake db:migrate test` - end + rails "generate", "scaffold", "user", "username:string", "password:string" + with_rails_env("test") { rails("db:migrate") } + output = rails("test") - assert_match(/7 runs, 12 assertions, 0 failures, 0 errors/, output) + assert_match(/7 runs, 9 assertions, 0 failures, 0 errors/, output) assert_no_match(/Errors running/, output) end @@ -204,93 +177,71 @@ module ApplicationTests end RUBY - output = Dir.chdir(app_path) do - `bin/rails generate scaffold user username:string password:string; - bin/rake db:migrate test` - end + rails "generate", "scaffold", "user", "username:string", "password:string" + with_rails_env("test") { rails("db:migrate") } + output = rails("test") assert_match(/5 runs, 7 assertions, 0 failures, 0 errors/, output) assert_no_match(/Errors running/, output) end - def test_scaffold_with_references_columns_tests_pass_when_belongs_to_is_optional - app_file "config/initializers/active_record_belongs_to_required_by_default.rb", - "Rails.application.config.active_record.belongs_to_required_by_default = false" - - output = Dir.chdir(app_path) do - `bin/rails generate scaffold LineItems product:references cart:belongs_to; - bin/rake db:migrate test` + def test_scaffold_with_references_columns_tests_pass_by_default + rails "generate", "model", "Product" + rails "generate", "model", "Cart" + rails "generate", "scaffold", "LineItems", "product:references", "cart:belongs_to" + with_rails_env("test") do + rails("db:migrate") + rails("webpacker:compile") end + output = rails("test") - assert_match(/7 runs, 12 assertions, 0 failures, 0 errors/, output) + assert_match(/7 runs, 9 assertions, 0 failures, 0 errors/, output) assert_no_match(/Errors running/, output) end - def test_db_test_clone_when_using_sql_format - add_to_config "config.active_record.schema_format = :sql" - output = Dir.chdir(app_path) do - `bin/rails generate scaffold user username:string; - bin/rake db:migrate; - bin/rake db:test:clone 2>&1 --trace` - end - assert_match(/Execute db:test:clone_structure/, output) - end - def test_db_test_prepare_when_using_sql_format add_to_config "config.active_record.schema_format = :sql" - output = Dir.chdir(app_path) do - `bin/rails generate scaffold user username:string; - bin/rake db:migrate; - bin/rake db:test:prepare 2>&1 --trace` - end + rails "generate", "scaffold", "user", "username:string" + rails "db:migrate" + output = rails("db:test:prepare", "--trace") assert_match(/Execute db:test:load_structure/, output) end def test_rake_dump_structure_should_respect_db_structure_env_variable - Dir.chdir(app_path) do - # ensure we have a schema_migrations table to dump - `bin/rake db:migrate db:structure:dump SCHEMA=db/my_structure.sql` - end - assert File.exist?(File.join(app_path, 'db', 'my_structure.sql')) + # ensure we have a schema_migrations table to dump + rails "db:migrate", "db:structure:dump", "SCHEMA=db/my_structure.sql" + assert File.exist?(File.join(app_path, "db", "my_structure.sql")) end def test_rake_dump_structure_should_be_called_twice_when_migrate_redo add_to_config "config.active_record.schema_format = :sql" - output = Dir.chdir(app_path) do - `bin/rails g model post title:string; - bin/rake db:migrate:redo 2>&1 --trace;` - end + rails "g", "model", "post", "title:string" + output = rails("db:migrate:redo", "--trace") # expect only Invoke db:structure:dump (first_time) assert_no_match(/^\*\* Invoke db:structure:dump\s+$/, output) end def test_rake_dump_schema_cache - Dir.chdir(app_path) do - `bin/rails generate model post title:string; - bin/rails generate model product name:string; - bin/rake db:migrate db:schema:cache:dump` - end - assert File.exist?(File.join(app_path, 'db', 'schema_cache.dump')) + rails "generate", "model", "post", "title:string" + rails "generate", "model", "product", "name:string" + rails "db:migrate", "db:schema:cache:dump" + assert File.exist?(File.join(app_path, "db", "schema_cache.yml")) end def test_rake_clear_schema_cache - Dir.chdir(app_path) do - `bin/rake db:schema:cache:dump db:schema:cache:clear` - end - assert !File.exist?(File.join(app_path, 'db', 'schema_cache.dump')) + rails "db:schema:cache:dump", "db:schema:cache:clear" + assert_not File.exist?(File.join(app_path, "db", "schema_cache.yml")) end def test_copy_templates - Dir.chdir(app_path) do - `bin/rake rails:templates:copy` - %w(controller mailer scaffold).each do |dir| - assert File.exist?(File.join(app_path, 'lib', 'templates', 'erb', dir)) - end - %w(controller helper scaffold_controller assets).each do |dir| - assert File.exist?(File.join(app_path, 'lib', 'templates', 'rails', dir)) - end + rails "app:templates:copy" + %w(controller mailer scaffold).each do |dir| + assert File.exist?(File.join(app_path, "lib", "templates", "erb", dir)) + end + %w(controller helper scaffold_controller assets).each do |dir| + assert File.exist?(File.join(app_path, "lib", "templates", "rails", dir)) end end @@ -298,18 +249,8 @@ module ApplicationTests app_file "config/initializers/dummy.rb", "puts 'Hello, World!'" app_file "template.rb", "" - output = Dir.chdir(app_path) do - `bin/rake rails:template LOCATION=template.rb` - end - + output = rails("app:template", "LOCATION=template.rb") assert_match(/Hello, World!/, output) end - - def test_tmp_clear_should_work_if_folder_missing - FileUtils.remove_dir("#{app_path}/tmp") - errormsg = Dir.chdir(app_path) { `bin/rake tmp:clear` } - assert_predicate $?, :success? - assert_empty errormsg - end end end diff --git a/railties/test/application/rendering_test.rb b/railties/test/application/rendering_test.rb index b01febd768..ab1591f388 100644 --- a/railties/test/application/rendering_test.rb +++ b/railties/test/application/rendering_test.rb @@ -1,14 +1,15 @@ -require 'isolation/abstract_unit' -require 'rack/test' +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rack/test" module ApplicationTests - class RoutingTest < ActiveSupport::TestCase + class RenderingTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation include Rack::Test::Methods def setup build_app - boot_rails end def teardown @@ -16,13 +17,13 @@ module ApplicationTests end test "Unknown format falls back to HTML template" do - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'pages/:id', to: 'pages#show' end RUBY - app_file 'app/controllers/pages_controller.rb', <<-RUBY + app_file "app/controllers/pages_controller.rb", <<-RUBY class PagesController < ApplicationController layout false @@ -31,14 +32,14 @@ module ApplicationTests end RUBY - app_file 'app/views/pages/show.html.erb', <<-RUBY + app_file "app/views/pages/show.html.erb", <<-RUBY <%= params[:id] %> RUBY - get '/pages/foo' + get "/pages/foo" assert_equal 200, last_response.status - get '/pages/foo.bar' + get "/pages/foo.bar" assert_equal 200, last_response.status end end diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb index cbada6be97..bec038fb51 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -1,5 +1,7 @@ -require 'isolation/abstract_unit' -require 'rack/test' +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rack/test" module ApplicationTests class RoutingTest < ActiveSupport::TestCase @@ -8,7 +10,6 @@ module ApplicationTests def setup build_app - boot_rails end def teardown @@ -21,6 +22,12 @@ module ApplicationTests assert_equal 200, last_response.status end + test "rails/info in development" do + app("development") + get "/rails/info" + assert_equal 302, last_response.status + end + test "rails/info/routes in development" do app("development") get "/rails/info/routes" @@ -33,28 +40,46 @@ module ApplicationTests assert_equal 200, last_response.status end + test "/rails/info routes are accessible with globbing route present" do + app("development") + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '*foo', to: 'foo#index' + end + RUBY + + get "/rails/info" + assert_equal 302, last_response.status + + get "rails/info/routes" + assert_equal 200, last_response.status + + get "rails/info/properties" + assert_equal 200, last_response.status + end + test "root takes precedence over internal welcome controller" do app("development") - get '/' - assert_match %r{<h1>Getting started</h1>} , last_response.body + assert_welcome get("/") controller :foo, <<-RUBY class FooController < ApplicationController def index - render text: "foo" + render plain: "foo" end end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do root to: "foo#index" end RUBY - get '/' - assert_equal 'foo', last_response.body + get "/" + assert_equal "foo", last_response.body end test "rails/welcome in production" do @@ -63,6 +88,12 @@ module ApplicationTests assert_equal 404, last_response.status end + test "rails/info in production" do + app("production") + get "/rails/info" + assert_equal 404, last_response.status + end + test "rails/info/routes in production" do app("production") get "/rails/info/routes" @@ -78,8 +109,8 @@ module ApplicationTests test "simple controller" do simple_controller - get '/foo' - assert_equal 'foo', last_response.body + get "/foo" + assert_equal "foo", last_response.body end test "simple controller with helper" do @@ -91,7 +122,7 @@ module ApplicationTests end RUBY - app_file 'app/helpers/bar_helper.rb', <<-RUBY + app_file "app/helpers/bar_helper.rb", <<-RUBY module BarHelper def foo_or_bar? "bar" @@ -99,18 +130,18 @@ module ApplicationTests end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller(/:action)' end RUBY - get '/foo' - assert_equal 'bar', last_response.body + get "/foo" + assert_equal "bar", last_response.body end test "mount rack app" do - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do mount lambda { |env| [200, {}, [env["PATH_INFO"]]] }, at: "/blog" # The line below is required because mount sometimes @@ -119,35 +150,35 @@ module ApplicationTests end RUBY - get '/blog/archives' - assert_equal '/archives', last_response.body + get "/blog/archives" + assert_equal "/archives", last_response.body end test "mount named rack app" do controller :foo, <<-RUBY class FooController < ApplicationController def index - render text: my_blog_path + render plain: my_blog_path end end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do mount lambda { |env| [200, {}, [env["PATH_INFO"]]] }, at: "/blog", as: "my_blog" get '/foo' => 'foo#index' end RUBY - get '/foo' - assert_equal '/blog', last_response.body + get "/foo" + assert_equal "/blog", last_response.body end test "multiple controllers" do controller :foo, <<-RUBY class FooController < ApplicationController def index - render text: "foo" + render plain: "foo" end end RUBY @@ -155,59 +186,59 @@ module ApplicationTests controller :bar, <<-RUBY class BarController < ActionController::Base def index - render text: "bar" + render plain: "bar" end end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller(/:action)' end RUBY - get '/foo' - assert_equal 'foo', last_response.body + get "/foo" + assert_equal "foo", last_response.body - get '/bar' - assert_equal 'bar', last_response.body + get "/bar" + assert_equal "bar", last_response.body end test "nested controller" do - controller 'foo', <<-RUBY + controller "foo", <<-RUBY class FooController < ApplicationController def index - render text: "foo" + render plain: "foo" end end RUBY - controller 'admin/foo', <<-RUBY + controller "admin/foo", <<-RUBY module Admin class FooController < ApplicationController def index - render text: "admin::foo" + render plain: "admin::foo" end end end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'admin/foo', to: 'admin/foo#index' get 'foo', to: 'foo#index' end RUBY - get '/foo' - assert_equal 'foo', last_response.body + get "/foo" + assert_equal "foo", last_response.body - get '/admin/foo' - assert_equal 'admin::foo', last_response.body + get "/admin/foo" + assert_equal "admin::foo", last_response.body end test "routes appending blocks" do - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller/:action' end @@ -219,60 +250,108 @@ module ApplicationTests end R - app 'development' + app "development" - get '/win' - assert_equal 'WIN', last_response.body + get "/win" + assert_equal "WIN", last_response.body - app_file 'config/routes.rb', <<-R + app_file "config/routes.rb", <<-R Rails.application.routes.draw do get 'lol' => 'hello#index' end R - get '/win' - assert_equal 'WIN', last_response.body + get "/win" + assert_equal "WIN", last_response.body end - {"development" => "baz", "production" => "bar"}.each do |mode, expected| + { + "development" => ["baz", "http://www.apple.com", "/dashboard"], + "production" => ["bar", "http://www.microsoft.com", "/profile"] + }.each do |mode, (expected_action, expected_url, expected_mapping)| test "reloads routes when configuration is changed in #{mode}" do controller :foo, <<-RUBY class FooController < ApplicationController def bar - render text: "bar" + render plain: "bar" end def baz - render text: "baz" + render plain: "baz" + end + + def custom + render plain: custom_url + end + + def mapping + render plain: url_for(User.new) + end + end + RUBY + + app_file "app/models/user.rb", <<-RUBY + class User + extend ActiveModel::Naming + include ActiveModel::Conversion + + def self.model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, "User") + end + + def persisted? + false end end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#bar' + get 'custom', to: 'foo#custom' + get 'mapping', to: 'foo#mapping' + + direct(:custom) { "http://www.microsoft.com" } + resolve("User") { "/profile" } end RUBY app(mode) - get '/foo' - assert_equal 'bar', last_response.body + get "/foo" + assert_equal "bar", last_response.body + + get "/custom" + assert_equal "http://www.microsoft.com", last_response.body - app_file 'config/routes.rb', <<-RUBY + get "/mapping" + assert_equal "/profile", last_response.body + + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#baz' + get 'custom', to: 'foo#custom' + get 'mapping', to: 'foo#mapping' + + direct(:custom) { "http://www.apple.com" } + resolve("User") { "/dashboard" } end RUBY sleep 0.1 - get '/foo' - assert_equal expected, last_response.body + get "/foo" + assert_equal expected_action, last_response.body + + get "/custom" + assert_equal expected_url, last_response.body + + get "/mapping" + assert_equal expected_mapping, last_response.body end end - test 'routes are loaded just after initialization' do + test "routes are loaded just after initialization" do require "#{app_path}/config/application" # Create the rack app just inside after initialize callback @@ -280,17 +359,17 @@ module ApplicationTests ::InitializeRackApp = lambda { |env| [200, {}, ["InitializeRackApp"]] } end - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: ::InitializeRackApp end RUBY - get '/foo' + get "/foo" assert_equal "InitializeRackApp", last_response.body end - test 'reload_routes! is part of Rails.application API' do + test "reload_routes! is part of Rails.application API" do app("development") assert_nothing_raised do Rails.application.reload_routes! @@ -298,36 +377,44 @@ module ApplicationTests end def test_root_path - app('development') + app("development") controller :foo, <<-RUBY class FooController < ApplicationController def index - render :text => "foo" + render plain: "foo" end end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', :to => 'foo#index' root :to => 'foo#index' end RUBY - remove_file 'public/index.html' + remove_file "public/index.html" - get '/' - assert_equal 'foo', last_response.body + get "/" + assert_equal "foo", last_response.body end - test 'routes are added and removed when reloading' do - app('development') + test "routes are added and removed when reloading" do + app("development") controller :foo, <<-RUBY class FooController < ApplicationController def index - render text: "foo" + render plain: "foo" + end + + def custom + render plain: custom_url + end + + def mapping + render plain: url_for(User.new) end end RUBY @@ -335,45 +422,74 @@ module ApplicationTests controller :bar, <<-RUBY class BarController < ApplicationController def index - render text: "bar" + render plain: "bar" end end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "app/models/user.rb", <<-RUBY + class User + extend ActiveModel::Naming + include ActiveModel::Conversion + + def self.model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, "User") + end + + def persisted? + false + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#index' end RUBY - get '/foo' - assert_equal 'foo', last_response.body - assert_equal '/foo', Rails.application.routes.url_helpers.foo_path + get "/foo" + assert_equal "foo", last_response.body + assert_equal "/foo", Rails.application.routes.url_helpers.foo_path - get '/bar' + get "/bar" assert_equal 404, last_response.status assert_raises NoMethodError do - assert_equal '/bar', Rails.application.routes.url_helpers.bar_path + assert_equal "/bar", Rails.application.routes.url_helpers.bar_path end - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#index' get 'bar', to: 'bar#index' + + get 'custom', to: 'foo#custom' + direct(:custom) { 'http://www.apple.com' } + + get 'mapping', to: 'foo#mapping' + resolve('User') { '/profile' } end RUBY Rails.application.reload_routes! - get '/foo' - assert_equal 'foo', last_response.body - assert_equal '/foo', Rails.application.routes.url_helpers.foo_path + get "/foo" + assert_equal "foo", last_response.body + assert_equal "/foo", Rails.application.routes.url_helpers.foo_path - get '/bar' - assert_equal 'bar', last_response.body - assert_equal '/bar', Rails.application.routes.url_helpers.bar_path + get "/bar" + assert_equal "bar", last_response.body + assert_equal "/bar", Rails.application.routes.url_helpers.bar_path - app_file 'config/routes.rb', <<-RUBY + get "/custom" + assert_equal "http://www.apple.com", last_response.body + assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.custom_url + + get "/mapping" + assert_equal "/profile", last_response.body + assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new) + + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#index' end @@ -381,24 +497,36 @@ module ApplicationTests Rails.application.reload_routes! - get '/foo' - assert_equal 'foo', last_response.body - assert_equal '/foo', Rails.application.routes.url_helpers.foo_path + get "/foo" + assert_equal "foo", last_response.body + assert_equal "/foo", Rails.application.routes.url_helpers.foo_path + + get "/bar" + assert_equal 404, last_response.status + assert_raises NoMethodError do + assert_equal "/bar", Rails.application.routes.url_helpers.bar_path + end + + get "/custom" + assert_equal 404, last_response.status + assert_raises NoMethodError do + assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.custom_url + end - get '/bar' + get "/mapping" assert_equal 404, last_response.status assert_raises NoMethodError do - assert_equal '/bar', Rails.application.routes.url_helpers.bar_path + assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new) end end - test 'named routes are cleared when reloading' do - app('development') + test "named routes are cleared when reloading" do + app("development") controller :foo, <<-RUBY class FooController < ApplicationController def index - render text: "foo" + render plain: "foo" end end RUBY @@ -406,63 +534,149 @@ module ApplicationTests controller :bar, <<-RUBY class BarController < ApplicationController def index - render text: "bar" + render plain: "bar" end end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "app/models/user.rb", <<-RUBY + class User + extend ActiveModel::Naming + include ActiveModel::Conversion + + def self.model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, "User") + end + + def persisted? + false + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':locale/foo', to: 'foo#index', as: 'foo' + get 'users', to: 'foo#users', as: 'users' + direct(:microsoft) { 'http://www.microsoft.com' } + resolve('User') { '/profile' } end RUBY - get '/en/foo' - assert_equal 'foo', last_response.body - assert_equal '/en/foo', Rails.application.routes.url_helpers.foo_path(:locale => 'en') + get "/en/foo" + assert_equal "foo", last_response.body + assert_equal "/en/foo", Rails.application.routes.url_helpers.foo_path(locale: "en") + assert_equal "http://www.microsoft.com", Rails.application.routes.url_helpers.microsoft_url + assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new) - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':locale/bar', to: 'bar#index', as: 'foo' + get 'users', to: 'foo#users', as: 'users' + direct(:apple) { 'http://www.apple.com' } end RUBY Rails.application.reload_routes! - get '/en/foo' + get "/en/foo" assert_equal 404, last_response.status - get '/en/bar' - assert_equal 'bar', last_response.body - assert_equal '/en/bar', Rails.application.routes.url_helpers.foo_path(:locale => 'en') + get "/en/bar" + assert_equal "bar", last_response.body + assert_equal "/en/bar", Rails.application.routes.url_helpers.foo_path(locale: "en") + assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.apple_url + assert_equal "/users", Rails.application.routes.url_helpers.polymorphic_path(User.new) + + assert_raises NoMethodError do + assert_equal "http://www.microsoft.com", Rails.application.routes.url_helpers.microsoft_url + end end - test 'resource routing with irregular inflection' do - app_file 'config/initializers/inflection.rb', <<-RUBY + test "resource routing with irregular inflection" do + app_file "config/initializers/inflection.rb", <<-RUBY ActiveSupport::Inflector.inflections do |inflect| inflect.irregular 'yazi', 'yazilar' end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do resources :yazilar end RUBY - controller 'yazilar', <<-RUBY + controller "yazilar", <<-RUBY class YazilarController < ApplicationController def index - render text: 'yazilar#index' + render plain: 'yazilar#index' end end RUBY - get '/yazilars' + get "/yazilars" assert_equal 404, last_response.status - get '/yazilar' + get "/yazilar" assert_equal 200, last_response.status end + + test "reloading routes removes methods and doesn't undefine them" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/url', to: 'url#index' + end + RUBY + + app_file "app/models/url_helpers.rb", <<-RUBY + module UrlHelpers + def foo_path + "/foo" + end + end + RUBY + + app_file "app/models/context.rb", <<-RUBY + class Context + include UrlHelpers + include Rails.application.routes.url_helpers + end + RUBY + + controller "url", <<-RUBY + class UrlController < ApplicationController + def index + context = Context.new + render plain: context.foo_path + end + end + RUBY + + get "/url" + assert_equal "/foo", last_response.body + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/url', to: 'url#index' + get '/bar', to: 'foo#index', as: 'foo' + end + RUBY + + Rails.application.reload_routes! + + get "/url" + assert_equal "/bar", last_response.body + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/url', to: 'url#index' + end + RUBY + + Rails.application.reload_routes! + + get "/url" + assert_equal "/foo", last_response.body + end end end diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb index 0c180339b4..8f5f48c281 100644 --- a/railties/test/application/runner_test.rb +++ b/railties/test/application/runner_test.rb @@ -1,5 +1,7 @@ -require 'isolation/abstract_unit' -require 'env_helpers' +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "env_helpers" module ApplicationTests class RunnerTest < ActiveSupport::TestCase @@ -8,7 +10,6 @@ module ApplicationTests def setup build_app - boot_rails # Lets create a model so we have something to play with app_file "app/models/user.rb", <<-MODEL @@ -25,15 +26,20 @@ module ApplicationTests end def test_should_include_runner_in_shebang_line_in_help_without_option - assert_match "/rails runner", Dir.chdir(app_path) { `bin/rails runner` } + assert_match "/rails runner", rails("runner", allow_failure: true) end def test_should_include_runner_in_shebang_line_in_help - assert_match "/rails runner", Dir.chdir(app_path) { `bin/rails runner --help` } + assert_match "/rails runner", rails("runner", "--help") end def test_should_run_ruby_statement - assert_match "42", Dir.chdir(app_path) { `bin/rails runner "puts User.count"` } + assert_match "42", rails("runner", "puts User.count") + end + + def test_should_set_argv_when_running_code + output = rails("runner", "puts ARGV.join(',')", "--foo", "a1", "-b", "a2", "a3", "--moo") + assert_equal "--foo,a1,-b,a2,a3,--moo", output.chomp end def test_should_run_file @@ -41,7 +47,16 @@ module ApplicationTests puts User.count SCRIPT - assert_match "42", Dir.chdir(app_path) { `bin/rails runner "bin/count_users.rb"` } + assert_match "42", rails("runner", "bin/count_users.rb") + end + + def test_no_minitest_loaded_in_production_mode + app_file "bin/print_features.rb", <<-SCRIPT + p $LOADED_FEATURES.grep(/minitest/) + SCRIPT + assert_match "[]", Dir.chdir(app_path) { + `RAILS_ENV=production bin/rails runner "bin/print_features.rb"` + } end def test_should_set_dollar_0_to_file @@ -49,7 +64,7 @@ module ApplicationTests puts $0 SCRIPT - assert_match "bin/dollar0.rb", Dir.chdir(app_path) { `bin/rails runner "bin/dollar0.rb"` } + assert_match "bin/dollar0.rb", rails("runner", "bin/dollar0.rb") end def test_should_set_dollar_program_name_to_file @@ -57,7 +72,23 @@ module ApplicationTests puts $PROGRAM_NAME SCRIPT - assert_match "bin/program_name.rb", Dir.chdir(app_path) { `bin/rails runner "bin/program_name.rb"` } + assert_match "bin/program_name.rb", rails("runner", "bin/program_name.rb") + end + + def test_passes_extra_args_to_file + app_file "bin/program_name.rb", <<-SCRIPT + p ARGV + SCRIPT + + assert_match %w( a b ).to_s, rails("runner", "bin/program_name.rb", "a", "b") + end + + def test_should_run_stdin + app_file "bin/count_users.rb", <<-SCRIPT + puts User.count + SCRIPT + + assert_match "42", Dir.chdir(app_path) { `cat bin/count_users.rb | bin/rails runner -` } end def test_with_hook @@ -67,23 +98,47 @@ module ApplicationTests end RUBY - assert_match "true", Dir.chdir(app_path) { `bin/rails runner "puts Rails.application.config.ran"` } + assert_match "true", rails("runner", "puts Rails.application.config.ran") end def test_default_environment - assert_match "development", Dir.chdir(app_path) { `bin/rails runner "puts Rails.env"` } + assert_match "development", rails("runner", "puts Rails.env") + end + + def test_runner_detects_syntax_errors + output = rails("runner", "puts 'hello world", allow_failure: true) + assert_not_predicate $?, :success? + assert_match "unterminated string meets end of file", output + end + + def test_runner_detects_bad_script_name + output = rails("runner", "iuiqwiourowe", allow_failure: true) + assert_not_predicate $?, :success? + assert_match "undefined local variable or method `iuiqwiourowe' for", output end def test_environment_with_rails_env with_rails_env "production" do - assert_match "production", Dir.chdir(app_path) { `bin/rails runner "puts Rails.env"` } + assert_match "production", rails("runner", "puts Rails.env") end end def test_environment_with_rack_env with_rack_env "production" do - assert_match "production", Dir.chdir(app_path) { `bin/rails runner "puts Rails.env"` } + assert_match "production", rails("runner", "puts Rails.env") + end + end + + def test_can_call_same_name_class_as_defined_in_thor + app_file "app/models/task.rb", <<-MODEL + class Task + def self.count + 42 + end end + MODEL + + assert_match "42", rails("runner", "puts Task.count") end end end diff --git a/railties/test/application/server_test.rb b/railties/test/application/server_test.rb new file mode 100644 index 0000000000..ab9e910aed --- /dev/null +++ b/railties/test/application/server_test.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "console_helpers" +require "rails/command" +require "rails/commands/server/server_command" + +module ApplicationTests + class ServerTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + include ConsoleHelpers + + def setup + build_app + end + + def teardown + teardown_app + end + + test "deprecate support of older `config.ru`" do + remove_file "config.ru" + app_file "config.ru", <<-RUBY + require_relative 'config/environment' + run AppTemplate::Application + RUBY + + server = Rails::Server.new(config: "#{app_path}/config.ru") + server.app + + log = File.read(Rails.application.config.paths["log"].first) + assert_match(/DEPRECATION WARNING: Using `Rails::Application` subclass to start the server is deprecated/, log) + end + + test "restart rails server with custom pid file path" do + skip "PTY unavailable" unless available_pty? + + File.open("#{app_path}/config/boot.rb", "w") do |f| + f.puts "ENV['BUNDLE_GEMFILE'] = '#{Bundler.default_gemfile}'" + f.puts "require 'bundler/setup'" + end + + primary, replica = PTY.open + pid = nil + + begin + pid = Process.spawn("#{app_path}/bin/rails server -P tmp/dummy.pid", in: replica, out: replica, err: replica) + assert_output("Listening", primary) + + rails("restart") + + assert_output("Restarting", primary) + assert_output("Inherited", primary) + ensure + kill(pid) if pid + end + end + + private + def kill(pid) + Process.kill("TERM", pid) + Process.wait(pid) + rescue Errno::ESRCH + end + end +end diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index 494e6dd7bd..140703e118 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -1,6 +1,7 @@ -require 'isolation/abstract_unit' -require 'active_support/core_ext/string/strip' -require 'env_helpers' +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "env_helpers" module ApplicationTests class TestRunnerTest < ActiveSupport::TestCase @@ -15,32 +16,56 @@ module ApplicationTests teardown_app end + def test_run_via_backwardscompatibility + require "minitest/rails_plugin" + + assert_nothing_raised do + Minitest.run_via[:ruby] = true + end + + assert Minitest.run_via[:ruby] + end + def test_run_single_file - create_test_file :models, 'foo' - create_test_file :models, 'bar' + create_test_file :models, "foo" + create_test_file :models, "bar" assert_match "1 runs, 1 assertions, 0 failures", run_test_command("test/models/foo_test.rb") end + def test_run_single_file_with_absolute_path + create_test_file :models, "foo" + create_test_file :models, "bar" + assert_match "1 runs, 1 assertions, 0 failures", run_test_command("#{app_path}/test/models/foo_test.rb") + end + def test_run_multiple_files - create_test_file :models, 'foo' - create_test_file :models, 'bar' + create_test_file :models, "foo" + create_test_file :models, "bar" assert_match "2 runs, 2 assertions, 0 failures", run_test_command("test/models/foo_test.rb test/models/bar_test.rb") end + def test_run_multiple_files_with_absolute_paths + create_test_file :models, "foo" + create_test_file :controllers, "foobar_controller" + create_test_file :models, "bar" + + assert_match "2 runs, 2 assertions, 0 failures", run_test_command("#{app_path}/test/models/foo_test.rb #{app_path}/test/controllers/foobar_controller_test.rb") + end + def test_run_file_with_syntax_error - app_file 'test/models/error_test.rb', <<-RUBY + app_file "test/models/error_test.rb", <<-RUBY require 'test_helper' def; end RUBY - error = capture(:stderr) { run_test_command('test/models/error_test.rb') } + error = capture(:stderr) { run_test_command("test/models/error_test.rb", stderr: true) } assert_match "syntax error", error end def test_run_models - create_test_file :models, 'foo' - create_test_file :models, 'bar' - create_test_file :controllers, 'foobar_controller' + create_test_file :models, "foo" + create_test_file :models, "bar" + create_test_file :controllers, "foobar_controller" run_test_command("test/models").tap do |output| assert_match "FooTest", output assert_match "BarTest", output @@ -49,9 +74,9 @@ module ApplicationTests end def test_run_helpers - create_test_file :helpers, 'foo_helper' - create_test_file :helpers, 'bar_helper' - create_test_file :controllers, 'foobar_controller' + create_test_file :helpers, "foo_helper" + create_test_file :helpers, "bar_helper" + create_test_file :controllers, "foobar_controller" run_test_command("test/helpers").tap do |output| assert_match "FooHelperTest", output assert_match "BarHelperTest", output @@ -60,12 +85,12 @@ module ApplicationTests end def test_run_units - skip "we no longer have the concept of unit tests. Just different directories..." - create_test_file :models, 'foo' - create_test_file :helpers, 'bar_helper' - create_test_file :unit, 'baz_unit' - create_test_file :controllers, 'foobar_controller' - run_test_units_command.tap do |output| + create_test_file :models, "foo" + create_test_file :helpers, "bar_helper" + create_test_file :unit, "baz_unit" + create_test_file :controllers, "foobar_controller" + + rails("test:units").tap do |output| assert_match "FooTest", output assert_match "BarHelperTest", output assert_match "BazUnitTest", output @@ -74,9 +99,9 @@ module ApplicationTests end def test_run_controllers - create_test_file :controllers, 'foo_controller' - create_test_file :controllers, 'bar_controller' - create_test_file :models, 'foo' + create_test_file :controllers, "foo_controller" + create_test_file :controllers, "bar_controller" + create_test_file :models, "foo" run_test_command("test/controllers").tap do |output| assert_match "FooControllerTest", output assert_match "BarControllerTest", output @@ -85,9 +110,9 @@ module ApplicationTests end def test_run_mailers - create_test_file :mailers, 'foo_mailer' - create_test_file :mailers, 'bar_mailer' - create_test_file :models, 'foo' + create_test_file :mailers, "foo_mailer" + create_test_file :mailers, "bar_mailer" + create_test_file :models, "foo" run_test_command("test/mailers").tap do |output| assert_match "FooMailerTest", output assert_match "BarMailerTest", output @@ -96,9 +121,9 @@ module ApplicationTests end def test_run_jobs - create_test_file :jobs, 'foo_job' - create_test_file :jobs, 'bar_job' - create_test_file :models, 'foo' + create_test_file :jobs, "foo_job" + create_test_file :jobs, "bar_job" + create_test_file :models, "foo" run_test_command("test/jobs").tap do |output| assert_match "FooJobTest", output assert_match "BarJobTest", output @@ -107,12 +132,12 @@ module ApplicationTests end def test_run_functionals - skip "we no longer have the concept of functional tests. Just different directories..." - create_test_file :mailers, 'foo_mailer' - create_test_file :controllers, 'bar_controller' - create_test_file :functional, 'baz_functional' - create_test_file :models, 'foo' - run_test_functionals_command.tap do |output| + create_test_file :mailers, "foo_mailer" + create_test_file :controllers, "bar_controller" + create_test_file :functional, "baz_functional" + create_test_file :models, "foo" + + rails("test:functionals").tap do |output| assert_match "FooMailerTest", output assert_match "BarControllerTest", output assert_match "BazFunctionalTest", output @@ -121,8 +146,8 @@ module ApplicationTests end def test_run_integration - create_test_file :integration, 'foo_integration' - create_test_file :models, 'foo' + create_test_file :integration, "foo_integration" + create_test_file :models, "foo" run_test_command("test/integration").tap do |output| assert_match "FooIntegration", output assert_match "1 runs, 1 assertions, 0 failures", output @@ -132,14 +157,14 @@ module ApplicationTests def test_run_all_suites suites = [:models, :helpers, :unit, :controllers, :mailers, :functional, :integration, :jobs] suites.each { |suite| create_test_file suite, "foo_#{suite}" } - run_test_command('') .tap do |output| + run_test_command("") .tap do |output| suites.each { |suite| assert_match "Foo#{suite.to_s.camelize}Test", output } assert_match "8 runs, 8 assertions, 0 failures", output end end def test_run_named_test - app_file 'test/unit/chu_2_koi_test.rb', <<-RUBY + app_file "test/unit/chu_2_koi_test.rb", <<-RUBY require 'test_helper' class Chu2KoiTest < ActiveSupport::TestCase @@ -153,14 +178,14 @@ module ApplicationTests end RUBY - run_test_command('-n test_rikka test/unit/chu_2_koi_test.rb').tap do |output| + run_test_command("-n test_rikka test/unit/chu_2_koi_test.rb").tap do |output| assert_match "Rikka", output assert_no_match "Sanae", output end end def test_run_matched_test - app_file 'test/unit/chu_2_koi_test.rb', <<-RUBY + app_file "test/unit/chu_2_koi_test.rb", <<-RUBY require 'test_helper' class Chu2KoiTest < ActiveSupport::TestCase @@ -174,7 +199,7 @@ module ApplicationTests end RUBY - run_test_command('-n /rikka/ test/unit/chu_2_koi_test.rb').tap do |output| + run_test_command("-n /rikka/ test/unit/chu_2_koi_test.rb").tap do |output| assert_match "Rikka", output assert_no_match "Sanae", output end @@ -195,14 +220,14 @@ module ApplicationTests def test_run_with_model skip "These feel a bit odd. Not sure we should keep supporting them." create_model_with_fixture - create_fixture_test 'models', 'user' + create_fixture_test "models", "user" assert_match "3 users", run_task(["test models/user"]) assert_match "3 users", run_task(["test app/models/user.rb"]) end def test_run_different_environment_using_env_var skip "no longer possible. Running tests in a different environment should be explicit" - app_file 'test/unit/env_test.rb', <<-RUBY + app_file "test/unit/env_test.rb", <<-RUBY require 'test_helper' class EnvTest < ActiveSupport::TestCase @@ -212,14 +237,14 @@ module ApplicationTests end RUBY - ENV['RAILS_ENV'] = 'development' - assert_match "development", run_test_command('test/unit/env_test.rb') + ENV["RAILS_ENV"] = "development" + assert_match "development", run_test_command("test/unit/env_test.rb") end def test_run_in_test_environment_by_default create_env_test - assert_match "Current Environment: test", run_test_command('test/unit/env_test.rb') + assert_match "Current Environment: test", run_test_command("test/unit/env_test.rb") end def test_run_different_environment @@ -231,22 +256,39 @@ module ApplicationTests def test_generated_scaffold_works_with_rails_test create_scaffold - assert_match "0 failures, 0 errors, 0 skips", run_test_command('') + assert_match "0 failures, 0 errors, 0 skips", run_test_command("") + end + + def test_generated_controller_works_with_rails_test + create_controller + assert_match "0 failures, 0 errors, 0 skips", run_test_command("") end def test_run_multiple_folders - create_test_file :models, 'account' - create_test_file :controllers, 'accounts_controller' + create_test_file :models, "account" + create_test_file :controllers, "accounts_controller" + + run_test_command("test/models test/controllers").tap do |output| + assert_match "AccountTest", output + assert_match "AccountsControllerTest", output + assert_match "2 runs, 2 assertions, 0 failures, 0 errors, 0 skips", output + end + end - run_test_command('test/models test/controllers').tap do |output| - assert_match 'AccountTest', output - assert_match 'AccountsControllerTest', output - assert_match '2 runs, 2 assertions, 0 failures, 0 errors, 0 skips', output + def test_run_multiple_folders_with_absolute_paths + create_test_file :models, "account" + create_test_file :controllers, "accounts_controller" + create_test_file :helpers, "foo_helper" + + run_test_command("#{app_path}/test/models #{app_path}/test/controllers").tap do |output| + assert_match "AccountTest", output + assert_match "AccountsControllerTest", output + assert_match "2 runs, 2 assertions, 0 failures, 0 errors, 0 skips", output end end def test_run_with_ruby_command - app_file 'test/models/post_test.rb', <<-RUBY + app_file "test/models/post_test.rb", <<-RUBY require 'test_helper' class PostTest < ActiveSupport::TestCase @@ -259,15 +301,15 @@ module ApplicationTests Dir.chdir(app_path) do `ruby -Itest test/models/post_test.rb`.tap do |output| - assert_match 'PostTest', output - assert_no_match 'is already defined in', output + assert_match "PostTest", output + assert_no_match "is already defined in", output end end end def test_mix_files_and_line_filters - create_test_file :models, 'account' - app_file 'test/models/post_test.rb', <<-RUBY + create_test_file :models, "account" + app_file "test/models/post_test.rb", <<-RUBY require 'test_helper' class PostTest < ActiveSupport::TestCase @@ -282,73 +324,503 @@ module ApplicationTests end RUBY - run_test_command('test/models/account_test.rb test/models/post_test.rb:4').tap do |output| - assert_match 'AccountTest', output - assert_match 'PostTest', output - assert_match '2 runs, 2 assertions', output + run_test_command("test/models/account_test.rb test/models/post_test.rb:4").tap do |output| + assert_match "AccountTest", output + assert_match "PostTest", output + assert_match "2 runs, 2 assertions", output + end + end + + def test_more_than_one_line_filter + app_file "test/models/post_test.rb", <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + test "first filter" do + puts 'PostTest:FirstFilter' + assert true + end + + test "second filter" do + puts 'PostTest:SecondFilter' + assert true + end + + test "line filter does not run this" do + assert true + end + end + RUBY + + run_test_command("test/models/post_test.rb:4:9").tap do |output| + assert_match "PostTest:FirstFilter", output + assert_match "PostTest:SecondFilter", output + assert_match "2 runs, 2 assertions", output + end + end + + def test_more_than_one_line_filter_with_multiple_files + app_file "test/models/account_test.rb", <<-RUBY + require 'test_helper' + + class AccountTest < ActiveSupport::TestCase + test "first filter" do + puts 'AccountTest:FirstFilter' + assert true + end + + test "second filter" do + puts 'AccountTest:SecondFilter' + assert true + end + + test "line filter does not run this" do + assert true + end + end + RUBY + + app_file "test/models/post_test.rb", <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + test "first filter" do + puts 'PostTest:FirstFilter' + assert true + end + + test "second filter" do + puts 'PostTest:SecondFilter' + assert true + end + + test "line filter does not run this" do + assert true + end + end + RUBY + + run_test_command("test/models/account_test.rb:4:9 test/models/post_test.rb:4:9").tap do |output| + assert_match "AccountTest:FirstFilter", output + assert_match "AccountTest:SecondFilter", output + assert_match "PostTest:FirstFilter", output + assert_match "PostTest:SecondFilter", output + assert_match "4 runs, 4 assertions", output end end def test_multiple_line_filters - create_test_file :models, 'account' - create_test_file :models, 'post' + create_test_file :models, "account" + create_test_file :models, "post" - run_test_command('test/models/account_test.rb:4 test/models/post_test.rb:4').tap do |output| - assert_match 'AccountTest', output - assert_match 'PostTest', output + run_test_command("test/models/account_test.rb:4 test/models/post_test.rb:4").tap do |output| + assert_match "AccountTest", output + assert_match "PostTest", output end end - def test_line_filter_without_line_runs_all_tests - create_test_file :models, 'account' + def test_line_filters_trigger_only_one_runnable + app_file "test/models/post_test.rb", <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + test 'truth' do + assert true + end + end + + class SecondPostTest < ActiveSupport::TestCase + test 'truth' do + assert false, 'ran second runnable' + end + end + RUBY - run_test_command('test/models/account_test.rb:').tap do |output| - assert_match 'AccountTest', output + # Pass seed guaranteeing failure. + run_test_command("test/models/post_test.rb:4 --seed 30410").tap do |output| + assert_no_match "ran second runnable", output + assert_match "1 runs, 1 assertions", output + end + end + + def test_line_filter_with_minitest_string_filter + app_file "test/models/post_test.rb", <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + test 'by line' do + puts 'by line' + assert true + end + + test 'by name' do + puts 'by name' + assert true + end + end + RUBY + + run_test_command("test/models/post_test.rb:4 -n test_by_name").tap do |output| + assert_match "by line", output + assert_match "by name", output + assert_match "2 runs, 2 assertions", output end end def test_shows_filtered_backtrace_by_default create_backtrace_test - assert_match 'Rails::BacktraceCleaner', run_test_command('test/unit/backtrace_test.rb') + assert_match "Rails::BacktraceCleaner", run_test_command("test/unit/backtrace_test.rb") end def test_backtrace_option create_backtrace_test - assert_match 'Minitest::BacktraceFilter', run_test_command('test/unit/backtrace_test.rb -b') - assert_match 'Minitest::BacktraceFilter', - run_test_command('test/unit/backtrace_test.rb --backtrace') + assert_match "Minitest::BacktraceFilter", run_test_command("test/unit/backtrace_test.rb -b") + assert_match "Minitest::BacktraceFilter", + run_test_command("test/unit/backtrace_test.rb --backtrace") end def test_show_full_backtrace_using_backtrace_environment_variable create_backtrace_test - switch_env 'BACKTRACE', 'true' do - assert_match 'Minitest::BacktraceFilter', run_test_command('test/unit/backtrace_test.rb') + switch_env "BACKTRACE", "true" do + assert_match "Minitest::BacktraceFilter", run_test_command("test/unit/backtrace_test.rb") end end def test_run_app_without_rails_loaded # Simulate a real Rails app boot. - app_file 'config/boot.rb', <<-RUBY - ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) + app_file "config/boot.rb", <<-RUBY + ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. RUBY - assert_match '0 runs, 0 assertions', run_test_command('') + assert_match "0 runs, 0 assertions", run_test_command("") + end + + def test_output_inline_by_default + create_test_file :models, "post", pass: false, print: false + + output = run_test_command("test/models/post_test.rb") + expect = %r{Running:\n\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/models/post_test.rb:6\]:\nwups!\n\nrails test test/models/post_test.rb:4\n\n\n\n} + assert_match expect, output + end + + def test_only_inline_failure_output + create_test_file :models, "post", pass: false + + output = run_test_command("test/models/post_test.rb") + assert_match %r{Finished in.*\n1 runs, 1 assertions}, output + end + + def test_fail_fast + create_test_file :models, "post", pass: false + + assert_match(/Interrupt/, + capture(:stderr) { run_test_command("test/models/post_test.rb --fail-fast", stderr: true) }) + end + + def test_run_in_parallel_with_processes + file_name = create_parallel_processes_test_file + + app_file "db/schema.rb", <<-RUBY + ActiveRecord::Schema.define(version: 1) do + create_table :users do |t| + t.string :name + end + end + RUBY + + output = run_test_command(file_name) + + assert_match %r{Finished in.*\n2 runs, 2 assertions}, output + assert_no_match "create_table(:users)", output + end + + def test_run_in_parallel_with_threads + app_path("/test/test_helper.rb") do |file_name| + file = File.read(file_name) + file.sub!(/parallelize\(([^\)]*)\)/, "parallelize(\\1, with: :threads)") + File.write(file_name, file) + end + + file_name = create_parallel_threads_test_file + + app_file "db/schema.rb", <<-RUBY + ActiveRecord::Schema.define(version: 1) do + create_table :users do |t| + t.string :name + end + end + RUBY + + output = run_test_command(file_name) + + assert_match %r{Finished in.*\n2 runs, 2 assertions}, output + assert_no_match "create_table(:users)", output + end + + def test_run_in_parallel_with_unmarshable_exception + file = app_file "test/fail_test.rb", <<-RUBY + require "test_helper" + class FailTest < ActiveSupport::TestCase + class BadError < StandardError + def initialize + super + @proc = ->{ } + end + end + + test "fail" do + raise BadError + assert true + end + end + RUBY + + output = run_test_command(file) + + assert_match "DRb::DRbRemoteError: FailTest::BadError", output + assert_match "1 runs, 0 assertions, 0 failures, 1 errors", output + end + + def test_run_in_parallel_with_unknown_object + create_scaffold + app_file "config/environments/test.rb", <<-RUBY + Rails.application.configure do + config.action_controller.allow_forgery_protection = true + config.action_dispatch.show_exceptions = false + end + RUBY + + output = run_test_command("-n test_should_create_user") + + assert_match "ActionController::InvalidAuthenticityToken", output + end + + def test_raise_error_when_specified_file_does_not_exist + error = capture(:stderr) { run_test_command("test/not_exists.rb", stderr: true) } + assert_match(%r{cannot load such file.+test/not_exists\.rb}, error) + end + + def test_pass_TEST_env_on_rake_test + create_test_file :models, "account" + create_test_file :models, "post", pass: false + # This specifically verifies TEST for backwards compatibility with rake test + # as `rails test` already supports running tests from a single file more cleanly. + output = Dir.chdir(app_path) { `bin/rake test TEST=test/models/post_test.rb` } + + assert_match "PostTest", output, "passing TEST= should run selected test" + assert_no_match "AccountTest", output, "passing TEST= should only run selected test" + assert_match "1 runs, 1 assertions", output + end + + def test_pass_rake_options + create_test_file :models, "account" + output = Dir.chdir(app_path) { `bin/rake --rakefile Rakefile --trace=stdout test` } + + assert_match "1 runs, 1 assertions", output + assert_match "Execute test", output + end + + def test_rails_db_create_all_restores_db_connection + create_test_file :models, "account" + rails "db:create:all", "db:migrate" + output = Dir.chdir(app_path) { `echo ".tables" | rails dbconsole` } + assert_match "ar_internal_metadata", output, "tables should be dumped" + end + + def test_rails_db_create_all_restores_db_connection_after_drop + create_test_file :models, "account" + rails "db:create:all" # create all to avoid warnings + rails "db:drop:all", "db:create:all", "db:migrate" + output = Dir.chdir(app_path) { `echo ".tables" | rails dbconsole` } + assert_match "ar_internal_metadata", output, "tables should be dumped" + end + + def test_rake_passes_TESTOPTS_to_minitest + create_test_file :models, "account" + output = Dir.chdir(app_path) { `bin/rake test TESTOPTS=-v` } + assert_match "AccountTest#test_truth", output, "passing TEST= should run selected test" + end + + def test_running_with_ruby_gets_test_env_by_default + # Subshells inherit `ENV`, so we need to ensure `RAILS_ENV` is set to + # nil before we run the tests in the test app. + re, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], nil + + file = create_test_for_env("test") + results = Dir.chdir(app_path) { + `ruby -Ilib:test #{file}`.each_line.map { |line| JSON.parse line } + } + assert_equal 1, results.length + failures = results.first["failures"] + flunk(failures.first) unless failures.empty? + + ensure + ENV["RAILS_ENV"] = re + end + + def test_running_with_ruby_can_set_env_via_cmdline + # Subshells inherit `ENV`, so we need to ensure `RAILS_ENV` is set to + # nil before we run the tests in the test app. + re, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], nil + + file = create_test_for_env("development") + results = Dir.chdir(app_path) { + `RAILS_ENV=development ruby -Ilib:test #{file}`.each_line.map { |line| JSON.parse line } + } + assert_equal 1, results.length + failures = results.first["failures"] + flunk(failures.first) unless failures.empty? + + ensure + ENV["RAILS_ENV"] = re + end + + def test_rake_passes_multiple_TESTOPTS_to_minitest + create_test_file :models, "account" + output = Dir.chdir(app_path) { `bin/rake test TESTOPTS='-v --seed=1234'` } + assert_match "AccountTest#test_truth", output, "passing TEST= should run selected test" + assert_match "seed=1234", output, "passing TEST= should run selected test" + end + + def test_rake_runs_multiple_test_tasks + create_test_file :models, "account" + create_test_file :controllers, "accounts_controller" + output = Dir.chdir(app_path) { `bin/rake test:models test:controllers TESTOPTS='-v'` } + assert_match "AccountTest#test_truth", output + assert_match "AccountsControllerTest#test_truth", output + end + + def test_rake_db_and_test_tasks_parses_args_correctly + create_test_file :models, "account" + output = Dir.chdir(app_path) { `bin/rake db:migrate test:models TESTOPTS='-v' && echo ".tables" | rails dbconsole` } + assert_match "AccountTest#test_truth", output + assert_match "ar_internal_metadata", output + end + + def test_warnings_option + app_file "test/models/warnings_test.rb", <<-RUBY + require 'test_helper' + def test_warnings + a = 1 + end + RUBY + assert_match(/warning: assigned but unused variable/, + capture(:stderr) { run_test_command("test/models/warnings_test.rb -w", stderr: true) }) + end + + def test_reset_sessions_before_rollback_on_system_tests + app_file "test/system/reset_session_before_rollback_test.rb", <<-RUBY + require "application_system_test_case" + + class ResetSessionBeforeRollbackTest < ApplicationSystemTestCase + def teardown_fixtures + puts "rollback" + super + end + + Capybara.singleton_class.prepend(Module.new do + def reset_sessions! + puts "reset sessions" + super + end + end) + + test "dummy" do + end + end + RUBY + + run_test_command("test/system/reset_session_before_rollback_test.rb").tap do |output| + assert_match "reset sessions\nrollback", output + assert_match "1 runs, 0 assertions, 0 failures, 0 errors, 0 skips", output + end + end + + def test_reset_sessions_on_failed_system_test_screenshot + app_file "test/system/reset_sessions_on_failed_system_test_screenshot_test.rb", <<~RUBY + require "application_system_test_case" + + class ResetSessionsOnFailedSystemTestScreenshotTest < ApplicationSystemTestCase + ActionDispatch::SystemTestCase.class_eval do + def take_failed_screenshot + raise Capybara::CapybaraError + end + end + + Capybara.instance_eval do + def reset_sessions! + puts "Capybara.reset_sessions! called" + end + end + + test "dummy" do + end + end + RUBY + output = run_test_command("test/system/reset_sessions_on_failed_system_test_screenshot_test.rb") + assert_match "Capybara.reset_sessions! called", output + end + + def test_system_tests_are_not_run_with_the_default_test_command + app_file "test/system/dummy_test.rb", <<-RUBY + require "application_system_test_case" + + class DummyTest < ApplicationSystemTestCase + test "something" do + assert true + end + end + RUBY + + run_test_command("").tap do |output| + assert_match "0 runs, 0 assertions, 0 failures, 0 errors, 0 skips", output + end + end + + def test_system_tests_are_not_run_through_rake_test + app_file "test/system/dummy_test.rb", <<-RUBY + require "application_system_test_case" + + class DummyTest < ApplicationSystemTestCase + test "something" do + assert true + end + end + RUBY + + output = Dir.chdir(app_path) { `bin/rake test` } + assert_match "0 runs, 0 assertions, 0 failures, 0 errors, 0 skips", output + end + + def test_system_tests_are_run_through_rake_test_when_given_in_TEST + app_file "test/system/dummy_test.rb", <<-RUBY + require "application_system_test_case" + + class DummyTest < ApplicationSystemTestCase + test "something" do + assert true + end + end + RUBY + + output = Dir.chdir(app_path) { `bin/rake test TEST=test/system/dummy_test.rb` } + assert_match "1 runs, 1 assertions, 0 failures, 0 errors, 0 skips", output end private - def run_test_command(arguments = 'test/unit/test_test.rb') - Dir.chdir(app_path) { `bin/rails t #{arguments}` } + def run_test_command(arguments = "test/unit/test_test.rb", **opts) + rails "t", *Shellwords.split(arguments), allow_failure: true, **opts end def create_model_with_fixture - script 'generate model user name:string' + rails "generate", "model", "user", "name:string" - app_file 'test/fixtures/users.yml', <<-YAML.strip_heredoc + app_file "test/fixtures/users.yml", <<~YAML vampire: id: 1 name: Koyomi Araragi @@ -363,7 +835,7 @@ module ApplicationTests run_migration end - def create_fixture_test(path = :unit, name = 'test') + def create_fixture_test(path = :unit, name = "test") app_file "test/#{path}/#{name}_test.rb", <<-RUBY require 'test_helper' @@ -376,7 +848,7 @@ module ApplicationTests end def create_backtrace_test - app_file 'test/unit/backtrace_test.rb', <<-RUBY + app_file "test/unit/backtrace_test.rb", <<-RUBY require 'test_helper' class BacktraceTest < ActiveSupport::TestCase @@ -388,24 +860,114 @@ module ApplicationTests end def create_schema - app_file 'db/schema.rb', '' + app_file "db/schema.rb", "" + end + + def create_test_for_env(env) + app_file "test/models/environment_test.rb", <<-RUBY + require 'test_helper' + class JSONReporter < Minitest::AbstractReporter + def record(result) + puts JSON.dump(klass: result.class.name, + name: result.name, + failures: result.failures, + assertions: result.assertions, + time: result.time) + end + end + + def Minitest.plugin_json_reporter_init(opts) + Minitest.reporter.reporters.clear + Minitest.reporter.reporters << JSONReporter.new + end + + Minitest.extensions << "rails" + Minitest.extensions << "json_reporter" + + # Minitest uses RubyGems to find plugins, and since RubyGems + # doesn't know about the Rails installation we're pointing at, + # Minitest won't require the Rails minitest plugin when we run + # these integration tests. So we have to manually require the + # Minitest plugin here. + require 'minitest/rails_plugin' + + class EnvironmentTest < ActiveSupport::TestCase + def test_environment + test_db = ActiveRecord::Base.configurations[#{env.dump}]["database"] + db_file = ActiveRecord::Base.connection_config[:database] + assert_match(test_db, db_file) + assert_equal #{env.dump}, ENV["RAILS_ENV"] + end + end + RUBY end - def create_test_file(path = :unit, name = 'test') + def create_test_file(path = :unit, name = "test", pass: true, print: true) app_file "test/#{path}/#{name}_test.rb", <<-RUBY require 'test_helper' class #{name.camelize}Test < ActiveSupport::TestCase def test_truth - puts "#{name.camelize}Test" - assert true + puts "#{name.camelize}Test" if #{print} + assert #{pass}, 'wups!' + end + end + RUBY + end + + def create_parallel_processes_test_file + app_file "test/models/parallel_test.rb", <<-RUBY + require 'test_helper' + + class ParallelTest < ActiveSupport::TestCase + RD1, WR1 = IO.pipe + RD2, WR2 = IO.pipe + + test "one" do + WR1.close + assert_equal "x", RD1.read(1) # blocks until two runs + + RD2.close + WR2.write "y" # Allow two to run + WR2.close + end + + test "two" do + RD1.close + WR1.write "x" # Allow one to run + WR1.close + + WR2.close + assert_equal "y", RD2.read(1) # blocks until one runs + end + end + RUBY + end + + def create_parallel_threads_test_file + app_file "test/models/parallel_test.rb", <<-RUBY + require 'test_helper' + + class ParallelTest < ActiveSupport::TestCase + Q1 = Queue.new + Q2 = Queue.new + test "one" do + assert_equal "x", Q1.pop # blocks until two runs + + Q2 << "y" + end + + test "two" do + Q1 << "x" + + assert_equal "y", Q2.pop # blocks until one runs end end RUBY end def create_env_test - app_file 'test/unit/env_test.rb', <<-RUBY + app_file "test/unit/env_test.rb", <<-RUBY require 'test_helper' class EnvTest < ActiveSupport::TestCase @@ -417,13 +979,17 @@ module ApplicationTests end def create_scaffold - script 'generate scaffold user name:string' - Dir.chdir(app_path) { File.exist?('app/models/user.rb') } + rails "generate", "scaffold", "user", "name:string" + assert File.exist?("#{app_path}/app/models/user.rb") run_migration end + def create_controller + rails "generate", "controller", "admin/dashboard", "index" + end + def run_migration - Dir.chdir(app_path) { `bin/rake db:migrate` } + rails "db:migrate" end end end diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb index 0e997f4ba7..fb43bebfbe 100644 --- a/railties/test/application/test_test.rb +++ b/railties/test/application/test_test.rb @@ -1,20 +1,26 @@ -require 'isolation/abstract_unit' +# frozen_string_literal: true + +require "isolation/abstract_unit" module ApplicationTests class TestTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation def setup + @old = ENV["PARALLEL_WORKERS"] + ENV["PARALLEL_WORKERS"] = "0" + build_app - boot_rails end def teardown + ENV["PARALLEL_WORKERS"] = @old + teardown_app end - test "truth" do - app_file 'test/unit/foo_test.rb', <<-RUBY + test "simple successful test" do + app_file "test/unit/foo_test.rb", <<-RUBY require 'test_helper' class FooTest < ActiveSupport::TestCase @@ -24,20 +30,52 @@ module ApplicationTests end RUBY - assert_successful_test_run 'unit/foo_test.rb' + assert_successful_test_run "unit/foo_test.rb" + end + + test "after_run" do + app_file "test/unit/foo_test.rb", <<-RUBY + require 'test_helper' + + Minitest.after_run { puts "WORLD" } + Minitest.after_run { puts "HELLO" } + + class FooTest < ActiveSupport::TestCase + def test_truth + assert true + end + end + RUBY + + result = assert_successful_test_run "unit/foo_test.rb" + assert_equal ["HELLO", "WORLD"], result.scan(/HELLO|WORLD/) # only once and in correct order + end + + test "simple failed test" do + app_file "test/unit/foo_test.rb", <<-RUBY + require 'test_helper' + + class FooTest < ActiveSupport::TestCase + def test_truth + assert false + end + end + RUBY + + assert_unsuccessful_run "unit/foo_test.rb", "Failure:\nFooTest#test_truth" end test "integration test" do - controller 'posts', <<-RUBY + controller "posts", <<-RUBY class PostsController < ActionController::Base end RUBY - app_file 'app/views/posts/index.html.erb', <<-HTML + app_file "app/views/posts/index.html.erb", <<-HTML Posts#index HTML - app_file 'test/integration/posts_test.rb', <<-RUBY + app_file "test/integration/posts_test.rb", <<-RUBY require 'test_helper' class PostsTest < ActionDispatch::IntegrationTest @@ -49,11 +87,11 @@ module ApplicationTests end RUBY - assert_successful_test_run 'integration/posts_test.rb' + assert_successful_test_run "integration/posts_test.rb" end test "enable full backtraces on test failures" do - app_file 'test/unit/failing_test.rb', <<-RUBY + app_file "test/unit/failing_test.rb", <<-RUBY require 'test_helper' class FailingTest < ActiveSupport::TestCase @@ -63,16 +101,16 @@ module ApplicationTests end RUBY - output = run_test_file('unit/failing_test.rb', env: { "BACKTRACE" => "1" }) + output = run_test_file("unit/failing_test.rb", env: { "BACKTRACE" => "1" }) assert_match %r{test/unit/failing_test\.rb}, output assert_match %r{test/unit/failing_test\.rb:4}, output end test "ruby schema migrations" do - output = script('generate model user name:string') + output = rails("generate", "model", "user", "name:string") version = output.match(/(\d+)_create_users\.rb/)[1] - app_file 'test/models/user_test.rb', <<-RUBY + app_file "test/models/user_test.rb", <<-RUBY require 'test_helper' class UserTest < ActiveSupport::TestCase @@ -81,11 +119,11 @@ module ApplicationTests end end RUBY - app_file 'db/schema.rb', '' + app_file "db/schema.rb", "" assert_unsuccessful_run "models/user_test.rb", "Migrations are pending" - app_file 'db/schema.rb', <<-RUBY + app_file "db/schema.rb", <<-RUBY ActiveRecord::Schema.define(version: #{version}) do create_table :users do |t| t.string :name @@ -93,7 +131,7 @@ module ApplicationTests end RUBY - app_file 'config/initializers/disable_maintain_test_schema.rb', <<-RUBY + app_file "config/initializers/disable_maintain_test_schema.rb", <<-RUBY Rails.application.config.active_record.maintain_test_schema = false RUBY @@ -101,15 +139,15 @@ module ApplicationTests File.delete "#{app_path}/config/initializers/disable_maintain_test_schema.rb" - result = assert_successful_test_run('models/user_test.rb') - assert !result.include?("create_table(:users)") + result = assert_successful_test_run("models/user_test.rb") + assert_not_includes result, "create_table(:users)" end test "sql structure migrations" do - output = script('generate model user name:string') + output = rails("generate", "model", "user", "name:string") version = output.match(/(\d+)_create_users\.rb/)[1] - app_file 'test/models/user_test.rb', <<-RUBY + app_file "test/models/user_test.rb", <<-RUBY require 'test_helper' class UserTest < ActiveSupport::TestCase @@ -119,21 +157,21 @@ module ApplicationTests end RUBY - app_file 'db/structure.sql', '' - app_file 'config/initializers/enable_sql_schema_format.rb', <<-RUBY + app_file "db/structure.sql", "" + app_file "config/initializers/enable_sql_schema_format.rb", <<-RUBY Rails.application.config.active_record.schema_format = :sql RUBY assert_unsuccessful_run "models/user_test.rb", "Migrations are pending" - app_file 'db/structure.sql', <<-SQL + app_file "db/structure.sql", <<-SQL CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL); CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version"); CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255)); INSERT INTO schema_migrations (version) VALUES ('#{version}'); SQL - app_file 'config/initializers/disable_maintain_test_schema.rb', <<-RUBY + app_file "config/initializers/disable_maintain_test_schema.rb", <<-RUBY Rails.application.config.active_record.maintain_test_schema = false RUBY @@ -141,14 +179,14 @@ module ApplicationTests File.delete "#{app_path}/config/initializers/disable_maintain_test_schema.rb" - assert_successful_test_run('models/user_test.rb') + assert_successful_test_run("models/user_test.rb") end test "sql structure migrations when adding column to existing table" do - output_1 = script('generate model user name:string') + output_1 = rails("generate", "model", "user", "name:string") version_1 = output_1.match(/(\d+)_create_users\.rb/)[1] - app_file 'test/models/user_test.rb', <<-RUBY + app_file "test/models/user_test.rb", <<-RUBY require 'test_helper' class UserTest < ActiveSupport::TestCase test "user" do @@ -157,23 +195,23 @@ module ApplicationTests end RUBY - app_file 'config/initializers/enable_sql_schema_format.rb', <<-RUBY + app_file "config/initializers/enable_sql_schema_format.rb", <<-RUBY Rails.application.config.active_record.schema_format = :sql RUBY - app_file 'db/structure.sql', <<-SQL + app_file "db/structure.sql", <<-SQL CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL); CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version"); CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255)); INSERT INTO schema_migrations (version) VALUES ('#{version_1}'); SQL - assert_successful_test_run('models/user_test.rb') + assert_successful_test_run("models/user_test.rb") - output_2 = script('generate migration add_email_to_users') + output_2 = rails("generate", "migration", "add_email_to_users") version_2 = output_2.match(/(\d+)_add_email_to_users\.rb/)[1] - app_file 'test/models/user_test.rb', <<-RUBY + app_file "test/models/user_test.rb", <<-RUBY require 'test_helper' class UserTest < ActiveSupport::TestCase @@ -183,7 +221,7 @@ module ApplicationTests end RUBY - app_file 'db/structure.sql', <<-SQL + app_file "db/structure.sql", <<-SQL CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL); CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version"); CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255), "email" varchar(255)); @@ -191,17 +229,17 @@ module ApplicationTests INSERT INTO schema_migrations (version) VALUES ('#{version_2}'); SQL - assert_successful_test_run('models/user_test.rb') + assert_successful_test_run("models/user_test.rb") end # TODO: would be nice if we could detect the schema change automatically. # For now, the user has to synchronize the schema manually. # This test-case serves as a reminder for this use-case. test "manually synchronize test schema after rollback" do - output = script('generate model user name:string') + output = rails("generate", "model", "user", "name:string") version = output.match(/(\d+)_create_users\.rb/)[1] - app_file 'test/models/user_test.rb', <<-RUBY + app_file "test/models/user_test.rb", <<-RUBY require 'test_helper' class UserTest < ActiveSupport::TestCase @@ -210,7 +248,7 @@ module ApplicationTests end end RUBY - app_file 'db/schema.rb', <<-RUBY + app_file "db/schema.rb", <<-RUBY ActiveRecord::Schema.define(version: #{version}) do create_table :users do |t| t.string :name @@ -221,7 +259,7 @@ module ApplicationTests assert_successful_test_run "models/user_test.rb" # Simulate `db:rollback` + edit of the migration file + `db:migrate` - app_file 'db/schema.rb', <<-RUBY + app_file "db/schema.rb", <<-RUBY ActiveRecord::Schema.define(version: #{version}) do create_table :users do |t| t.string :name @@ -232,7 +270,7 @@ module ApplicationTests assert_successful_test_run "models/user_test.rb" - Dir.chdir(app_path) { `bin/rake db:test:prepare` } + rails "db:test:prepare" assert_unsuccessful_run "models/user_test.rb", <<-ASSERTION Expected: ["id", "name"] @@ -241,10 +279,10 @@ Expected: ["id", "name"] end test "hooks for plugins" do - output = script('generate model user name:string') + output = rails("generate", "model", "user", "name:string") version = output.match(/(\d+)_create_users\.rb/)[1] - app_file 'lib/tasks/hooks.rake', <<-RUBY + app_file "lib/tasks/hooks.rake", <<-RUBY task :before_hook do has_user_table = ActiveRecord::Base.connection.table_exists?('users') puts "before: " + has_user_table.to_s @@ -259,7 +297,7 @@ Expected: ["id", "name"] Rake::Task[:after_hook].invoke end RUBY - app_file 'test/models/user_test.rb', <<-RUBY + app_file "test/models/user_test.rb", <<-RUBY require 'test_helper' class UserTest < ActiveSupport::TestCase test "user" do @@ -269,7 +307,7 @@ Expected: ["id", "name"] RUBY # Simulate `db:migrate` - app_file 'db/schema.rb', <<-RUBY + app_file "db/schema.rb", <<-RUBY ActiveRecord::Schema.define(version: #{version}) do create_table :users do |t| t.string :name @@ -290,7 +328,7 @@ Expected: ["id", "name"] def assert_unsuccessful_run(name, message) result = run_test_file(name) assert_not_equal 0, $?.to_i - assert result.include?(message) + assert_includes result, message result end @@ -301,7 +339,7 @@ Expected: ["id", "name"] end def run_test_file(name, options = {}) - Dir.chdir(app_path) { `bin/rails test "#{app_path}/test/#{name}" 2>&1` } + rails "test", "#{app_path}/test/#{name}", allow_failure: true end end end diff --git a/railties/test/application/url_generation_test.rb b/railties/test/application/url_generation_test.rb index 894e18cb39..f22b9fda3d 100644 --- a/railties/test/application/url_generation_test.rb +++ b/railties/test/application/url_generation_test.rb @@ -1,4 +1,6 @@ -require 'isolation/abstract_unit' +# frozen_string_literal: true + +require "isolation/abstract_unit" module ApplicationTests class UrlGenerationTest < ActiveSupport::TestCase @@ -9,13 +11,11 @@ module ApplicationTests end test "it works" do - boot_rails require "rails" require "action_controller/railtie" require "action_view/railtie" class MyApp < Rails::Application - secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" config.session_store :cookie_store, key: "_myapp_session" config.active_support.deprecation = :log config.eager_load = false @@ -28,7 +28,7 @@ module ApplicationTests class ::OmgController < ::ApplicationController def index - render text: omg_path + render plain: omg_path end end @@ -36,7 +36,7 @@ module ApplicationTests get "/" => "omg#index", as: :omg end - require 'rack/test' + require "rack/test" extend Rack::Test::Methods get "/" @@ -44,12 +44,11 @@ module ApplicationTests end def test_routes_know_the_relative_root - boot_rails require "rails" require "action_controller/railtie" require "action_view/railtie" - relative_url = '/hello' + relative_url = "/hello" ENV["RAILS_RELATIVE_URL_ROOT"] = relative_url app = Class.new(Rails::Application) assert_equal relative_url, app.routes.relative_url_root diff --git a/railties/test/application/version_test.rb b/railties/test/application/version_test.rb new file mode 100644 index 0000000000..ae85cf8f05 --- /dev/null +++ b/railties/test/application/version_test.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rails/gem_version" + +class VersionTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test "command works" do + output = rails("version") + assert_equal "Rails #{Rails.gem_version}\n", output + end + + test "short-cut alias works" do + output = rails("-v") + assert_equal "Rails #{Rails.gem_version}\n", output + end +end diff --git a/railties/test/backtrace_cleaner_test.rb b/railties/test/backtrace_cleaner_test.rb index 2dd74f8fd1..90e084ddca 100644 --- a/railties/test/backtrace_cleaner_test.rb +++ b/railties/test/backtrace_cleaner_test.rb @@ -1,24 +1,26 @@ -require 'abstract_unit' -require 'rails/backtrace_cleaner' +# frozen_string_literal: true -class BacktraceCleanerVendorGemTest < ActiveSupport::TestCase +require "abstract_unit" +require "rails/backtrace_cleaner" + +class BacktraceCleanerTest < ActiveSupport::TestCase def setup @cleaner = Rails::BacktraceCleaner.new end - test "should format installed gems correctly" do - @backtrace = [ "#{Gem.path[0]}/gems/nosuchgem-1.2.3/lib/foo.rb" ] - @result = @cleaner.clean(@backtrace, :all) - assert_equal "nosuchgem (1.2.3) lib/foo.rb", @result[0] + test "should consider traces from irb lines as User code" do + backtrace = [ "(irb):1", + "/Path/to/rails/railties/lib/rails/commands/console.rb:77:in `start'", + "bin/rails:4:in `<main>'" ] + result = @cleaner.clean(backtrace) + assert_equal "(irb):1", result[0] + assert_equal 1, result.length end - test "should format installed gems not in Gem.default_dir correctly" do - @target_dir = Gem.path.detect { |p| p != Gem.default_dir } - # skip this test if default_dir is the only directory on Gem.path - if @target_dir - @backtrace = [ "#{@target_dir}/gems/nosuchgem-1.2.3/lib/foo.rb" ] - @result = @cleaner.clean(@backtrace, :all) - assert_equal "nosuchgem (1.2.3) lib/foo.rb", @result[0] - end + test "should omit ActionView template methods names" do + method_name = ActionView::Template.new(nil, "app/views/application/index.html.erb", nil, {}).send :method_name + backtrace = [ "app/views/application/index.html.erb:4:in `block in #{method_name}'"] + result = @cleaner.clean(backtrace, :all) + assert_equal "app/views/application/index.html.erb:4", result[0] end end diff --git a/railties/test/code_statistics_calculator_test.rb b/railties/test/code_statistics_calculator_test.rb index 46445a001a..e763cfb376 100644 --- a/railties/test/code_statistics_calculator_test.rb +++ b/railties/test/code_statistics_calculator_test.rb @@ -1,12 +1,14 @@ -require 'abstract_unit' -require 'rails/code_statistics_calculator' +# frozen_string_literal: true + +require "abstract_unit" +require "rails/code_statistics_calculator" class CodeStatisticsCalculatorTest < ActiveSupport::TestCase def setup @code_statistics_calculator = CodeStatisticsCalculator.new end - test 'calculate statistics using #add_by_file_path' do + test "calculate statistics using #add_by_file_path" do code = <<-RUBY def foo puts 'foo' @@ -14,7 +16,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase end RUBY - temp_file 'stats.rb', code do |path| + temp_file "stats.rb", code do |path| @code_statistics_calculator.add_by_file_path path assert_equal 4, @code_statistics_calculator.lines @@ -24,7 +26,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase end end - test 'count number of methods in MiniTest file' do + test "count number of methods in minitest file" do code = <<-RUBY class FooTest < ActionController::TestCase test 'expectation' do @@ -37,13 +39,13 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase end RUBY - temp_file 'foo_test.rb', code do |path| + temp_file "foo_test.rb", code do |path| @code_statistics_calculator.add_by_file_path path assert_equal 2, @code_statistics_calculator.methods end end - test 'add statistics to another using #add' do + test "add statistics to another using #add" do code_statistics_calculator_1 = CodeStatisticsCalculator.new(1, 2, 3, 4) @code_statistics_calculator.add(code_statistics_calculator_1) @@ -52,7 +54,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 3, @code_statistics_calculator.classes assert_equal 4, @code_statistics_calculator.methods - code_statistics_calculator_2 = CodeStatisticsCalculator.new(2, 3, 4, 5) + code_statistics_calculator_2 = CodeStatisticsCalculator.new(2, 3, 4, 5) @code_statistics_calculator.add(code_statistics_calculator_2) assert_equal 3, @code_statistics_calculator.lines @@ -61,7 +63,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 9, @code_statistics_calculator.methods end - test 'accumulate statistics using #add_by_io' do + test "accumulate statistics using #add_by_io" do code_statistics_calculator_1 = CodeStatisticsCalculator.new(1, 2, 3, 4) @code_statistics_calculator.add(code_statistics_calculator_1) @@ -82,7 +84,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 6, @code_statistics_calculator.methods end - test 'calculate number of Ruby methods' do + test "calculate number of Ruby methods" do code = <<-'CODE' def foo puts 'foo' @@ -101,7 +103,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 3, @code_statistics_calculator.methods end - test 'calculate Ruby LOCs' do + test "calculate Ruby LOCs" do code = <<-'CODE' def foo puts 'foo' @@ -119,7 +121,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 5, @code_statistics_calculator.code_lines end - test 'calculate number of Ruby classes' do + test "calculate number of Ruby classes" do code = <<-'CODE' class Foo < Bar def foo @@ -138,7 +140,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 2, @code_statistics_calculator.classes end - test 'skip Ruby comments' do + test "skip Ruby comments" do code = <<-'CODE' =begin class Foo @@ -160,7 +162,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 0, @code_statistics_calculator.methods end - test 'calculate number of JS methods' do + test "calculate number of JS methods" do code = <<-'CODE' function foo(x, y, z) { doX(); @@ -179,7 +181,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 3, @code_statistics_calculator.methods end - test 'calculate JS LOCs' do + test "calculate JS LOCs" do code = <<-'CODE' function foo() alert('foo'); @@ -196,7 +198,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 4, @code_statistics_calculator.code_lines end - test 'skip JS comments' do + test "skip JS comments" do code = <<-'CODE' /* * var f = function () { @@ -216,7 +218,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 0, @code_statistics_calculator.methods end - test 'calculate number of CoffeeScript methods' do + test "calculate number of CoffeeScript methods" do code = <<-'CODE' square = (x) -> x * x @@ -235,7 +237,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 4, @code_statistics_calculator.methods end - test 'calculate CoffeeScript LOCs' do + test "calculate CoffeeScript LOCs" do code = <<-'CODE' # Assignment: number = 42 @@ -256,7 +258,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 3, @code_statistics_calculator.code_lines end - test 'calculate number of CoffeeScript classes' do + test "calculate number of CoffeeScript classes" do code = <<-'CODE' class Animal constructor: (@name) -> @@ -277,7 +279,7 @@ class CodeStatisticsCalculatorTest < ActiveSupport::TestCase assert_equal 2, @code_statistics_calculator.classes end - test 'skip CoffeeScript comments' do + test "skip CoffeeScript comments" do code = <<-'CODE' ### class Animal @@ -299,9 +301,25 @@ class Animal assert_equal 0, @code_statistics_calculator.methods end + test "count rake tasks" do + code = <<-'CODE' + task :test_task do + puts 'foo' + end + + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :rake) + + assert_equal 4, @code_statistics_calculator.lines + assert_equal 3, @code_statistics_calculator.code_lines + assert_equal 0, @code_statistics_calculator.classes + assert_equal 0, @code_statistics_calculator.methods + end + private def temp_file(name, content) - dir = File.expand_path '../fixtures/tmp', __FILE__ + dir = File.expand_path "fixtures/tmp", __dir__ path = "#{dir}/#{name}" FileUtils.mkdir_p dir diff --git a/railties/test/code_statistics_test.rb b/railties/test/code_statistics_test.rb index 1b1ff80bc1..7ad1ac3094 100644 --- a/railties/test/code_statistics_test.rb +++ b/railties/test/code_statistics_test.rb @@ -1,10 +1,12 @@ -require 'abstract_unit' -require 'rails/code_statistics' +# frozen_string_literal: true + +require "abstract_unit" +require "rails/code_statistics" class CodeStatisticsTest < ActiveSupport::TestCase def setup - @tmp_path = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp')) - @dir_js = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp', 'lib.js')) + @tmp_path = File.expand_path("fixtures/tmp", __dir__) + @dir_js = File.join(@tmp_path, "lib.js") FileUtils.mkdir_p(@dir_js) end @@ -12,9 +14,21 @@ class CodeStatisticsTest < ActiveSupport::TestCase FileUtils.rm_rf(@tmp_path) end - test 'ignores directories that happen to have source files extensions' do + test "ignores directories that happen to have source files extensions" do + assert_nothing_raised do + @code_statistics = CodeStatistics.new(["tmp dir", @tmp_path]) + end + end + + test "ignores hidden files" do + File.write File.join(@tmp_path, ".example.rb"), <<-CODE + def foo + puts 'foo' + end + CODE + assert_nothing_raised do - @code_statistics = CodeStatistics.new(['tmp dir', @tmp_path]) + CodeStatistics.new(["hidden file", @tmp_path]) end end end diff --git a/railties/test/command/base_test.rb b/railties/test/command/base_test.rb new file mode 100644 index 0000000000..a49ae8aae7 --- /dev/null +++ b/railties/test/command/base_test.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "rails/command" +require "rails/commands/generate/generate_command" +require "rails/commands/secrets/secrets_command" + +class Rails::Command::BaseTest < ActiveSupport::TestCase + test "printing commands" do + assert_equal %w(generate), Rails::Command::GenerateCommand.printing_commands + assert_equal %w(secrets:setup secrets:edit secrets:show), Rails::Command::SecretsCommand.printing_commands + end +end diff --git a/railties/test/command/spellchecker_test.rb b/railties/test/command/spellchecker_test.rb new file mode 100644 index 0000000000..e6a7a3957c --- /dev/null +++ b/railties/test/command/spellchecker_test.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "rails/command/spellchecker" + +class Rails::Command::SpellcheckerTest < ActiveSupport::TestCase + test "suggests a word correction from dictionary" do + assert_equal "thin", Rails::Command::Spellchecker.suggest("tin", from: %w(puma thin cgi)) + end +end diff --git a/railties/test/commands/console_test.rb b/railties/test/commands/console_test.rb index de0cf0ba9e..0b2fe204f8 100644 --- a/railties/test/commands/console_test.rb +++ b/railties/test/commands/console_test.rb @@ -1,6 +1,9 @@ -require 'abstract_unit' -require 'env_helpers' -require 'rails/commands/console' +# frozen_string_literal: true + +require "abstract_unit" +require "env_helpers" +require "rails/command" +require "rails/commands/console/console_command" class Rails::ConsoleTest < ActiveSupport::TestCase include EnvHelpers @@ -17,37 +20,36 @@ class Rails::ConsoleTest < ActiveSupport::TestCase def test_sandbox_option console = Rails::Console.new(app, parse_arguments(["--sandbox"])) - assert console.sandbox? + assert_predicate console, :sandbox? end def test_short_version_of_sandbox_option console = Rails::Console.new(app, parse_arguments(["-s"])) - assert console.sandbox? + assert_predicate console, :sandbox? end def test_no_options console = Rails::Console.new(app, parse_arguments([])) - assert !console.sandbox? + assert_not_predicate console, :sandbox? end def test_start start - assert app.console.started? + assert_predicate app.console, :started? assert_match(/Loading \w+ environment \(Rails/, output) end def test_start_with_sandbox start ["--sandbox"] - - assert app.console.started? + assert_predicate app.console, :started? assert app.sandbox assert_match(/Loading \w+ environment in sandbox \(Rails/, output) end def test_console_with_environment - start ["-e production"] + start ["-e", "production"] assert_match(/\sproduction\s/, output) end @@ -64,52 +66,73 @@ class Rails::ConsoleTest < ActiveSupport::TestCase end def test_default_environment_with_rails_env - with_rails_env 'special-production' do + with_rails_env "special-production" do start assert_match(/\sspecial-production\s/, output) end end def test_default_environment_with_rack_env - with_rack_env 'production' do + with_rack_env "production" do start assert_match(/\sproduction\s/, output) end end def test_e_option - start ['-e', 'special-production'] + start ["-e", "special-production"] assert_match(/\sspecial-production\s/, output) end + def test_e_option_is_properly_expanded + start ["-e", "prod"] + assert_match(/\sproduction\s/, output) + end + def test_environment_option - start ['--environment=special-production'] + start ["--environment=special-production"] assert_match(/\sspecial-production\s/, output) end def test_rails_env_is_production_when_first_argument_is_p - start ['p'] - assert_match(/\sproduction\s/, output) + assert_deprecated do + start ["p"] + assert_match(/\sproduction\s/, output) + end end def test_rails_env_is_test_when_first_argument_is_t - start ['t'] - assert_match(/\stest\s/, output) + assert_deprecated do + start ["t"] + assert_match(/\stest\s/, output) + end end def test_rails_env_is_development_when_argument_is_d - start ['d'] - assert_match(/\sdevelopment\s/, output) + assert_deprecated do + start ["d"] + assert_match(/\sdevelopment\s/, output) + end end def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present - stubbed_console = Class.new(Rails::Console) do - def available_environments - ['dev'] + Rails::Command::ConsoleCommand.class_eval do + alias_method :old_environments, :available_environments + + define_method :available_environments do + ["dev"] end end - options = stubbed_console.parse_arguments(['dev']) - assert_match('dev', options[:environment]) + + assert_deprecated do + assert_match("dev", parse_arguments(["dev"])[:environment]) + end + ensure + Rails::Command::ConsoleCommand.class_eval do + undef_method :available_environments + alias_method :available_environments, :old_environments + undef_method :old_environments + end end attr_reader :output @@ -117,38 +140,37 @@ class Rails::ConsoleTest < ActiveSupport::TestCase private - def start(argv = []) - rails_console = Rails::Console.new(app, parse_arguments(argv)) - @output = capture(:stdout) { rails_console.start } - end + def start(argv = []) + rails_console = Rails::Console.new(app, parse_arguments(argv)) + @output = capture(:stdout) { rails_console.start } + end - def app - @app ||= build_app(FakeConsole) - end + def app + @app ||= build_app(FakeConsole) + end - def build_app(console) - mocked_console = Class.new do - attr_reader :sandbox, :console + def build_app(console) + mocked_console = Class.new do + attr_accessor :sandbox + attr_reader :console - def initialize(console) - @console = console - end + def initialize(console) + @console = console + end - def config - self - end - - def sandbox=(arg) - @sandbox = arg - end + def config + self + end - def load_console + def load_console + end end + mocked_console.new(console) end - mocked_console.new(console) - end - def parse_arguments(args) - Rails::Console.parse_arguments(args) - end + def parse_arguments(args) + command = Rails::Command::ConsoleCommand.new([], args) + command.send(:extract_environment_option_from_argument) + command.options + end end diff --git a/railties/test/commands/credentials_test.rb b/railties/test/commands/credentials_test.rb new file mode 100644 index 0000000000..7842b0db61 --- /dev/null +++ b/railties/test/commands/credentials_test.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "env_helpers" +require "rails/command" +require "rails/commands/credentials/credentials_command" + +class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation, EnvHelpers + + setup { build_app } + + teardown { teardown_app } + + test "edit without editor gives hint" do + run_edit_command(editor: "").tap do |output| + assert_match "No $EDITOR to open file in", output + assert_match "rails credentials:edit", output + end + end + + test "edit credentials" do + # Run twice to ensure credentials can be reread after first edit pass. + 2.times do + assert_match(/access_key_id: 123/, run_edit_command) + end + end + + test "edit command does not add master key to gitignore when already exist" do + run_edit_command + + Dir.chdir(app_path) do + gitignore = File.read(".gitignore") + assert_equal 1, gitignore.scan(%r|config/master\.key|).length + end + end + + test "edit command does not overwrite by default if credentials already exists" do + run_edit_command(editor: "eval echo api_key: abc >") + assert_match(/api_key: abc/, run_show_command) + + run_edit_command + assert_match(/api_key: abc/, run_show_command) + end + + test "edit command does not add master key when `RAILS_MASTER_KEY` env specified" do + Dir.chdir(app_path) do + key = IO.binread("config/master.key").strip + FileUtils.rm("config/master.key") + + switch_env("RAILS_MASTER_KEY", key) do + assert_match(/access_key_id: 123/, run_edit_command) + assert_not File.exist?("config/master.key") + end + end + end + + test "edit command modifies file specified by environment option" do + assert_match(/access_key_id: 123/, run_edit_command(environment: "production")) + Dir.chdir(app_path) do + assert File.exist?("config/credentials/production.key") + assert File.exist?("config/credentials/production.yml.enc") + end + end + + test "show credentials" do + assert_match(/access_key_id: 123/, run_show_command) + end + + test "show command raise error when require_master_key is specified and key does not exist" do + remove_file "config/master.key" + add_to_config "config.require_master_key = true" + + assert_match(/Missing encryption key to decrypt file with/, run_show_command(allow_failure: true)) + end + + test "show command does not raise error when require_master_key is false and master key does not exist" do + remove_file "config/master.key" + add_to_config "config.require_master_key = false" + + assert_match(/Missing 'config\/master\.key' to decrypt credentials/, run_show_command) + end + + test "show command displays content specified by environment option" do + run_edit_command(environment: "production") + + assert_match(/access_key_id: 123/, run_show_command(environment: "production")) + end + + private + def run_edit_command(editor: "cat", environment: nil, **options) + switch_env("EDITOR", editor) do + args = environment ? ["--environment", environment] : [] + rails "credentials:edit", args, **options + end + end + + def run_show_command(environment: nil, **options) + args = environment ? ["--environment", environment] : [] + rails "credentials:show", args, **options + end +end diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index 7950ed6aa7..5e3eca6585 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -1,30 +1,31 @@ -require 'abstract_unit' -require 'minitest/mock' -require 'rails/commands/dbconsole' - -class Rails::DBConsoleTest < ActiveSupport::TestCase +# frozen_string_literal: true +require "abstract_unit" +require "minitest/mock" +require "rails/command" +require "rails/commands/dbconsole/dbconsole_command" +class Rails::DBConsoleTest < ActiveSupport::TestCase def setup - Rails::DBConsole.const_set('APP_PATH', 'rails/all') + Rails::DBConsole.const_set("APP_PATH", "rails/all") end def teardown - Rails::DBConsole.send(:remove_const, 'APP_PATH') - %w[PGUSER PGHOST PGPORT PGPASSWORD DATABASE_URL].each{|key| ENV.delete(key)} + Rails::DBConsole.send(:remove_const, "APP_PATH") + %w[PGUSER PGHOST PGPORT PGPASSWORD DATABASE_URL].each { |key| ENV.delete(key) } end def test_config_with_db_config_only config_sample = { - "test"=> { - "adapter"=> "sqlite3", - "host"=> "localhost", - "port"=> "9000", - "database"=> "foo_test", - "user"=> "foo", - "password"=> "bar", - "pool"=> "5", - "timeout"=> "3000" + "test" => { + "adapter" => "sqlite3", + "host" => "localhost", + "port" => "9000", + "database" => "foo_test", + "user" => "foo", + "password" => "bar", + "pool" => "5", + "timeout" => "3000" } } app_db_config(config_sample) do @@ -41,7 +42,7 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase end def test_config_with_database_url_only - ENV['DATABASE_URL'] = 'postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000' + ENV["DATABASE_URL"] = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000" expected = { "adapter" => "postgresql", "host" => "localhost", @@ -60,7 +61,7 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase def test_config_choose_database_url_if_exists host = "database-url-host.com" - ENV['DATABASE_URL'] = "postgresql://foo:bar@#{host}:9000/foo_test?pool=5&timeout=3000" + ENV["DATABASE_URL"] = "postgresql://foo:bar@#{host}:9000/foo_test?pool=5&timeout=3000" sample_config = { "test" => { "adapter" => "postgresql", @@ -81,144 +82,197 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase def test_env assert_equal "test", Rails::DBConsole.new.environment - ENV['RAILS_ENV'] = nil - ENV['RACK_ENV'] = nil + ENV["RAILS_ENV"] = nil + ENV["RACK_ENV"] = nil Rails.stub(:respond_to?, false) do assert_equal "development", Rails::DBConsole.new.environment - ENV['RACK_ENV'] = "rack_env" + ENV["RACK_ENV"] = "rack_env" assert_equal "rack_env", Rails::DBConsole.new.environment - ENV['RAILS_ENV'] = "rails_env" + ENV["RAILS_ENV"] = "rails_env" assert_equal "rails_env", Rails::DBConsole.new.environment end ensure - ENV['RAILS_ENV'] = "test" - ENV['RACK_ENV'] = nil + ENV["RAILS_ENV"] = "test" + ENV["RACK_ENV"] = nil end def test_rails_env_is_development_when_argument_is_dev - Rails::DBConsole.stub(:available_environments, ['development', 'test']) do - options = Rails::DBConsole.send(:parse_arguments, ['dev']) - assert_match('development', options[:environment]) + assert_deprecated do + stub_available_environments([ "development", "test" ]) do + assert_match("development", parse_arguments([ "dev" ])[:environment]) + end + end + end + + def test_rails_env_is_development_when_environment_option_is_dev + stub_available_environments([ "development", "test" ]) do + assert_match("development", parse_arguments([ "-e", "dev" ])[:environment]) end end def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present - Rails::DBConsole.stub(:available_environments, ['dev']) do - options = Rails::DBConsole.send(:parse_arguments, ['dev']) - assert_match('dev', options[:environment]) + assert_deprecated do + stub_available_environments([ "dev" ]) do + assert_match("dev", parse_arguments([ "dev" ])[:environment]) + end end end def test_mysql - start(adapter: 'mysql', database: 'db') - assert !aborted - assert_equal [%w[mysql mysql5], 'db'], dbconsole.find_cmd_and_exec_args + start(adapter: "mysql2", database: "db") + assert_not aborted + assert_equal [%w[mysql mysql5], "db"], dbconsole.find_cmd_and_exec_args end def test_mysql_full - start(adapter: 'mysql', database: 'db', host: 'locahost', port: 1234, socket: 'socket', username: 'user', password: 'qwerty', encoding: 'UTF-8') - assert !aborted - assert_equal [%w[mysql mysql5], '--host=locahost', '--port=1234', '--socket=socket', '--user=user', '--default-character-set=UTF-8', '-p', 'db'], dbconsole.find_cmd_and_exec_args + start(adapter: "mysql2", database: "db", host: "locahost", port: 1234, socket: "socket", username: "user", password: "qwerty", encoding: "UTF-8") + assert_not aborted + assert_equal [%w[mysql mysql5], "--host=locahost", "--port=1234", "--socket=socket", "--user=user", "--default-character-set=UTF-8", "-p", "db"], dbconsole.find_cmd_and_exec_args end def test_mysql_include_password - start({adapter: 'mysql', database: 'db', username: 'user', password: 'qwerty'}, ['-p']) - assert !aborted - assert_equal [%w[mysql mysql5], '--user=user', '--password=qwerty', 'db'], dbconsole.find_cmd_and_exec_args + start({ adapter: "mysql2", database: "db", username: "user", password: "qwerty" }, ["-p"]) + assert_not aborted + assert_equal [%w[mysql mysql5], "--user=user", "--password=qwerty", "db"], dbconsole.find_cmd_and_exec_args end def test_postgresql - start(adapter: 'postgresql', database: 'db') - assert !aborted - assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args + start(adapter: "postgresql", database: "db") + assert_not aborted + assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args end def test_postgresql_full - start(adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3', host: 'host', port: 5432) - assert !aborted - assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args - assert_equal 'user', ENV['PGUSER'] - assert_equal 'host', ENV['PGHOST'] - assert_equal '5432', ENV['PGPORT'] - assert_not_equal 'q1w2e3', ENV['PGPASSWORD'] + start(adapter: "postgresql", database: "db", username: "user", password: "q1w2e3", host: "host", port: 5432) + assert_not aborted + assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args + assert_equal "user", ENV["PGUSER"] + assert_equal "host", ENV["PGHOST"] + assert_equal "5432", ENV["PGPORT"] + assert_not_equal "q1w2e3", ENV["PGPASSWORD"] end def test_postgresql_include_password - start({adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3'}, ['-p']) - assert !aborted - assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args - assert_equal 'user', ENV['PGUSER'] - assert_equal 'q1w2e3', ENV['PGPASSWORD'] + start({ adapter: "postgresql", database: "db", username: "user", password: "q1w2e3" }, ["-p"]) + assert_not aborted + assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args + assert_equal "user", ENV["PGUSER"] + assert_equal "q1w2e3", ENV["PGPASSWORD"] end def test_sqlite3 - start(adapter: 'sqlite3', database: 'db.sqlite3') - assert !aborted - assert_equal ['sqlite3', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args + start(adapter: "sqlite3", database: "db.sqlite3") + assert_not aborted + assert_equal ["sqlite3", Rails.root.join("db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args end def test_sqlite3_mode - start({adapter: 'sqlite3', database: 'db.sqlite3'}, ['--mode', 'html']) - assert !aborted - assert_equal ['sqlite3', '-html', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args + start({ adapter: "sqlite3", database: "db.sqlite3" }, ["--mode", "html"]) + assert_not aborted + assert_equal ["sqlite3", "-html", Rails.root.join("db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args end def test_sqlite3_header - start({adapter: 'sqlite3', database: 'db.sqlite3'}, ['--header']) - assert_equal ['sqlite3', '-header', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args + start({ adapter: "sqlite3", database: "db.sqlite3" }, ["--header"]) + assert_equal ["sqlite3", "-header", Rails.root.join("db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args end def test_sqlite3_db_absolute_path - start(adapter: 'sqlite3', database: '/tmp/db.sqlite3') - assert !aborted - assert_equal ['sqlite3', '/tmp/db.sqlite3'], dbconsole.find_cmd_and_exec_args + start(adapter: "sqlite3", database: "/tmp/db.sqlite3") + assert_not aborted + assert_equal ["sqlite3", "/tmp/db.sqlite3"], dbconsole.find_cmd_and_exec_args end def test_sqlite3_db_without_defined_rails_root Rails.stub(:respond_to?, false) do - start(adapter: 'sqlite3', database: 'config/db.sqlite3') - assert !aborted - assert_equal ['sqlite3', Rails.root.join('../config/db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args + start(adapter: "sqlite3", database: "config/db.sqlite3") + assert_not aborted + assert_equal ["sqlite3", Rails.root.join("../config/db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args end end def test_oracle - start(adapter: 'oracle', database: 'db', username: 'user', password: 'secret') - assert !aborted - assert_equal ['sqlplus', 'user@db'], dbconsole.find_cmd_and_exec_args + start(adapter: "oracle", database: "db", username: "user", password: "secret") + assert_not aborted + assert_equal ["sqlplus", "user@db"], dbconsole.find_cmd_and_exec_args end def test_oracle_include_password - start({adapter: 'oracle', database: 'db', username: 'user', password: 'secret'}, ['-p']) - assert !aborted - assert_equal ['sqlplus', 'user/secret@db'], dbconsole.find_cmd_and_exec_args + start({ adapter: "oracle", database: "db", username: "user", password: "secret" }, ["-p"]) + assert_not aborted + assert_equal ["sqlplus", "user/secret@db"], dbconsole.find_cmd_and_exec_args + end + + def test_sqlserver + start(adapter: "sqlserver", database: "db", username: "user", password: "secret", host: "localhost", port: 1433) + assert_not aborted + assert_equal ["sqsh", "-D", "db", "-U", "user", "-P", "secret", "-S", "localhost:1433"], dbconsole.find_cmd_and_exec_args end def test_unknown_command_line_client - start(adapter: 'unknown', database: 'db') + start(adapter: "unknown", database: "db") assert aborted assert_match(/Unknown command-line client for db/, output) end + def test_primary_is_automatically_picked_with_3_level_configuration + sample_config = { + "test" => { + "primary" => { + "adapter" => "postgresql" + } + } + } + + app_db_config(sample_config) do + assert_equal "postgresql", Rails::DBConsole.new.config["adapter"] + end + end + + def test_specifying_a_custom_connection_and_environment + stub_available_environments(["development"]) do + dbconsole = parse_arguments(["-c", "custom", "-e", "development"]) + + assert_equal "development", dbconsole[:environment] + assert_equal "custom", dbconsole.connection + end + end + + def test_specifying_a_missing_connection + app_db_config({}) do + e = assert_raises(ActiveRecord::AdapterNotSpecified) do + Rails::Command.invoke(:dbconsole, ["-c", "i_do_not_exist"]) + end + + assert_includes e.message, "'i_do_not_exist' connection is not configured." + end + end + + def test_specifying_a_missing_environment + app_db_config({}) do + e = assert_raises(ActiveRecord::AdapterNotSpecified) do + Rails::Command.invoke(:dbconsole) + end + + assert_includes e.message, "'test' database is not configured." + end + end + def test_print_help_short stdout = capture(:stdout) do - start({}, ['-h']) + Rails::Command.invoke(:dbconsole, ["-h"]) end - assert aborted - assert_equal '', output - assert_match(/Usage:.*dbconsole/, stdout) + assert_match(/rails dbconsole \[environment\]/, stdout) end def test_print_help_long stdout = capture(:stdout) do - start({}, ['--help']) + Rails::Command.invoke(:dbconsole, ["--help"]) end - assert aborted - assert_equal '', output - assert_match(/Usage:.*dbconsole/, stdout) + assert_match(/rails dbconsole \[environment\]/, stdout) end attr_reader :aborted, :output @@ -226,39 +280,76 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase private - def app_db_config(results) - Rails.application.config.stub(:database_configuration, results || {}) do - yield + def app_db_config(results) + Rails.application.config.stub(:database_configuration, results || {}) do + yield + end end - end - def dbconsole - @dbconsole ||= Class.new(Rails::DBConsole) do - attr_reader :find_cmd_and_exec_args + def make_dbconsole + Class.new(Rails::DBConsole) do + attr_reader :find_cmd_and_exec_args - def find_cmd_and_exec(*args) - @find_cmd_and_exec_args = args + def find_cmd_and_exec(*args) + @find_cmd_and_exec_args = args + end end - end.new(nil) - end + end + + attr_reader :dbconsole - def start(config = {}, argv = []) - dbconsole.stub(:config, config.stringify_keys) do - dbconsole.stub(:arguments, argv) do - capture_abort { dbconsole.start } + def start(config = {}, argv = []) + @dbconsole = make_dbconsole.new(parse_arguments(argv)) + @dbconsole.stub(:config, config.stringify_keys) do + capture_abort { @dbconsole.start } end end - end - def capture_abort - @aborted = false - @output = capture(:stderr) do - begin - yield - rescue SystemExit - @aborted = true + def capture_abort + @aborted = false + @output = capture(:stderr) do + begin + yield + rescue SystemExit + @aborted = true + end end end - end + def stub_available_environments(environments) + Rails::Command::DbconsoleCommand.class_eval do + alias_method :old_environments, :available_environments + + define_method :available_environments do + environments + end + end + + yield + ensure + Rails::Command::DbconsoleCommand.class_eval do + undef_method :available_environments + alias_method :available_environments, :old_environments + undef_method :old_environments + end + end + + def parse_arguments(args) + Rails::Command::DbconsoleCommand.class_eval do + alias_method :old_perform, :perform + define_method(:perform) do + extract_environment_option_from_argument + + options + end + end + + Rails::Command.invoke(:dbconsole, args) + ensure + Rails::Command::DbconsoleCommand.class_eval do + undef_method :perform + alias_method :perform, :old_perform + undef_method :old_perform + end + end end diff --git a/railties/test/commands/dev_test.rb b/railties/test/commands/dev_test.rb new file mode 100644 index 0000000000..ae8516fe9a --- /dev/null +++ b/railties/test/commands/dev_test.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rails/command" + +class Rails::Command::DevTest < ActiveSupport::TestCase + setup :build_app + teardown :teardown_app + + test "`rails dev:cache` creates both caching and restart file when restart file doesn't exist and dev caching is currently off" do + Dir.chdir(app_path) do + assert_not File.exist?("tmp/caching-dev.txt") + assert_not File.exist?("tmp/restart.txt") + + assert_equal <<~OUTPUT, run_dev_cache_command + Development mode is now being cached. + OUTPUT + + assert File.exist?("tmp/caching-dev.txt") + assert File.exist?("tmp/restart.txt") + end + end + + test "`rails dev:cache` creates caching file and touches restart file when dev caching is currently off" do + Dir.chdir(app_path) do + app_file("tmp/restart.txt", "") + + assert_not File.exist?("tmp/caching-dev.txt") + assert File.exist?("tmp/restart.txt") + restart_file_time_before = File.mtime("tmp/restart.txt") + + assert_equal <<~OUTPUT, run_dev_cache_command + Development mode is now being cached. + OUTPUT + + assert File.exist?("tmp/caching-dev.txt") + restart_file_time_after = File.mtime("tmp/restart.txt") + assert_operator restart_file_time_before, :<, restart_file_time_after + end + end + + test "`rails dev:cache` removes caching file and touches restart file when dev caching is currently on" do + Dir.chdir(app_path) do + app_file("tmp/caching-dev.txt", "") + app_file("tmp/restart.txt", "") + + assert File.exist?("tmp/caching-dev.txt") + assert File.exist?("tmp/restart.txt") + restart_file_time_before = File.mtime("tmp/restart.txt") + + assert_equal <<~OUTPUT, run_dev_cache_command + Development mode is no longer being cached. + OUTPUT + + assert_not File.exist?("tmp/caching-dev.txt") + restart_file_time_after = File.mtime("tmp/restart.txt") + assert_operator restart_file_time_before, :<, restart_file_time_after + end + end + + private + def run_dev_cache_command + rails "dev:cache" + end +end diff --git a/railties/test/commands/encrypted_test.rb b/railties/test/commands/encrypted_test.rb new file mode 100644 index 0000000000..8b608fe8c0 --- /dev/null +++ b/railties/test/commands/encrypted_test.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "env_helpers" +require "rails/command" +require "rails/commands/encrypted/encrypted_command" + +class Rails::Command::EncryptedCommandTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation, EnvHelpers + + setup :build_app + teardown :teardown_app + + test "edit without editor gives hint" do + run_edit_command("config/tokens.yml.enc", editor: "").tap do |output| + assert_match "No $EDITOR to open file in", output + assert_match "rails encrypted:edit", output + end + end + + test "edit encrypted file" do + # Run twice to ensure file can be reread after first edit pass. + 2.times do + assert_match(/access_key_id: 123/, run_edit_command("config/tokens.yml.enc")) + end + end + + test "edit command does not add master key to gitignore when already exist" do + run_edit_command("config/tokens.yml.enc") + + Dir.chdir(app_path) do + assert_match "/config/master.key", File.read(".gitignore") + end + end + + test "edit command does not add master key when `RAILS_MASTER_KEY` env specified" do + Dir.chdir(app_path) do + key = IO.binread("config/master.key").strip + FileUtils.rm("config/master.key") + + switch_env("RAILS_MASTER_KEY", key) do + run_edit_command("config/tokens.yml.enc") + assert_not File.exist?("config/master.key") + end + end + end + + test "edit encrypts file with custom key" do + run_edit_command("config/tokens.yml.enc", key: "config/tokens.key") + + Dir.chdir(app_path) do + assert File.exist?("config/tokens.yml.enc") + assert File.exist?("config/tokens.key") + + assert_match "/config/tokens.key", File.read(".gitignore") + end + + assert_match(/access_key_id: 123/, run_edit_command("config/tokens.yml.enc", key: "config/tokens.key")) + end + + test "show encrypted file with custom key" do + run_edit_command("config/tokens.yml.enc", key: "config/tokens.key") + + assert_match(/access_key_id: 123/, run_show_command("config/tokens.yml.enc", key: "config/tokens.key")) + end + + test "show command raise error when require_master_key is specified and key does not exist" do + add_to_config "config.require_master_key = true" + + assert_match(/Missing encryption key to decrypt file with/, + run_show_command("config/tokens.yml.enc", key: "unexist.key", allow_failure: true)) + end + + test "show command does not raise error when require_master_key is false and master key does not exist" do + remove_file "config/master.key" + add_to_config "config.require_master_key = false" + + assert_match(/Missing 'config\/master\.key' to decrypt data/, run_show_command("config/tokens.yml.enc")) + end + + test "won't corrupt encrypted file when passed wrong key" do + run_edit_command("config/tokens.yml.enc", key: "config/tokens.key") + + assert_match "passed the wrong key", + run_edit_command("config/tokens.yml.enc", allow_failure: true) + + assert_match(/access_key_id: 123/, run_show_command("config/tokens.yml.enc", key: "config/tokens.key")) + end + + private + def run_edit_command(file, key: nil, editor: "cat", **options) + switch_env("EDITOR", editor) do + rails "encrypted:edit", prepare_args(file, key), **options + end + end + + def run_show_command(file, key: nil, **options) + rails "encrypted:show", prepare_args(file, key), **options + end + + def prepare_args(file, key) + args = [ file ] + args.push("--key", key) if key + args + end +end diff --git a/railties/test/commands/initializers_test.rb b/railties/test/commands/initializers_test.rb new file mode 100644 index 0000000000..bdfbb3021c --- /dev/null +++ b/railties/test/commands/initializers_test.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rails/command" + +class Rails::Command::InitializersTest < ActiveSupport::TestCase + setup :build_app + teardown :teardown_app + + test "`rails initializers` prints out defined initializers invoked by Rails" do + initial_output = run_initializers_command + initial_output_length = initial_output.split("\n").length + + assert_operator initial_output_length, :>, 0 + assert_not initial_output.include?("set_added_test_module") + + add_to_config <<-RUBY + initializer(:set_added_test_module) { } + RUBY + + final_output = run_initializers_command + final_output_length = final_output.split("\n").length + + assert_equal 1, (final_output_length - initial_output_length) + assert final_output.include?("set_added_test_module") + end + + private + def run_initializers_command + rails "initializers" + end +end diff --git a/railties/test/commands/notes_test.rb b/railties/test/commands/notes_test.rb new file mode 100644 index 0000000000..147019e299 --- /dev/null +++ b/railties/test/commands/notes_test.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rails/command" +require "rails/commands/notes/notes_command" + +class Rails::Command::NotesTest < ActiveSupport::TestCase + setup :build_app + teardown :teardown_app + + test "`rails notes` displays results for default directories and default annotations with aligned line number and annotation tag" do + app_file "app/controllers/some_controller.rb", "# OPTIMIZE: note in app directory" + app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory" + app_file "db/some_seeds.rb", "# FIXME: note in db directory" + app_file "lib/some_file.rb", "# TODO: note in lib directory" + app_file "test/some_test.rb", "\n" * 100 + "# FIXME: note in test directory" + + app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory" + + assert_equal <<~OUTPUT, run_notes_command + app/controllers/some_controller.rb: + * [ 1] [OPTIMIZE] note in app directory + + config/initializers/some_initializer.rb: + * [ 1] [TODO] note in config directory + + db/some_seeds.rb: + * [ 1] [FIXME] note in db directory + + lib/some_file.rb: + * [ 1] [TODO] note in lib directory + + test/some_test.rb: + * [101] [FIXME] note in test directory + + OUTPUT + end + + test "`rails notes` displays an empty string when no results were found" do + assert_equal "", run_notes_command + end + + test "`rails notes --annotations` displays results for a single annotation without being prefixed by a tag" do + app_file "db/some_seeds.rb", "# FIXME: note in db directory" + app_file "test/some_test.rb", "# FIXME: note in test directory" + + app_file "app/controllers/some_controller.rb", "# OPTIMIZE: note in app directory" + app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory" + + assert_equal <<~OUTPUT, run_notes_command(["--annotations", "FIXME"]) + db/some_seeds.rb: + * [1] note in db directory + + test/some_test.rb: + * [1] note in test directory + + OUTPUT + end + + test "`rails notes --annotations` displays results for multiple annotations being prefixed by a tag" do + app_file "app/controllers/some_controller.rb", "# FOOBAR: note in app directory" + app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory" + app_file "lib/some_file.rb", "# TODO: note in lib directory" + + app_file "test/some_test.rb", "# FIXME: note in test directory" + + assert_equal <<~OUTPUT, run_notes_command(["--annotations", "FOOBAR", "TODO"]) + app/controllers/some_controller.rb: + * [1] [FOOBAR] note in app directory + + config/initializers/some_initializer.rb: + * [1] [TODO] note in config directory + + lib/some_file.rb: + * [1] [TODO] note in lib directory + + OUTPUT + end + + test "displays results from additional directories added to the default directories from a config file" do + app_file "db/some_seeds.rb", "# FIXME: note in db directory" + app_file "lib/some_file.rb", "# TODO: note in lib directory" + app_file "spec/spec_helper.rb", "# TODO: note in spec" + app_file "spec/models/user_spec.rb", "# TODO: note in model spec" + + add_to_config "config.annotations.register_directories \"spec\"" + + assert_equal <<~OUTPUT, run_notes_command + db/some_seeds.rb: + * [1] [FIXME] note in db directory + + lib/some_file.rb: + * [1] [TODO] note in lib directory + + spec/models/user_spec.rb: + * [1] [TODO] note in model spec + + spec/spec_helper.rb: + * [1] [TODO] note in spec + + OUTPUT + end + + test "displays results from additional file extensions added to the default extensions from a config file" do + add_to_config "config.assets.precompile = []" + add_to_config %q{ config.annotations.register_extensions("scss", "sass") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } } + app_file "db/some_seeds.rb", "# FIXME: note in db directory" + app_file "app/assets/stylesheets/application.css.scss", "// TODO: note in scss" + app_file "app/assets/stylesheets/application.css.sass", "// TODO: note in sass" + + assert_equal <<~OUTPUT, run_notes_command + app/assets/stylesheets/application.css.sass: + * [1] [TODO] note in sass + + app/assets/stylesheets/application.css.scss: + * [1] [TODO] note in scss + + db/some_seeds.rb: + * [1] [FIXME] note in db directory + + OUTPUT + end + + private + def run_notes_command(args = []) + rails "notes", args + end +end diff --git a/railties/test/commands/routes_test.rb b/railties/test/commands/routes_test.rb new file mode 100644 index 0000000000..693e532c5b --- /dev/null +++ b/railties/test/commands/routes_test.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rails/command" +require "rails/commands/routes/routes_command" +require "io/console/size" + +class Rails::Command::RoutesTest < ActiveSupport::TestCase + setup :build_app + teardown :teardown_app + + test "singular resource output in rails routes" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + resource :post + resource :user_permission + end + RUBY + + expected_post_output = [" Prefix Verb URI Pattern Controller#Action", + " new_post GET /post/new(.:format) posts#new", + "edit_post GET /post/edit(.:format) posts#edit", + " post GET /post(.:format) posts#show", + " PATCH /post(.:format) posts#update", + " PUT /post(.:format) posts#update", + " DELETE /post(.:format) posts#destroy", + " POST /post(.:format) posts#create\n"].join("\n") + + output = run_routes_command(["-c", "PostController"]) + assert_equal expected_post_output, output + + expected_perm_output = [" Prefix Verb URI Pattern Controller#Action", + " new_user_permission GET /user_permission/new(.:format) user_permissions#new", + "edit_user_permission GET /user_permission/edit(.:format) user_permissions#edit", + " user_permission GET /user_permission(.:format) user_permissions#show", + " PATCH /user_permission(.:format) user_permissions#update", + " PUT /user_permission(.:format) user_permissions#update", + " DELETE /user_permission(.:format) user_permissions#destroy", + " POST /user_permission(.:format) user_permissions#create\n"].join("\n") + + output = run_routes_command(["-c", "UserPermissionController"]) + assert_equal expected_perm_output, output + end + + test "rails routes with global search key" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/cart', to: 'cart#show' + post '/cart', to: 'cart#create' + get '/basketballs', to: 'basketball#index' + end + RUBY + + output = run_routes_command(["-g", "show"]) + assert_equal <<~MESSAGE, output + Prefix Verb URI Pattern Controller#Action + cart GET /cart(.:format) cart#show + rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show + rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show + rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show + MESSAGE + + output = run_routes_command(["-g", "POST"]) + assert_equal <<~MESSAGE, output + Prefix Verb URI Pattern Controller#Action + POST /cart(.:format) cart#create + rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create + MESSAGE + + output = run_routes_command(["-g", "basketballs"]) + assert_equal " Prefix Verb URI Pattern Controller#Action\n" \ + "basketballs GET /basketballs(.:format) basketball#index\n", output + end + + test "rails routes with controller search key" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/cart', to: 'cart#show' + get '/basketball', to: 'basketball#index' + get '/user_permission', to: 'user_permission#index' + end + RUBY + + expected_cart_output = "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n" + output = run_routes_command(["-c", "cart"]) + assert_equal expected_cart_output, output + + output = run_routes_command(["-c", "Cart"]) + assert_equal expected_cart_output, output + + output = run_routes_command(["-c", "CartController"]) + assert_equal expected_cart_output, output + + expected_perm_output = [" Prefix Verb URI Pattern Controller#Action", + "user_permission GET /user_permission(.:format) user_permission#index\n"].join("\n") + output = run_routes_command(["-c", "user_permission"]) + assert_equal expected_perm_output, output + + output = run_routes_command(["-c", "UserPermission"]) + assert_equal expected_perm_output, output + + output = run_routes_command(["-c", "UserPermissionController"]) + assert_equal expected_perm_output, output + end + + test "rails routes with namespaced controller search key" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + namespace :admin do + resource :post + resource :user_permission + end + end + RUBY + + expected_post_output = [" Prefix Verb URI Pattern Controller#Action", + " new_admin_post GET /admin/post/new(.:format) admin/posts#new", + "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit", + " admin_post GET /admin/post(.:format) admin/posts#show", + " PATCH /admin/post(.:format) admin/posts#update", + " PUT /admin/post(.:format) admin/posts#update", + " DELETE /admin/post(.:format) admin/posts#destroy", + " POST /admin/post(.:format) admin/posts#create\n"].join("\n") + + output = run_routes_command(["-c", "Admin::PostController"]) + assert_equal expected_post_output, output + + output = run_routes_command(["-c", "PostController"]) + assert_equal expected_post_output, output + + expected_perm_output = [" Prefix Verb URI Pattern Controller#Action", + " new_admin_user_permission GET /admin/user_permission/new(.:format) admin/user_permissions#new", + "edit_admin_user_permission GET /admin/user_permission/edit(.:format) admin/user_permissions#edit", + " admin_user_permission GET /admin/user_permission(.:format) admin/user_permissions#show", + " PATCH /admin/user_permission(.:format) admin/user_permissions#update", + " PUT /admin/user_permission(.:format) admin/user_permissions#update", + " DELETE /admin/user_permission(.:format) admin/user_permissions#destroy", + " POST /admin/user_permission(.:format) admin/user_permissions#create\n"].join("\n") + + output = run_routes_command(["-c", "Admin::UserPermissionController"]) + assert_equal expected_perm_output, output + + output = run_routes_command(["-c", "UserPermissionController"]) + assert_equal expected_perm_output, output + end + + test "rails routes displays message when no routes are defined" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + end + RUBY + + assert_equal <<~MESSAGE, run_routes_command + Prefix Verb URI Pattern Controller#Action + rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show + rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show + rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show + update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update + rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create + MESSAGE + end + + test "rails routes with expanded option" do + begin + previous_console_winsize = IO.console.winsize + IO.console.winsize = [0, 27] + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/cart', to: 'cart#show' + end + RUBY + + output = run_routes_command(["--expanded"]) + + assert_equal <<~MESSAGE, output + --[ Route 1 ]-------------- + Prefix | cart + Verb | GET + URI | /cart(.:format) + Controller#Action | cart#show + --[ Route 2 ]-------------- + Prefix | rails_service_blob + Verb | GET + URI | /rails/active_storage/blobs/:signed_id/*filename(.:format) + Controller#Action | active_storage/blobs#show + --[ Route 3 ]-------------- + Prefix | rails_blob_representation + Verb | GET + URI | /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) + Controller#Action | active_storage/representations#show + --[ Route 4 ]-------------- + Prefix | rails_disk_service + Verb | GET + URI | /rails/active_storage/disk/:encoded_key/*filename(.:format) + Controller#Action | active_storage/disk#show + --[ Route 5 ]-------------- + Prefix | update_rails_disk_service + Verb | PUT + URI | /rails/active_storage/disk/:encoded_token(.:format) + Controller#Action | active_storage/disk#update + --[ Route 6 ]-------------- + Prefix | rails_direct_uploads + Verb | POST + URI | /rails/active_storage/direct_uploads(.:format) + Controller#Action | active_storage/direct_uploads#create + MESSAGE + ensure + IO.console.winsize = previous_console_winsize + end + end + + private + def run_routes_command(args = []) + rails "routes", args + end +end diff --git a/railties/test/commands/secrets_test.rb b/railties/test/commands/secrets_test.rb new file mode 100644 index 0000000000..6b9f284a0c --- /dev/null +++ b/railties/test/commands/secrets_test.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "env_helpers" +require "rails/command" +require "rails/commands/secrets/secrets_command" + +class Rails::Command::SecretsCommandTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation, EnvHelpers + + setup :build_app + teardown :teardown_app + + test "edit without editor gives hint" do + assert_match "No $EDITOR to open decrypted secrets in", run_edit_command(editor: "") + end + + test "encrypted secrets are deprecated when using credentials" do + assert_match "Encrypted secrets is deprecated", run_setup_command + assert_equal 1, $?.exitstatus + assert_not File.exist?("config/secrets.yml.enc") + end + + test "encrypted secrets are deprecated when running edit without setup" do + assert_match "Encrypted secrets is deprecated", run_setup_command + assert_equal 1, $?.exitstatus + assert_not File.exist?("config/secrets.yml.enc") + end + + test "encrypted secrets are deprecated for 5.1 config/secrets.yml apps" do + Dir.chdir(app_path) do + FileUtils.rm("config/credentials.yml.enc") + FileUtils.touch("config/secrets.yml") + + assert_match "Encrypted secrets is deprecated", run_setup_command + assert_equal 1, $?.exitstatus + assert_not File.exist?("config/secrets.yml.enc") + end + end + + test "edit secrets" do + prevent_deprecation + + # Run twice to ensure encrypted secrets can be reread after first edit pass. + 2.times do + assert_match(/external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289/, run_edit_command) + end + end + + test "show secrets" do + prevent_deprecation + + assert_match(/external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289/, run_show_command) + end + + private + def prevent_deprecation + Dir.chdir(app_path) do + File.write("config/secrets.yml.key", "f731758c639da2604dfb6bf3d1025de8") + File.write("config/secrets.yml.enc", "sEB0mHxDbeP1/KdnMk00wyzPFACl9K6t0cZWn5/Mfx/YbTHvnI07vrneqHg9kaH3wOS7L6pIQteu1P077OtE4BSx/ZRc/sgQPHyWu/tXsrfHqnPNpayOF/XZqizE91JacSFItNMWpuPsp9ynbzz+7cGhoB1S4aPNIU6u0doMrzdngDbijsaAFJmsHIQh6t/QHoJx--8aMoE0PvUWmw1Iqz--ldFqnM/K0g9k17M8PKoN/Q==") + end + end + + def run_edit_command(editor: "cat") + switch_env("EDITOR", editor) do + rails "secrets:edit", allow_failure: true + end + end + + def run_show_command + rails "secrets:show", allow_failure: true + end + + def run_setup_command + rails "secrets:setup", allow_failure: true + end +end diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index ba688f1e9e..e5b1da6ea4 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -1,88 +1,284 @@ -require 'abstract_unit' -require 'env_helpers' -require 'rails/commands/server' +# frozen_string_literal: true -class Rails::ServerTest < ActiveSupport::TestCase +require "isolation/abstract_unit" +require "env_helpers" +require "rails/command" +require "rails/commands/server/server_command" + +class Rails::Command::ServerCommandTest < ActiveSupport::TestCase include EnvHelpers def test_environment_with_server_option - args = ["thin", "-e", "production"] - options = Rails::Server::Options.new.parse!(args) - assert_equal 'production', options[:environment] - assert_equal 'thin', options[:server] + args = ["-u", "thin", "-e", "production"] + options = parse_arguments(args) + assert_equal "production", options[:environment] + assert_equal "thin", options[:server] end def test_environment_without_server_option - args = ["-e", "production"] - options = Rails::Server::Options.new.parse!(args) - assert_equal 'production', options[:environment] + args = ["-e", "production"] + options = parse_arguments(args) + assert_equal "production", options[:environment] assert_nil options[:server] end + def test_explicit_using_option + args = ["-u", "thin"] + options = parse_arguments(args) + assert_equal "thin", options[:server] + end + + def test_using_server_mistype + assert_match(/Could not find server "tin". Maybe you meant "thin"?/, run_command("--using", "tin")) + end + + def test_using_positional_argument_deprecation + assert_match(/DEPRECATION WARNING/, run_command("tin")) + end + + def test_using_known_server_that_isnt_in_the_gemfile + assert_match(/Could not load server "unicorn". Maybe you need to the add it to the Gemfile/, run_command("-u", "unicorn")) + end + + def test_daemon_with_option + args = ["-d"] + options = parse_arguments(args) + assert_equal true, options[:daemonize] + end + + def test_daemon_without_option + args = [] + options = parse_arguments(args) + assert_equal false, options[:daemonize] + end + def test_server_option_without_environment - args = ["thin"] - options = Rails::Server::Options.new.parse!(args) - assert_nil options[:environment] - assert_equal 'thin', options[:server] + args = ["-u", "thin"] + with_rack_env nil do + with_rails_env nil do + options = parse_arguments(args) + assert_equal "development", options[:environment] + assert_equal "thin", options[:server] + end + end end def test_environment_with_rails_env with_rack_env nil do - with_rails_env 'production' do - server = Rails::Server.new - assert_equal 'production', server.options[:environment] + with_rails_env "production" do + options = parse_arguments + assert_equal "production", options[:environment] end end end def test_environment_with_rack_env with_rails_env nil do - with_rack_env 'production' do - server = Rails::Server.new - assert_equal 'production', server.options[:environment] + with_rack_env "production" do + options = parse_arguments + assert_equal "production", options[:environment] end end end + def test_environment_with_port + switch_env "PORT", "1234" do + options = parse_arguments + assert_equal 1234, options[:Port] + end + end + + def test_environment_with_host + switch_env "HOST", "1.2.3.4" do + assert_deprecated do + options = parse_arguments + assert_equal "1.2.3.4", options[:Host] + end + end + end + + def test_environment_with_binding + switch_env "BINDING", "1.2.3.4" do + options = parse_arguments + assert_equal "1.2.3.4", options[:Host] + end + end + + def test_caching_without_option + args = [] + options = parse_arguments(args) + assert_nil options[:caching] + end + + def test_caching_with_option + args = ["--dev-caching"] + options = parse_arguments(args) + assert_equal true, options[:caching] + + args = ["--no-dev-caching"] + options = parse_arguments(args) + assert_equal false, options[:caching] + end + + def test_early_hints_with_option + args = ["--early-hints"] + options = parse_arguments(args) + assert_equal true, options[:early_hints] + end + + def test_early_hints_is_nil_by_default + args = [] + options = parse_arguments(args) + assert_nil options[:early_hints] + end + def test_log_stdout with_rack_env nil do with_rails_env nil do args = [] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal true, options[:log_stdout] args = ["-e", "development"] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal true, options[:log_stdout] + args = ["-e", "development", "-d"] + options = parse_arguments(args) + assert_equal false, options[:log_stdout] + args = ["-e", "production"] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal false, options[:log_stdout] - with_rack_env 'development' do + args = ["-e", "development", "--no-log-to-stdout"] + options = parse_arguments(args) + assert_equal false, options[:log_stdout] + + args = ["-e", "production", "--log-to-stdout"] + options = parse_arguments(args) + assert_equal true, options[:log_stdout] + + with_rack_env "development" do args = [] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal true, options[:log_stdout] end - with_rack_env 'production' do + with_rack_env "production" do args = [] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal false, options[:log_stdout] end - with_rails_env 'development' do + with_rails_env "development" do args = [] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal true, options[:log_stdout] end - with_rails_env 'production' do + with_rails_env "production" do args = [] - options = Rails::Server::Options.new.parse!(args) + options = parse_arguments(args) assert_equal false, options[:log_stdout] end end end end + + def test_host + with_rails_env "development" do + options = parse_arguments([]) + assert_equal "localhost", options[:Host] + end + + with_rails_env "production" do + options = parse_arguments([]) + assert_equal "0.0.0.0", options[:Host] + end + + with_rails_env "development" do + args = ["-b", "127.0.0.1"] + options = parse_arguments(args) + assert_equal "127.0.0.1", options[:Host] + end + end + + def test_argument_precedence_over_environment_variable + switch_env "PORT", "1234" do + args = ["-p", "5678"] + options = parse_arguments(args) + assert_equal 5678, options[:Port] + end + + switch_env "PORT", "1234" do + args = ["-p", "3000"] + options = parse_arguments(args) + assert_equal 3000, options[:Port] + end + + switch_env "BINDING", "1.2.3.4" do + args = ["-b", "127.0.0.1"] + options = parse_arguments(args) + assert_equal "127.0.0.1", options[:Host] + end + end + + def test_records_user_supplied_options + server_options = parse_arguments(["-p", 3001]) + assert_equal [:Port], server_options[:user_supplied_options] + + server_options = parse_arguments(["--port", 3001]) + assert_equal [:Port], server_options[:user_supplied_options] + + server_options = parse_arguments(["-p3001", "-C", "--binding", "127.0.0.1"]) + assert_equal [:Port, :Host, :caching], server_options[:user_supplied_options] + + server_options = parse_arguments(["--port=3001"]) + assert_equal [:Port], server_options[:user_supplied_options] + + switch_env "BINDING", "1.2.3.4" do + server_options = parse_arguments + assert_equal [:Host], server_options[:user_supplied_options] + end + end + + def test_default_options + server = Rails::Server.new + old_default_options = server.default_options + + Dir.chdir("..") do + assert_equal old_default_options, server.default_options + end + end + + def test_restart_command_contains_customized_options + original_args = ARGV.dup + args = %w(-p 4567 -b 127.0.0.1 -c dummy_config.ru -d -e test -P tmp/server.pid -C) + ARGV.replace args + + expected = "bin/rails server -p 4567 -b 127.0.0.1 -c dummy_config.ru -d -e test -P tmp/server.pid -C --restart" + + assert_equal expected, parse_arguments(args)[:restart_cmd] + ensure + ARGV.replace original_args + end + + def test_served_url + args = %w(-u webrick -b 127.0.0.1 -p 4567) + server = Rails::Server.new(parse_arguments(args)) + assert_equal "http://127.0.0.1:4567", server.served_url + end + + private + def run_command(*args) + build_app + rails "server", *args + ensure + teardown_app + end + + def parse_arguments(args = []) + Rails::Command::ServerCommand.new([], args).server_options + end end diff --git a/railties/test/configuration/middleware_stack_proxy_test.rb b/railties/test/configuration/middleware_stack_proxy_test.rb index d5072614cf..bc72b7f0c9 100644 --- a/railties/test/configuration/middleware_stack_proxy_test.rb +++ b/railties/test/configuration/middleware_stack_proxy_test.rb @@ -1,8 +1,10 @@ -require 'active_support' -require 'active_support/testing/autorun' -require 'rails/configuration' -require 'active_support/test_case' -require 'minitest/mock' +# frozen_string_literal: true + +require "active_support" +require "active_support/testing/autorun" +require "rails/configuration" +require "active_support/test_case" +require "minitest/mock" module Rails module Configuration @@ -50,12 +52,12 @@ module Rails private - def assert_playback(msg_name, args) - mock = Minitest::Mock.new - mock.expect :send, nil, [msg_name, args] - @stack.merge_into(mock) - mock.verify - end + def assert_playback(msg_name, args) + mock = Minitest::Mock.new + mock.expect :send, nil, [msg_name, args] + @stack.merge_into(mock) + mock.verify + end end end end diff --git a/railties/test/console_helpers.rb b/railties/test/console_helpers.rb new file mode 100644 index 0000000000..67f55fdc45 --- /dev/null +++ b/railties/test/console_helpers.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +begin + require "pty" +rescue LoadError +end + +module ConsoleHelpers + def assert_output(expected, io, timeout = 10) + timeout = Time.now + timeout + + output = +"" + until output.include?(expected) || Time.now > timeout + if IO.select([io], [], [], 0.1) + output << io.read(1) + end + end + + assert_includes output, expected, "#{expected.inspect} expected, but got:\n\n#{output}" + end + + def available_pty? + defined?(PTY) && PTY.respond_to?(:open) + end +end diff --git a/railties/test/credentials_test.rb b/railties/test/credentials_test.rb new file mode 100644 index 0000000000..03370e0fc7 --- /dev/null +++ b/railties/test/credentials_test.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" + +class Rails::CredentialsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + setup :build_app + teardown :teardown_app + + test "reads credentials from environment specific path" do + with_credentials do |content, key| + Dir.chdir(app_path) do + Dir.mkdir("config/credentials") + File.write("config/credentials/production.yml.enc", content) + File.write("config/credentials/production.key", key) + end + + app("production") + + assert_equal "revealed", Rails.application.credentials.mystery + end + end + + test "reads credentials from customized path and key" do + with_credentials do |content, key| + Dir.chdir(app_path) do + Dir.mkdir("config/credentials") + File.write("config/credentials/staging.yml.enc", content) + File.write("config/credentials/staging.key", key) + end + + add_to_env_config("production", "config.credentials.content_path = config.root.join('config/credentials/staging.yml.enc')") + add_to_env_config("production", "config.credentials.key_path = config.root.join('config/credentials/staging.key')") + app("production") + + assert_equal "revealed", Rails.application.credentials.mystery + end + end + + private + def with_credentials + key = "2117e775dc2024d4f49ddf3aeb585919" + # secret_key_base: secret + # mystery: revealed + content = "vgvKu4MBepIgZ5VHQMMPwnQNsLlWD9LKmJHu3UA/8yj6x+3fNhz3DwL9brX7UA==--qLdxHP6e34xeTAiI--nrcAsleXuo9NqiEuhntAhw==" + yield(content, key) + end +end diff --git a/railties/test/engine/commands_test.rb b/railties/test/engine/commands_test.rb new file mode 100644 index 0000000000..0e5167578d --- /dev/null +++ b/railties/test/engine/commands_test.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "console_helpers" + +class Rails::Engine::CommandsTest < ActiveSupport::TestCase + include ConsoleHelpers + + def setup + @destination_root = Dir.mktmpdir("bukkits") + Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --mountable` } + end + + def teardown + FileUtils.rm_rf(@destination_root) + end + + def test_help_command_work_inside_engine + output = capture(:stderr) do + Dir.chdir(plugin_path) { `bin/rails --help` } + end + assert_no_match "NameError", output + end + + def test_runner_command_work_inside_engine + output = capture(:stdout) do + Dir.chdir(plugin_path) { system({ "SKIP_REQUIRE_WEBPACKER" => "true" }, "bin/rails runner 'puts Rails.env'") } + end + + assert_equal "test", output.strip + end + + def test_console_command_work_inside_engine + skip "PTY unavailable" unless available_pty? + + primary, replica = PTY.open + spawn_command("console", replica) + assert_output(">", primary) + ensure + primary.puts "quit" + end + + def test_dbconsole_command_work_inside_engine + skip "PTY unavailable" unless available_pty? + + primary, replica = PTY.open + spawn_command("dbconsole", replica) + assert_output("sqlite>", primary) + ensure + primary.puts ".exit" + end + + def test_server_command_work_inside_engine + skip "PTY unavailable" unless available_pty? + + primary, replica = PTY.open + pid = spawn_command("server", replica) + assert_output("Listening on", primary) + ensure + kill(pid) + end + + private + def plugin_path + "#{@destination_root}/bukkits" + end + + def spawn_command(command, fd) + Process.spawn( + { "SKIP_REQUIRE_WEBPACKER" => "true" }, + "#{plugin_path}/bin/rails #{command}", + in: fd, out: fd, err: fd + ) + end + + def kill(pid) + Process.kill("TERM", pid) + Process.wait(pid) + rescue Errno::ESRCH + end +end diff --git a/railties/test/engine/test_test.rb b/railties/test/engine/test_test.rb new file mode 100644 index 0000000000..18af85a0aa --- /dev/null +++ b/railties/test/engine/test_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class Rails::Engine::TestTest < ActiveSupport::TestCase + setup do + @destination_root = Dir.mktmpdir("bukkits") + Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --mountable` } + end + + teardown do + FileUtils.rm_rf(@destination_root) + end + + test "automatically synchronize test schema" do + Dir.chdir(plugin_path) do + # In order to confirm that migration files are loaded, generate multiple migration files. + `bin/rails generate model user name:string; + bin/rails generate model todo name:string; + RAILS_ENV=development bin/rails db:migrate` + + output = `bin/rails test test/models/bukkits/user_test.rb` + assert_includes(output, "0 runs, 0 assertions, 0 failures, 0 errors, 0 skips") + end + end + + private + def plugin_path + "#{@destination_root}/bukkits" + end +end diff --git a/railties/test/engine_test.rb b/railties/test/engine_test.rb index f46fb748f5..19379e200c 100644 --- a/railties/test/engine_test.rb +++ b/railties/test/engine_test.rb @@ -1,4 +1,6 @@ -require 'abstract_unit' +# frozen_string_literal: true + +require "abstract_unit" class EngineTest < ActiveSupport::TestCase test "reports routes as available only if they're actually present" do @@ -9,7 +11,7 @@ class EngineTest < ActiveSupport::TestCase end end - assert !engine.routes? + assert_not_predicate engine, :routes? end def test_application_can_be_subclassed diff --git a/railties/test/env_helpers.rb b/railties/test/env_helpers.rb index 330fe150ca..336832b867 100644 --- a/railties/test/env_helpers.rb +++ b/railties/test/env_helpers.rb @@ -1,30 +1,32 @@ -require 'rails' +# frozen_string_literal: true + +require "rails" module EnvHelpers private - def with_rails_env(env) - Rails.instance_variable_set :@_env, nil - switch_env 'RAILS_ENV', env do - switch_env 'RACK_ENV', nil do - yield + def with_rails_env(env) + Rails.instance_variable_set :@_env, nil + switch_env "RAILS_ENV", env do + switch_env "RACK_ENV", nil do + yield + end end end - end - def with_rack_env(env) - Rails.instance_variable_set :@_env, nil - switch_env 'RACK_ENV', env do - switch_env 'RAILS_ENV', nil do - yield + def with_rack_env(env) + Rails.instance_variable_set :@_env, nil + switch_env "RACK_ENV", env do + switch_env "RAILS_ENV", nil do + yield + end end end - end - def switch_env(key, value) - old, ENV[key] = ENV[key], value - yield - ensure - ENV[key] = old - end + def switch_env(key, value) + old, ENV[key] = ENV[key], value + yield + ensure + ENV[key] = old + end end diff --git a/railties/test/fixtures/about_yml_plugins/bad_about_yml/about.yml b/railties/test/fixtures/about_yml_plugins/bad_about_yml/about.yml deleted file mode 100644 index fe80872a16..0000000000 --- a/railties/test/fixtures/about_yml_plugins/bad_about_yml/about.yml +++ /dev/null @@ -1 +0,0 @@ -# an empty YAML file - any content in here seems to get parsed as a string
\ No newline at end of file diff --git a/railties/test/fixtures/about_yml_plugins/bad_about_yml/init.rb b/railties/test/fixtures/about_yml_plugins/bad_about_yml/init.rb deleted file mode 100644 index d4262f8971..0000000000 --- a/railties/test/fixtures/about_yml_plugins/bad_about_yml/init.rb +++ /dev/null @@ -1 +0,0 @@ -# intentionally empty
\ No newline at end of file diff --git a/railties/test/fixtures/about_yml_plugins/plugin_without_about_yml/init.rb b/railties/test/fixtures/about_yml_plugins/plugin_without_about_yml/init.rb deleted file mode 100644 index d4262f8971..0000000000 --- a/railties/test/fixtures/about_yml_plugins/plugin_without_about_yml/init.rb +++ /dev/null @@ -1 +0,0 @@ -# intentionally empty
\ No newline at end of file diff --git a/railties/test/fixtures/lib/create_test_dummy_template.rb b/railties/test/fixtures/lib/create_test_dummy_template.rb index e4378bbd1a..b9eb6a912d 100644 --- a/railties/test/fixtures/lib/create_test_dummy_template.rb +++ b/railties/test/fixtures/lib/create_test_dummy_template.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + create_dummy_app("spec/dummy") diff --git a/railties/test/fixtures/lib/generators/active_record/fixjour_generator.rb b/railties/test/fixtures/lib/generators/active_record/fixjour_generator.rb index a7d079a1bc..f196971f20 100644 --- a/railties/test/fixtures/lib/generators/active_record/fixjour_generator.rb +++ b/railties/test/fixtures/lib/generators/active_record/fixjour_generator.rb @@ -1,4 +1,6 @@ -require 'rails/generators/active_record' +# frozen_string_literal: true + +require "rails/generators/active_record" module ActiveRecord module Generators diff --git a/railties/test/fixtures/lib/generators/fixjour_generator.rb b/railties/test/fixtures/lib/generators/fixjour_generator.rb index ef3e9edbed..22197835a8 100644 --- a/railties/test/fixtures/lib/generators/fixjour_generator.rb +++ b/railties/test/fixtures/lib/generators/fixjour_generator.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class FixjourGenerator < Rails::Generators::NamedBase end diff --git a/railties/test/fixtures/lib/generators/model_generator.rb b/railties/test/fixtures/lib/generators/model_generator.rb index 9098a8a354..3009472c3d 100644 --- a/railties/test/fixtures/lib/generators/model_generator.rb +++ b/railties/test/fixtures/lib/generators/model_generator.rb @@ -1 +1,3 @@ -raise "I should never be loaded"
\ No newline at end of file +# frozen_string_literal: true + +raise "I should never be loaded" diff --git a/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb b/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb index 078b0f9412..5a847a8bd2 100644 --- a/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb +++ b/railties/test/fixtures/lib/generators/usage_template/usage_template_generator.rb @@ -1,5 +1,7 @@ -require 'rails/generators' +# frozen_string_literal: true + +require "rails/generators" class UsageTemplateGenerator < Rails::Generators::Base - source_root File.expand_path("templates", File.dirname(__FILE__)) + source_root File.expand_path("templates", __dir__) end diff --git a/railties/test/fixtures/lib/rails/generators/foobar/foobar_generator.rb b/railties/test/fixtures/lib/rails/generators/foobar/foobar_generator.rb index d1de8c56fa..159843866c 100644 --- a/railties/test/fixtures/lib/rails/generators/foobar/foobar_generator.rb +++ b/railties/test/fixtures/lib/rails/generators/foobar/foobar_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Foobar class FoobarGenerator < Rails::Generators::Base end diff --git a/railties/test/fixtures/lib/template.rb b/railties/test/fixtures/lib/template.rb index c14a1a8784..44083c25e8 100644 --- a/railties/test/fixtures/lib/template.rb +++ b/railties/test/fixtures/lib/template.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + say "It works from file!" diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index 2857dae07e..a2b35124c5 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -1,7 +1,8 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/app/app_generator' -require 'env_helpers' -require 'minitest/mock' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/app/app_generator" +require "env_helpers" class ActionsTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -12,240 +13,422 @@ class ActionsTest < Rails::Generators::TestCase def setup Rails.application = TestApp::Application - @mock_generator = Minitest::Mock.new super end def teardown Rails.application = TestApp::Application.instance - @mock_generator.verify end def test_invoke_other_generator_with_shortcut - action :invoke, 'model', ['my_model'] - assert_file 'app/models/my_model.rb', /MyModel/ + action :invoke, "model", ["my_model"] + assert_file "app/models/my_model.rb", /MyModel/ end def test_invoke_other_generator_with_full_namespace - action :invoke, 'rails:model', ['my_model'] - assert_file 'app/models/my_model.rb', /MyModel/ + action :invoke, "rails:model", ["my_model"] + assert_file "app/models/my_model.rb", /MyModel/ end def test_create_file_should_write_data_to_file_path - action :create_file, 'lib/test_file.rb', 'heres test data' - assert_file 'lib/test_file.rb', 'heres test data' + action :create_file, "lib/test_file.rb", "heres test data" + assert_file "lib/test_file.rb", "heres test data" end def test_create_file_should_write_block_contents_to_file_path - action(:create_file, 'lib/test_file.rb'){ 'heres block data' } - assert_file 'lib/test_file.rb', 'heres block data' + action(:create_file, "lib/test_file.rb") { "heres block data" } + assert_file "lib/test_file.rb", "heres block data" end def test_add_source_adds_source_to_gemfile run_generator - action :add_source, 'http://gems.github.com' - assert_file 'Gemfile', /source 'http:\/\/gems\.github\.com'/ + action :add_source, "http://gems.github.com" + assert_file "Gemfile", /source 'http:\/\/gems\.github\.com'/ end def test_add_source_with_block_adds_source_to_gemfile_with_gem run_generator - action :add_source, 'http://gems.github.com' do - gem 'rspec-rails' + action :add_source, "http://gems.github.com" do + gem "rspec-rails" end - assert_file 'Gemfile', /source 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend/ + assert_file "Gemfile", /source 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend/ + end + + def test_add_source_with_block_adds_source_to_gemfile_after_gem + run_generator + action :gem, "will-paginate" + action :add_source, "http://gems.github.com" do + gem "rspec-rails" + end + assert_file "Gemfile", /gem 'will-paginate'\nsource 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend/ end def test_gem_should_put_gem_dependency_in_gemfile run_generator - action :gem, 'will-paginate' - assert_file 'Gemfile', /gem 'will\-paginate'/ + action :gem, "will-paginate" + assert_file "Gemfile", /gem 'will\-paginate'/ end def test_gem_with_version_should_include_version_in_gemfile run_generator - - action :gem, 'rspec', '>=2.0.0.a5' - - assert_file 'Gemfile', /gem 'rspec', '>=2.0.0.a5'/ + action :gem, "rspec", ">= 2.0.0.a5" + action :gem, "RedCloth", ">= 4.1.0", "< 4.2.0" + action :gem, "nokogiri", version: ">= 1.4.2" + action :gem, "faker", version: [">= 0.1.0", "< 0.3.0"] + + assert_file "Gemfile" do |content| + assert_match(/gem 'rspec', '>= 2\.0\.0\.a5'/, content) + assert_match(/gem 'RedCloth', '>= 4\.1\.0', '< 4\.2\.0'/, content) + assert_match(/gem 'nokogiri', '>= 1\.4\.2'/, content) + assert_match(/gem 'faker', '>= 0\.1\.0', '< 0\.3\.0'/, content) + end end def test_gem_should_insert_on_separate_lines run_generator - File.open('Gemfile', 'a') {|f| f.write('# Some content...') } + File.open("Gemfile", "a") { |f| f.write("# Some content...") } - action :gem, 'rspec' - action :gem, 'rspec-rails' + action :gem, "rspec" + action :gem, "rspec-rails" - assert_file 'Gemfile', /^gem 'rspec'$/ - assert_file 'Gemfile', /^gem 'rspec-rails'$/ + assert_file "Gemfile", /^gem 'rspec'$/ + assert_file "Gemfile", /^gem 'rspec-rails'$/ end def test_gem_should_include_options run_generator - action :gem, 'rspec', github: 'dchelimsky/rspec', tag: '1.2.9.rc1' + action :gem, "rspec", github: "dchelimsky/rspec", tag: "1.2.9.rc1" - assert_file 'Gemfile', /gem 'rspec', github: 'dchelimsky\/rspec', tag: '1\.2\.9\.rc1'/ + assert_file "Gemfile", /gem 'rspec', github: 'dchelimsky\/rspec', tag: '1\.2\.9\.rc1'/ end def test_gem_with_non_string_options run_generator - action :gem, 'rspec', require: false - action :gem, 'rspec-rails', group: [:development, :test] + action :gem, "rspec", require: false + action :gem, "rspec-rails", group: [:development, :test] - assert_file 'Gemfile', /^gem 'rspec', require: false$/ - assert_file 'Gemfile', /^gem 'rspec-rails', group: \[:development, :test\]$/ + assert_file "Gemfile", /^gem 'rspec', require: false$/ + assert_file "Gemfile", /^gem 'rspec-rails', group: \[:development, :test\]$/ end def test_gem_falls_back_to_inspect_if_string_contains_single_quote run_generator - action :gem, 'rspec', ">=2.0'0" + action :gem, "rspec", ">=2.0'0" + + assert_file "Gemfile", /^gem 'rspec', ">=2\.0'0"$/ + end + + def test_gem_works_even_if_frozen_string_is_passed_as_argument + run_generator + + action :gem, -"frozen_gem", -"1.0.0" - assert_file 'Gemfile', /^gem 'rspec', ">=2\.0'0"$/ + assert_file "Gemfile", /^gem 'frozen_gem', '1.0.0'$/ end def test_gem_group_should_wrap_gems_in_a_group run_generator action :gem_group, :development, :test do - gem 'rspec-rails' + gem "rspec-rails" end action :gem_group, :test do - gem 'fakeweb' + gem "fakeweb" end - assert_file 'Gemfile', /\ngroup :development, :test do\n gem 'rspec-rails'\nend\n\ngroup :test do\n gem 'fakeweb'\nend/ + assert_file "Gemfile", /\ngroup :development, :test do\n gem 'rspec-rails'\nend\n\ngroup :test do\n gem 'fakeweb'\nend/ + end + + def test_github_should_create_an_indented_block + run_generator + + action :github, "user/repo" do + gem "foo" + gem "bar" + gem "baz" + end + + assert_file "Gemfile", /\ngithub 'user\/repo' do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend/ + end + + def test_github_should_create_an_indented_block_with_options + run_generator + + action :github, "user/repo", a: "correct", other: true do + gem "foo" + gem "bar" + gem "baz" + end + + assert_file "Gemfile", /\ngithub 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend/ + end + + def test_github_should_create_an_indented_block_within_a_group + run_generator + + action :gem_group, :magic do + github "user/repo", a: "correct", other: true do + gem "foo" + gem "bar" + gem "baz" + end + end + + assert_file "Gemfile", /\ngroup :magic do\n github 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\n end\nend\n/ end def test_environment_should_include_data_in_environment_initializer_block run_generator autoload_paths = 'config.autoload_paths += %w["#{Rails.root}/app/extras"]' action :environment, autoload_paths - assert_file 'config/application.rb', / class Application < Rails::Application\n #{Regexp.escape(autoload_paths)}/ + assert_file "config/application.rb", / class Application < Rails::Application\n #{Regexp.escape(autoload_paths)}\n/ end def test_environment_should_include_data_in_environment_initializer_block_with_env_option run_generator autoload_paths = 'config.autoload_paths += %w["#{Rails.root}/app/extras"]' - action :environment, autoload_paths, env: 'development' - assert_file "config/environments/development.rb", /Rails\.application\.configure do\n #{Regexp.escape(autoload_paths)}/ + action :environment, autoload_paths, env: "development" + assert_file "config/environments/development.rb", /Rails\.application\.configure do\n #{Regexp.escape(autoload_paths)}\n/ end def test_environment_with_block_should_include_block_contents_in_environment_initializer_block run_generator action :environment do - _ = '# This wont be added'# assignment to silence parse-time warning "unused literal ignored" - '# This will be added' + _ = "# This wont be added"# assignment to silence parse-time warning "unused literal ignored" + "# This will be added" end - assert_file 'config/application.rb' do |content| + assert_file "config/application.rb" do |content| assert_match(/# This will be added/, content) assert_no_match(/# This wont be added/, content) end end + def test_environment_with_block_should_include_block_contents_with_multiline_data_in_environment_initializer_block + run_generator + data = <<-RUBY + config.encoding = "utf-8" + config.time_zone = "UTC" + RUBY + action(:environment) { data } + assert_file "config/application.rb", / class Application < Rails::Application\n#{Regexp.escape(data.strip_heredoc.indent(4))}/ + end + + def test_environment_should_include_block_contents_with_multiline_data_in_environment_initializer_block_with_env_option + run_generator + data = <<-RUBY + config.encoding = "utf-8" + config.time_zone = "UTC" + RUBY + action(:environment, nil, env: "development") { data } + assert_file "config/environments/development.rb", /Rails\.application\.configure do\n#{Regexp.escape(data.strip_heredoc.indent(2))}/ + end + def test_git_with_symbol_should_run_command_using_git_scm - @mock_generator.expect(:call, nil, ['git init']) - generator.stub(:run, @mock_generator) do + assert_called_with(generator, :run, ["git init"]) do action :git, :init end end def test_git_with_hash_should_run_each_command_using_git_scm - @mock_generator.expect(:call, nil, ["git rm README"]) - @mock_generator.expect(:call, nil, ["git add ."]) - generator.stub(:run, @mock_generator) do - action :git, rm: 'README', add: '.' + assert_called_with(generator, :run, [ ["git rm README"], ["git add ."] ]) do + action :git, rm: "README", add: "." end end def test_vendor_should_write_data_to_file_in_vendor - action :vendor, 'vendor_file.rb', '# vendor data' - assert_file 'vendor/vendor_file.rb', '# vendor data' + action :vendor, "vendor_file.rb", "# vendor data" + assert_file "vendor/vendor_file.rb", "# vendor data\n" + end + + def test_vendor_should_write_data_to_file_with_block_in_vendor + code = <<-RUBY + puts "one" + puts "two" + puts "three" + RUBY + action(:vendor, "vendor_file.rb") { code } + assert_file "vendor/vendor_file.rb", code.strip_heredoc end def test_lib_should_write_data_to_file_in_lib - action :lib, 'my_library.rb', 'class MyLibrary' - assert_file 'lib/my_library.rb', 'class MyLibrary' + action :lib, "my_library.rb", "class MyLibrary" + assert_file "lib/my_library.rb", "class MyLibrary\n" + end + + def test_lib_should_write_data_to_file_with_block_in_lib + code = <<-RUBY + class MyLib + MY_CONSTANT = 123 + end + RUBY + action(:lib, "my_library.rb") { code } + assert_file "lib/my_library.rb", code.strip_heredoc end def test_rakefile_should_write_date_to_file_in_lib_tasks - action :rakefile, 'myapp.rake', 'task run: [:environment]' - assert_file 'lib/tasks/myapp.rake', 'task run: [:environment]' + action :rakefile, "myapp.rake", "task run: [:environment]" + assert_file "lib/tasks/myapp.rake", "task run: [:environment]\n" + end + + def test_rakefile_should_write_date_to_file_with_block_in_lib_tasks + code = <<-RUBY + task rock: :environment do + puts "Rockin'" + end + RUBY + action(:rakefile, "myapp.rake") { code } + assert_file "lib/tasks/myapp.rake", code.strip_heredoc end def test_initializer_should_write_date_to_file_in_config_initializers - action :initializer, 'constants.rb', 'MY_CONSTANT = 42' - assert_file 'config/initializers/constants.rb', 'MY_CONSTANT = 42' + action :initializer, "constants.rb", "MY_CONSTANT = 42" + assert_file "config/initializers/constants.rb", "MY_CONSTANT = 42\n" + end + + def test_initializer_should_write_date_to_file_with_block_in_config_initializers + code = <<-RUBY + MyLib.configure do |config| + config.value = 123 + end + RUBY + action(:initializer, "constants.rb") { code } + assert_file "config/initializers/constants.rb", code.strip_heredoc end def test_generate_should_run_script_generate_with_argument_and_options - @mock_generator.expect(:call, nil, ['bin/rails generate model MyModel', verbose: false]) - generator.stub(:run_ruby_script, @mock_generator) do - action :generate, 'model', 'MyModel' + assert_called_with(generator, :run_ruby_script, ["bin/rails generate model MyModel", verbose: false]) do + action :generate, "model", "MyModel" end end def test_rake_should_run_rake_command_with_default_env - @mock_generator.expect(:call, nil, ["rake log:clear RAILS_ENV=development", verbose: false]) - generator.stub(:run, @mock_generator) do + assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=development", verbose: false]) do with_rails_env nil do - action :rake, 'log:clear' + action :rake, "log:clear" end end end def test_rake_with_env_option_should_run_rake_command_in_env - @mock_generator.expect(:call, nil, ['rake log:clear RAILS_ENV=production', verbose: false]) - generator.stub(:run, @mock_generator) do - action :rake, 'log:clear', env: 'production' + assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=production", verbose: false]) do + action :rake, "log:clear", env: "production" end end - def test_rake_with_rails_env_variable_should_run_rake_command_in_env - @mock_generator.expect(:call, nil, ['rake log:clear RAILS_ENV=production', verbose: false]) - generator.stub(:run, @mock_generator) do + test "rake with RAILS_ENV variable should run rake command in env" do + assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=production", verbose: false]) do with_rails_env "production" do - action :rake, 'log:clear' + action :rake, "log:clear" end end end - def test_env_option_should_win_over_rails_env_variable_when_running_rake - @mock_generator.expect(:call, nil, ['rake log:clear RAILS_ENV=production', verbose: false]) - generator.stub(:run, @mock_generator) do + test "env option should win over RAILS_ENV variable when running rake" do + assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=production", verbose: false]) do with_rails_env "staging" do - action :rake, 'log:clear', env: 'production' + action :rake, "log:clear", env: "production" end end end - def test_rake_with_sudo_option_should_run_rake_command_with_sudo - @mock_generator.expect(:call, nil, ["sudo rake log:clear RAILS_ENV=development", verbose: false]) - generator.stub(:run, @mock_generator) do + test "rake with sudo option should run rake command with sudo" do + assert_called_with(generator, :run, ["sudo rake log:clear RAILS_ENV=development", verbose: false]) do with_rails_env nil do - action :rake, 'log:clear', sudo: true + action :rake, "log:clear", sudo: true + end + end + end + + test "rake command with capture option should run rake command with capture" do + assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=development", verbose: false, capture: true]) do + with_rails_env nil do + action :rake, "log:clear", capture: true + end + end + end + + test "rails command should run rails_command with default env" do + assert_called_with(generator, :run, ["rails log:clear RAILS_ENV=development", verbose: false]) do + with_rails_env nil do + action :rails_command, "log:clear" + end + end + end + + test "rails command with env option should run rails_command with same env" do + assert_called_with(generator, :run, ["rails log:clear RAILS_ENV=production", verbose: false]) do + action :rails_command, "log:clear", env: "production" + end + end + + test "rails command with RAILS_ENV variable should run rails_command in env" do + assert_called_with(generator, :run, ["rails log:clear RAILS_ENV=production", verbose: false]) do + with_rails_env "production" do + action :rails_command, "log:clear" + end + end + end + + def test_env_option_should_win_over_rails_env_variable_when_running_rails + assert_called_with(generator, :run, ["rails log:clear RAILS_ENV=production", verbose: false]) do + with_rails_env "staging" do + action :rails_command, "log:clear", env: "production" + end + end + end + + test "rails command with sudo option should run rails_command with sudo" do + assert_called_with(generator, :run, ["sudo rails log:clear RAILS_ENV=development", verbose: false]) do + with_rails_env nil do + action :rails_command, "log:clear", sudo: true + end + end + end + + test "rails command with capture option should run rails_command with capture" do + assert_called_with(generator, :run, ["rails log:clear RAILS_ENV=development", verbose: false, capture: true]) do + with_rails_env nil do + action :rails_command, "log:clear", capture: true end end end def test_capify_should_run_the_capify_command - @mock_generator.expect(:call, nil, ['capify .', verbose: false]) - generator.stub(:run, @mock_generator) do - action :capify! + content = capture(:stderr) do + assert_called_with(generator, :run, ["capify .", verbose: false]) do + action :capify! + end end + assert_match(/DEPRECATION WARNING: `capify!` is deprecated/, content) end def test_route_should_add_data_to_the_routes_block_in_config_routes run_generator route_command = "route '/login', controller: 'sessions', action: 'new'" action :route, route_command - assert_file 'config/routes.rb', /#{Regexp.escape(route_command)}/ + assert_file "config/routes.rb", /#{Regexp.escape(route_command)}/ + end + + def test_route_should_be_idempotent + run_generator + route_path = File.expand_path("config/routes.rb", destination_root) + + # runs first time, not asserting + action :route, "root 'welcome#index'" + content_1 = File.read(route_path) + + # runs second time + action :route, "root 'welcome#index'" + content_2 = File.read(route_path) + + assert_equal content_1, content_2 end def test_route_should_add_data_with_an_new_line @@ -255,11 +438,18 @@ class ActionsTest < Rails::Generators::TestCase content = File.read(route_path) # Remove all of the comments and blank lines from the routes file - content.gsub!(/^ \#.*\n/, '') - content.gsub!(/^\n/, '') + content.gsub!(/^ \#.*\n/, "") + content.gsub!(/^\n/, "") + + File.write(route_path, content) + + routes = <<-F +Rails.application.routes.draw do + root 'welcome#index' +end +F - File.open(route_path, "wb") { |file| file.write(content) } - assert_file "config/routes.rb", /\.routes\.draw do\n root 'welcome#index'\nend\n\z/ + assert_file "config/routes.rb", routes action :route, "resources :product_lines" @@ -274,8 +464,7 @@ F def test_readme run_generator - 2.times { @mock_generator.expect(:call, destination_root,[]) } - Rails::Generators::AppGenerator.stub(:source_root, @mock_generator) do + assert_called(Rails::Generators::AppGenerator, :source_root, times: 2, returns: destination_root) do assert_match "application up and running", action(:readme, "README.md") end end @@ -283,8 +472,7 @@ F def test_readme_with_quiet generator(default_arguments, quiet: true) run_generator - 2.times { @mock_generator.expect(:call, destination_root,[]) } - Rails::Generators::AppGenerator.stub(:source_root, @mock_generator) do + assert_called(Rails::Generators::AppGenerator, :source_root, times: 2, returns: destination_root) do assert_no_match "application up and running", action(:readme, "README.md") end end @@ -307,10 +495,9 @@ F assert_equal("", action(:log, :yes, "YES")) end - protected + private def action(*args, &block) - capture(:stdout){ generator.send(*args, &block) } + capture(:stdout) { generator.send(*args, &block) } end - end diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb index 9978ad0da1..4f2894e71e 100644 --- a/railties/test/generators/api_app_generator_test.rb +++ b/railties/test/generators/api_app_generator_test.rb @@ -1,17 +1,19 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/app/app_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/app/app_generator" class ApiAppGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper tests Rails::Generators::AppGenerator - arguments [destination_root, '--api'] + arguments [destination_root, "--api"] def setup Rails.application = TestApp::Application super - Kernel::silence_warnings do + Kernel.silence_warnings do Thor::Base.shell.send(:attr_accessor, :always_force) @shell = Thor::Base.shell.new @shell.send(:always_force=, true) @@ -33,64 +35,147 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase def test_api_modified_files run_generator + assert_file ".gitignore" do |content| + assert_no_match(/\/public\/assets/, content) + end + assert_file "Gemfile" do |content| - assert_no_match(/gem 'coffee-rails'/, content) - assert_no_match(/gem 'jquery-rails'/, content) assert_no_match(/gem 'sass-rails'/, content) - assert_no_match(/gem 'jbuilder'/, content) - assert_match(/gem 'active_model_serializers'/, content) + assert_no_match(/gem 'web-console'/, content) + assert_no_match(/gem 'capybara'/, content) + assert_no_match(/gem 'selenium-webdriver'/, content) + assert_match(/# gem 'jbuilder'/, content) + assert_match(/# gem 'rack-cors'/, content) + end + + assert_file "config/application.rb", /config\.api_only = true/ + assert_file "app/controllers/application_controller.rb", /ActionController::API/ + end + + def test_generator_if_skip_action_cable_is_given + run_generator [destination_root, "--api", "--skip-action-cable"] + assert_file "config/application.rb", /#\s+require\s+["']action_cable\/engine["']/ + assert_no_file "config/cable.yml" + assert_no_file "app/channels" + assert_file "Gemfile" do |content| + assert_no_match(/redis/, content) end + end - assert_file "config/application.rb" do |content| - assert_match(/config.api_only = true/, content) + def test_generator_if_skip_action_mailer_is_given + run_generator [destination_root, "--api", "--skip-action-mailer"] + assert_file "config/application.rb", /#\s+require\s+["']action_mailer\/railtie["']/ + assert_file "config/environments/development.rb" do |content| + assert_no_match(/config\.action_mailer/, content) + end + assert_file "config/environments/test.rb" do |content| + assert_no_match(/config\.action_mailer/, content) + end + assert_file "config/environments/production.rb" do |content| + assert_no_match(/config\.action_mailer/, content) end + assert_no_directory "app/mailers" + assert_no_directory "test/mailers" + assert_no_directory "app/views" + end - assert_file "config/initializers/cors.rb" + def test_app_update_does_not_generate_unnecessary_config_files + run_generator - assert_file "config/initializers/wrap_parameters.rb" + generator = Rails::Generators::AppGenerator.new ["rails"], + { api: true, update: true }, { destination_root: destination_root, shell: @shell } + quietly { generator.send(:update_config_files) } - assert_file "app/controllers/application_controller.rb", /ActionController::API/ + assert_no_file "config/initializers/cookies_serializer.rb" + assert_no_file "config/initializers/assets.rb" + assert_no_file "config/initializers/content_security_policy.rb" end - private + def test_app_update_does_not_generate_unnecessary_bin_files + run_generator - def default_files - files = %W( - .gitignore - Gemfile - Rakefile - config.ru - app/controllers - app/mailers - app/models - config/environments - config/initializers - config/locales - db - lib - lib/tasks - log - test/fixtures - test/controllers - test/integration - test/models - tmp - vendor - ) - files.concat %w(bin/bundle bin/rails bin/rake) - files - end + generator = Rails::Generators::AppGenerator.new ["rails"], + { api: true, update: true }, { destination_root: destination_root, shell: @shell } + quietly { generator.send(:update_bin_files) } - def skipped_files - %w(app/assets - app/helpers - app/views - config/initializers/assets.rb - config/initializers/cookies_serializer.rb - config/initializers/session_store.rb - lib/assets - vendor/assets - test/helpers - tmp/cache/assets) + assert_no_file "bin/yarn" end + + private + + def default_files + %w(.gitignore + .ruby-version + README.md + Gemfile + Rakefile + config.ru + app/channels + app/controllers + app/mailers + app/models + app/views/layouts + app/views/layouts/mailer.html.erb + app/views/layouts/mailer.text.erb + bin/rails + bin/rake + bin/setup + bin/update + config/application.rb + config/boot.rb + config/cable.yml + config/environment.rb + config/environments + config/environments/development.rb + config/environments/production.rb + config/environments/test.rb + config/initializers + config/initializers/application_controller_renderer.rb + config/initializers/backtrace_silencers.rb + config/initializers/cors.rb + config/initializers/filter_parameter_logging.rb + config/initializers/inflections.rb + config/initializers/mime_types.rb + config/initializers/wrap_parameters.rb + config/locales + config/locales/en.yml + config/puma.rb + config/routes.rb + config/credentials.yml.enc + config/spring.rb + config/storage.yml + db + db/seeds.rb + lib + lib/tasks + log + test/fixtures + test/controllers + test/integration + test/models + tmp + vendor + ) + end + + def skipped_files + %w(app/assets + app/helpers + app/views/layouts/application.html.erb + bin/yarn + config/initializers/assets.rb + config/initializers/cookies_serializer.rb + config/initializers/content_security_policy.rb + lib/assets + test/helpers + tmp/cache/assets + public/404.html + public/422.html + public/500.html + public/apple-touch-icon-precomposed.png + public/apple-touch-icon.png + public/favicon.ico + package.json + ) + end end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index daf362357c..32d00f3157 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -1,37 +1,83 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/app/app_generator' -require 'generators/shared_generator_tests' -require 'mocha/setup' # FIXME: stop using mocha +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/app/app_generator" +require "generators/shared_generator_tests" DEFAULT_APP_FILES = %w( .gitignore + .ruby-version README.md Gemfile Rakefile config.ru - app/assets/javascripts - app/assets/stylesheets + app/assets/config/manifest.js app/assets/images + app/javascript + app/javascript/channels + app/javascript/channels/consumer.js + app/javascript/channels/index.js + app/javascript/packs/application.js + app/assets/stylesheets + app/assets/stylesheets/application.css + app/channels/application_cable/channel.rb + app/channels/application_cable/connection.rb app/controllers + app/controllers/application_controller.rb app/controllers/concerns app/helpers + app/helpers/application_helper.rb app/mailers + app/mailers/application_mailer.rb app/models + app/models/application_record.rb app/models/concerns app/jobs + app/jobs/application_job.rb app/views/layouts - bin/bundle + app/views/layouts/application.html.erb + app/views/layouts/mailer.html.erb + app/views/layouts/mailer.text.erb bin/rails bin/rake bin/setup + bin/update + bin/yarn + config/application.rb + config/boot.rb + config/cable.yml + config/environment.rb config/environments + config/environments/development.rb + config/environments/production.rb + config/environments/test.rb config/initializers + config/initializers/application_controller_renderer.rb + config/initializers/assets.rb + config/initializers/backtrace_silencers.rb + config/initializers/cookies_serializer.rb + config/initializers/content_security_policy.rb + config/initializers/filter_parameter_logging.rb + config/initializers/inflections.rb + config/initializers/mime_types.rb + config/initializers/wrap_parameters.rb config/locales + config/locales/en.yml + config/puma.rb + config/routes.rb + config/credentials.yml.enc + config/spring.rb + config/storage.yml db + db/seeds.rb lib lib/tasks lib/assets log + package.json + public + storage + test/application_system_test_case.rb test/test_helper.rb test/fixtures test/fixtures/files @@ -40,13 +86,12 @@ DEFAULT_APP_FILES = %w( test/helpers test/mailers test/integration + test/system vendor - vendor/assets - vendor/assets/stylesheets - vendor/assets/javascripts tmp tmp/cache tmp/cache/assets + tmp/storage ) class AppGeneratorTest < Rails::Generators::TestCase @@ -60,13 +105,22 @@ class AppGeneratorTest < Rails::Generators::TestCase ::DEFAULT_APP_FILES end + def test_skip_bundle + assert_not_called(generator([destination_root], skip_bundle: true, skip_webpack_install: true), :bundle_command) do + quietly { generator.invoke_all } + # skip_bundle is only about running bundle install, ensure the Gemfile is still + # generated. + assert_file "Gemfile" + end + end + def test_assets run_generator - assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+'application', media: 'all', 'data-turbolinks-track' => true/) - assert_file("app/views/layouts/application.html.erb", /javascript_include_tag\s+'application', 'data-turbolinks-track' => true/) + assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+'application', media: 'all', 'data-turbolinks-track': 'reload'/) + assert_file("app/views/layouts/application.html.erb", /javascript_pack_tag\s+'application', 'data-turbolinks-track': 'reload'/) assert_file("app/assets/stylesheets/application.css") - assert_file("app/assets/javascripts/application.js") + assert_file("app/javascript/packs/application.js") end def test_application_job_file_present @@ -75,7 +129,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_invalid_application_name_raises_an_error - content = capture(:stderr){ run_generator [File.join(destination_root, "43-things")] } + content = capture(:stderr) { run_generator [File.join(destination_root, "43-things")] } assert_equal "Invalid application name 43-things. Please give a name which does not start with numbers.\n", content end @@ -86,12 +140,12 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_application_new_exits_with_non_zero_code_on_invalid_application_name - quietly { system 'rails new test --no-rc' } + quietly { system "rails new test --no-rc" } assert_equal false, $?.success? end def test_application_new_exits_with_message_and_non_zero_code_when_generating_inside_existing_rails_directory - app_root = File.join(destination_root, 'myfirstapp') + app_root = File.join(destination_root, "myfirstapp") run_generator [app_root] output = nil Dir.chdir(app_root) do @@ -102,7 +156,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_application_new_show_help_message_inside_existing_rails_directory - app_root = File.join(destination_root, 'myfirstapp') + app_root = File.join(destination_root, "myfirstapp") run_generator [app_root] output = Dir.chdir(app_root) do `rails new --help` @@ -117,35 +171,31 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator [app_root] - Rails.application.config.root = app_moved_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) - - FileUtils.mv(app_root, app_moved_root) + stub_rails_application(app_moved_root) do + Rails.application.stub(:is_a?, -> *args { Rails::Application }) do + FileUtils.mv(app_root, app_moved_root) - # make sure we are in correct dir - FileUtils.cd(app_moved_root) + # make sure we are in correct dir + FileUtils.cd(app_moved_root) - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, - destination_root: app_moved_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_file "myapp_moved/config/environment.rb", /Rails\.application\.initialize!/ - assert_file "myapp_moved/config/initializers/session_store.rb", /_myapp_session/ + generator = Rails::Generators::AppGenerator.new ["rails"], [], + destination_root: app_moved_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "myapp_moved/config/environment.rb", /Rails\.application\.initialize!/ + end + end end - def test_rails_update_generates_correct_session_key - app_root = File.join(destination_root, 'myapp') + def test_app_update_generates_correct_session_key + app_root = File.join(destination_root, "myapp") run_generator [app_root] - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) - - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_file "myapp/config/initializers/session_store.rb", /_myapp_session/ + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + end end def test_new_application_use_json_serialzier @@ -154,98 +204,272 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file("config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/) end - def test_rails_update_keep_the_cookie_serializer_if_it_is_already_configured - app_root = File.join(destination_root, 'myapp') - run_generator [app_root] + def test_new_application_not_include_api_initializers + run_generator - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) + assert_no_file "config/initializers/cors.rb" + end - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/) + def test_new_application_doesnt_need_defaults + run_generator + assert_no_file "config/initializers/new_framework_defaults_6_0.rb" end - def test_rails_update_does_not_create_callback_terminator_initializer - app_root = File.join(destination_root, 'myapp') + def test_new_application_load_defaults + app_root = File.join(destination_root, "myfirstapp") run_generator [app_root] - FileUtils.rm("#{app_root}/config/initializers/callback_terminator.rb") + output = nil - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) + assert_file "#{app_root}/config/application.rb", /\s+config\.load_defaults #{Rails::VERSION::STRING.to_f}/ - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_no_file "#{app_root}/config/initializers/callback_terminator.rb" + Dir.chdir(app_root) do + output = `SKIP_REQUIRE_WEBPACKER=true ./bin/rails r "puts Rails.application.config.assets.unknown_asset_fallback"` + end + + assert_equal "false\n", output end - def test_rails_update_does_not_remove_callback_terminator_initializer_if_already_present - app_root = File.join(destination_root, 'myapp') - run_generator [app_root] + def test_csp_initializer_include_connect_src_example + run_generator - FileUtils.touch("#{app_root}/config/initializers/callback_terminator.rb") + assert_file "config/initializers/content_security_policy.rb" do |content| + assert_match(/# policy\.connect_src/, content) + end + end - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) + def test_app_update_keep_the_cookie_serializer_if_it_is_already_configured + app_root = File.join(destination_root, "myapp") + run_generator [app_root] - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_file "#{app_root}/config/initializers/callback_terminator.rb" + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/) + end end - def test_rails_update_set_the_cookie_serializer_to_marchal_if_it_is_not_already_configured - app_root = File.join(destination_root, 'myapp') + def test_app_update_set_the_cookie_serializer_to_marshal_if_it_is_not_already_configured + app_root = File.join(destination_root, "myapp") run_generator [app_root] FileUtils.rm("#{app_root}/config/initializers/cookies_serializer.rb") - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) - - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/) + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file("#{app_root}/config/initializers/cookies_serializer.rb", + /Valid options are :json, :marshal, and :hybrid\.\nRails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/) + end end - def test_rails_update_does_not_create_active_record_belongs_to_required_by_default - app_root = File.join(destination_root, 'myapp') + def test_app_update_create_new_framework_defaults + app_root = File.join(destination_root, "myapp") run_generator [app_root] - FileUtils.rm("#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb") + assert_no_file "#{app_root}/config/initializers/new_framework_defaults_6_0.rb" - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { update: true }, { destination_root: app_root, shell: @shell } + generator.send(:app_const) + quietly { generator.send(:update_config_files) } - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_no_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb" + assert_file "#{app_root}/config/initializers/new_framework_defaults_6_0.rb" + end + end + + def test_app_update_does_not_create_rack_cors + app_root = File.join(destination_root, "myapp") + run_generator [app_root] + + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_no_file "#{app_root}/config/initializers/cors.rb" + end end - def test_rails_update_does_not_remove_active_record_belongs_to_required_by_default_if_already_present - app_root = File.join(destination_root, 'myapp') + def test_app_update_does_not_remove_rack_cors_if_already_present + app_root = File.join(destination_root, "myapp") run_generator [app_root] - FileUtils.touch("#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb") + FileUtils.touch("#{app_root}/config/initializers/cors.rb") + + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + assert_file "#{app_root}/config/initializers/cors.rb" + end + end + + def test_app_update_does_not_generate_yarn_contents_when_bin_yarn_is_not_used + app_root = File.join(destination_root, "myapp") + run_generator [app_root, "--skip-javascript"] + + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_javascript: true }, { destination_root: app_root, shell: @shell } + generator.send(:app_const) + quietly { generator.send(:update_bin_files) } + + assert_no_file "#{app_root}/bin/yarn" + + assert_file "#{app_root}/bin/setup" do |content| + assert_no_match(/system\('bin\/yarn'\)/, content) + end + + assert_file "#{app_root}/bin/update" do |content| + assert_no_match(/system\('bin\/yarn'\)/, content) + end + end + end + + def test_app_update_does_not_generate_assets_initializer_when_skip_sprockets_is_given + app_root = File.join(destination_root, "myapp") + run_generator [app_root, "--skip-sprockets"] + + stub_rails_application(app_root) do + generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_sprockets: true }, { destination_root: app_root, shell: @shell } + generator.send(:app_const) + quietly { generator.send(:update_config_files) } + + assert_no_file "#{app_root}/config/initializers/assets.rb" + end + end + + def test_app_update_does_not_generate_spring_contents_when_skip_spring_is_given + app_root = File.join(destination_root, "myapp") + run_generator [app_root, "--skip-spring"] + + FileUtils.cd(app_root) do + quietly { system("bin/rails app:update") } + end + + assert_no_file "#{app_root}/config/spring.rb" + end + + def test_app_update_does_not_generate_action_cable_contents_when_skip_action_cable_is_given + app_root = File.join(destination_root, "myapp") + run_generator [app_root, "--skip-action-cable"] + + FileUtils.cd(app_root) do + quietly { system("bin/rails app:update") } + end + + assert_no_file "#{app_root}/config/cable.yml" + assert_file "#{app_root}/config/environments/production.rb" do |content| + assert_no_match(/config\.action_cable/, content) + end + end + + def test_app_update_does_not_generate_bootsnap_contents_when_skip_bootsnap_is_given + app_root = File.join(destination_root, "myapp") + run_generator [app_root, "--skip-bootsnap"] + + FileUtils.cd(app_root) do + quietly { system("bin/rails app:update") } + end + + assert_file "#{app_root}/config/boot.rb" do |content| + assert_no_match(/require 'bootsnap\/setup'/, content) + end + end + + def test_gem_for_active_storage + run_generator + assert_file "Gemfile", /^# gem 'image_processing'/ + end + + def test_gem_for_active_storage_when_skip_active_storage_is_given + run_generator [destination_root, "--skip-active-storage"] + + assert_no_gem "image_processing" + end + + def test_app_update_does_not_generate_active_storage_contents_when_skip_active_storage_is_given + app_root = File.join(destination_root, "myapp") + run_generator [app_root, "--skip-active-storage"] + + FileUtils.cd(app_root) do + quietly { system("bin/rails app:update") } + end + + assert_file "#{app_root}/config/environments/development.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end + + assert_file "#{app_root}/config/environments/production.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end + + assert_file "#{app_root}/config/environments/test.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end + + assert_no_file "#{app_root}/config/storage.yml" + end + + def test_app_update_does_not_generate_active_storage_contents_when_skip_active_record_is_given + app_root = File.join(destination_root, "myapp") + run_generator [app_root, "--skip-active-record"] + + FileUtils.cd(app_root) do + quietly { system("bin/rails app:update") } + end + + assert_file "#{app_root}/config/environments/development.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end + + assert_file "#{app_root}/config/environments/production.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end + + assert_file "#{app_root}/config/environments/test.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end + + assert_no_file "#{app_root}/config/storage.yml" + end + + def test_app_update_does_not_change_config_target_version + run_generator + + FileUtils.cd(destination_root) do + config = "config/application.rb" + content = File.read(config) + File.write(config, content.gsub(/config\.load_defaults #{Rails::VERSION::STRING.to_f}/, "config.load_defaults 5.1")) + quietly { system("bin/rails app:update") } + end + + assert_file "config/application.rb", /\s+config\.load_defaults 5\.1/ + end + + def test_app_update_does_not_change_app_name_when_app_name_is_hyphenated_name + app_root = File.join(destination_root, "hyphenated-app") + run_generator [app_root, "-d", "postgresql"] + + assert_file "#{app_root}/config/database.yml" do |content| + assert_match(/hyphenated_app_development/, content) + assert_no_match(/hyphenated-app_development/, content) + end + + assert_file "#{app_root}/config/cable.yml" do |content| + assert_match(/hyphenated_app/, content) + assert_no_match(/hyphenated-app/, content) + end - Rails.application.config.root = app_root - Rails.application.class.stubs(:name).returns("Myapp") - Rails.application.stubs(:is_a?).returns(Rails::Application) + FileUtils.cd(app_root) do + quietly { system("bin/rails app:update") } + end - generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell - generator.send(:app_const) - quietly { generator.send(:update_config_files) } - assert_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb" + assert_file "#{app_root}/config/cable.yml" do |content| + assert_match(/hyphenated_app/, content) + assert_no_match(/hyphenated-app/, content) + end end def test_application_names_are_not_singularized @@ -256,7 +480,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_gemfile_has_no_whitespace_errors run_generator absolute = File.expand_path("Gemfile", destination_root) - File.open(absolute, 'r') do |f| + File.open(absolute, "r") do |f| f.each_line do |line| assert_no_match %r{/^[ \t]+$/}, line end @@ -273,13 +497,13 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_config_another_database + def test_config_mysql_database run_generator([destination_root, "-d", "mysql"]) assert_file "config/database.yml", /mysql/ if defined?(JRUBY_VERSION) assert_gem "activerecord-jdbcmysql-adapter" else - assert_gem "mysql2" + assert_gem "mysql2", "'>= 0.4.4'" end end @@ -294,7 +518,7 @@ class AppGeneratorTest < Rails::Generators::TestCase if defined?(JRUBY_VERSION) assert_gem "activerecord-jdbcpostgresql-adapter" else - assert_gem "pg" + assert_gem "pg", "'>= 0.18', '< 2.0'" end end @@ -331,122 +555,109 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_generator_without_skips - run_generator - assert_file "config/application.rb", /\s+require\s+["']rails\/all["']/ - assert_file "config/environments/development.rb" do |content| - assert_match(/config\.action_mailer\.raise_delivery_errors = false/, content) - end - assert_file "config/environments/test.rb" do |content| - assert_match(/config\.action_mailer\.delivery_method = :test/, content) - end - assert_file "config/environments/production.rb" do |content| - assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content) - end + def test_generator_defaults_to_puma_version + run_generator [destination_root] + assert_gem "puma", "'~> 3.11'" end - def test_generator_if_skip_active_record_is_given - run_generator [destination_root, "--skip-active-record"] - assert_no_file "config/database.yml" - assert_no_file "config/initializers/active_record_belongs_to_required_by_default.rb" - assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ - assert_file "test/test_helper.rb" do |helper_content| - assert_no_match(/fixtures :all/, helper_content) - end + def test_generator_if_skip_puma_is_given + run_generator [destination_root, "--skip-puma"] + assert_no_file "config/puma.rb" + assert_no_gem "puma" end - def test_generator_if_skip_action_mailer_is_given - run_generator [destination_root, "--skip-action-mailer"] - assert_file "config/application.rb", /#\s+require\s+["']action_mailer\/railtie["']/ - assert_file "config/environments/development.rb" do |content| - assert_no_match(/config\.action_mailer/, content) - end - assert_file "config/environments/test.rb" do |content| - assert_no_match(/config\.action_mailer/, content) - end - assert_file "config/environments/production.rb" do |content| - assert_no_match(/config\.action_mailer/, content) - end - end + def test_generator_has_assets_gems + run_generator - def test_generator_if_skip_sprockets_is_given - run_generator [destination_root, "--skip-sprockets"] - assert_no_file "config/initializers/assets.rb" - assert_file "config/application.rb" do |content| - assert_match(/#\s+require\s+["']sprockets\/railtie["']/, content) - end - assert_file "Gemfile" do |content| - assert_no_match(/sass-rails/, content) - assert_no_match(/uglifier/, content) - assert_match(/coffee-rails/, content) - end - assert_file "config/environments/development.rb" do |content| - assert_no_match(/config\.assets\.debug = true/, content) - end - assert_file "config/environments/production.rb" do |content| - assert_no_match(/config\.assets\.digest = true/, content) - assert_no_match(/config\.assets\.js_compressor = :uglifier/, content) - assert_no_match(/config\.assets\.css_compressor = :sass/, content) - end + assert_gem "sass-rails" end - def test_inclusion_of_javascript_runtime + def test_action_cable_redis_gems run_generator - if defined?(JRUBY_VERSION) - assert_gem "therubyrhino" - else - assert_file "Gemfile", /# gem 'therubyracer', platforms: :ruby/ - end + assert_file "Gemfile", /^# gem 'redis'/ end - def test_jquery_is_the_default_javascript_library - run_generator - assert_file "app/assets/javascripts/application.js" do |contents| - assert_match %r{^//= require jquery}, contents - assert_match %r{^//= require jquery_ujs}, contents - end - assert_gem "jquery-rails" + def test_generator_if_skip_test_is_given + run_generator [destination_root, "--skip-test"] + + assert_file "config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/ + + assert_no_gem "capybara" + assert_no_gem "selenium-webdriver" + assert_no_gem "chromedriver-helper" + + assert_no_directory("test") + end + + def test_generator_if_skip_system_test_is_given + run_generator [destination_root, "--skip-system-test"] + assert_no_gem "capybara" + assert_no_gem "selenium-webdriver" + assert_no_gem "chromedriver-helper" + + assert_directory("test") + + assert_no_directory("test/system") end - def test_other_javascript_libraries - run_generator [destination_root, '-j', 'prototype'] - assert_file "app/assets/javascripts/application.js" do |contents| - assert_match %r{^//= require prototype}, contents - assert_match %r{^//= require prototype_ujs}, contents + def test_does_not_generate_system_test_files_if_skip_system_test_is_given + run_generator [destination_root, "--skip-system-test"] + + Dir.chdir(destination_root) do + quietly { `./bin/rails g scaffold User` } + + assert_no_file("test/application_system_test_case.rb") + assert_no_file("test/system/users_test.rb") end - assert_gem "prototype-rails" end def test_javascript_is_skipped_if_required run_generator [destination_root, "--skip-javascript"] - assert_no_file "app/assets/javascripts" - assert_no_file "vendor/assets/javascripts" + assert_no_file "app/javascript" assert_file "app/views/layouts/application.html.erb" do |contents| assert_match(/stylesheet_link_tag\s+'application', media: 'all' %>/, contents) - assert_no_match(/javascript_include_tag\s+'application' \%>/, contents) - end - - assert_file "Gemfile" do |content| - assert_no_match(/coffee-rails/, content) - assert_no_match(/jquery-rails/, content) + assert_no_match(/javascript_pack_tag\s+'application'/, contents) end end def test_inclusion_of_jbuilder run_generator - assert_gem 'jbuilder' + assert_gem "jbuilder" end def test_inclusion_of_a_debugger run_generator if defined?(JRUBY_VERSION) || RUBY_ENGINE == "rbx" - assert_file "Gemfile" do |content| - assert_no_match(/byebug/, content) - end + assert_no_gem "byebug" + else + assert_gem "byebug" + end + end + + def test_inclusion_of_listen_related_configuration_by_default + run_generator + if RbConfig::CONFIG["host_os"] =~ /darwin|linux/ + assert_listen_related_configuration else - assert_gem 'byebug' + assert_no_listen_related_configuration + end + end + + def test_non_inclusion_of_listen_related_configuration_if_skip_listen + run_generator [destination_root, "--skip-listen"] + assert_no_listen_related_configuration + end + + def test_evented_file_update_checker_config + run_generator + assert_file "config/environments/development.rb" do |content| + if RbConfig::CONFIG["host_os"] =~ /darwin|linux/ + assert_match(/^\s*config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + else + assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + end end end @@ -456,13 +667,15 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_usage_read_from_file - File.expects(:read).returns("USAGE FROM FILE") - assert_equal "USAGE FROM FILE", Rails::Generators::AppGenerator.desc + assert_called(File, :read, returns: "USAGE FROM FILE") do + assert_equal "USAGE FROM FILE", Rails::Generators::AppGenerator.desc + end end def test_default_usage - Rails::Generators::AppGenerator.expects(:usage_path).returns(nil) - assert_match(/Create rails files for app generator/, Rails::Generators::AppGenerator.desc) + assert_called(Rails::Generators::AppGenerator, :usage_path, returns: nil) do + assert_match(/Create rails files for app generator/, Rails::Generators::AppGenerator.desc) + end end def test_default_namespace @@ -470,55 +683,48 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_file_is_added_for_backwards_compatibility - action :file, 'lib/test_file.rb', 'heres test data' - assert_file 'lib/test_file.rb', 'heres test data' - end - - def test_tests_are_removed_from_frameworks_if_skip_test_is_given - run_generator [destination_root, "--skip-test"] - assert_file "config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/ + action :file, "lib/test_file.rb", "heres test data" + assert_file "lib/test_file.rb", "heres test data" end - def test_no_active_record_or_tests_if_skips_given - run_generator [destination_root, "--skip-test", "--skip-active-record"] - assert_file "config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/ - assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ - assert_file "config/application.rb", /\s+require\s+["']active_job\/railtie["']/ + def test_pretend_option + output = run_generator [File.join(destination_root, "myapp"), "--pretend"] + assert_no_match(/run bundle install/, output) + assert_no_match(/run git init/, output) end - def test_new_hash_style - run_generator - assert_file "config/initializers/session_store.rb" do |file| - assert_match(/config.session_store :cookie_store, key: '_.+_session'/, file) - end + def test_quiet_option + output = run_generator [File.join(destination_root, "myapp"), "--quiet"] + assert_empty output end - def test_pretend_option - output = run_generator [File.join(destination_root, "myapp"), "--pretend"] - assert_no_match(/run bundle install/, output) + def test_force_option_overwrites_every_file_except_master_key + run_generator [File.join(destination_root, "myapp")] + output = run_generator [File.join(destination_root, "myapp"), "--force"] + assert_match(/force/, output) + assert_no_match("force config/master.key", output) end def test_application_name_with_spaces path = File.join(destination_root, "foo bar") # This also applies to MySQL apps but not with SQLite - run_generator [path, "-d", 'postgresql'] + run_generator [path, "-d", "postgresql"] assert_file "foo bar/config/database.yml", /database: foo_bar_development/ - assert_file "foo bar/config/initializers/session_store.rb", /key: '_foo_bar/ end def test_web_console run_generator - assert_gem 'web-console' + assert_gem "web-console" end def test_web_console_with_dev_option - run_generator [destination_root, "--dev"] + run_generator [destination_root, "--dev", "--skip-bundle"] assert_file "Gemfile" do |content| assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) - assert_no_match(/gem 'web-console', '~> 2.0'/, content) + assert_no_match(/\Agem 'web-console', '>= 3\.3\.0'\z/, content) end end @@ -527,92 +733,202 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "Gemfile" do |content| assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) - assert_no_match(/gem 'web-console', '~> 2.0'/, content) + assert_no_match(/\Agem 'web-console', '>= 3\.3\.0'\z/, content) end end + def test_generation_runs_bundle_install + generator([destination_root], skip_webpack_install: true) + + assert_bundler_command_called("install") + end + + def test_dev_option + generator([destination_root], dev: true, skip_webpack_install: true) + + assert_bundler_command_called("install") + rails_path = File.expand_path("../../..", Rails.root) + assert_file "Gemfile", /^gem\s+["']rails["'],\s+path:\s+["']#{Regexp.escape(rails_path)}["']$/ + end + + def test_edge_option + generator([destination_root], edge: true, skip_webpack_install: true) + + assert_bundler_command_called("install") + assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["']$} + end + def test_spring run_generator - assert_gem 'spring' + assert_gem "spring" + end + + def test_bundler_binstub + generator([destination_root], skip_webpack_install: true) + + assert_bundler_command_called("binstubs bundler") end def test_spring_binstubs jruby_skip "spring doesn't run on JRuby" - generator.stubs(:bundle_command).with('install') - generator.expects(:bundle_command).with('exec spring binstub --all').once - quietly { generator.invoke_all } + + generator([destination_root], skip_webpack_install: true) + + assert_bundler_command_called("exec spring binstub --all") end def test_spring_no_fork jruby_skip "spring doesn't run on JRuby" - Process.stubs(:respond_to?).with(:fork).returns(false) - run_generator + assert_called_with(Process, :respond_to?, [[:fork], [:fork]], returns: false) do + run_generator - assert_file "Gemfile" do |content| - assert_no_match(/spring/, content) + assert_no_gem "spring" end end def test_skip_spring run_generator [destination_root, "--skip-spring"] - assert_file "Gemfile" do |content| - assert_no_match(/spring/, content) - end + assert_no_file "config/spring.rb" + assert_no_gem "spring" end def test_spring_with_dev_option - run_generator [destination_root, "--dev"] + run_generator [destination_root, "--dev", "--skip-bundle"] - assert_file "Gemfile" do |content| - assert_no_match(/spring/, content) + assert_no_gem "spring" + end + + def test_skip_javascript_option + command_check = -> command, *_ do + @called ||= 0 + if command == "webpacker:install" + @called += 1 + assert_equal 0, @called, "webpacker:install expected not to be called, but was called #{@called} times." + end + end + + generator([destination_root], skip_javascript: true).stub(:rails_command, command_check) do + generator.stub :bundle_command, nil do + quietly { generator.invoke_all } + end + end + + assert_no_gem "webpacker" + assert_file "config/initializers/content_security_policy.rb" do |content| + assert_no_match(/policy\.connect_src/, content) end end + def test_webpack_option_with_js_framework + command_check = -> command, *_ do + case command + when "webpacker:install" + @webpacker ||= 0 + @webpacker += 1 + assert_equal 1, @webpacker, "webpacker:install expected to be called once, but was called #{@webpacker} times." + when "webpacker:install:react" + @react ||= 0 + @react += 1 + assert_equal 1, @react, "webpacker:install:react expected to be called once, but was called #{@react} times." + end + end + + generator([destination_root], webpack: "react").stub(:rails_command, command_check) do + generator.stub :bundle_command, nil do + quietly { generator.invoke_all } + end + end + + assert_gem "webpacker" + end + + def test_skip_webpack_install + command_check = -> command do + if command == "webpacker:install" + assert false, "webpacker:install expected not to be called." + end + end + + generator([destination_root], skip_webpack_install: true).stub(:rails_command, command_check) do + quietly { generator.invoke_all } + end + + assert_gem "webpacker" + end + def test_generator_if_skip_turbolinks_is_given run_generator [destination_root, "--skip-turbolinks"] - assert_file "Gemfile" do |content| - assert_no_match(/turbolinks/, content) - end + assert_no_gem "turbolinks" assert_file "app/views/layouts/application.html.erb" do |content| assert_no_match(/data-turbolinks-track/, content) end - assert_file "app/assets/javascripts/application.js" do |content| + assert_file "app/javascript/packs/application.js" do |content| assert_no_match(/turbolinks/, content) end end - def test_gitignore_when_sqlite3 - run_generator + def test_bootsnap + run_generator [destination_root, "--no-skip-bootsnap"] - assert_file '.gitignore' do |content| - assert_match(/sqlite3/, content) + unless defined?(JRUBY_VERSION) + assert_gem "bootsnap" + assert_file "config/boot.rb" do |content| + assert_match(/require 'bootsnap\/setup'/, content) + end + else + assert_no_gem "bootsnap" + assert_file "config/boot.rb" do |content| + assert_no_match(/require 'bootsnap\/setup'/, content) + end end end - def test_gitignore_when_no_active_record - run_generator [destination_root, '--skip-active-record'] + def test_skip_bootsnap + run_generator [destination_root, "--skip-bootsnap"] - assert_file '.gitignore' do |content| - assert_no_match(/sqlite/i, content) + assert_no_gem "bootsnap" + assert_file "config/boot.rb" do |content| + assert_no_match(/require 'bootsnap\/setup'/, content) end end - def test_gitignore_when_non_sqlite3_db - run_generator([destination_root, "-d", "mysql"]) + def test_bootsnap_with_dev_option + run_generator [destination_root, "--dev", "--skip-bundle"] - assert_file '.gitignore' do |content| - assert_no_match(/sqlite/i, content) + assert_no_gem "bootsnap" + assert_file "config/boot.rb" do |content| + assert_no_match(/require 'bootsnap\/setup'/, content) end end + def test_inclusion_of_ruby_version + run_generator + + assert_file "Gemfile" do |content| + assert_match(/ruby '#{RUBY_VERSION}'/, content) + end + assert_file ".ruby-version" do |content| + if ENV["RBENV_VERSION"] + assert_match(/#{ENV["RBENV_VERSION"]}/, content) + elsif ENV["rvm_ruby_string"] + assert_match(/#{ENV["rvm_ruby_string"]}/, content) + else + assert_match(/#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}/, content) + end + end + end + + def test_version_control_initializes_git_repo + run_generator [destination_root] + assert_directory ".git" + end + def test_create_keeps run_generator folders_with_keep = %w( app/assets/images - app/mailers - app/models app/controllers/concerns app/models/concerns lib/tasks @@ -626,7 +942,6 @@ class AppGeneratorTest < Rails::Generators::TestCase test/helpers test/integration tmp - vendor/assets/stylesheets ) folders_with_keep.each do |folder| assert_file("#{folder}/.keep") @@ -635,7 +950,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_psych_gem run_generator - gem_regex = /gem 'psych',\s+'~> 2.0',\s+platforms: :rbx/ + gem_regex = /gem 'psych',\s+'~> 2\.0',\s+platforms: :rbx/ assert_file "Gemfile" do |content| if defined?(Rubinius) @@ -647,27 +962,115 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_after_bundle_callback - path = 'http://example.org/rails_template' - template = %{ after_bundle { run 'echo ran after_bundle' } } + path = "http://example.org/rails_template" + template = +%{ after_bundle { run 'echo ran after_bundle' } } template.instance_eval "def read; self; end" # Make the string respond to read - generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) + check_open = -> *args do + assert_equal [ path, "Accept" => "application/x-thor-template" ], args + template + end + + sequence = ["git init", "install", "binstubs bundler", "exec spring binstub --all", "webpacker:install", "echo ran after_bundle"] + @sequence_step ||= 0 + ensure_bundler_first = -> command, options = nil do + assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}" + @sequence_step += 1 + end - bundler_first = sequence('bundle, binstubs, after_bundle') - generator.expects(:bundle_command).with('install').once.in_sequence(bundler_first) - generator.expects(:bundle_command).with('exec spring binstub --all').in_sequence(bundler_first) - generator.expects(:run).with('echo ran after_bundle').in_sequence(bundler_first) + generator([destination_root], template: path).stub(:open, check_open, template) do + generator.stub(:bundle_command, ensure_bundler_first) do + generator.stub(:run, ensure_bundler_first) do + generator.stub(:rails_command, ensure_bundler_first) do + quietly { generator.invoke_all } + end + end + end + end - quietly { generator.invoke_all } + assert_equal 6, @sequence_step end - protected + def test_gitignore + run_generator - def action(*args, &block) - capture(:stdout) { generator.send(*args, &block) } + assert_file ".gitignore" do |content| + assert_match(/config\/master\.key/, content) + end end - def assert_gem(gem) - assert_file "Gemfile", /^\s*gem\s+["']#{gem}["']$*/ + def test_system_tests_directory_generated + run_generator + + assert_directory("test/system") + assert_file("test/system/.keep") end + + unless Gem.win_platform? + def test_master_key_is_only_readable_by_the_owner + run_generator + + stat = File.stat("config/master.key") + assert_equal "100600", sprintf("%o", stat.mode) + end + end + + private + def stub_rails_application(root) + Rails.application.config.root = root + Rails.application.class.stub(:name, "Myapp") do + yield + end + end + + def action(*args, &block) + capture(:stdout) { generator.send(*args, &block) } + end + + def assert_gem(gem, constraint = nil) + if constraint + assert_file "Gemfile", /^\s*gem\s+["']#{gem}["'], #{constraint}$*/ + else + assert_file "Gemfile", /^\s*gem\s+["']#{gem}["']$*/ + end + end + + def assert_no_gem(gem) + assert_file "Gemfile" do |content| + assert_no_match(gem, content) + end + end + + def assert_listen_related_configuration + assert_gem "listen" + assert_gem "spring-watcher-listen" + + assert_file "config/environments/development.rb" do |content| + assert_match(/^\s*config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + end + end + + def assert_no_listen_related_configuration + assert_no_gem "listen" + + assert_file "config/environments/development.rb" do |content| + assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content) + end + end + + def assert_bundler_command_called(target_command) + command_check = -> command do + @command_called ||= 0 + + case command + when target_command + @command_called += 1 + assert_equal 1, @command_called, "#{command} expected to be called once, but was called #{@command_called} times." + end + end + + generator.stub :bundle_command, command_check do + quietly { generator.invoke_all } + end + end end diff --git a/railties/test/generators/application_record_generator_test.rb b/railties/test/generators/application_record_generator_test.rb new file mode 100644 index 0000000000..2c0aa7211b --- /dev/null +++ b/railties/test/generators/application_record_generator_test.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/application_record/application_record_generator" + +class ApplicationRecordGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + + def test_application_record_skeleton_is_created + run_generator + assert_file "app/models/application_record.rb" do |record| + assert_match(/class ApplicationRecord < ActiveRecord::Base/, record) + assert_match(/self\.abstract_class = true/, record) + end + end +end diff --git a/railties/test/generators/argv_scrubber_test.rb b/railties/test/generators/argv_scrubber_test.rb index 31e07bc8da..9ef61dc978 100644 --- a/railties/test/generators/argv_scrubber_test.rb +++ b/railties/test/generators/argv_scrubber_test.rb @@ -1,7 +1,9 @@ -require 'active_support/test_case' -require 'active_support/testing/autorun' -require 'rails/generators/rails/app/app_generator' -require 'tempfile' +# frozen_string_literal: true + +require "active_support/test_case" +require "active_support/testing/autorun" +require "rails/generators/rails/app/app_generator" +require "tempfile" module Rails module Generators @@ -11,7 +13,7 @@ module Rails # *must* act this way, I just want to prevent regressions. def test_version - ['-v', '--version'].each do |str| + ["-v", "--version"].each do |str| scrubber = ARGVScrubber.new [str] output = nil exit_code = nil @@ -26,37 +28,37 @@ module Rails end def test_default_help - argv = ['zomg', 'how', 'are', 'you'] + argv = ["zomg", "how", "are", "you"] scrubber = ARGVScrubber.new argv args = scrubber.prepare! - assert_equal ['--help'] + argv.drop(1), args + assert_equal ["--help"] + argv.drop(1), args end def test_prepare_returns_args - scrubber = ARGVScrubber.new ['hi mom'] + scrubber = ARGVScrubber.new ["hi mom"] args = scrubber.prepare! - assert_equal '--help', args.first + assert_equal "--help", args.first end def test_no_mutations - scrubber = ARGVScrubber.new ['hi mom'].freeze + scrubber = ARGVScrubber.new ["hi mom"].freeze args = scrubber.prepare! - assert_equal '--help', args.first + assert_equal "--help", args.first end def test_new_command_no_rc scrubber = Class.new(ARGVScrubber) { def self.default_rc_file - File.join(Dir.tmpdir, 'whatever') + File.join(Dir.tmpdir, "whatever") end - }.new ['new'] + }.new ["new"] args = scrubber.prepare! assert_equal [], args end def test_new_homedir_rc - file = Tempfile.new 'myrcfile' - file.puts '--hello-world' + file = Tempfile.new "myrcfile" + file.puts "--hello-world" file.flush message = nil @@ -65,10 +67,10 @@ module Rails file.path end define_method(:puts) { |msg| message = msg } - }.new ['new'] + }.new ["new"] args = scrubber.prepare! - assert_equal ['--hello-world'], args - assert_match 'hello-world', message + assert_equal ["--hello-world"], args + assert_match "hello-world", message assert_match file.path, message ensure file.close @@ -76,33 +78,32 @@ module Rails end def test_rc_whitespace_separated - file = Tempfile.new 'myrcfile' - file.puts '--hello --world' + file = Tempfile.new "myrcfile" + file.puts "--hello --world" file.flush - message = nil scrubber = Class.new(ARGVScrubber) { - define_method(:puts) { |msg| message = msg } - }.new ['new', "--rc=#{file.path}"] + define_method(:puts) { |msg| } + }.new ["new", "--rc=#{file.path}"] args = scrubber.prepare! - assert_equal ['--hello', '--world'], args + assert_equal ["--hello", "--world"], args ensure file.close file.unlink end def test_new_rc_option - file = Tempfile.new 'myrcfile' - file.puts '--hello-world' + file = Tempfile.new "myrcfile" + file.puts "--hello-world" file.flush message = nil scrubber = Class.new(ARGVScrubber) { define_method(:puts) { |msg| message = msg } - }.new ['new', "--rc=#{file.path}"] + }.new ["new", "--rc=#{file.path}"] args = scrubber.prepare! - assert_equal ['--hello-world'], args - assert_match 'hello-world', message + assert_equal ["--hello-world"], args + assert_match "hello-world", message assert_match file.path, message ensure file.close @@ -110,14 +111,14 @@ module Rails end def test_new_rc_option_and_custom_options - file = Tempfile.new 'myrcfile' - file.puts '--hello' - file.puts '--world' + file = Tempfile.new "myrcfile" + file.puts "--hello" + file.puts "--world" file.flush scrubber = Class.new(ARGVScrubber) { define_method(:puts) { |msg| } - }.new ['new', 'tenderapp', '--love', "--rc=#{file.path}"] + }.new ["new", "tenderapp", "--love", "--rc=#{file.path}"] args = scrubber.prepare! assert_equal ["tenderapp", "--hello", "--world", "--love"], args @@ -127,7 +128,7 @@ module Rails end def test_no_rc - scrubber = ARGVScrubber.new ['new', '--no-rc'] + scrubber = ARGVScrubber.new ["new", "--no-rc"] args = scrubber.prepare! assert_equal [], args end diff --git a/railties/test/generators/assets_generator_test.rb b/railties/test/generators/assets_generator_test.rb index a2b94f2e50..83d2429acf 100644 --- a/railties/test/generators/assets_generator_test.rb +++ b/railties/test/generators/assets_generator_test.rb @@ -1,5 +1,7 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/assets/assets_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/assets/assets_generator" class AssetsGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -7,13 +9,11 @@ class AssetsGeneratorTest < Rails::Generators::TestCase def test_assets run_generator - assert_file "app/assets/javascripts/posts.js" assert_file "app/assets/stylesheets/posts.css" end def test_skipping_assets - run_generator ["posts", "--no-stylesheets", "--no-javascripts"] - assert_no_file "app/assets/javascripts/posts.js" + run_generator ["posts", "--no-stylesheets"] assert_no_file "app/assets/stylesheets/posts.css" end end diff --git a/railties/test/generators/channel_generator_test.rb b/railties/test/generators/channel_generator_test.rb new file mode 100644 index 0000000000..1cb8465539 --- /dev/null +++ b/railties/test/generators/channel_generator_test.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/channel/channel_generator" + +class ChannelGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + tests Rails::Generators::ChannelGenerator + + def test_application_cable_skeleton_is_created + run_generator ["books"] + + assert_file "app/channels/application_cable/channel.rb" do |cable| + assert_match(/module ApplicationCable\n class Channel < ActionCable::Channel::Base\n/, cable) + end + + assert_file "app/channels/application_cable/connection.rb" do |cable| + assert_match(/module ApplicationCable\n class Connection < ActionCable::Connection::Base\n/, cable) + end + end + + def test_channel_is_created + run_generator ["chat"] + + assert_file "app/channels/chat_channel.rb" do |channel| + assert_match(/class ChatChannel < ApplicationCable::Channel/, channel) + end + + assert_file "app/javascript/channels/chat_channel.js" do |channel| + assert_match(/import consumer from "\.\/consumer"\s+consumer\.subscriptions\.create\("ChatChannel/, channel) + end + end + + def test_channel_with_multiple_actions_is_created + run_generator ["chat", "speak", "mute"] + + assert_file "app/channels/chat_channel.rb" do |channel| + assert_match(/class ChatChannel < ApplicationCable::Channel/, channel) + assert_match(/def speak/, channel) + assert_match(/def mute/, channel) + end + + assert_file "app/javascript/channels/chat_channel.js" do |channel| + assert_match(/import consumer from "\.\/consumer"\s+consumer\.subscriptions\.create\("ChatChannel/, channel) + assert_match(/,\n\n speak/, channel) + assert_match(/,\n\n mute: function\(\) \{\n return this\.perform\('mute'\);\n \}\n\}\);/, channel) + end + end + + def test_channel_asset_is_not_created_when_skip_assets_is_passed + run_generator ["chat", "--skip-assets"] + + assert_file "app/channels/chat_channel.rb" do |channel| + assert_match(/class ChatChannel < ApplicationCable::Channel/, channel) + end + + assert_no_file "app/javascript/channels/chat_channel.js" + end + + def test_consumer_js_is_created_if_not_present_already + run_generator ["chat"] + FileUtils.rm("#{destination_root}/app/javascript/channels/index.js") + FileUtils.rm("#{destination_root}/app/javascript/channels/consumer.js") + run_generator ["camp"] + + assert_file "app/javascript/channels/index.js" + assert_file "app/javascript/channels/consumer.js" + end + + def test_channel_on_revoke + run_generator ["chat"] + run_generator ["chat"], behavior: :revoke + + assert_no_file "app/channels/chat_channel.rb" + assert_no_file "app/javascript/channels/chat_channel.js" + + assert_file "app/channels/application_cable/channel.rb" + assert_file "app/channels/application_cable/connection.rb" + assert_file "app/javascript/channels/index.js" + assert_file "app/javascript/channels/consumer.js" + end + + def test_channel_suffix_is_not_duplicated + run_generator ["chat_channel"] + + assert_no_file "app/channels/chat_channel_channel.rb" + assert_file "app/channels/chat_channel.rb" + + assert_no_file "app/javascript/channels/chat_channel_channel.js" + assert_file "app/javascript/channels/chat_channel.js" + end +end diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb index 1351151afb..8786756c68 100644 --- a/railties/test/generators/controller_generator_test.rb +++ b/railties/test/generators/controller_generator_test.rb @@ -1,5 +1,7 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/controller/controller_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/controller/controller_generator" class ControllerGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -19,7 +21,7 @@ class ControllerGeneratorTest < Rails::Generators::TestCase def test_check_class_collision Object.send :const_set, :ObjectController, Class.new - content = capture(:stderr){ run_generator ["object"] } + content = capture(:stderr) { run_generator ["object"] } assert_match(/The name 'ObjectController' is either already used in your application or reserved/, content) ensure Object.send :remove_const, :ObjectController @@ -37,13 +39,11 @@ class ControllerGeneratorTest < Rails::Generators::TestCase def test_invokes_assets run_generator - assert_file "app/assets/javascripts/account.js" assert_file "app/assets/stylesheets/account.css" end def test_does_not_invoke_assets_if_required run_generator ["account", "--skip-assets"] - assert_no_file "app/assets/javascripts/account.js" assert_no_file "app/assets/stylesheets/account.css" end @@ -65,7 +65,7 @@ class ControllerGeneratorTest < Rails::Generators::TestCase def test_add_routes run_generator - assert_file "config/routes.rb", /get 'account\/foo'/, /get 'account\/bar'/ + assert_file "config/routes.rb", /^ get 'account\/foo'/, /^ get 'account\/bar'/ end def test_skip_routes @@ -100,4 +100,37 @@ class ControllerGeneratorTest < Rails::Generators::TestCase assert_match(/^ namespace :admin do\n get 'dashboard\/index'\n end$/, route) end end + + def test_namespaced_routes_with_multiple_actions_are_created_in_routes + run_generator ["admin/dashboard", "index", "show"] + assert_file "config/routes.rb" do |route| + assert_match(/^ namespace :admin do\n get 'dashboard\/index'\n get 'dashboard\/show'\n end$/, route) + end + end + + def test_does_not_add_routes_when_action_is_not_specified + run_generator ["admin/dashboard"] + assert_file "config/routes.rb" do |routes| + assert_no_match(/namespace :admin/, routes) + end + end + + def test_controller_suffix_is_not_duplicated + run_generator ["account_controller"] + + assert_no_file "app/controllers/account_controller_controller.rb" + assert_file "app/controllers/account_controller.rb" + + assert_no_file "app/views/account_controller/" + assert_file "app/views/account/" + + assert_no_file "test/controllers/account_controller_controller_test.rb" + assert_file "test/controllers/account_controller_test.rb" + + assert_no_file "app/helpers/account_controller_helper.rb" + assert_file "app/helpers/account_helper.rb" + + assert_no_file "app/assets/stylesheets/account_controller.css" + assert_file "app/assets/stylesheets/account.css" + end end diff --git a/railties/test/generators/create_migration_test.rb b/railties/test/generators/create_migration_test.rb index e16a77479a..2ae38045c5 100644 --- a/railties/test/generators/create_migration_test.rb +++ b/railties/test/generators/create_migration_test.rb @@ -1,5 +1,7 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/migration/migration_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/migration/migration_generator" class CreateMigrationTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -19,12 +21,12 @@ class CreateMigrationTest < Rails::Generators::TestCase end def create_migration(destination_path = default_destination_path, config = {}, generator_options = {}, &block) - migration_name = File.basename(destination_path, '.rb') + migration_name = File.basename(destination_path, ".rb") generator([migration_name], generator_options) generator.set_migration_assigns!(destination_path) dir, base = File.split(destination_path) - timestamped_destination_path = File.join(dir, ["%migration_number%", base].join('_')) + timestamped_destination_path = File.join(dir, ["%migration_number%", base].join("_")) @migration = Rails::Generators::Actions::CreateMigration.new(generator, timestamped_destination_path, block || "contents", config) end @@ -46,7 +48,7 @@ class CreateMigrationTest < Rails::Generators::TestCase def test_invoke create_migration - assert_match(/create db\/migrate\/1_create_articles.rb\n/, invoke!) + assert_match(/create db\/migrate\/1_create_articles\.rb\n/, invoke!) assert_file @migration.destination end @@ -67,8 +69,8 @@ class CreateMigrationTest < Rails::Generators::TestCase migration_exists! create_migration - assert_match(/identical db\/migrate\/1_create_articles.rb\n/, invoke!) - assert @migration.identical? + assert_match(/identical db\/migrate\/1_create_articles\.rb\n/, invoke!) + assert_predicate @migration, :identical? end def test_invoke_when_exists_not_identical @@ -84,8 +86,8 @@ class CreateMigrationTest < Rails::Generators::TestCase create_migration(dest, force: true) { "different content" } stdout = invoke! - assert_match(/remove db\/migrate\/1_migration.rb\n/, stdout) - assert_match(/create db\/migrate\/2_migration.rb\n/, stdout) + assert_match(/remove db\/migrate\/1_migration\.rb\n/, stdout) + assert_match(/create db\/migrate\/2_migration\.rb\n/, stdout) assert_file @migration.destination assert_no_file @existing_migration.destination end @@ -97,8 +99,8 @@ class CreateMigrationTest < Rails::Generators::TestCase end stdout = invoke! - assert_match(/remove db\/migrate\/1_create_articles.rb\n/, stdout) - assert_match(/create db\/migrate\/2_create_articles.rb\n/, stdout) + assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, stdout) + assert_match(/create db\/migrate\/2_create_articles\.rb\n/, stdout) assert_no_file @migration.destination end @@ -106,7 +108,7 @@ class CreateMigrationTest < Rails::Generators::TestCase migration_exists! create_migration(default_destination_path, {}, { skip: true }) { "different content" } - assert_match(/skip db\/migrate\/2_create_articles.rb\n/, invoke!) + assert_match(/skip db\/migrate\/2_create_articles\.rb\n/, invoke!) assert_no_file @migration.destination end @@ -114,7 +116,7 @@ class CreateMigrationTest < Rails::Generators::TestCase migration_exists! create_migration - assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!) + assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, revoke!) assert_no_file @existing_migration.destination end @@ -122,13 +124,13 @@ class CreateMigrationTest < Rails::Generators::TestCase migration_exists! create_migration(default_destination_path, {}, { pretend: true }) - assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!) + assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, revoke!) assert_file @existing_migration.destination end def test_revoke_when_no_exists create_migration - assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!) + assert_match(/remove db\/migrate\/1_create_articles\.rb\n/, revoke!) end end diff --git a/railties/test/generators/generated_attribute_test.rb b/railties/test/generators/generated_attribute_test.rb index ee7c009305..772b4f6f0d 100644 --- a/railties/test/generators/generated_attribute_test.rb +++ b/railties/test/generators/generated_attribute_test.rb @@ -1,5 +1,7 @@ -require 'generators/generators_test_helper' -require 'rails/generators/generated_attribute' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/generated_attribute" class GeneratedAttributeTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -51,7 +53,7 @@ class GeneratedAttributeTest < Rails::Generators::TestCase end def test_default_value_is_decimal - assert_field_default_value :decimal, '9.99' + assert_field_default_value :decimal, "9.99" end def test_default_value_is_datetime @@ -65,7 +67,7 @@ class GeneratedAttributeTest < Rails::Generators::TestCase end def test_default_value_is_string - assert_field_default_value :string, 'MyString' + assert_field_default_value :string, "MyString" end def test_default_value_for_type @@ -74,7 +76,7 @@ class GeneratedAttributeTest < Rails::Generators::TestCase end def test_default_value_is_text - assert_field_default_value :text, 'MyText' + assert_field_default_value :text, "MyText" end def test_default_value_is_boolean @@ -89,64 +91,64 @@ class GeneratedAttributeTest < Rails::Generators::TestCase def test_default_value_is_empty_string %w(foo bar baz).each do |attribute_type| - assert_field_default_value attribute_type, '' + assert_field_default_value attribute_type, "" end end def test_human_name assert_equal( - 'Full name', - create_generated_attribute(:string, 'full_name').human_name + "Full name", + create_generated_attribute(:string, "full_name").human_name ) end def test_reference_is_true %w(references belongs_to).each do |attribute_type| - assert create_generated_attribute(attribute_type).reference? + assert_predicate create_generated_attribute(attribute_type), :reference? end end def test_reference_is_false %w(foo bar baz).each do |attribute_type| - assert !create_generated_attribute(attribute_type).reference? + assert_not_predicate create_generated_attribute(attribute_type), :reference? end end def test_polymorphic_reference_is_true %w(references belongs_to).each do |attribute_type| - assert create_generated_attribute("#{attribute_type}{polymorphic}").polymorphic? + assert_predicate create_generated_attribute("#{attribute_type}{polymorphic}"), :polymorphic? end end def test_polymorphic_reference_is_false %w(foo bar baz).each do |attribute_type| - assert !create_generated_attribute("#{attribute_type}{polymorphic}").polymorphic? + assert_not_predicate create_generated_attribute("#{attribute_type}{polymorphic}"), :polymorphic? end end def test_blank_type_defaults_to_string_raises_exception - assert_equal :string, create_generated_attribute(nil, 'title').type - assert_equal :string, create_generated_attribute("", 'title').type + assert_equal :string, create_generated_attribute(nil, "title").type + assert_equal :string, create_generated_attribute("", "title").type end def test_handles_index_names_for_references - assert_equal "post", create_generated_attribute('string', 'post').index_name - assert_equal "post_id", create_generated_attribute('references', 'post').index_name - assert_equal "post_id", create_generated_attribute('belongs_to', 'post').index_name - assert_equal ["post_id", "post_type"], create_generated_attribute('references{polymorphic}', 'post').index_name + assert_equal "post", create_generated_attribute("string", "post").index_name + assert_equal "post_id", create_generated_attribute("references", "post").index_name + assert_equal "post_id", create_generated_attribute("belongs_to", "post").index_name + assert_equal ["post_id", "post_type"], create_generated_attribute("references{polymorphic}", "post").index_name end def test_handles_column_names_for_references - assert_equal "post", create_generated_attribute('string', 'post').column_name - assert_equal "post_id", create_generated_attribute('references', 'post').column_name - assert_equal "post_id", create_generated_attribute('belongs_to', 'post').column_name + assert_equal "post", create_generated_attribute("string", "post").column_name + assert_equal "post_id", create_generated_attribute("references", "post").column_name + assert_equal "post_id", create_generated_attribute("belongs_to", "post").column_name end def test_parse_required_attribute_with_index att = Rails::Generators::GeneratedAttribute.parse("supplier:references{required}:index") assert_equal "supplier", att.name assert_equal :references, att.type - assert att.has_index? - assert att.required? + assert_predicate att, :has_index? + assert_predicate att, :required? end end diff --git a/railties/test/generators/generator_generator_test.rb b/railties/test/generators/generator_generator_test.rb index dcfeaaa8e0..eaa964cabc 100644 --- a/railties/test/generators/generator_generator_test.rb +++ b/railties/test/generators/generator_generator_test.rb @@ -1,5 +1,7 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/generator/generator_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/generator/generator_generator" class GeneratorGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -12,7 +14,7 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase lib/generators/awesome lib/generators/awesome/USAGE lib/generators/awesome/templates - ).each{ |path| assert_file path } + ).each { |path| assert_file path } assert_file "lib/generators/awesome/awesome_generator.rb", /class AwesomeGenerator < Rails::Generators::NamedBase/ @@ -28,7 +30,7 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase lib/generators/rails/awesome lib/generators/rails/awesome/USAGE lib/generators/rails/awesome/templates - ).each{ |path| assert_file path } + ).each { |path| assert_file path } assert_file "lib/generators/rails/awesome/awesome_generator.rb", /class Rails::AwesomeGenerator < Rails::Generators::NamedBase/ @@ -44,7 +46,7 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase lib/generators/ lib/generators/USAGE lib/generators/templates - ).each{ |path| assert_file path } + ).each { |path| assert_file path } assert_file "lib/generators/awesome_generator.rb", /class AwesomeGenerator < Rails::Generators::NamedBase/ @@ -60,7 +62,7 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase lib/generators/rails lib/generators/rails/USAGE lib/generators/rails/templates - ).each{ |path| assert_file path } + ).each { |path| assert_file path } assert_file "lib/generators/rails/awesome_generator.rb", /class Rails::AwesomeGenerator < Rails::Generators::NamedBase/ diff --git a/railties/test/generators/generator_test.rb b/railties/test/generators/generator_test.rb index 7871399dd7..5f7daf5ac3 100644 --- a/railties/test/generators/generator_test.rb +++ b/railties/test/generators/generator_test.rb @@ -1,6 +1,8 @@ -require 'active_support/test_case' -require 'active_support/testing/autorun' -require 'rails/generators/app_base' +# frozen_string_literal: true + +require "active_support/test_case" +require "active_support/testing/autorun" +require "rails/generators/app_base" module Rails module Generators @@ -20,47 +22,47 @@ module Rails end def test_construction - klass = make_builder_class - assert klass.start(['new', 'blah']) + klass = make_builder_class + assert klass.start(["new", "blah"]) end def test_add_gem klass = make_builder_class - generator = klass.start(['new', 'blah']) - generator.gemfile_entry 'tenderlove' - assert_includes generator.gemfile_entries.map(&:name), 'tenderlove' + generator = klass.start(["new", "blah"]) + generator.gemfile_entry "tenderlove" + assert_includes generator.gemfile_entries.map(&:name), "tenderlove" end def test_add_gem_with_version klass = make_builder_class - generator = klass.start(['new', 'blah']) - generator.gemfile_entry 'tenderlove', '2.0.0' + generator = klass.start(["new", "blah"]) + generator.gemfile_entry "tenderlove", "2.0.0" assert generator.gemfile_entries.find { |gfe| - gfe.name == 'tenderlove' && gfe.version == '2.0.0' + gfe.name == "tenderlove" && gfe.version == "2.0.0" } end def test_add_github_gem klass = make_builder_class - generator = klass.start(['new', 'blah']) - generator.gemfile_entry 'tenderlove', github: 'hello world' + generator = klass.start(["new", "blah"]) + generator.gemfile_entry "tenderlove", github: "hello world" assert generator.gemfile_entries.find { |gfe| - gfe.name == 'tenderlove' && gfe.options[:github] == 'hello world' + gfe.name == "tenderlove" && gfe.options[:github] == "hello world" } end def test_add_path_gem klass = make_builder_class - generator = klass.start(['new', 'blah']) - generator.gemfile_entry 'tenderlove', path: 'hello world' + generator = klass.start(["new", "blah"]) + generator.gemfile_entry "tenderlove", path: "hello world" assert generator.gemfile_entries.find { |gfe| - gfe.name == 'tenderlove' && gfe.options[:path] == 'hello world' + gfe.name == "tenderlove" && gfe.options[:path] == "hello world" } end def test_filter klass = make_builder_class - generator = klass.start(['new', 'blah']) + generator = klass.start(["new", "blah"]) gems = generator.gemfile_entries generator.add_gem_entry_filter { |gem| gem.name != gems.first.name @@ -70,7 +72,7 @@ module Rails def test_two_filters klass = make_builder_class - generator = klass.start(['new', 'blah']) + generator = klass.start(["new", "blah"]) gems = generator.gemfile_entries generator.add_gem_entry_filter { |gem| gem.name != gems.first.name @@ -80,6 +82,21 @@ module Rails } assert_equal gems.drop(2), generator.gemfile_entries end + + def test_recommended_rails_versions + klass = make_builder_class + generator = klass.start(["new", "blah"]) + + specifier_for = -> v { generator.send(:rails_version_specifier, Gem::Version.new(v)) } + + assert_equal "~> 4.1.13", specifier_for["4.1.13"] + assert_equal "~> 4.1.6.rc1", specifier_for["4.1.6.rc1"] + assert_equal ["~> 4.1.7", ">= 4.1.7.1"], specifier_for["4.1.7.1"] + assert_equal ["~> 4.1.7", ">= 4.1.7.1.2"], specifier_for["4.1.7.1.2"] + assert_equal ["~> 4.1.7", ">= 4.1.7.1.rc2"], specifier_for["4.1.7.1.rc2"] + assert_equal "~> 4.2.0.beta1", specifier_for["4.2.0.beta1"] + assert_equal "~> 5.0.0.beta1", specifier_for["5.0.0.beta1"] + end end end end diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb index 62ca0ecb4b..25d5dba1d8 100644 --- a/railties/test/generators/generators_test_helper.rb +++ b/railties/test/generators/generators_test_helper.rb @@ -1,14 +1,17 @@ -require 'abstract_unit' -require 'active_support/core_ext/module/remove_method' -require 'active_support/testing/stream' -require 'rails/generators' -require 'rails/generators/test_case' +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/core_ext/module/remove_method" +require "active_support/testing/stream" +require "active_support/testing/method_call_assertions" +require "rails/generators" +require "rails/generators/test_case" module Rails class << self remove_possible_method :root def root - @root ||= Pathname.new(File.expand_path('../../fixtures', __FILE__)) + @root ||= Pathname.new(File.expand_path("../fixtures", __dir__)) end end end @@ -19,12 +22,13 @@ Rails.application.config.generators.templates = [File.join(Rails.root, "lib", "t # Rails.application.config.generators to Rails::Generators Rails.application.load_generators -require 'active_record' -require 'action_dispatch' -require 'action_view' +require "active_record" +require "action_dispatch" +require "action_view" module GeneratorsTestHelper include ActiveSupport::Testing::Stream + include ActiveSupport::Testing::MethodCallAssertions def self.included(base) base.class_eval do @@ -32,17 +36,31 @@ module GeneratorsTestHelper setup :prepare_destination begin - base.tests Rails::Generators.const_get(base.name.sub(/Test$/, '')) + base.tests Rails::Generators.const_get(base.name.sub(/Test$/, "")) rescue end end end + def with_secondary_database_configuration + original_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = { + test: { + secondary: { + database: "db/secondary.sqlite3", + migrations_paths: "db/secondary_migrate", + }, + }, + } + yield + ensure + ActiveRecord::Base.configurations = original_configurations + end + def copy_routes - routes = File.expand_path("../../../lib/rails/generators/rails/app/templates/config/routes.rb", __FILE__) + routes = File.expand_path("../../lib/rails/generators/rails/app/templates/config/routes.rb.tt", __dir__) destination = File.join(destination_root, "config") FileUtils.mkdir_p(destination) - FileUtils.cp routes, destination + FileUtils.cp routes, File.join(destination, "routes.rb") end - end diff --git a/railties/test/generators/helper_generator_test.rb b/railties/test/generators/helper_generator_test.rb index add04f21a4..4cdb6adf82 100644 --- a/railties/test/generators/helper_generator_test.rb +++ b/railties/test/generators/helper_generator_test.rb @@ -1,5 +1,7 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/helper/helper_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/helper/helper_generator" ObjectHelper = Class.new AnotherObjectHelperTest = Class.new @@ -14,7 +16,7 @@ class HelperGeneratorTest < Rails::Generators::TestCase end def test_check_class_collision - content = capture(:stderr){ run_generator ["object"] } + content = capture(:stderr) { run_generator ["object"] } assert_match(/The name 'ObjectHelper' is either already used in your application or reserved/, content) end diff --git a/railties/test/generators/integration_test_generator_test.rb b/railties/test/generators/integration_test_generator_test.rb index d05ed76d24..82791f1a27 100644 --- a/railties/test/generators/integration_test_generator_test.rb +++ b/railties/test/generators/integration_test_generator_test.rb @@ -1,12 +1,18 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/integration_test/integration_test_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/integration_test/integration_test_generator" class IntegrationTestGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper - arguments %w(integration) def test_integration_test_skeleton_is_created - run_generator + run_generator %w(integration) assert_file "test/integration/integration_test.rb", /class IntegrationTest < ActionDispatch::IntegrationTest/ end + + def test_namespaced_integration_test_skeleton_is_created + run_generator %w(iguchi/integration) + assert_file "test/integration/iguchi/integration_test.rb", /class Iguchi::IntegrationTest < ActionDispatch::IntegrationTest/ + end end diff --git a/railties/test/generators/job_generator_test.rb b/railties/test/generators/job_generator_test.rb index 7fd8f2062f..234ba6dad7 100644 --- a/railties/test/generators/job_generator_test.rb +++ b/railties/test/generators/job_generator_test.rb @@ -1,5 +1,7 @@ -require 'generators/generators_test_helper' -require 'rails/generators/job/job_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/job/job_generator" class JobGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -26,4 +28,21 @@ class JobGeneratorTest < Rails::Generators::TestCase assert_match(/queue_as :admin/, job) end end + + def test_application_job_skeleton_is_created + run_generator ["refresh_counters"] + assert_file "app/jobs/application_job.rb" do |job| + assert_match(/class ApplicationJob < ActiveJob::Base/, job) + end + end + + def test_job_suffix_is_not_duplicated + run_generator ["notifier_job"] + + assert_no_file "app/jobs/notifier_job_job.rb" + assert_file "app/jobs/notifier_job.rb" + + assert_no_file "test/jobs/notifier_job_job_test.rb" + assert_file "test/jobs/notifier_job_test.rb" + end end diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb index f01e8cd2d9..ddac6e1a1e 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -1,5 +1,7 @@ -require 'generators/generators_test_helper' -require 'rails/generators/mailer/mailer_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/mailer/mailer_generator" class MailerGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -9,16 +11,13 @@ class MailerGeneratorTest < Rails::Generators::TestCase run_generator assert_file "app/mailers/notifier_mailer.rb" do |mailer| assert_match(/class NotifierMailer < ApplicationMailer/, mailer) - assert_no_match(/default from: "from@example.com"/, mailer) + assert_no_match(/default from: "from@example\.com"/, mailer) assert_no_match(/layout :mailer_notifier/, mailer) end - end - def test_application_mailer_skeleton_is_created - run_generator assert_file "app/mailers/application_mailer.rb" do |mailer| assert_match(/class ApplicationMailer < ActionMailer::Base/, mailer) - assert_match(/default from: "from@example.com"/, mailer) + assert_match(/default from: 'from@example\.com'/, mailer) assert_match(/layout 'mailer'/, mailer) end end @@ -33,7 +32,7 @@ class MailerGeneratorTest < Rails::Generators::TestCase def test_check_class_collision Object.send :const_set, :NotifierMailer, Class.new - content = capture(:stderr){ run_generator } + content = capture(:stderr) { run_generator } assert_match(/The name 'NotifierMailer' is either already used in your application or reserved/, content) ensure Object.send :remove_const, :NotifierMailer @@ -51,18 +50,18 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_match(/class NotifierMailerPreview < ActionMailer::Preview/, preview) assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier_mailer\/foo/, preview) assert_instance_method :foo, preview do |foo| - assert_match(/NotifierMailer.foo/, foo) + assert_match(/NotifierMailer\.foo/, foo) end assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier_mailer\/bar/, preview) assert_instance_method :bar, preview do |bar| - assert_match(/NotifierMailer.bar/, bar) + assert_match(/NotifierMailer\.bar/, bar) end end end def test_check_test_class_collision Object.send :const_set, :NotifierMailerTest, Class.new - content = capture(:stderr){ run_generator } + content = capture(:stderr) { run_generator } assert_match(/The name 'NotifierMailerTest' is either already used in your application or reserved/, content) ensure Object.send :remove_const, :NotifierMailerTest @@ -70,7 +69,7 @@ class MailerGeneratorTest < Rails::Generators::TestCase def test_check_preview_class_collision Object.send :const_set, :NotifierMailerPreview, Class.new - content = capture(:stderr){ run_generator } + content = capture(:stderr) { run_generator } assert_match(/The name 'NotifierMailerPreview' is either already used in your application or reserved/, content) ensure Object.send :remove_const, :NotifierMailerPreview @@ -106,15 +105,13 @@ class MailerGeneratorTest < Rails::Generators::TestCase end assert_file "app/views/layouts/mailer.html.erb" do |view| - assert_match(%r{<html>\n <body>\n <%= yield %>\n </body>\n</html>}, view) + assert_match(%r{<body>\n <%= yield %>\n </body>}, view) end end def test_invokes_default_template_engine_even_with_no_action run_generator ["notifier"] assert_file "app/views/notifier_mailer" - assert_file "app/views/layouts/mailer.text.erb" - assert_file "app/views/layouts/mailer.html.erb" end def test_logs_if_the_template_engine_cannot_be_found @@ -142,12 +139,12 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_file "app/mailers/notifier_mailer.rb" do |mailer| assert_instance_method :foo, mailer do |foo| - assert_match(/mail to: "to@example.org"/, foo) + assert_match(/mail to: "to@example\.org"/, foo) assert_match(/@greeting = "Hi"/, foo) end assert_instance_method :bar, mailer do |bar| - assert_match(/mail to: "to@example.org"/, bar) + assert_match(/mail to: "to@example\.org"/, bar) assert_match(/@greeting = "Hi"/, bar) end end @@ -162,10 +159,6 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_no_file "app/views/notifier/bar.text.erb" assert_no_file "app/views/notifier/foo.html.erb" assert_no_file "app/views/notifier/bar.html.erb" - - assert_file "app/mailers/application_mailer.rb" - assert_file "app/views/layouts/mailer.text.erb" - assert_file "app/views/layouts/mailer.html.erb" end def test_mailer_suffix_is_not_duplicated diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index 57bc220558..5812cbdfc9 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -1,5 +1,7 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/migration/migration_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/migration/migration_generator" class MigrationGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -7,7 +9,7 @@ class MigrationGeneratorTest < Rails::Generators::TestCase def test_migration migration = "change_title_body_from_posts" run_generator [migration] - assert_migration "db/migrate/#{migration}.rb", /class ChangeTitleBodyFromPosts < ActiveRecord::Migration/ + assert_migration "db/migrate/#{migration}.rb", /class ChangeTitleBodyFromPosts < ActiveRecord::Migration\[[0-9.]+\]/ end def test_migrations_generated_simultaneously @@ -17,7 +19,7 @@ class MigrationGeneratorTest < Rails::Generators::TestCase run_generator [migration] file_name = migration_file_name "db/migrate/#{migration}.rb" - File.basename(file_name).split('_').first + File.basename(file_name).split("_").first end assert_not_equal first_migration_number, second_migration_number @@ -26,7 +28,7 @@ class MigrationGeneratorTest < Rails::Generators::TestCase def test_migration_with_class_name migration = "ChangeTitleBodyFromPosts" run_generator [migration] - assert_migration "db/migrate/change_title_body_from_posts.rb", /class #{migration} < ActiveRecord::Migration/ + assert_migration "db/migrate/change_title_body_from_posts.rb", /class #{migration} < ActiveRecord::Migration\[[0-9.]+\]/ end def test_migration_with_invalid_file_name @@ -48,6 +50,17 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_add_migration_with_table_having_from_in_title + migration = "add_email_address_to_excluded_from_campaign" + run_generator [migration, "email_address:string"] + + assert_migration "db/migrate/#{migration}.rb" do |content| + assert_method :change, content do |change| + assert_match(/add_column :excluded_from_campaigns, :email_address, :string/, change) + end + end + end + def test_remove_migration_with_indexed_attribute migration = "remove_title_body_from_posts" run_generator [migration, "title:string:index", "body:text"] @@ -73,14 +86,25 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_remove_migration_with_table_having_to_in_title + migration = "remove_email_address_from_sent_to_user" + run_generator [migration, "email_address:string"] + + assert_migration "db/migrate/#{migration}.rb" do |content| + assert_method :change, content do |change| + assert_match(/remove_column :sent_to_users, :email_address, :string/, change) + end + end + end + def test_remove_migration_with_references_options migration = "remove_references_from_books" run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"] assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| - assert_match(/remove_reference :books, :author, index: true/, change) - assert_match(/remove_reference :books, :distributor, polymorphic: true, index: true/, change) + assert_match(/remove_reference :books, :author/, change) + assert_match(/remove_reference :books, :distributor, polymorphic: true/, change) end end end @@ -166,8 +190,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| - assert_match(/add_reference :books, :author, index: true/, change) - assert_match(/add_reference :books, :distributor, polymorphic: true, index: true/, change) + assert_match(/add_reference :books, :author/, change) + assert_match(/add_reference :books, :distributor, polymorphic: true/, change) end end end @@ -178,8 +202,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| - assert_match(/add_reference :books, :author, index: true, null: false/, change) - assert_match(/add_reference :books, :distributor, polymorphic: true, index: true, null: false/, change) + assert_match(/add_reference :books, :author, null: false/, change) + assert_match(/add_reference :books, :distributor, polymorphic: true, null: false/, change) end end end @@ -204,8 +228,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| assert_match(/create_join_table :artists, :musics/, change) - assert_match(/# t.index \[:artist_id, :music_id\]/, change) - assert_match(/ t.index \[:music_id, :artist_id\], unique: true/, change) + assert_match(/# t\.index \[:artist_id, :music_id\]/, change) + assert_match(/ t\.index \[:music_id, :artist_id\], unique: true/, change) end end end @@ -221,6 +245,26 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_add_uuid_to_create_table_migration + run_generator ["create_books", "--primary_key_type=uuid"] + assert_migration "db/migrate/create_books.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :books, id: :uuid/, change) + end + end + end + + def test_database_puts_migrations_in_configured_folder + with_secondary_database_configuration do + run_generator ["create_books", "--database=secondary"] + assert_migration "db/secondary_migrate/create_books.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :books/, change) + end + end + end + end + def test_should_create_empty_migrations_if_name_not_start_with_add_or_remove_or_create migration = "delete_books" run_generator [migration, "title:string", "content:text"] @@ -239,7 +283,7 @@ class MigrationGeneratorTest < Rails::Generators::TestCase def test_migration_with_singular_table_name with_singular_table_name do migration = "add_title_body_to_post" - run_generator [migration, 'title:string'] + run_generator [migration, "title:string"] assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| assert_match(/add_column :post, :title, :string/, change) @@ -256,8 +300,8 @@ class MigrationGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/#{migration}.rb" do |content| assert_method :change, content do |change| assert_match(/create_join_table :artist, :music/, change) - assert_match(/# t.index \[:artist_id, :music_id\]/, change) - assert_match(/ t.index \[:music_id, :artist_id\], unique: true/, change) + assert_match(/# t\.index \[:artist_id, :music_id\]/, change) + assert_match(/ t\.index \[:music_id, :artist_id\], unique: true/, change) end end end @@ -300,6 +344,17 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_add_migration_to_configured_path + old_paths = Rails.application.config.paths["db/migrate"] + Rails.application.config.paths.add "db/migrate", with: "db2/migrate" + + migration = "migration_in_custom_path" + run_generator [migration] + assert_migration "db2/migrate/#{migration}.rb", /.*/ + ensure + Rails.application.config.paths["db/migrate"] = old_paths + end + private def with_singular_table_name diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index abd3ff50a4..b06db6dd8a 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -1,11 +1,17 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/model/model_generator' -require 'active_support/core_ext/string/strip' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/model/model_generator" class ModelGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper arguments %w(Account name:string age:integer) + def setup + super + Rails::Generators::ModelHelpers.skip_warn = false + end + def test_help_shows_invoked_generators_options content = run_generator ["--help"] assert_match(/ActiveRecord options:/, content) @@ -26,7 +32,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_invokes_default_orm run_generator - assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/ + assert_file "app/models/account.rb", /class Account < ApplicationRecord/ end def test_model_with_parent_option @@ -36,12 +42,24 @@ class ModelGeneratorTest < Rails::Generators::TestCase end def test_plural_names_are_singularized - content = run_generator ["accounts".freeze] - assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/ + content = run_generator ["accounts"] + assert_file "app/models/account.rb", /class Account < ApplicationRecord/ assert_file "test/models/account_test.rb", /class AccountTest/ assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content) end + def test_unknown_inflection_rule_are_warned + content = run_generator ["porsche"] + assert_match("[WARNING] Rails cannot recover singular form from its plural form 'porsches'.\nPlease setup custom inflection rules for this noun before running the generator in config/initializers/inflections.rb.", content) + assert_file "app/models/porsche.rb", /class Porsche < ApplicationRecord/ + + uncountable_content = run_generator ["sheep"] + assert_no_match("[WARNING] Rails cannot recover singular form from its plural form", uncountable_content) + + regular_content = run_generator ["account"] + assert_no_match("[WARNING] Rails cannot recover singular form from its plural form", regular_content) + end + def test_model_with_underscored_parent_option run_generator ["account", "--parent", "admin/account"] assert_file "app/models/account.rb", /class Account < Admin::Account/ @@ -52,17 +70,17 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_file "app/models/admin.rb", /module Admin/ assert_file "app/models/admin.rb", /def self\.table_name_prefix/ assert_file "app/models/admin.rb", /'admin_'/ - assert_file "app/models/admin/account.rb", /class Admin::Account < ActiveRecord::Base/ + assert_file "app/models/admin/account.rb", /class Admin::Account < ApplicationRecord/ end def test_migration run_generator - assert_migration "db/migrate/create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration/ + assert_migration "db/migrate/create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration\[[0-9.]+\]/ end def test_migration_with_namespace run_generator ["Gallery::Image"] - assert_migration "db/migrate/create_gallery_images", /class CreateGalleryImages < ActiveRecord::Migration/ + assert_migration "db/migrate/create_gallery_images", /class CreateGalleryImages < ActiveRecord::Migration\[[0-9.]+\]/ assert_no_migration "db/migrate/create_images" end @@ -70,7 +88,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase run_generator ["Admin::Gallery::Image"] assert_no_migration "db/migrate/create_images" assert_no_migration "db/migrate/create_gallery_images" - assert_migration "db/migrate/create_admin_gallery_images", /class CreateAdminGalleryImages < ActiveRecord::Migration/ + assert_migration "db/migrate/create_admin_gallery_images", /class CreateAdminGalleryImages < ActiveRecord::Migration\[[0-9.]+\]/ assert_migration "db/migrate/create_admin_gallery_images", /create_table :admin_gallery_images/ end @@ -80,7 +98,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_no_migration "db/migrate/create_images" assert_no_migration "db/migrate/create_gallery_images" assert_no_migration "db/migrate/create_admin_gallery_images" - assert_migration "db/migrate/create_admin_gallery_image", /class CreateAdminGalleryImage < ActiveRecord::Migration/ + assert_migration "db/migrate/create_admin_gallery_image", /class CreateAdminGalleryImage < ActiveRecord::Migration\[[0-9.]+\]/ assert_migration "db/migrate/create_admin_gallery_image", /create_table :admin_gallery_image/ ensure ActiveRecord::Base.pluralize_table_names = true @@ -89,7 +107,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_with_namespaces_in_model_name_without_plurization ActiveRecord::Base.pluralize_table_names = false run_generator ["Gallery::Image"] - assert_migration "db/migrate/create_gallery_image", /class CreateGalleryImage < ActiveRecord::Migration/ + assert_migration "db/migrate/create_gallery_image", /class CreateGalleryImage < ActiveRecord::Migration\[[0-9.]+\]/ assert_no_migration "db/migrate/create_gallery_images" ensure ActiveRecord::Base.pluralize_table_names = true @@ -98,7 +116,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_without_pluralization ActiveRecord::Base.pluralize_table_names = false run_generator - assert_migration "db/migrate/create_account", /class CreateAccount < ActiveRecord::Migration/ + assert_migration "db/migrate/create_account", /class CreateAccount < ActiveRecord::Migration\[[0-9.]+\]/ assert_no_migration "db/migrate/create_accounts" ensure ActiveRecord::Base.pluralize_table_names = true @@ -193,14 +211,25 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_without_timestamps ActiveRecord::Base.timestamped_migrations = false run_generator ["account"] - assert_file "db/migrate/001_create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration/ + assert_file "db/migrate/001_create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration\[[0-9.]+\]/ run_generator ["project"] - assert_file "db/migrate/002_create_projects.rb", /class CreateProjects < ActiveRecord::Migration/ + assert_file "db/migrate/002_create_projects.rb", /class CreateProjects < ActiveRecord::Migration\[[0-9.]+\]/ ensure ActiveRecord::Base.timestamped_migrations = true end + def test_migration_with_configured_path + old_paths = Rails.application.config.paths["db/migrate"] + Rails.application.config.paths.add "db/migrate", with: "db2/migrate" + + run_generator + + assert_migration "db2/migrate/create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration\[[0-9.]+\]/ + ensure + Rails.application.config.paths["db/migrate"] = old_paths + end + def test_model_with_references_attribute_generates_belongs_to_associations run_generator ["product", "name:string", "supplier:references"] assert_file "app/models/product.rb", /belongs_to :supplier/ @@ -223,7 +252,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_with_timestamps run_generator - assert_migration "db/migrate/create_accounts.rb", /t.timestamps/ + assert_migration "db/migrate/create_accounts.rb", /t\.timestamps/ end def test_migration_timestamps_are_skipped @@ -231,7 +260,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/create_accounts.rb" do |m| assert_method :change, m do |up| - assert_no_match(/t.timestamps/, up) + assert_no_match(/t\.timestamps/, up) end end end @@ -239,24 +268,24 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_is_skipped_with_skip_option run_generator output = run_generator ["Account", "--skip"] - assert_match %r{skip\s+db/migrate/\d+_create_accounts.rb}, output + assert_match %r{skip\s+db/migrate/\d+_create_accounts\.rb}, output end def test_migration_is_ignored_as_identical_with_skip_option run_generator ["Account"] output = run_generator ["Account", "--skip"] - assert_match %r{identical\s+db/migrate/\d+_create_accounts.rb}, output + assert_match %r{identical\s+db/migrate/\d+_create_accounts\.rb}, output end def test_migration_is_skipped_on_skip_behavior run_generator output = run_generator ["Account"], behavior: :skip - assert_match %r{skip\s+db/migrate/\d+_create_accounts.rb}, output + assert_match %r{skip\s+db/migrate/\d+_create_accounts\.rb}, output end def test_migration_error_is_not_shown_on_revoke run_generator - error = capture(:stderr){ run_generator ["Account"], behavior: :revoke } + error = capture(:stderr) { run_generator ["Account"], behavior: :revoke } assert_no_match(/Another migration is already named create_accounts/, error) end @@ -281,31 +310,31 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_file "test/fixtures/accounts.yml", /name: MyString/, /age: 1/ assert_generated_fixture("test/fixtures/accounts.yml", - {"one"=>{"name"=>"MyString", "age"=>1}, "two"=>{"name"=>"MyString", "age"=>1}}) + "one" => { "name" => "MyString", "age" => 1 }, "two" => { "name" => "MyString", "age" => 1 }) end def test_fixtures_use_the_references_ids run_generator ["LineItem", "product:references", "cart:belongs_to"] - assert_file "test/fixtures/line_items.yml", /product: \n cart: / + assert_file "test/fixtures/line_items.yml", /product: one\n cart: one/ assert_generated_fixture("test/fixtures/line_items.yml", - {"one"=>{"product"=>nil, "cart"=>nil}, "two"=>{"product"=>nil, "cart"=>nil}}) + "one" => { "product" => "one", "cart" => "one" }, "two" => { "product" => "two", "cart" => "two" }) end def test_fixtures_use_the_references_ids_and_type run_generator ["LineItem", "product:references{polymorphic}", "cart:belongs_to"] - assert_file "test/fixtures/line_items.yml", /product: \n product_type: Product\n cart: / + assert_file "test/fixtures/line_items.yml", /product: one\n product_type: Product\n cart: one/ assert_generated_fixture("test/fixtures/line_items.yml", - {"one"=>{"product"=>nil, "product_type"=>"Product", "cart"=>nil}, - "two"=>{"product"=>nil, "product_type"=>"Product", "cart"=>nil}}) + "one" => { "product" => "one", "product_type" => "Product", "cart" => "one" }, + "two" => { "product" => "two", "product_type" => "Product", "cart" => "two" }) end def test_fixtures_respect_reserved_yml_keywords run_generator ["LineItem", "no:integer", "Off:boolean", "ON:boolean"] assert_generated_fixture("test/fixtures/line_items.yml", - {"one"=>{"no"=>1, "Off"=>false, "ON"=>false}, "two"=>{"no"=>1, "Off"=>false, "ON"=>false}}) + "one" => { "no" => 1, "Off" => false, "ON" => false }, "two" => { "no" => 1, "Off" => false, "ON" => false }) end def test_fixture_is_skipped @@ -324,52 +353,52 @@ class ModelGeneratorTest < Rails::Generators::TestCase ActiveRecord::Base.pluralize_table_names = false run_generator assert_generated_fixture("test/fixtures/account.yml", - {"one"=>{"name"=>"MyString", "age"=>1}, "two"=>{"name"=>"MyString", "age"=>1}}) + "one" => { "name" => "MyString", "age" => 1 }, "two" => { "name" => "MyString", "age" => 1 }) ensure ActiveRecord::Base.pluralize_table_names = original_pluralize_table_name end def test_check_class_collision - content = capture(:stderr){ run_generator ["object"] } + content = capture(:stderr) { run_generator ["object"] } assert_match(/The name 'Object' is either already used in your application or reserved/, content) end - def test_index_is_added_for_belongs_to_association - run_generator ["account", "supplier:belongs_to"] + def test_index_is_skipped_for_belongs_to_association + run_generator ["account", "supplier:belongs_to", "--no-indexes"] assert_migration "db/migrate/create_accounts.rb" do |m| assert_method :change, m do |up| - assert_match(/index: true/, up) + assert_no_match(/index: true/, up) end end end - def test_index_is_added_for_references_association - run_generator ["account", "supplier:references"] + def test_index_is_skipped_for_references_association + run_generator ["account", "supplier:references", "--no-indexes"] assert_migration "db/migrate/create_accounts.rb" do |m| assert_method :change, m do |up| - assert_match(/index: true/, up) + assert_no_match(/index: true/, up) end end end - def test_index_is_skipped_for_belongs_to_association - run_generator ["account", "supplier:belongs_to", "--no-indexes"] - - assert_migration "db/migrate/create_accounts.rb" do |m| - assert_method :change, m do |up| - assert_no_match(/index: true/, up) + def test_add_uuid_to_create_table_migration + run_generator ["account", "--primary_key_type=uuid"] + assert_migration "db/migrate/create_accounts.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :accounts, id: :uuid/, change) end end end - def test_index_is_skipped_for_references_association - run_generator ["account", "supplier:references", "--no-indexes"] - - assert_migration "db/migrate/create_accounts.rb" do |m| - assert_method :change, m do |up| - assert_no_match(/index: true/, up) + def test_database_puts_migrations_in_configured_folder + with_secondary_database_configuration do + run_generator ["account", "--database=secondary"] + assert_migration "db/secondary_migrate/create_accounts.rb" do |content| + assert_method :change, content do |change| + assert_match(/create_table :accounts/, change) + end end end end @@ -377,10 +406,10 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_required_belongs_to_adds_required_association run_generator ["account", "supplier:references{required}"] - expected_file = <<-FILE.strip_heredoc - class Account < ActiveRecord::Base - belongs_to :supplier, required: true - end + expected_file = <<~FILE + class Account < ApplicationRecord + belongs_to :supplier, required: true + end FILE assert_file "app/models/account.rb", expected_file end @@ -388,10 +417,10 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_required_polymorphic_belongs_to_generages_correct_model run_generator ["account", "supplier:references{required,polymorphic}"] - expected_file = <<-FILE.strip_heredoc - class Account < ActiveRecord::Base - belongs_to :supplier, polymorphic: true, required: true - end + expected_file = <<~FILE + class Account < ApplicationRecord + belongs_to :supplier, polymorphic: true, required: true + end FILE assert_file "app/models/account.rb", expected_file end @@ -399,10 +428,10 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_required_and_polymorphic_are_order_independent run_generator ["account", "supplier:references{polymorphic.required}"] - expected_file = <<-FILE.strip_heredoc - class Account < ActiveRecord::Base - belongs_to :supplier, polymorphic: true, required: true - end + expected_file = <<~FILE + class Account < ApplicationRecord + belongs_to :supplier, polymorphic: true, required: true + end FILE assert_file "app/models/account.rb", expected_file end @@ -450,11 +479,11 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_token_option_adds_has_secure_token run_generator ["user", "token:token", "auth_token:token"] - expected_file = <<-FILE.strip_heredoc - class User < ActiveRecord::Base - has_secure_token - has_secure_token :auth_token - end + expected_file = <<~FILE + class User < ApplicationRecord + has_secure_token + has_secure_token :auth_token + end FILE assert_file "app/models/user.rb", expected_file end diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb index 1c32fc1bfd..4e61b660d7 100644 --- a/railties/test/generators/named_base_test.rb +++ b/railties/test/generators/named_base_test.rb @@ -1,85 +1,108 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator' -require 'minitest/mock' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/scaffold_controller/scaffold_controller_generator" class NamedBaseTest < Rails::Generators::TestCase include GeneratorsTestHelper tests Rails::Generators::ScaffoldControllerGenerator def test_named_generator_with_underscore - g = generator ['line_item'] - assert_name g, 'line_item', :name + g = generator ["line_item"] + assert_name g, "line_item", :name assert_name g, %w(), :class_path - assert_name g, 'LineItem', :class_name - assert_name g, 'line_item', :file_path - assert_name g, 'line_item', :file_name - assert_name g, 'Line item', :human_name - assert_name g, 'line_item', :singular_name - assert_name g, 'line_items', :plural_name - assert_name g, 'line_item', :i18n_scope - assert_name g, 'line_items', :table_name + assert_name g, "LineItem", :class_name + assert_name g, "line_item", :file_path + assert_name g, "line_item", :file_name + assert_name g, "Line item", :human_name + assert_name g, "line_item", :singular_name + assert_name g, "line_items", :plural_name + assert_name g, "line_item", :i18n_scope + assert_name g, "line_items", :table_name end def test_named_generator_attributes - g = generator ['admin/foo'] - assert_name g, 'admin/foo', :name + g = generator ["admin/foo"] + assert_name g, "admin/foo", :name assert_name g, %w(admin), :class_path - assert_name g, 'Admin::Foo', :class_name - assert_name g, 'admin/foo', :file_path - assert_name g, 'foo', :file_name - assert_name g, 'Foo', :human_name - assert_name g, 'foo', :singular_name - assert_name g, 'foos', :plural_name - assert_name g, 'admin.foo', :i18n_scope - assert_name g, 'admin_foos', :table_name + assert_name g, "Admin::Foo", :class_name + assert_name g, "admin/foo", :file_path + assert_name g, "foo", :file_name + assert_name g, "Foo", :human_name + assert_name g, "foo", :singular_name + assert_name g, "foos", :plural_name + assert_name g, "admin.foo", :i18n_scope + assert_name g, "admin_foos", :table_name + assert_name g, "admin/foos", :controller_name + assert_name g, %w(admin), :controller_class_path + assert_name g, "Admin::Foos", :controller_class_name + assert_name g, "admin/foos", :controller_file_path + assert_name g, "foos", :controller_file_name + assert_name g, "admin.foos", :controller_i18n_scope + assert_name g, "admin_foo", :singular_route_name + assert_name g, "admin_foos", :plural_route_name + assert_name g, "@admin_foo", :redirect_resource_name + assert_name g, "admin_foo", :model_resource_name + assert_name g, "admin_foos", :index_helper end def test_named_generator_attributes_as_ruby - g = generator ['Admin::Foo'] - assert_name g, 'Admin::Foo', :name + g = generator ["Admin::Foo"] + assert_name g, "Admin::Foo", :name assert_name g, %w(admin), :class_path - assert_name g, 'Admin::Foo', :class_name - assert_name g, 'admin/foo', :file_path - assert_name g, 'foo', :file_name - assert_name g, 'foo', :singular_name - assert_name g, 'Foo', :human_name - assert_name g, 'foos', :plural_name - assert_name g, 'admin.foo', :i18n_scope - assert_name g, 'admin_foos', :table_name + assert_name g, "Admin::Foo", :class_name + assert_name g, "admin/foo", :file_path + assert_name g, "foo", :file_name + assert_name g, "foo", :singular_name + assert_name g, "Foo", :human_name + assert_name g, "foos", :plural_name + assert_name g, "admin.foo", :i18n_scope + assert_name g, "admin_foos", :table_name + assert_name g, "Admin::Foos", :controller_name + assert_name g, %w(admin), :controller_class_path + assert_name g, "Admin::Foos", :controller_class_name + assert_name g, "admin/foos", :controller_file_path + assert_name g, "foos", :controller_file_name + assert_name g, "admin.foos", :controller_i18n_scope + assert_name g, "admin_foo", :singular_route_name + assert_name g, "admin_foos", :plural_route_name + assert_name g, "@admin_foo", :redirect_resource_name + assert_name g, "admin_foo", :model_resource_name + assert_name g, "admin_foos", :index_helper end def test_named_generator_attributes_without_pluralized original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names ActiveRecord::Base.pluralize_table_names = false - g = generator ['admin/foo'] - assert_name g, 'admin_foo', :table_name + g = generator ["admin/foo"] + assert_name g, "admin_foo", :table_name ensure ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names end - def test_scaffold_plural_names - g = generator ['admin/foo'] - assert_name g, 'admin/foos', :controller_name + def test_namespaced_scaffold_plural_names + g = generator ["admin/foo"] + assert_name g, "admin/foos", :controller_name assert_name g, %w(admin), :controller_class_path - assert_name g, 'Admin::Foos', :controller_class_name - assert_name g, 'admin/foos', :controller_file_path - assert_name g, 'foos', :controller_file_name - assert_name g, 'admin.foos', :controller_i18n_scope + assert_name g, "Admin::Foos", :controller_class_name + assert_name g, "admin/foos", :controller_file_path + assert_name g, "foos", :controller_file_name + assert_name g, "admin.foos", :controller_i18n_scope end - def test_scaffold_plural_names_as_ruby - g = generator ['Admin::Foo'] - assert_name g, 'Admin::Foos', :controller_name + def test_namespaced_scaffold_plural_names_as_ruby + g = generator ["Admin::Foo"] + assert_name g, "Admin::Foos", :controller_name assert_name g, %w(admin), :controller_class_path - assert_name g, 'Admin::Foos', :controller_class_name - assert_name g, 'admin/foos', :controller_file_path - assert_name g, 'foos', :controller_file_name - assert_name g, 'admin.foos', :controller_i18n_scope + assert_name g, "Admin::Foos", :controller_class_name + assert_name g, "admin/foos", :controller_file_path + assert_name g, "foos", :controller_file_name + assert_name g, "admin.foos", :controller_i18n_scope end def test_application_name - g = generator ['Admin::Foo'] + g = generator ["Admin::Foo"] Rails.stub(:application, Object.new) do assert_name g, "object", :application_name end @@ -90,49 +113,62 @@ class NamedBaseTest < Rails::Generators::TestCase end def test_index_helper - g = generator ['Post'] - assert_name g, 'posts', :index_helper + g = generator ["Post"] + assert_name g, "posts", :index_helper end def test_index_helper_to_pluralize_once - g = generator ['Stadium'] - assert_name g, 'stadia', :index_helper + g = generator ["Stadium"] + assert_name g, "stadia", :index_helper end def test_index_helper_with_uncountable - g = generator ['Sheep'] - assert_name g, 'sheep_index', :index_helper + g = generator ["Sheep"] + assert_name g, "sheep_index", :index_helper end def test_hide_namespace - g = generator ['Hidden'] - g.class.stub(:namespace, 'hidden') do - assert !Rails::Generators.hidden_namespaces.include?('hidden') + g = generator ["Hidden"] + g.class.stub(:namespace, "hidden") do + assert_not_includes Rails::Generators.hidden_namespaces, "hidden" g.class.hide! - assert Rails::Generators.hidden_namespaces.include?('hidden') + assert_includes Rails::Generators.hidden_namespaces, "hidden" end end def test_scaffold_plural_names_with_model_name_option - g = generator ['Admin::Foo'], model_name: 'User' - assert_name g, 'user', :singular_name - assert_name g, 'User', :name - assert_name g, 'user', :file_path - assert_name g, 'User', :class_name - assert_name g, 'user', :file_name - assert_name g, 'User', :human_name - assert_name g, 'users', :plural_name - assert_name g, 'user', :i18n_scope - assert_name g, 'users', :table_name - assert_name g, 'Admin::Foos', :controller_name + g = generator ["Admin::Foo"], model_name: "User" + assert_name g, "user", :singular_name + assert_name g, "User", :name + assert_name g, "user", :file_path + assert_name g, "User", :class_name + assert_name g, "user", :file_name + assert_name g, "User", :human_name + assert_name g, "users", :plural_name + assert_name g, "user", :i18n_scope + assert_name g, "users", :table_name + assert_name g, "Admin::Foos", :controller_name assert_name g, %w(admin), :controller_class_path - assert_name g, 'Admin::Foos', :controller_class_name - assert_name g, 'admin/foos', :controller_file_path - assert_name g, 'foos', :controller_file_name - assert_name g, 'admin.foos', :controller_i18n_scope + assert_name g, "Admin::Foos", :controller_class_name + assert_name g, "admin/foos", :controller_file_path + assert_name g, "foos", :controller_file_name + assert_name g, "admin.foos", :controller_i18n_scope + assert_name g, "admin_user", :singular_route_name + assert_name g, "admin_users", :plural_route_name + assert_name g, "[:admin, @user]", :redirect_resource_name + assert_name g, "[:admin, user]", :model_resource_name + assert_name g, "admin_users", :index_helper + end + + def test_scaffold_plural_names + g = generator ["User"] + assert_name g, "@user", :redirect_resource_name + assert_name g, "user", :model_resource_name + assert_name g, "user", :singular_route_name + assert_name g, "users", :plural_route_name end - protected + private def assert_name(generator, value, method) assert_equal value, generator.send(method) diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index e839b67960..4b75a31f17 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -1,8 +1,11 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/controller/controller_generator' -require 'rails/generators/rails/model/model_generator' -require 'rails/generators/mailer/mailer_generator' -require 'rails/generators/rails/scaffold/scaffold_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/controller/controller_generator" +require "rails/generators/rails/model/model_generator" +require "rails/generators/mailer/mailer_generator" +require "rails/generators/rails/scaffold/scaffold_generator" +require "rails/generators/rails/application_record/application_record_generator" class NamespacedGeneratorTestCase < Rails::Generators::TestCase include GeneratorsTestHelper @@ -91,7 +94,7 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase def test_adds_namespace_to_model run_generator - assert_file "app/models/test_app/account.rb", /module TestApp/, / class Account < ActiveRecord::Base/ + assert_file "app/models/test_app/account.rb", /module TestApp/, / class Account < ApplicationRecord/ end def test_model_with_namespace @@ -99,17 +102,17 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase assert_file "app/models/test_app/admin.rb", /module TestApp/, /module Admin/ assert_file "app/models/test_app/admin.rb", /def self\.table_name_prefix/ assert_file "app/models/test_app/admin.rb", /'test_app_admin_'/ - assert_file "app/models/test_app/admin/account.rb", /module TestApp/, /class Admin::Account < ActiveRecord::Base/ + assert_file "app/models/test_app/admin/account.rb", /module TestApp/, /class Admin::Account < ApplicationRecord/ end def test_migration run_generator - assert_migration "db/migrate/create_test_app_accounts.rb", /create_table :test_app_accounts/, /class CreateTestAppAccounts < ActiveRecord::Migration/ + assert_migration "db/migrate/create_test_app_accounts.rb", /create_table :test_app_accounts/, /class CreateTestAppAccounts < ActiveRecord::Migration\[[0-9.]+\]/ end def test_migration_with_namespace run_generator ["Gallery::Image"] - assert_migration "db/migrate/create_test_app_gallery_images", /class CreateTestAppGalleryImages < ActiveRecord::Migration/ + assert_migration "db/migrate/create_test_app_gallery_images", /class CreateTestAppGalleryImages < ActiveRecord::Migration\[[0-9.]+\]/ assert_no_migration "db/migrate/create_test_app_images" end @@ -117,7 +120,7 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase run_generator ["Admin::Gallery::Image"] assert_no_migration "db/migrate/create_images" assert_no_migration "db/migrate/create_gallery_images" - assert_migration "db/migrate/create_test_app_admin_gallery_images", /class CreateTestAppAdminGalleryImages < ActiveRecord::Migration/ + assert_migration "db/migrate/create_test_app_admin_gallery_images", /class CreateTestAppAdminGalleryImages < ActiveRecord::Migration\[[0-9.]+\]/ assert_migration "db/migrate/create_test_app_admin_gallery_images", /create_table :test_app_admin_gallery_images/ end @@ -127,7 +130,7 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase assert_no_migration "db/migrate/create_images" assert_no_migration "db/migrate/create_gallery_images" assert_no_migration "db/migrate/create_test_app_admin_gallery_images" - assert_migration "db/migrate/create_test_app_admin_gallery_image", /class CreateTestAppAdminGalleryImage < ActiveRecord::Migration/ + assert_migration "db/migrate/create_test_app_admin_gallery_image", /class CreateTestAppAdminGalleryImage < ActiveRecord::Migration\[[0-9.]+\]/ assert_migration "db/migrate/create_test_app_admin_gallery_image", /create_table :test_app_admin_gallery_image/ ensure ActiveRecord::Base.pluralize_table_names = true @@ -149,7 +152,7 @@ class NamespacedMailerGeneratorTest < NamespacedGeneratorTestCase assert_file "app/mailers/test_app/notifier_mailer.rb" do |mailer| assert_match(/module TestApp/, mailer) assert_match(/class NotifierMailer < ApplicationMailer/, mailer) - assert_no_match(/default from: "from@example.com"/, mailer) + assert_no_match(/default from: "from@example\.com"/, mailer) end end @@ -201,7 +204,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase run_generator # Model - assert_file "app/models/test_app/product_line.rb", /module TestApp\n class ProductLine < ActiveRecord::Base/ + assert_file "app/models/test_app/product_line.rb", /module TestApp\n class ProductLine < ApplicationRecord/ assert_file "test/models/test_app/product_line_test.rb", /module TestApp\n class ProductLineTest < ActiveSupport::TestCase/ assert_file "test/fixtures/test_app/product_lines.yml" assert_migration "db/migrate/create_test_app_product_lines.rb" @@ -218,7 +221,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase /class ProductLinesController < ApplicationController/ assert_file "test/controllers/test_app/product_lines_controller_test.rb", - /module TestApp\n class ProductLinesControllerTest < ActionController::TestCase/ + /module TestApp\n class ProductLinesControllerTest < ActionDispatch::IntegrationTest/ # Views %w(index edit new show _form).each do |view| @@ -268,7 +271,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Model assert_file "app/models/test_app/admin.rb", /module TestApp\n module Admin/ - assert_file "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ActiveRecord::Base/ + assert_file "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ApplicationRecord/ assert_file "test/models/test_app/admin/role_test.rb", /module TestApp\n class Admin::RoleTest < ActiveSupport::TestCase/ assert_file "test/fixtures/test_app/admin/roles.yml" assert_migration "db/migrate/create_test_app_admin_roles.rb" @@ -281,10 +284,11 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Controller assert_file "app/controllers/test_app/admin/roles_controller.rb" do |content| assert_match(/module TestApp\n class Admin::RolesController < ApplicationController/, content) + assert_match(%r(require_dependency "test_app/application_controller"), content) end assert_file "test/controllers/test_app/admin/roles_controller_test.rb", - /module TestApp\n class Admin::RolesControllerTest < ActionController::TestCase/ + /module TestApp\n class Admin::RolesControllerTest < ActionDispatch::IntegrationTest/ # Views %w(index edit new show _form).each do |view| @@ -335,7 +339,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Model assert_file "app/models/test_app/admin/user/special.rb", /module TestApp\n module Admin/ - assert_file "app/models/test_app/admin/user/special/role.rb", /module TestApp\n class Admin::User::Special::Role < ActiveRecord::Base/ + assert_file "app/models/test_app/admin/user/special/role.rb", /module TestApp\n class Admin::User::Special::Role < ApplicationRecord/ assert_file "test/models/test_app/admin/user/special/role_test.rb", /module TestApp\n class Admin::User::Special::RoleTest < ActiveSupport::TestCase/ assert_file "test/fixtures/test_app/admin/user/special/roles.yml" assert_migration "db/migrate/create_test_app_admin_user_special_roles.rb" @@ -351,7 +355,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase end assert_file "test/controllers/test_app/admin/user/special/roles_controller_test.rb", - /module TestApp\n class Admin::User::Special::RolesControllerTest < ActionController::TestCase/ + /module TestApp\n class Admin::User::Special::RolesControllerTest < ActionDispatch::IntegrationTest/ # Views %w(index edit new show _form).each do |view| @@ -395,4 +399,38 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Stylesheets (should not be removed) assert_file "app/assets/stylesheets/scaffold.css" end + + def test_api_scaffold_with_namespace_on_invoke + run_generator [ "admin/role", "name:string", "description:string", "--api" ] + + # Model + assert_file "app/models/test_app/admin.rb", /module TestApp\n module Admin/ + assert_file "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ApplicationRecord/ + assert_file "test/models/test_app/admin/role_test.rb", /module TestApp\n class Admin::RoleTest < ActiveSupport::TestCase/ + assert_file "test/fixtures/test_app/admin/roles.yml" + assert_migration "db/migrate/create_test_app_admin_roles.rb" + + # Route + assert_file "config/routes.rb" do |route| + assert_match(/^ namespace :admin do\n resources :roles\n end$/, route) + end + + # Controller + assert_file "app/controllers/test_app/admin/roles_controller.rb" do |content| + assert_match(/module TestApp\n class Admin::RolesController < ApplicationController/, content) + assert_match(%r(require_dependency "test_app/application_controller"), content) + end + assert_file "test/controllers/test_app/admin/roles_controller_test.rb", + /module TestApp\n class Admin::RolesControllerTest < ActionDispatch::IntegrationTest/ + end +end + +class NamespacedApplicationRecordGeneratorTest < NamespacedGeneratorTestCase + include GeneratorsTestHelper + tests Rails::Generators::ApplicationRecordGenerator + + def test_adds_namespace_to_application_record + run_generator + assert_file "app/models/test_app/application_record.rb", /module TestApp/, / class ApplicationRecord < ActiveRecord::Base/ + end end diff --git a/railties/test/generators/orm_test.rb b/railties/test/generators/orm_test.rb index 88ae930554..6eaf2fbfd3 100644 --- a/railties/test/generators/orm_test.rb +++ b/railties/test/generators/orm_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "generators/generators_test_helper" require "rails/generators/rails/scaffold_controller/scaffold_controller_generator" diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 73e68863f4..66286fc554 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -1,13 +1,15 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/plugin/plugin_generator' -require 'generators/shared_generator_tests' -require 'mocha/setup' # FIXME: stop using mocha +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/plugin/plugin_generator" +require "generators/shared_generator_tests" +require "rails/engine/updater" DEFAULT_PLUGIN_FILES = %w( .gitignore Gemfile Rakefile - README.rdoc + README.md bukkits.gemspec MIT-LICENSE lib @@ -24,23 +26,27 @@ class PluginGeneratorTest < Rails::Generators::TestCase destination File.join(Rails.root, "tmp/bukkits") arguments [destination_root] + def application_path + "#{destination_root}/test/dummy" + end + # brings setup, teardown, and some tests include SharedGeneratorTests def test_invalid_plugin_name_raises_an_error - content = capture(:stderr){ run_generator [File.join(destination_root, "my_plugin-31fr-extension")] } + content = capture(:stderr) { run_generator [File.join(destination_root, "my_plugin-31fr-extension")] } assert_equal "Invalid plugin name my_plugin-31fr-extension. Please give a name which does not contain a namespace starting with numeric characters.\n", content - content = capture(:stderr){ run_generator [File.join(destination_root, "things4.3")] } + content = capture(:stderr) { run_generator [File.join(destination_root, "things4.3")] } assert_equal "Invalid plugin name things4.3. Please give a name which uses only alphabetic, numeric, \"_\" or \"-\" characters.\n", content - content = capture(:stderr){ run_generator [File.join(destination_root, "43things")] } + content = capture(:stderr) { run_generator [File.join(destination_root, "43things")] } assert_equal "Invalid plugin name 43things. Please give a name which does not start with numbers.\n", content - content = capture(:stderr){ run_generator [File.join(destination_root, "plugin")] } + content = capture(:stderr) { run_generator [File.join(destination_root, "plugin")] } assert_equal "Invalid plugin name plugin. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n", content - content = capture(:stderr){ run_generator [File.join(destination_root, "Digest")] } + content = capture(:stderr) { run_generator [File.join(destination_root, "Digest")] } assert_equal "Invalid plugin name Digest, constant Digest is already in use. Please choose another plugin name.\n", content end @@ -48,7 +54,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator [File.join(destination_root, "hyphenated-name")] assert_no_file "hyphenated-name/lib/hyphenated-name.rb" assert_no_file "hyphenated-name/lib/hyphenated_name.rb" - assert_file "hyphenated-name/lib/hyphenated/name.rb", /module Hyphenated\n module Name\n # Your code goes here...\n end\nend/ + assert_file "hyphenated-name/lib/hyphenated/name.rb", /module Hyphenated\n module Name\n # Your code goes here\.\.\.\n end\nend/ end def test_correct_file_in_lib_folder_of_camelcase_plugin_name @@ -59,14 +65,33 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_generating_without_options run_generator - assert_file "README.rdoc", /Bukkits/ + assert_file "README.md", /Bukkits/ assert_no_file "config/routes.rb" + assert_no_file "app/assets/config/bukkits_manifest.js" assert_file "test/test_helper.rb" do |content| - assert_match(/require.+test\/dummy\/config\/environment/, content) + assert_match(/require_relative.+test\/dummy\/config\/environment/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+test\/dummy\/db\/migrate/, content) assert_match(/Minitest\.backtrace_filter = Minitest::BacktraceFilter\.new/, content) + assert_match(/Rails::TestUnitReporter\.executable = 'bin\/test'/, content) end + assert_file "lib/bukkits/railtie.rb", /module Bukkits\n class Railtie < ::Rails::Railtie\n end\nend/ + assert_file "lib/bukkits.rb", /require "bukkits\/railtie"/ assert_file "test/bukkits_test.rb", /assert_kind_of Module, Bukkits/ + assert_file "bin/test" + assert_no_file "bin/rails" + end + + def test_generating_in_full_mode_with_almost_of_all_skip_options + run_generator [destination_root, "--full", "-M", "-O", "-C", "-S", "-T", "--skip-active-storage"] + assert_file "bin/rails" do |content| + assert_no_match(/\s+require\s+["']rails\/all["']/, content) + end + assert_file "bin/rails", /#\s+require\s+["']active_record\/railtie["']/ + assert_file "bin/rails", /#\s+require\s+["']active_storage\/engine["']/ + assert_file "bin/rails", /#\s+require\s+["']action_mailer\/railtie["']/ + assert_file "bin/rails", /#\s+require\s+["']action_cable\/engine["']/ + assert_file "bin/rails", /#\s+require\s+["']sprockets\/railtie["']/ + assert_file "bin/rails", /#\s+require\s+["']rails\/test_unit\/railtie["']/ end def test_generating_test_files_in_full_mode @@ -76,8 +101,13 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "test/integration/navigation_test.rb", /ActionDispatch::IntegrationTest/ end + def test_inclusion_of_git_source + run_generator [destination_root] + assert_file "Gemfile", /git_source/ + end + def test_inclusion_of_a_debugger - run_generator [destination_root, '--full'] + run_generator [destination_root, "--full"] if defined?(JRUBY_VERSION) || RUBY_ENGINE == "rbx" assert_file "Gemfile" do |content| assert_no_match(/byebug/, content) @@ -91,25 +121,25 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "-T", "--full"] assert_no_directory "test/integration/" - assert_no_file "test" + assert_no_directory "test" assert_file "Rakefile" do |contents| assert_no_match(/APP_RAKEFILE/, contents) end + assert_file "bin/rails" do |contents| + assert_no_match(/APP_PATH/, contents) + end end def test_generating_adds_dummy_app_rake_tasks_without_unit_test_files - run_generator [destination_root, "-T", "--mountable", '--dummy-path', 'my_dummy_app'] + run_generator [destination_root, "-T", "--mountable", "--dummy-path", "my_dummy_app"] assert_file "Rakefile", /APP_RAKEFILE/ + assert_file "bin/rails", /APP_PATH/ end def test_generating_adds_dummy_app_without_javascript_and_assets_deps run_generator assert_file "test/dummy/app/assets/stylesheets/application.css" - - assert_file "test/dummy/app/assets/javascripts/application.js" do |contents| - assert_no_match(/jquery/, contents) - end end def test_ensure_that_plugin_options_are_not_passed_to_app_generator @@ -120,8 +150,8 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_ensure_that_test_dummy_can_be_generated_from_a_template FileUtils.cd(Rails.root) run_generator([destination_root, "-m", "lib/create_test_dummy_template.rb", "--skip-test"]) - assert_file "spec/dummy" - assert_no_file "test" + assert_directory "spec/dummy" + assert_no_directory "test" end def test_database_entry_is_generated_for_sqlite3_by_default_in_full_mode @@ -140,78 +170,47 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "--skip-active-record"] assert_file "bukkits.gemspec" do |contents| - assert_no_match(/s.add_development_dependency "sqlite3"/, contents) + assert_no_match(/s\.add_development_dependency "sqlite3"/, contents) end end - def test_app_generator_without_skips - run_generator - assert_file "test/dummy/config/application.rb", /\s+require\s+["']rails\/all["']/ - assert_file "test/dummy/config/environments/development.rb" do |content| - assert_match(/config\.action_mailer\.raise_delivery_errors = false/, content) - end - assert_file "test/dummy/config/environments/test.rb" do |content| - assert_match(/config\.action_mailer\.delivery_method = :test/, content) - end - assert_file "test/dummy/config/environments/production.rb" do |content| - assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content) - end - end - - def test_active_record_is_removed_from_frameworks_if_skip_active_record_is_given - run_generator [destination_root, "--skip-active-record"] - assert_file "test/dummy/config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ - end - def test_ensure_that_skip_active_record_option_is_passed_to_app_generator run_generator [destination_root, "--skip_active_record"] - assert_no_file "test/dummy/config/database.yml" assert_file "test/test_helper.rb" do |contents| assert_no_match(/ActiveRecord/, contents) end end - def test_action_mailer_is_removed_from_frameworks_if_skip_action_mailer_is_given - run_generator [destination_root, "--skip-action-mailer"] - assert_file "test/dummy/config/application.rb", /#\s+require\s+["']action_mailer\/railtie["']/ - assert_file "test/dummy/config/environments/development.rb" do |content| - assert_no_match(/config\.action_mailer/, content) - end - assert_file "test/dummy/config/environments/test.rb" do |content| - assert_no_match(/config\.action_mailer/, content) - end - assert_file "test/dummy/config/environments/production.rb" do |content| - assert_no_match(/config\.action_mailer/, content) - end - end - def test_ensure_that_database_option_is_passed_to_app_generator run_generator [destination_root, "--database", "postgresql"] assert_file "test/dummy/config/database.yml", /postgres/ end - def test_generation_runs_bundle_install_with_full_and_mountable - result = run_generator [destination_root, "--mountable", "--full", "--dev"] - assert_match(/run bundle install/, result) - assert $?.success?, "Command failed: #{result}" - assert_file "#{destination_root}/Gemfile.lock" do |contents| - assert_match(/bukkits/, contents) - end + def test_generation_runs_bundle_install + assert_generates_without_bundler end - def test_skipping_javascripts_without_mountable_option - run_generator - assert_no_file "app/assets/javascripts/bukkits/application.js" + def test_dev_option + assert_generates_without_bundler(dev: true) + rails_path = File.expand_path("../../..", Rails.root) + assert_file "Gemfile", /^gem\s+["']rails["'],\s+path:\s+["']#{Regexp.escape(rails_path)}["']$/ end - def test_javascripts_generation - run_generator [destination_root, "--mountable"] - assert_file "app/assets/javascripts/bukkits/application.js" + def test_edge_option + assert_generates_without_bundler(edge: true) + assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["']$} + end + + def test_generation_does_not_run_bundle_install_with_full_and_mountable + assert_generates_without_bundler(mountable: true, full: true, dev: true) + assert_no_file "#{destination_root}/Gemfile.lock" end - def test_skip_javascripts + def test_skip_javascript run_generator [destination_root, "--skip-javascript", "--mountable"] - assert_no_file "app/assets/javascripts/bukkits/application.js" + assert_file "app/views/layouts/bukkits/application.html.erb" do |content| + assert_no_match "javascript_pack_tag", content + end end def test_template_from_dir_pwd @@ -222,28 +221,27 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_ensure_that_tests_work run_generator FileUtils.cd destination_root - quietly { system 'bundle install' } - assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`) + quietly { system "bundle install" } + assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bin/test 2>&1`) end def test_ensure_that_tests_works_in_full_mode run_generator [destination_root, "--full", "--skip_active_record"] FileUtils.cd destination_root - quietly { system 'bundle install' } + quietly { system "bundle install" } assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`) end def test_ensure_that_migration_tasks_work_with_mountable_option run_generator [destination_root, "--mountable"] FileUtils.cd destination_root - quietly { system 'bundle install' } - output = `bundle exec rake db:migrate 2>&1` + quietly { system "bundle install" } + output = `bin/rails db:migrate 2>&1` assert $?.success?, "Command failed: #{output}" end def test_creating_engine_in_full_mode run_generator [destination_root, "--full"] - assert_file "app/assets/javascripts/bukkits" assert_file "app/assets/stylesheets/bukkits" assert_file "app/assets/images/bukkits" assert_file "app/models" @@ -251,7 +249,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "app/views" assert_file "app/helpers" assert_file "app/mailers" - assert_file "bin/rails" + assert_file "bin/rails", /\s+require\s+["']rails\/all["']/ assert_file "config/routes.rb", /Rails.application.routes.draw do/ assert_file "lib/bukkits/engine.rb", /module Bukkits\n class Engine < ::Rails::Engine\n end\nend/ assert_file "lib/bukkits.rb", /require "bukkits\/engine"/ @@ -259,7 +257,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_creating_engine_with_hyphenated_name_in_full_mode run_generator [File.join(destination_root, "hyphenated-name"), "--full"] - assert_file "hyphenated-name/app/assets/javascripts/hyphenated/name" + assert_no_file "hyphenated-name/app/assets/javascripts/hyphenated/name" assert_file "hyphenated-name/app/assets/stylesheets/hyphenated/name" assert_file "hyphenated-name/app/assets/images/hyphenated/name" assert_file "hyphenated-name/app/models" @@ -271,12 +269,12 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "hyphenated-name/config/routes.rb", /Rails.application.routes.draw do/ assert_file "hyphenated-name/lib/hyphenated/name/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n end\n end\nend/ assert_file "hyphenated-name/lib/hyphenated/name.rb", /require "hyphenated\/name\/engine"/ - assert_file "hyphenated-name/bin/rails", /\.\.\/\.\.\/lib\/hyphenated\/name\/engine/ + assert_file "hyphenated-name/bin/rails", /\.\.\/lib\/hyphenated\/name\/engine/ end def test_creating_engine_with_hyphenated_and_underscored_name_in_full_mode run_generator [File.join(destination_root, "my_hyphenated-name"), "--full"] - assert_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name" + assert_no_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name" assert_file "my_hyphenated-name/app/assets/stylesheets/my_hyphenated/name" assert_file "my_hyphenated-name/app/assets/images/my_hyphenated/name" assert_file "my_hyphenated-name/app/models" @@ -285,106 +283,124 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "my_hyphenated-name/app/helpers" assert_file "my_hyphenated-name/app/mailers" assert_file "my_hyphenated-name/bin/rails" - assert_file "my_hyphenated-name/config/routes.rb", /Rails.application.routes.draw do/ + assert_file "my_hyphenated-name/config/routes.rb", /Rails\.application\.routes\.draw do/ assert_file "my_hyphenated-name/lib/my_hyphenated/name/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n end\n end\nend/ assert_file "my_hyphenated-name/lib/my_hyphenated/name.rb", /require "my_hyphenated\/name\/engine"/ - assert_file "my_hyphenated-name/bin/rails", /\.\.\/\.\.\/lib\/my_hyphenated\/name\/engine/ + assert_file "my_hyphenated-name/bin/rails", /\.\.\/lib\/my_hyphenated\/name\/engine/ end def test_being_quiet_while_creating_dummy_application - assert_no_match(/create\s+config\/application.rb/, run_generator) + assert_no_match(/create\s+config\/application\.rb/, run_generator) end def test_create_mountable_application_with_mountable_option run_generator [destination_root, "--mountable"] - assert_file "app/assets/javascripts/bukkits" + assert_no_file "app/assets/javascripts/bukkits" assert_file "app/assets/stylesheets/bukkits" assert_file "app/assets/images/bukkits" - assert_file "config/routes.rb", /Bukkits::Engine.routes.draw do/ + assert_file "config/routes.rb", /Bukkits::Engine\.routes\.draw do/ assert_file "lib/bukkits/engine.rb", /isolate_namespace Bukkits/ assert_file "test/dummy/config/routes.rb", /mount Bukkits::Engine => "\/bukkits"/ assert_file "app/controllers/bukkits/application_controller.rb", /module Bukkits\n class ApplicationController < ActionController::Base/ + assert_file "app/models/bukkits/application_record.rb", /module Bukkits\n class ApplicationRecord < ActiveRecord::Base/ + assert_file "app/jobs/bukkits/application_job.rb", /module Bukkits\n class ApplicationJob < ActiveJob::Base/ + assert_file "app/mailers/bukkits/application_mailer.rb", /module Bukkits\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n/ assert_file "app/helpers/bukkits/application_helper.rb", /module Bukkits\n module ApplicationHelper/ assert_file "app/views/layouts/bukkits/application.html.erb" do |contents| assert_match "<title>Bukkits</title>", contents + assert_match "<%= csrf_meta_tags %>", contents + assert_match "<%= csp_meta_tag %>", contents assert_match(/stylesheet_link_tag\s+['"]bukkits\/application['"]/, contents) - assert_match(/javascript_include_tag\s+['"]bukkits\/application['"]/, contents) + assert_no_match(/javascript_include_tag\s+['"]bukkits\/application['"]/, contents) + assert_match "<%= yield %>", contents end assert_file "test/test_helper.rb" do |content| assert_match(/ActiveRecord::Migrator\.migrations_paths.+\.\.\/test\/dummy\/db\/migrate/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+<<.+\.\.\/db\/migrate/, content) + assert_match(/ActionDispatch::IntegrationTest\.fixture_path = ActiveSupport::TestCase\.fixture_pat/, content) + assert_no_match(/Rails::TestUnitReporter\.executable = 'bin\/test'/, content) end + assert_no_file "bin/test" end def test_create_mountable_application_with_mountable_option_and_hypenated_name run_generator [File.join(destination_root, "hyphenated-name"), "--mountable"] - assert_file "hyphenated-name/app/assets/javascripts/hyphenated/name" + assert_no_file "hyphenated-name/app/assets/javascripts/hyphenated/name" assert_file "hyphenated-name/app/assets/stylesheets/hyphenated/name" assert_file "hyphenated-name/app/assets/images/hyphenated/name" - assert_file "hyphenated-name/config/routes.rb", /Hyphenated::Name::Engine.routes.draw do/ - assert_file "hyphenated-name/lib/hyphenated/name/version.rb", /module Hyphenated\n module Name\n VERSION = "0.0.1"\n end\nend/ + assert_file "hyphenated-name/config/routes.rb", /Hyphenated::Name::Engine\.routes\.draw do/ + assert_file "hyphenated-name/lib/hyphenated/name/version.rb", /module Hyphenated\n module Name\n VERSION = '0\.1\.0'\n end\nend/ assert_file "hyphenated-name/lib/hyphenated/name/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Hyphenated::Name\n end\n end\nend/ assert_file "hyphenated-name/lib/hyphenated/name.rb", /require "hyphenated\/name\/engine"/ assert_file "hyphenated-name/test/dummy/config/routes.rb", /mount Hyphenated::Name::Engine => "\/hyphenated-name"/ - assert_file "hyphenated-name/app/controllers/hyphenated/name/application_controller.rb", /module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n end\n end\nend/ + assert_file "hyphenated-name/app/controllers/hyphenated/name/application_controller.rb", /module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\nend\n/ + assert_file "hyphenated-name/app/models/hyphenated/name/application_record.rb", /module Hyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\nend/ + assert_file "hyphenated-name/app/jobs/hyphenated/name/application_job.rb", /module Hyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ + assert_file "hyphenated-name/app/mailers/hyphenated/name/application_mailer.rb", /module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n end\n end\nend/ assert_file "hyphenated-name/app/helpers/hyphenated/name/application_helper.rb", /module Hyphenated\n module Name\n module ApplicationHelper\n end\n end\nend/ assert_file "hyphenated-name/app/views/layouts/hyphenated/name/application.html.erb" do |contents| assert_match "<title>Hyphenated name</title>", contents assert_match(/stylesheet_link_tag\s+['"]hyphenated\/name\/application['"]/, contents) - assert_match(/javascript_include_tag\s+['"]hyphenated\/name\/application['"]/, contents) + assert_no_match(/javascript_include_tag\s+['"]hyphenated\/name\/application['"]/, contents) end end def test_create_mountable_application_with_mountable_option_and_hypenated_and_underscored_name run_generator [File.join(destination_root, "my_hyphenated-name"), "--mountable"] - assert_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name" + assert_no_file "my_hyphenated-name/app/assets/javascripts/my_hyphenated/name" assert_file "my_hyphenated-name/app/assets/stylesheets/my_hyphenated/name" assert_file "my_hyphenated-name/app/assets/images/my_hyphenated/name" - assert_file "my_hyphenated-name/config/routes.rb", /MyHyphenated::Name::Engine.routes.draw do/ - assert_file "my_hyphenated-name/lib/my_hyphenated/name/version.rb", /module MyHyphenated\n module Name\n VERSION = "0.0.1"\n end\nend/ + assert_file "my_hyphenated-name/config/routes.rb", /MyHyphenated::Name::Engine\.routes\.draw do/ + assert_file "my_hyphenated-name/lib/my_hyphenated/name/version.rb", /module MyHyphenated\n module Name\n VERSION = '0\.1\.0'\n end\nend/ assert_file "my_hyphenated-name/lib/my_hyphenated/name/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace MyHyphenated::Name\n end\n end\nend/ assert_file "my_hyphenated-name/lib/my_hyphenated/name.rb", /require "my_hyphenated\/name\/engine"/ assert_file "my_hyphenated-name/test/dummy/config/routes.rb", /mount MyHyphenated::Name::Engine => "\/my_hyphenated-name"/ - assert_file "my_hyphenated-name/app/controllers/my_hyphenated/name/application_controller.rb", /module MyHyphenated\n module Name\n class ApplicationController < ActionController::Base\n end\n end\nend/ + assert_file "my_hyphenated-name/app/controllers/my_hyphenated/name/application_controller.rb", /module MyHyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\nend\n/ + assert_file "my_hyphenated-name/app/models/my_hyphenated/name/application_record.rb", /module MyHyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\nend/ + assert_file "my_hyphenated-name/app/jobs/my_hyphenated/name/application_job.rb", /module MyHyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ + assert_file "my_hyphenated-name/app/mailers/my_hyphenated/name/application_mailer.rb", /module MyHyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n end\n end\nend/ assert_file "my_hyphenated-name/app/helpers/my_hyphenated/name/application_helper.rb", /module MyHyphenated\n module Name\n module ApplicationHelper\n end\n end\nend/ assert_file "my_hyphenated-name/app/views/layouts/my_hyphenated/name/application.html.erb" do |contents| assert_match "<title>My hyphenated name</title>", contents assert_match(/stylesheet_link_tag\s+['"]my_hyphenated\/name\/application['"]/, contents) - assert_match(/javascript_include_tag\s+['"]my_hyphenated\/name\/application['"]/, contents) + assert_no_match(/javascript_include_tag\s+['"]my_hyphenated\/name\/application['"]/, contents) end end def test_create_mountable_application_with_mountable_option_and_multiple_hypenates_in_name run_generator [File.join(destination_root, "deep-hyphenated-name"), "--mountable"] - assert_file "deep-hyphenated-name/app/assets/javascripts/deep/hyphenated/name" + assert_no_file "deep-hyphenated-name/app/assets/javascripts/deep/hyphenated/name" assert_file "deep-hyphenated-name/app/assets/stylesheets/deep/hyphenated/name" assert_file "deep-hyphenated-name/app/assets/images/deep/hyphenated/name" - assert_file "deep-hyphenated-name/config/routes.rb", /Deep::Hyphenated::Name::Engine.routes.draw do/ - assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/version.rb", /module Deep\n module Hyphenated\n module Name\n VERSION = "0.0.1"\n end\n end\nend/ + assert_file "deep-hyphenated-name/config/routes.rb", /Deep::Hyphenated::Name::Engine\.routes\.draw do/ + assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/version.rb", /module Deep\n module Hyphenated\n module Name\n VERSION = '0\.1\.0'\n end\n end\nend/ assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/engine.rb", /module Deep\n module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Deep::Hyphenated::Name\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/lib/deep/hyphenated/name.rb", /require "deep\/hyphenated\/name\/engine"/ assert_file "deep-hyphenated-name/test/dummy/config/routes.rb", /mount Deep::Hyphenated::Name::Engine => "\/deep-hyphenated-name"/ - assert_file "deep-hyphenated-name/app/controllers/deep/hyphenated/name/application_controller.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n end\n end\n end\nend/ + assert_file "deep-hyphenated-name/app/controllers/deep/hyphenated/name/application_controller.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\n end\nend\n/ + assert_file "deep-hyphenated-name/app/models/deep/hyphenated/name/application_record.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\n end\nend/ + assert_file "deep-hyphenated-name/app/jobs/deep/hyphenated/name/application_job.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/ + assert_file "deep-hyphenated-name/app/mailers/deep/hyphenated/name/application_mailer.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example\.com'\n layout 'mailer'\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/app/helpers/deep/hyphenated/name/application_helper.rb", /module Deep\n module Hyphenated\n module Name\n module ApplicationHelper\n end\n end\n end\nend/ assert_file "deep-hyphenated-name/app/views/layouts/deep/hyphenated/name/application.html.erb" do |contents| assert_match "<title>Deep hyphenated name</title>", contents assert_match(/stylesheet_link_tag\s+['"]deep\/hyphenated\/name\/application['"]/, contents) - assert_match(/javascript_include_tag\s+['"]deep\/hyphenated\/name\/application['"]/, contents) + assert_no_match(/javascript_include_tag\s+['"]deep\/hyphenated\/name\/application['"]/, contents) end end def test_creating_gemspec run_generator - assert_file "bukkits.gemspec", /s.name\s+= "bukkits"/ - assert_file "bukkits.gemspec", /s.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.rdoc"\]/ - assert_file "bukkits.gemspec", /s.test_files = Dir\["test\/\*\*\/\*"\]/ - assert_file "bukkits.gemspec", /s.version\s+ = Bukkits::VERSION/ + assert_file "bukkits.gemspec", /spec\.name\s+= "bukkits"/ + assert_file "bukkits.gemspec", /spec\.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.md"\]/ + assert_file "bukkits.gemspec", /spec\.version\s+ = Bukkits::VERSION/ end def test_usage_of_engine_commands run_generator [destination_root, "--full"] - assert_file "bin/rails", /ENGINE_PATH = File.expand_path\('..\/..\/lib\/bukkits\/engine', __FILE__\)/ - assert_file "bin/rails", /ENGINE_ROOT = File.expand_path\('..\/..', __FILE__\)/ + assert_file "bin/rails", /ENGINE_PATH = File\.expand_path\('\.\.\/lib\/bukkits\/engine', __dir__\)/ + assert_file "bin/rails", /ENGINE_ROOT = File\.expand_path\('\.\.', __dir__\)/ + assert_file "bin/rails", %r|APP_PATH = File\.expand_path\('\.\./test/dummy/config/application', __dir__\)| assert_file "bin/rails", /require 'rails\/all'/ assert_file "bin/rails", /require 'rails\/engine\/commands'/ end @@ -400,7 +416,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "spec/dummy/config/application.rb" assert_no_file "test/dummy" assert_file "test/test_helper.rb" do |content| - assert_match(/require.+spec\/dummy\/config\/environment/, content) + assert_match(/require_relative.+spec\/dummy\/config\/environment/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+spec\/dummy\/db\/migrate/, content) end end @@ -411,22 +427,29 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "spec/fake/config/application.rb" assert_no_file "test/dummy" assert_file "test/test_helper.rb" do |content| - assert_match(/require.+spec\/fake\/config\/environment/, content) + assert_match(/require_relative.+spec\/fake\/config\/environment/, content) assert_match(/ActiveRecord::Migrator\.migrations_paths.+spec\/fake\/db\/migrate/, content) end end def test_creating_dummy_without_tests_but_with_dummy_path run_generator [destination_root, "--dummy_path", "spec/dummy", "--skip-test"] - assert_file "spec/dummy" - assert_file "spec/dummy/config/application.rb" - assert_no_file "test" - assert_no_file "test/test_helper.rb" - assert_file '.gitignore' do |contents| + assert_directory "spec/dummy" + assert_file "spec/dummy/config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/ + assert_no_directory "test" + assert_file ".gitignore" do |contents| assert_match(/spec\/dummy/, contents) end end + def test_dummy_appplication_skip_listen_by_default + run_generator + + assert_file "test/dummy/config/environments/development.rb" do |contents| + assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, contents) + end + end + def test_ensure_that_gitignore_can_be_generated_from_a_template_for_dummy_path FileUtils.cd(Rails.root) run_generator([destination_root, "--dummy_path", "spec/dummy", "--skip-test"]) @@ -435,14 +458,26 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end + def test_unnecessary_files_are_not_generated_in_dummy_application + run_generator + assert_no_file "test/dummy/.gitignore" + assert_no_file "test/dummy/db/seeds.rb" + assert_no_file "test/dummy/Gemfile" + assert_no_file "test/dummy/public/robots.txt" + assert_no_file "test/dummy/README.md" + assert_no_file "test/dummy/config/master.key" + assert_no_file "test/dummy/config/credentials.yml.enc" + assert_no_directory "test/dummy/lib/tasks" + assert_no_directory "test/dummy/test" + assert_no_directory "test/dummy/vendor" + assert_no_directory "test/dummy/.git" + end + def test_skipping_test_files run_generator [destination_root, "--skip-test"] - assert_no_file "test" - assert_file "bukkits.gemspec" do |contents| - assert_no_match(/s.test_files = Dir\["test\/\*\*\/\*"\]/, contents) - end - assert_file '.gitignore' do |contents| - assert_no_match(/test\dummy/, contents) + assert_no_directory "test" + assert_file ".gitignore" do |contents| + assert_no_match(/test\/dummy/, contents) end end @@ -450,10 +485,9 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "--skip-gemspec"] assert_no_file "bukkits.gemspec" assert_file "Gemfile" do |contents| - assert_no_match('gemspec', contents) - assert_match(/gem 'rails', '~> #{Rails.version}'/, contents) + assert_no_match("gemspec", contents) + assert_match(/gem 'rails'/, contents) assert_match_sqlite3(contents) - assert_no_match(/# gem "jquery-rails"/, contents) end end @@ -461,8 +495,8 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "--skip-gemspec", "--full"] assert_no_file "bukkits.gemspec" assert_file "Gemfile" do |contents| - assert_no_match('gemspec', contents) - assert_match(/gem 'rails', '~> #{Rails.version}'/, contents) + assert_no_match("gemspec", contents) + assert_match(/gem 'rails'/, contents) assert_match_sqlite3(contents) end end @@ -470,21 +504,37 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_creating_plugin_in_app_directory_adds_gemfile_entry # simulate application existence gemfile_path = "#{Rails.root}/Gemfile" - Object.const_set('APP_PATH', Rails.root) + Object.const_set("APP_PATH", Rails.root) FileUtils.touch gemfile_path + File.write(gemfile_path, "#foo") run_generator - assert_file gemfile_path, /gem 'bukkits', path: 'tmp\/bukkits'/ + assert_file gemfile_path, /^gem 'bukkits', path: 'tmp\/bukkits'/ + ensure + Object.send(:remove_const, "APP_PATH") + FileUtils.rm gemfile_path + end + + def test_creating_plugin_only_specify_plugin_name_in_app_directory_adds_gemfile_entry + # simulate application existence + gemfile_path = "#{Rails.root}/Gemfile" + Object.const_set("APP_PATH", Rails.root) + FileUtils.touch gemfile_path + + FileUtils.cd(destination_root) + run_generator ["bukkits"] + + assert_file gemfile_path, /gem 'bukkits', path: 'bukkits'/ ensure - Object.send(:remove_const, 'APP_PATH') + Object.send(:remove_const, "APP_PATH") FileUtils.rm gemfile_path end def test_skipping_gemfile_entry # simulate application existence gemfile_path = "#{Rails.root}/Gemfile" - Object.const_set('APP_PATH', Rails.root) + Object.const_set("APP_PATH", Rails.root) FileUtils.touch gemfile_path run_generator [destination_root, "--skip-gemfile-entry"] @@ -493,7 +543,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_no_match(/gem 'bukkits', path: 'tmp\/bukkits'/, contents) end ensure - Object.send(:remove_const, 'APP_PATH') + Object.send(:remove_const, "APP_PATH") FileUtils.rm gemfile_path end @@ -534,7 +584,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase name = "TODO: Write your name" email = "TODO: Write your email address" - run_generator [destination_root, '--skip-git'] + run_generator [destination_root, "--skip-git"] assert_file "MIT-LICENSE" do |contents| assert_match name, contents end @@ -544,20 +594,188 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end -protected - def action(*args, &block) - silence(:stdout){ generator.send(*args, &block) } + def test_skipping_useless_folders_generation_for_api_engines + ["--full", "--mountable"].each do |option| + run_generator [destination_root, option, "--api"] + + assert_no_directory "app/assets" + assert_no_directory "app/helpers" + assert_no_directory "app/views" + + FileUtils.rm_rf destination_root + end end - def default_files - ::DEFAULT_PLUGIN_FILES + def test_application_controller_parent_for_mountable_api_plugins + run_generator [destination_root, "--mountable", "--api"] + + assert_file "app/controllers/bukkits/application_controller.rb" do |content| + assert_match "ApplicationController < ActionController::API", content + end end - def assert_match_sqlite3(contents) - if defined?(JRUBY_VERSION) - assert_match(/group :development do\n gem 'activerecord-jdbcsqlite3-adapter'\nend/, contents) - else - assert_match(/group :development do\n gem 'sqlite3'\nend/, contents) + def test_dummy_api_application_for_api_plugins + run_generator [destination_root, "--api"] + + assert_file "test/dummy/config/application.rb" do |content| + assert_match "config.api_only = true", content + end + end + + def test_api_generators_configuration_for_api_engines + run_generator [destination_root, "--full", "--api"] + + assert_file "lib/bukkits/engine.rb" do |content| + assert_match "config.generators.api_only = true", content end end + + def test_scaffold_generator_for_mountable_api_plugins + run_generator [destination_root, "--mountable", "--api"] + + capture(:stdout) do + `#{destination_root}/bin/rails g scaffold article` + end + + assert_file "app/models/bukkits/article.rb" + assert_file "app/controllers/bukkits/articles_controller.rb" do |content| + assert_match "only: [:show, :update, :destroy]", content + end + + assert_no_directory "app/assets" + assert_no_directory "app/helpers" + assert_no_directory "app/views" + end + + def test_model_with_existent_application_record_in_mountable_engine + run_generator [destination_root, "--mountable"] + capture(:stdout) do + `#{destination_root}/bin/rails g model article` + end + + assert_file "app/models/bukkits/article.rb", /class Article < ApplicationRecord/ + end + + def test_generate_application_mailer_when_does_not_exist_in_mountable_engine + run_generator [destination_root, "--mountable"] + FileUtils.rm "#{destination_root}/app/mailers/bukkits/application_mailer.rb" + capture(:stdout) do + `#{destination_root}/bin/rails g mailer User` + end + + assert_file "#{destination_root}/app/mailers/bukkits/application_mailer.rb" do |mailer| + assert_match(/module Bukkits/, mailer) + assert_match(/class ApplicationMailer < ActionMailer::Base/, mailer) + end + end + + def test_generate_mailer_layouts_when_does_not_exist_in_mountable_engine + run_generator [destination_root, "--mountable"] + capture(:stdout) do + `#{destination_root}/bin/rails g mailer User` + end + + assert_file "#{destination_root}/app/views/layouts/bukkits/mailer.text.erb" do |view| + assert_match(/<%= yield %>/, view) + end + + assert_file "#{destination_root}/app/views/layouts/bukkits/mailer.html.erb" do |view| + assert_match(%r{<body>\n <%= yield %>\n </body>}, view) + end + end + + def test_generate_application_job_when_does_not_exist_in_mountable_engine + run_generator [destination_root, "--mountable"] + FileUtils.rm "#{destination_root}/app/jobs/bukkits/application_job.rb" + capture(:stdout) do + `#{destination_root}/bin/rails g job refresh_counters` + end + + assert_file "#{destination_root}/app/jobs/bukkits/application_job.rb" do |record| + assert_match(/module Bukkits/, record) + assert_match(/class ApplicationJob < ActiveJob::Base/, record) + end + end + + def test_app_update_generates_bin_file + run_generator [destination_root, "--mountable"] + + Object.const_set("ENGINE_ROOT", destination_root) + FileUtils.rm("#{destination_root}/bin/rails") + + quietly { Rails::Engine::Updater.run(:create_bin_files) } + + assert_file "#{destination_root}/bin/rails" do |content| + assert_match(%r|APP_PATH = File\.expand_path\('\.\./test/dummy/config/application', __dir__\)|, content) + end + ensure + Object.send(:remove_const, "ENGINE_ROOT") + end + + def test_after_bundle_callback + path = "http://example.org/rails_template" + template = +%{ after_bundle { run "echo ran after_bundle" } } + template.instance_eval "def read; self; end" # Make the string respond to read + + check_open = -> *args do + assert_equal [ path, "Accept" => "application/x-thor-template" ], args + template + end + + sequence = ["echo ran after_bundle"] + @sequence_step ||= 0 + ensure_bundler_first = -> command do + assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}" + @sequence_step += 1 + end + + content = nil + generator([destination_root], template: path).stub(:open, check_open, template) do + generator.stub(:bundle_command, ensure_bundler_first) do + generator.stub(:run, ensure_bundler_first) do + silence_stream($stdout) do + content = capture(:stderr) { generator.invoke_all } + end + end + end + end + + assert_equal 1, @sequence_step + assert_match(/DEPRECATION WARNING: `after_bundle` is deprecated/, content) + end + + private + + def action(*args, &block) + silence(:stdout) { generator.send(*args, &block) } + end + + def default_files + ::DEFAULT_PLUGIN_FILES + end + + def assert_match_sqlite3(contents) + if defined?(JRUBY_VERSION) + assert_match(/group :development do\n gem 'activerecord-jdbcsqlite3-adapter'\nend/, contents) + else + assert_match(/group :development do\n gem 'sqlite3'\nend/, contents) + end + end + + def assert_generates_without_bundler(options = {}) + generator([destination_root], options) + + command_check = -> command do + case command + when "install" + flunk "install expected to not be called" + when "exec spring binstub --all" + # Called when running tests with spring, let through unscathed. + end + end + + generator.stub :bundle_command, command_check do + quietly { generator.invoke_all } + end + end end diff --git a/railties/test/generators/plugin_test_helper.rb b/railties/test/generators/plugin_test_helper.rb new file mode 100644 index 0000000000..528f8d88f9 --- /dev/null +++ b/railties/test/generators/plugin_test_helper.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "tmpdir" + +module PluginTestHelper + def create_test_file(name, pass: true) + plugin_file "test/#{name}_test.rb", <<-RUBY + require 'test_helper' + + class #{name.camelize}Test < ActiveSupport::TestCase + def test_truth + puts "#{name.camelize}Test" + assert #{pass}, 'wups!' + end + end + RUBY + end + + def plugin_file(path, contents, mode: "w") + FileUtils.mkdir_p File.dirname("#{plugin_path}/#{path}") + File.open("#{plugin_path}/#{path}", mode) do |f| + f.puts contents + end + end +end diff --git a/railties/test/generators/plugin_test_runner_test.rb b/railties/test/generators/plugin_test_runner_test.rb new file mode 100644 index 0000000000..89c3f1e496 --- /dev/null +++ b/railties/test/generators/plugin_test_runner_test.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require "generators/plugin_test_helper" + +class PluginTestRunnerTest < ActiveSupport::TestCase + include PluginTestHelper + + def setup + @destination_root = Dir.mktmpdir("bukkits") + Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --skip-bundle` } + plugin_file "test/dummy/db/schema.rb", "" + end + + def teardown + FileUtils.rm_rf(@destination_root) + end + + def test_run_single_file + create_test_file "foo" + create_test_file "bar" + assert_match "1 runs, 1 assertions, 0 failures", run_test_command("test/foo_test.rb") + end + + def test_run_multiple_files + create_test_file "foo" + create_test_file "bar" + assert_match "2 runs, 2 assertions, 0 failures", run_test_command("test/foo_test.rb test/bar_test.rb") + end + + def test_mix_files_and_line_filters + create_test_file "account" + plugin_file "test/post_test.rb", <<-RUBY + require 'test_helper' + + class PostTest < ActiveSupport::TestCase + def test_post + puts 'PostTest' + assert true + end + + def test_line_filter_does_not_run_this + assert true + end + end + RUBY + + run_test_command("test/account_test.rb test/post_test.rb:4").tap do |output| + assert_match "AccountTest", output + assert_match "PostTest", output + assert_match "2 runs, 2 assertions", output + end + end + + def test_multiple_line_filters + create_test_file "account" + create_test_file "post" + + run_test_command("test/account_test.rb:4 test/post_test.rb:4").tap do |output| + assert_match "AccountTest", output + assert_match "PostTest", output + end + end + + def test_output_inline_by_default + create_test_file "post", pass: false + + output = run_test_command("test/post_test.rb") + expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/post_test.rb:6\]:\nwups!\n\nbin/test (/private)?#{plugin_path}/test/post_test.rb:4} + assert_match expect, output + end + + def test_only_inline_failure_output + create_test_file "post", pass: false + + output = run_test_command("test/post_test.rb") + assert_match %r{Finished in.*\n1 runs, 1 assertions}, output + end + + def test_fail_fast + create_test_file "post", pass: false + + assert_match(/Interrupt/, + capture(:stderr) { run_test_command("test/post_test.rb --fail-fast") }) + end + + def test_raise_error_when_specified_file_does_not_exist + error = capture(:stderr) { run_test_command("test/not_exists.rb") } + assert_match(%r{cannot load such file.+test/not_exists\.rb}, error) + end + + def test_executed_only_once + create_test_file "foo" + result = run_test_command("test/foo_test.rb") + assert_equal 1, result.scan(/1 runs, 1 assertions, 0 failures/).length + end + + def test_warnings_option + plugin_file "test/models/warnings_test.rb", <<-RUBY + require 'test_helper' + def test_warnings + a = 1 + end + RUBY + assert_match(/warning: assigned but unused variable/, + capture(:stderr) { run_test_command("test/models/warnings_test.rb -w") }) + end + + def test_run_rake_test + create_test_file "foo" + result = Dir.chdir(plugin_path) { `rake test TEST=test/foo_test.rb` } + assert_match "1 runs, 1 assertions, 0 failures", result + end + + private + def plugin_path + "#{@destination_root}/bukkits" + end + + def run_test_command(arguments) + Dir.chdir(plugin_path) { `bin/test #{arguments}` } + end +end diff --git a/railties/test/generators/resource_generator_test.rb b/railties/test/generators/resource_generator_test.rb index 581d80d60e..b99b4baf6b 100644 --- a/railties/test/generators/resource_generator_test.rb +++ b/railties/test/generators/resource_generator_test.rb @@ -1,11 +1,17 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/resource/resource_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/resource/resource_generator" class ResourceGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper arguments %w(account) - setup :copy_routes + def setup + super + copy_routes + Rails::Generators::ModelHelpers.skip_warn = false + end def test_help_with_inherited_options content = run_generator ["--help"] @@ -33,7 +39,7 @@ class ResourceGeneratorTest < Rails::Generators::TestCase def test_resource_controller_with_pluralized_class_name run_generator assert_file "app/controllers/accounts_controller.rb", /class AccountsController < ApplicationController/ - assert_file "test/controllers/accounts_controller_test.rb", /class AccountsControllerTest < ActionController::TestCase/ + assert_file "test/controllers/accounts_controller_test.rb", /class AccountsControllerTest < ActionDispatch::IntegrationTest/ assert_file "app/helpers/accounts_helper.rb", /module AccountsHelper/ end @@ -59,21 +65,21 @@ class ResourceGeneratorTest < Rails::Generators::TestCase end def test_plural_names_are_singularized - content = run_generator ["accounts".freeze] - assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/ + content = run_generator ["accounts"] + assert_file "app/models/account.rb", /class Account < ApplicationRecord/ assert_file "test/models/account_test.rb", /class AccountTest/ assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content) end def test_plural_names_can_be_forced content = run_generator ["accounts", "--force-plural"] - assert_file "app/models/accounts.rb", /class Accounts < ActiveRecord::Base/ + assert_file "app/models/accounts.rb", /class Accounts < ApplicationRecord/ assert_file "test/models/accounts_test.rb", /class AccountsTest/ assert_no_match(/\[WARNING\]/, content) end def test_mass_nouns_do_not_throw_warnings - content = run_generator ["sheep".freeze] + content = run_generator ["sheep"] assert_no_match(/\[WARNING\]/, content) end diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index 648b8d9325..fd5aa817b4 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -1,5 +1,7 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/scaffold_controller/scaffold_controller_generator" module Unknown module Generators @@ -56,7 +58,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_file "app/controllers/users_controller.rb" do |content| assert_match(/def user_params/, content) - assert_match(/params\[:user\]/, content) + assert_match(/params\.fetch\(:user, \{\}\)/, content) end end @@ -104,10 +106,10 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase run_generator ["User", "name:string", "age:integer", "organization:references{polymorphic}"] assert_file "test/controllers/users_controller_test.rb" do |content| - assert_match(/class UsersControllerTest < ActionController::TestCase/, content) + assert_match(/class UsersControllerTest < ActionDispatch::IntegrationTest/, content) assert_match(/test "should get index"/, content) - assert_match(/post :create, params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content) - assert_match(/patch :update, params: \{ id: @user, user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content) + assert_match(/post users_url, params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content) + assert_match(/patch user_url\(@user\), params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content) end end @@ -115,10 +117,10 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase run_generator ["User"] assert_file "test/controllers/users_controller_test.rb" do |content| - assert_match(/class UsersControllerTest < ActionController::TestCase/, content) + assert_match(/class UsersControllerTest < ActionDispatch::IntegrationTest/, content) assert_match(/test "should get index"/, content) - assert_match(/post :create, params: \{ user: \{ \} \}/, content) - assert_match(/patch :update, params: \{ id: @user, user: \{ \} \}/, content) + assert_match(/post users_url, params: \{ user: \{ \} \}/, content) + assert_match(/patch user_url\(@user\), params: \{ user: \{ \} \}/, content) end end @@ -172,6 +174,29 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_instance_method :index, content do |m| assert_match("@users = User.all", m) end + + assert_instance_method :create, content do |m| + assert_match("redirect_to [:admin, @user]", m) + end + + assert_instance_method :update, content do |m| + assert_match("redirect_to [:admin, @user]", m) + end + end + + assert_file "app/views/admin/users/index.html.erb" do |content| + assert_match("'Show', [:admin, user]", content) + assert_match("'Edit', edit_admin_user_path(user)", content) + assert_match("'Destroy', [:admin, user]", content) + assert_match("'New User', new_admin_user_path", content) + end + + assert_file "app/views/admin/users/new.html.erb" do |content| + assert_match("'Back', admin_users_path", content) + end + + assert_file "app/views/admin/users/_form.html.erb" do |content| + assert_match("model: [:admin, user]", content) end end @@ -182,7 +207,8 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase Dir.chdir(engine_path) do quietly { `bin/rails g controller dashboard foo` } - assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`) + quietly { `bin/rails db:migrate RAILS_ENV=test` } + assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) end end @@ -193,7 +219,8 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase Dir.chdir(engine_path) do quietly { `bin/rails g controller dashboard foo` } - assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`) + quietly { `bin/rails db:migrate RAILS_ENV=test` } + assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) end end @@ -230,16 +257,22 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_match(/@user\.destroy/, m) end end + + assert_no_file "app/views/users/index.html.erb" + assert_no_file "app/views/users/edit.html.erb" + assert_no_file "app/views/users/show.html.erb" + assert_no_file "app/views/users/new.html.erb" + assert_no_file "app/views/users/_form.html.erb" end def test_api_controller_tests run_generator ["User", "name:string", "age:integer", "organization:references{polymorphic}", "--api"] assert_file "test/controllers/users_controller_test.rb" do |content| - assert_match(/class UsersControllerTest < ActionController::TestCase/, content) + assert_match(/class UsersControllerTest < ActionDispatch::IntegrationTest/, content) assert_match(/test "should get index"/, content) - assert_match(/post :create, params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content) - assert_match(/patch :update, params: \{ id: @user, user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}/, content) + assert_match(/post users_url, params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}, as: :json/, content) + assert_match(/patch user_url\(@user\), params: \{ user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \} \}, as: :json/, content) assert_no_match(/assert_redirected_to/, content) end end diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index debf1140ab..3cffdddd38 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -1,5 +1,7 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/scaffold/scaffold_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/scaffold/scaffold_generator" class ScaffoldGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -11,11 +13,11 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase run_generator # Model - assert_file "app/models/product_line.rb", /class ProductLine < ActiveRecord::Base/ + assert_file "app/models/product_line.rb", /class ProductLine < ApplicationRecord/ assert_file "test/models/product_line_test.rb", /class ProductLineTest < ActiveSupport::TestCase/ assert_file "test/fixtures/product_lines.yml" - assert_migration "db/migrate/create_product_lines.rb", /belongs_to :product, index: true/ - assert_migration "db/migrate/create_product_lines.rb", /references :user, index: true/ + assert_migration "db/migrate/create_product_lines.rb", /belongs_to :product/ + assert_migration "db/migrate/create_product_lines.rb", /references :user/ # Route assert_file "config/routes.rb" do |route| @@ -57,9 +59,17 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end assert_file "test/controllers/product_lines_controller_test.rb" do |test| - assert_match(/class ProductLinesControllerTest < ActionController::TestCase/, test) - assert_match(/post :create, params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) - assert_match(/patch :update, params: \{ id: @product_line, product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) + assert_match(/class ProductLinesControllerTest < ActionDispatch::IntegrationTest/, test) + assert_match(/post product_lines_url, params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) + assert_match(/patch product_line_url\(@product_line\), params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) + end + + # System tests + assert_file "test/system/product_lines_test.rb" do |test| + assert_match(/class ProductLinesTest < ApplicationSystemTestCase/, test) + assert_match(/visit product_lines_url/, test) + assert_match(/fill_in "Title", with: @product_line\.title/, test) + assert_match(/assert_text "Product line was successfully updated"/, test) end # Views @@ -74,8 +84,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end assert_file "app/views/product_lines/_form.html.erb" do |test| - assert_match 'product_line', test - assert_no_match '@product_line', test + assert_match "product_line", test + assert_no_match "@product_line", test end # Helpers @@ -83,7 +93,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Assets assert_file "app/assets/stylesheets/scaffold.css" - assert_file "app/assets/javascripts/product_lines.js" assert_file "app/assets/stylesheets/product_lines.css" end @@ -91,11 +100,11 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase run_generator %w(product_line title:string product:belongs_to user:references --api --no-template-engine --no-helper --no-assets) # Model - assert_file "app/models/product_line.rb", /class ProductLine < ActiveRecord::Base/ + assert_file "app/models/product_line.rb", /class ProductLine < ApplicationRecord/ assert_file "test/models/product_line_test.rb", /class ProductLineTest < ActiveSupport::TestCase/ assert_file "test/fixtures/product_lines.yml" - assert_migration "db/migrate/create_product_lines.rb", /belongs_to :product, index: true/ - assert_migration "db/migrate/create_product_lines.rb", /references :user, index: true/ + assert_migration "db/migrate/create_product_lines.rb", /belongs_to :product/ + assert_migration "db/migrate/create_product_lines.rb", /references :user/ # Route assert_file "config/routes.rb" do |route| @@ -135,12 +144,15 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end assert_file "test/controllers/product_lines_controller_test.rb" do |test| - assert_match(/class ProductLinesControllerTest < ActionController::TestCase/, test) - assert_match(/post :create, params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) - assert_match(/patch :update, params: \{ id: @product_line, product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) + assert_match(/class ProductLinesControllerTest < ActionDispatch::IntegrationTest/, test) + assert_match(/post product_lines_url, params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) + assert_match(/patch product_line_url\(@product_line\), params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) assert_no_match(/assert_redirected_to/, test) end + # System tests + assert_no_file "test/system/product_lines_test.rb" + # Views assert_no_file "app/views/layouts/product_lines.html.erb" @@ -153,7 +165,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Assets assert_no_file "app/assets/stylesheets/scaffold.css" - assert_no_file "app/assets/javascripts/product_lines.js" assert_no_file "app/assets/stylesheets/product_lines.css" end @@ -161,10 +172,20 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase run_generator ["product_line"] assert_file "test/controllers/product_lines_controller_test.rb" do |content| - assert_match(/class ProductLinesControllerTest < ActionController::TestCase/, content) + assert_match(/class ProductLinesControllerTest < ActionDispatch::IntegrationTest/, content) assert_match(/test "should get index"/, content) - assert_match(/post :create, params: \{ product_line: \{ \} \}/, content) - assert_match(/patch :update, params: \{ id: @product_line, product_line: \{ \} \}/, content) + assert_match(/post product_lines_url, params: \{ product_line: \{ \} \}/, content) + assert_match(/patch product_line_url\(@product_line\), params: \{ product_line: \{ \} \}/, content) + end + end + + def test_system_tests_without_attributes + run_generator ["product_line"] + + assert_file "test/system/product_lines_test.rb" do |content| + assert_match(/class ProductLinesTest < ApplicationSystemTestCase/, content) + assert_match(/test "visiting the index"/, content) + assert_no_match(/fill_in/, content) end end @@ -187,6 +208,9 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_no_file "app/controllers/product_lines_controller.rb" assert_no_file "test/controllers/product_lines_controller_test.rb" + # System tests + assert_no_file "test/system/product_lines_test.rb" + # Views assert_no_file "app/views/product_lines" assert_no_file "app/views/layouts/product_lines.html.erb" @@ -196,7 +220,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Assets assert_file "app/assets/stylesheets/scaffold.css", /:visited/ - assert_no_file "app/assets/javascripts/product_lines.js" assert_no_file "app/assets/stylesheets/product_lines.css" end @@ -205,7 +228,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Model assert_file "app/models/admin.rb", /module Admin/ - assert_file "app/models/admin/role.rb", /class Admin::Role < ActiveRecord::Base/ + assert_file "app/models/admin/role.rb", /class Admin::Role < ApplicationRecord/ assert_file "test/models/admin/role_test.rb", /class Admin::RoleTest < ActiveSupport::TestCase/ assert_file "test/fixtures/admin/roles.yml" assert_migration "db/migrate/create_admin_roles.rb" @@ -250,10 +273,20 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end assert_file "test/controllers/admin/roles_controller_test.rb", - /class Admin::RolesControllerTest < ActionController::TestCase/ + /class Admin::RolesControllerTest < ActionDispatch::IntegrationTest/ + + assert_file "test/system/admin/roles_test.rb", + /class Admin::RolesTest < ApplicationSystemTestCase/ # Views - %w(index edit new show _form).each do |view| + assert_file "app/views/admin/roles/index.html.erb" do |content| + assert_match("'Show', admin_role", content) + assert_match("'Edit', edit_admin_role_path(admin_role)", content) + assert_match("'Destroy', admin_role", content) + assert_match("'New Admin Role', new_admin_role_path", content) + end + + %w(edit new show _form).each do |view| assert_file "app/views/admin/roles/#{view}.html.erb" end assert_no_file "app/views/layouts/admin/roles.html.erb" @@ -263,13 +296,12 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Assets assert_file "app/assets/stylesheets/scaffold.css", /:visited/ - assert_file "app/assets/javascripts/admin/roles.js" assert_file "app/assets/stylesheets/admin/roles.css" end def test_scaffold_with_namespace_on_revoke run_generator [ "admin/role", "name:string", "description:string" ] - run_generator [ "admin/role" ], :behavior => :revoke + run_generator [ "admin/role" ], behavior: :revoke # Model assert_file "app/models/admin.rb" # ( should not be remove ) @@ -287,6 +319,9 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_no_file "app/controllers/admin/roles_controller.rb" assert_no_file "test/controllers/admin/roles_controller_test.rb" + # System tests + assert_no_file "test/system/admin/roles_test.rb" + # Views assert_no_file "app/views/admin/roles" assert_no_file "app/views/layouts/admin/roles.html.erb" @@ -296,7 +331,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Assets assert_file "app/assets/stylesheets/scaffold.css" - assert_no_file "app/assets/javascripts/admin/roles.js" assert_no_file "app/assets/stylesheets/admin/roles.css" end @@ -308,9 +342,9 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase content = File.read(route_path).gsub(/\.routes\.draw do/) do |match| "#{match} |map|" end - File.open(route_path, "wb") { |file| file.write(content) } + File.write(route_path, content) - run_generator ["product_line"], :behavior => :revoke + run_generator ["product_line"], behavior: :revoke assert_file "config/routes.rb", /\.routes\.draw do\s*\|map\|\s*$/ end @@ -322,13 +356,13 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase content = File.read(route_path) # Remove all of the comments and blank lines from the routes file - content.gsub!(/^ \#.*\n/, '') - content.gsub!(/^\n/, '') + content.gsub!(/^ \#.*\n/, "") + content.gsub!(/^\n/, "") - File.open(route_path, "wb") { |file| file.write(content) } + File.write(route_path, content) assert_file "config/routes.rb", /\.routes\.draw do\n resources :product_lines\nend\n\z/ - run_generator ["product_line"], :behavior => :revoke + run_generator ["product_line"], behavior: :revoke assert_file "config/routes.rb", /\.routes\.draw do\nend\n\z/ end @@ -341,28 +375,24 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase def test_scaffold_generator_no_assets_with_switch_no_assets run_generator [ "posts", "--no-assets" ] assert_no_file "app/assets/stylesheets/scaffold.css" - assert_no_file "app/assets/javascripts/posts.js" assert_no_file "app/assets/stylesheets/posts.css" end def test_scaffold_generator_no_assets_with_switch_assets_false run_generator [ "posts", "--assets=false" ] assert_no_file "app/assets/stylesheets/scaffold.css" - assert_no_file "app/assets/javascripts/posts.js" assert_no_file "app/assets/stylesheets/posts.css" end def test_scaffold_generator_no_scaffold_stylesheet_with_switch_no_scaffold_stylesheet run_generator [ "posts", "--no-scaffold-stylesheet" ] assert_no_file "app/assets/stylesheets/scaffold.css" - assert_file "app/assets/javascripts/posts.js" assert_file "app/assets/stylesheets/posts.css" end def test_scaffold_generator_no_scaffold_stylesheet_with_switch_scaffold_stylesheet_false run_generator [ "posts", "--scaffold-stylesheet=false" ] assert_no_file "app/assets/stylesheets/scaffold.css" - assert_file "app/assets/javascripts/posts.js" assert_file "app/assets/stylesheets/posts.css" end @@ -390,17 +420,9 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase def test_scaffold_generator_no_stylesheets run_generator [ "posts", "--no-stylesheets" ] assert_no_file "app/assets/stylesheets/scaffold.css" - assert_file "app/assets/javascripts/posts.js" assert_no_file "app/assets/stylesheets/posts.css" end - def test_scaffold_generator_no_javascripts - run_generator [ "posts", "--no-javascripts" ] - assert_file "app/assets/stylesheets/scaffold.css" - assert_no_file "app/assets/javascripts/posts.js" - assert_file "app/assets/stylesheets/posts.css" - end - def test_scaffold_generator_outputs_error_message_on_missing_attribute_type run_generator ["post", "title", "body:text", "author"] @@ -413,8 +435,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end end - def test_scaffold_generator_belongs_to - run_generator ["account", "name", "currency:belongs_to"] + def test_scaffold_generator_belongs_to_and_references + run_generator ["account", "name", "currency:belongs_to", "user:references"] assert_file "app/models/account.rb", /belongs_to :currency/ @@ -427,13 +449,31 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_file "app/controllers/accounts_controller.rb" do |content| assert_instance_method :account_params, content do |m| - assert_match(/permit\(:name, :currency_id\)/, m) + assert_match(/permit\(:name, :currency_id, :user_id\)/, m) end end assert_file "app/views/accounts/_form.html.erb" do |content| - assert_match(/^\W{4}<%= f\.text_field :name %>/, content) - assert_match(/^\W{4}<%= f\.text_field :currency_id %>/, content) + assert_match(/^\W{4}<%= form\.text_field :name %>/, content) + assert_match(/^\W{4}<%= form\.text_field :currency_id %>/, content) + end + + assert_file "app/views/accounts/index.html.erb" do |content| + assert_match(/^\W{8}<td><%= account\.name %><\/td>/, content) + assert_match(/^\W{8}<td><%= account\.user_id %><\/td>/, content) + end + + assert_file "app/views/accounts/show.html.erb" do |content| + assert_match(/^\W{2}<%= @account\.name %>/, content) + assert_match(/^\W{2}<%= @account\.user_id %>/, content) + end + end + + def test_scaffold_generator_database + with_secondary_database_configuration do + run_generator ["posts", "--database=secondary"] + + assert_migration "db/secondary_migrate/create_posts.rb" end end @@ -456,8 +496,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end assert_file "app/views/users/_form.html.erb" do |content| - assert_match(/<%= f\.password_field :password %>/, content) - assert_match(/<%= f\.password_field :password_confirmation %>/, content) + assert_match(/<%= form\.password_field :password %>/, content) + assert_match(/<%= form\.password_field :password_confirmation %>/, content) end assert_file "app/views/users/index.html.erb" do |content| @@ -473,6 +513,11 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_match(/password_confirmation: 'secret'/, content) end + assert_file "test/system/users_test.rb" do |content| + assert_match(/fill_in "Password", with: 'secret'/, content) + assert_match(/fill_in "Password confirmation", with: 'secret'/, content) + end + assert_file "test/fixtures/users.yml" do |content| assert_match(/password_digest: <%= BCrypt::Password.create\('secret'\) %>/, content) end @@ -486,9 +531,29 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase Dir.chdir(engine_path) do quietly do `bin/rails g scaffold User name:string age:integer; - bundle exec rake db:migrate` + bin/rails db:migrate` + end + assert_match(/8 runs, 10 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + + def test_scaffold_tests_pass_by_default_inside_namespaced_mountable_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits-admin --mountable` } + + engine_path = File.join(destination_root, "bukkits-admin") + + Dir.chdir(engine_path) do + quietly do + `bin/rails g scaffold User name:string age:integer; + bin/rails db:migrate` end - assert_match(/8 runs, 13 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`) + + assert_file "bukkits-admin/app/controllers/bukkits/admin/users_controller.rb" do |content| + assert_match(/module Bukkits::Admin/, content) + assert_match(/class UsersController < ApplicationController/, content) + end + + assert_match(/8 runs, 10 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) end end @@ -500,9 +565,94 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase Dir.chdir(engine_path) do quietly do `bin/rails g scaffold User name:string age:integer; - bundle exec rake db:migrate` + bin/rails db:migrate` + end + assert_match(/8 runs, 10 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + + def test_scaffold_tests_pass_by_default_inside_api_mountable_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --mountable --api` } + + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly do + `bin/rails g scaffold User name:string age:integer; + bin/rails db:migrate` + end + assert_match(/6 runs, 8 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + + def test_scaffold_tests_pass_by_default_inside_api_full_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --full --api` } + + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly do + `bin/rails g scaffold User name:string age:integer; + bin/rails db:migrate` end - assert_match(/8 runs, 13 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`) + assert_match(/6 runs, 8 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + + def test_scaffold_on_invoke_inside_mountable_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --mountable` } + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly { `bin/rails generate scaffold User name:string age:integer` } + + assert File.exist?("app/models/bukkits/user.rb") + assert File.exist?("test/models/bukkits/user_test.rb") + assert File.exist?("test/fixtures/bukkits/users.yml") + + assert File.exist?("app/controllers/bukkits/users_controller.rb") + assert File.exist?("test/controllers/bukkits/users_controller_test.rb") + + assert File.exist?("test/system/bukkits/users_test.rb") + + assert File.exist?("app/views/bukkits/users/index.html.erb") + assert File.exist?("app/views/bukkits/users/edit.html.erb") + assert File.exist?("app/views/bukkits/users/show.html.erb") + assert File.exist?("app/views/bukkits/users/new.html.erb") + assert File.exist?("app/views/bukkits/users/_form.html.erb") + + assert File.exist?("app/helpers/bukkits/users_helper.rb") + + assert File.exist?("app/assets/stylesheets/bukkits/users.css") + end + end + + def test_scaffold_on_revoke_inside_mountable_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --mountable` } + engine_path = File.join(destination_root, "bukkits") + + Dir.chdir(engine_path) do + quietly { `bin/rails generate scaffold User name:string age:integer` } + quietly { `bin/rails destroy scaffold User` } + + assert_not File.exist?("app/models/bukkits/user.rb") + assert_not File.exist?("test/models/bukkits/user_test.rb") + assert_not File.exist?("test/fixtures/bukkits/users.yml") + + assert_not File.exist?("app/controllers/bukkits/users_controller.rb") + assert_not File.exist?("test/controllers/bukkits/users_controller_test.rb") + + assert_not File.exist?("test/system/bukkits/users_test.rb") + + assert_not File.exist?("app/views/bukkits/users/index.html.erb") + assert_not File.exist?("app/views/bukkits/users/edit.html.erb") + assert_not File.exist?("app/views/bukkits/users/show.html.erb") + assert_not File.exist?("app/views/bukkits/users/new.html.erb") + assert_not File.exist?("app/views/bukkits/users/_form.html.erb") + + assert_not File.exist?("app/helpers/bukkits/users_helper.rb") + + assert_not File.exist?("app/assets/stylesheets/bukkits/users.css") end end end diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 77372fb514..2dda856f25 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # # Tests, setup, and teardown common to the application and plugin generator suites. # @@ -5,9 +7,9 @@ module SharedGeneratorTests def setup Rails.application = TestApp::Application super - Rails::Generators::AppGenerator.instance_variable_set('@desc', nil) + Rails::Generators::AppGenerator.instance_variable_set("@desc", nil) - Kernel::silence_warnings do + Kernel.silence_warnings do Thor::Base.shell.send(:attr_accessor, :always_force) @shell = Thor::Base.shell.new @shell.send(:always_force=, true) @@ -16,34 +18,27 @@ module SharedGeneratorTests def teardown super - Rails::Generators::AppGenerator.instance_variable_set('@desc', nil) + Rails::Generators::AppGenerator.instance_variable_set("@desc", nil) Rails.application = TestApp::Application.instance end + def application_path + destination_root + end + def test_skeleton_is_created run_generator default_files.each { |path| assert_file path } end - def assert_generates_with_bundler(options = {}) - generator([destination_root], options) - generator.expects(:bundle_command).with('install').once - generator.stubs(:bundle_command).with('exec spring binstub --all') - quietly { generator.invoke_all } - end - - def test_generation_runs_bundle_install - assert_generates_with_bundler - end - def test_plugin_new_generate_pretend run_generator ["testapp", "--pretend"] - default_files.each{ |path| assert_no_file File.join("testapp",path) } + default_files.each { |path| assert_no_file File.join("testapp", path) } end def test_invalid_database_option_raises_an_error - content = capture(:stderr){ run_generator([destination_root, "-d", "unknown"]) } + content = capture(:stderr) { run_generator([destination_root, "-d", "unknown"]) } assert_match(/Invalid value for \-\-database option/, content) end @@ -55,15 +50,15 @@ module SharedGeneratorTests def test_name_collision_raises_an_error reserved_words = %w[application destroy plugin runner test] reserved_words.each do |reserved| - content = capture(:stderr){ run_generator [File.join(destination_root, reserved)] } - assert_match(/Invalid \w+ name #{reserved}. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n/, content) + content = capture(:stderr) { run_generator [File.join(destination_root, reserved)] } + assert_match(/Invalid \w+ name #{reserved}\. Please give a name which does not match one of the reserved rails words: application, destroy, plugin, runner, test\n/, content) end end def test_name_raises_an_error_if_name_already_used_constant %w{ String Hash Class Module Set Symbol }.each do |ruby_class| - content = capture(:stderr){ run_generator [File.join(destination_root, ruby_class)] } - assert_match(/Invalid \w+ name #{ruby_class}, constant #{ruby_class} is already in use. Please choose another \w+ name.\n/, content) + content = capture(:stderr) { run_generator [File.join(destination_root, ruby_class)] } + assert_match(/Invalid \w+ name #{ruby_class}, constant #{ruby_class} is already in use\. Please choose another \w+ name\.\n/, content) end end @@ -79,7 +74,7 @@ module SharedGeneratorTests def test_template_raises_an_error_with_invalid_path quietly do - content = capture(:stderr){ run_generator([destination_root, "-m", "non/existent/path"]) } + content = capture(:stderr) { run_generator([destination_root, "-m", "non/existent/path"]) } assert_match(/The template \[.*\] could not be loaded/, content) assert_match(/non\/existent\/path/, content) @@ -88,52 +83,268 @@ module SharedGeneratorTests def test_template_is_executed_when_supplied_an_https_path path = "https://gist.github.com/josevalim/103208/raw/" - template = %{ say "It works!" } + template = +%{ say "It works!" } template.instance_eval "def read; self; end" # Make the string respond to read - generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) - quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } + check_open = -> *args do + assert_equal [ path, "Accept" => "application/x-thor-template" ], args + template + end + + generator([destination_root], template: path, skip_webpack_install: true).stub(:open, check_open, template) do + generator.stub :bundle_command, nil do + quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } + end + end end - def test_dev_option - assert_generates_with_bundler dev: true - rails_path = File.expand_path('../../..', Rails.root) - assert_file 'Gemfile', /^gem\s+["']rails["'],\s+path:\s+["']#{Regexp.escape(rails_path)}["']$/ + def test_skip_gemfile + assert_not_called(generator([destination_root], skip_gemfile: true, skip_webpack_install: true), :bundle_command) do + quietly { generator.invoke_all } + assert_no_file "Gemfile" + end end - def test_edge_option - assert_generates_with_bundler edge: true - assert_file 'Gemfile', %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["']$} + def test_skip_git + run_generator [destination_root, "--skip-git", "--full"] + assert_no_file(".gitignore") + assert_no_directory(".git") end - def test_skip_gemfile - generator([destination_root], skip_gemfile: true).expects(:bundle_command).never - quietly { generator.invoke_all } - assert_no_file 'Gemfile' + def test_skip_keeps + run_generator [destination_root, "--skip-keeps", "--full"] + + assert_file ".gitignore" do |content| + assert_no_match(/\.keep/, content) + end + + assert_no_file("app/models/concerns/.keep") end - def test_skip_bundle - generator([destination_root], skip_bundle: true).expects(:bundle_command).never - quietly { generator.invoke_all } + def test_default_frameworks_are_required_when_others_are_removed + run_generator [ + destination_root, + "--skip-active-record", + "--skip-active-storage", + "--skip-action-mailer", + "--skip-action-cable", + "--skip-sprockets" + ] - # skip_bundle is only about running bundle install, ensure the Gemfile is still - # generated. - assert_file 'Gemfile' + assert_file "#{application_path}/config/application.rb", /^require\s+["']rails["']/ + assert_file "#{application_path}/config/application.rb", /^require\s+["']active_model\/railtie["']/ + assert_file "#{application_path}/config/application.rb", /^require\s+["']active_job\/railtie["']/ + assert_file "#{application_path}/config/application.rb", /^# require\s+["']active_record\/railtie["']/ + assert_file "#{application_path}/config/application.rb", /^# require\s+["']active_storage\/engine["']/ + assert_file "#{application_path}/config/application.rb", /^require\s+["']action_controller\/railtie["']/ + assert_file "#{application_path}/config/application.rb", /^# require\s+["']action_mailer\/railtie["']/ + assert_file "#{application_path}/config/application.rb", /^require\s+["']action_view\/railtie["']/ + assert_file "#{application_path}/config/application.rb", /^# require\s+["']action_cable\/engine["']/ + assert_file "#{application_path}/config/application.rb", /^# require\s+["']sprockets\/railtie["']/ + assert_file "#{application_path}/config/application.rb", /^require\s+["']rails\/test_unit\/railtie["']/ end - def test_skip_git - run_generator [destination_root, '--skip-git', '--full'] - assert_no_file('.gitignore') - assert_file('app/mailers/.keep') + def test_generator_without_skips + run_generator + assert_file "#{application_path}/config/application.rb", /\s+require\s+["']rails\/all["']/ + assert_file "#{application_path}/config/environments/development.rb" do |content| + assert_match(/config\.action_mailer\.raise_delivery_errors = false/, content) + end + assert_file "#{application_path}/config/environments/test.rb" do |content| + assert_match(/config\.action_mailer\.delivery_method = :test/, content) + end + assert_file "#{application_path}/config/environments/production.rb" do |content| + assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content) + assert_match(/^ # config\.require_master_key = true/, content) + end end - def test_skip_keeps - run_generator [destination_root, '--skip-keeps', '--full'] + def test_gitignore_when_sqlite3 + run_generator - assert_file '.gitignore' do |content| - assert_no_match(/\.keep/, content) + assert_file ".gitignore" do |content| + assert_match(/sqlite3/, content) + end + end + + def test_gitignore_when_non_sqlite3_db + run_generator([destination_root, "-d", "mysql"]) + + assert_file ".gitignore" do |content| + assert_no_match(/sqlite/i, content) + end + end + + def test_generator_if_skip_active_record_is_given + run_generator [destination_root, "--skip-active-record"] + assert_no_directory "#{application_path}/db/" + assert_no_file "#{application_path}/config/database.yml" + assert_no_file "#{application_path}/app/models/application_record.rb" + assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ + assert_file "test/test_helper.rb" do |helper_content| + assert_no_match(/fixtures :all/, helper_content) + end + assert_file "#{application_path}/bin/setup" do |setup_content| + assert_no_match(/db:setup/, setup_content) + end + assert_file "#{application_path}/bin/update" do |update_content| + assert_no_match(/db:migrate/, update_content) + end + assert_file ".gitignore" do |content| + assert_no_match(/sqlite/i, content) + end + end + + def test_generator_for_active_storage + run_generator + + unless generator_class.name == "Rails::Generators::PluginGenerator" + assert_file "#{application_path}/app/javascript/packs/application.js" do |content| + assert_match(/^import \* as ActiveStorage from "activestorage"\nActiveStorage.start\(\)/, content) + end + end + + assert_file "#{application_path}/config/environments/development.rb" do |content| + assert_match(/config\.active_storage/, content) + end + + assert_file "#{application_path}/config/environments/production.rb" do |content| + assert_match(/config\.active_storage/, content) + end + + assert_file "#{application_path}/config/environments/test.rb" do |content| + assert_match(/config\.active_storage/, content) + end + + assert_file "#{application_path}/config/storage.yml" + assert_directory "#{application_path}/storage" + assert_directory "#{application_path}/tmp/storage" + + assert_file ".gitignore" do |content| + assert_match(/\/storage\//, content) end + end + + def test_generator_if_skip_active_storage_is_given + run_generator [destination_root, "--skip-active-storage"] + + assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']active_storage\/engine["']/ + + assert_file "#{application_path}/app/javascript/packs/application.js" do |content| + assert_no_match(/activestorage/, content) + end + + assert_file "#{application_path}/config/environments/development.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end + + assert_file "#{application_path}/config/environments/production.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end + + assert_file "#{application_path}/config/environments/test.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end + + assert_no_file "#{application_path}/config/storage.yml" + assert_no_directory "#{application_path}/storage" + assert_no_directory "#{application_path}/tmp/storage" + + assert_file ".gitignore" do |content| + assert_no_match(/\/storage\//, content) + end + end + + def test_generator_does_not_generate_active_storage_contents_if_skip_active_record_is_given + run_generator [destination_root, "--skip-active-record"] + + assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']active_storage\/engine["']/ + + assert_file "#{application_path}/app/javascript/packs/application.js" do |content| + assert_no_match(/^import * as ActiveStorage from "activestorage"\nActiveStorage.start\(\)/, content) + end + + assert_file "#{application_path}/config/environments/development.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end + + assert_file "#{application_path}/config/environments/production.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end + + assert_file "#{application_path}/config/environments/test.rb" do |content| + assert_no_match(/config\.active_storage/, content) + end + + assert_no_file "#{application_path}/config/storage.yml" + assert_no_directory "#{application_path}/storage" + assert_no_directory "#{application_path}/tmp/storage" + + assert_file ".gitignore" do |content| + assert_no_match(/\/storage\//, content) + end + end + + def test_generator_if_skip_action_mailer_is_given + run_generator [destination_root, "--skip-action-mailer"] + assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_mailer\/railtie["']/ + assert_file "#{application_path}/config/environments/development.rb" do |content| + assert_no_match(/config\.action_mailer/, content) + end + assert_file "#{application_path}/config/environments/test.rb" do |content| + assert_no_match(/config\.action_mailer/, content) + end + assert_file "#{application_path}/config/environments/production.rb" do |content| + assert_no_match(/config\.action_mailer/, content) + end + assert_no_directory "#{application_path}/app/mailers" + assert_no_directory "#{application_path}/test/mailers" + end + + def test_generator_if_skip_action_cable_is_given + run_generator [destination_root, "--skip-action-cable"] + assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_cable\/engine["']/ + assert_no_file "#{application_path}/config/cable.yml" + assert_no_file "#{application_path}/app/javascript/consumer.js" + assert_no_directory "#{application_path}/app/javascript/channels" + assert_no_directory "#{application_path}/app/channels" + assert_file "Gemfile" do |content| + assert_no_match(/redis/, content) + end + end + + def test_generator_if_skip_sprockets_is_given + run_generator [destination_root, "--skip-sprockets"] + + assert_no_file "#{application_path}/config/initializers/assets.rb" + + assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']sprockets\/railtie["']/ + + assert_file "Gemfile" do |content| + assert_no_match(/sass-rails/, content) + end + + assert_file "#{application_path}/config/environments/development.rb" do |content| + assert_no_match(/config\.assets\.debug/, content) + end + + assert_file "#{application_path}/config/environments/production.rb" do |content| + assert_no_match(/config\.assets\.digest/, content) + assert_no_match(/config\.assets\.css_compressor/, content) + assert_no_match(/config\.assets\.compile/, content) + end + end + + def test_generator_for_yarn + skip "#34009 disabled JS by default for plugins" if generator_class.name == "Rails::Generators::PluginGenerator" + run_generator + assert_file "#{application_path}/package.json", /dependencies/ + assert_file "#{application_path}/bin/yarn" + end - assert_no_file('app/mailers/.keep') + def test_generator_for_yarn_skipped + run_generator([destination_root, "--skip-javascript"]) + assert_no_file "#{application_path}/package.json" + assert_no_file "#{application_path}/bin/yarn" end end diff --git a/railties/test/generators/system_test_generator_test.rb b/railties/test/generators/system_test_generator_test.rb new file mode 100644 index 0000000000..efa70a050b --- /dev/null +++ b/railties/test/generators/system_test_generator_test.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/system_test/system_test_generator" + +class SystemTestGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + arguments %w(user) + + def test_system_test_skeleton_is_created + run_generator + assert_file "test/system/users_test.rb", /class UsersTest < ApplicationSystemTestCase/ + end + + def test_namespaced_system_test_skeleton_is_created + run_generator %w(admin/user) + assert_file "test/system/admin/users_test.rb", /class Admin::UsersTest < ApplicationSystemTestCase/ + end +end diff --git a/railties/test/generators/task_generator_test.rb b/railties/test/generators/task_generator_test.rb index d5bd44b9db..5f162919d8 100644 --- a/railties/test/generators/task_generator_test.rb +++ b/railties/test/generators/task_generator_test.rb @@ -1,5 +1,7 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/task/task_generator' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/task/task_generator" class TaskGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -15,10 +17,10 @@ class TaskGeneratorTest < Rails::Generators::TestCase end def test_task_on_revoke - task_path = 'lib/tasks/feeds.rake' + task_path = "lib/tasks/feeds.rake" run_generator assert_file task_path - run_generator ['feeds'], behavior: :revoke + run_generator ["feeds"], behavior: :revoke assert_no_file task_path end end diff --git a/railties/test/generators/test_runner_in_engine_test.rb b/railties/test/generators/test_runner_in_engine_test.rb new file mode 100644 index 0000000000..bd102a32b5 --- /dev/null +++ b/railties/test/generators/test_runner_in_engine_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "generators/plugin_test_helper" + +class TestRunnerInEngineTest < ActiveSupport::TestCase + include PluginTestHelper + + def setup + @destination_root = Dir.mktmpdir("bukkits") + Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --full --skip-bundle` } + plugin_file "test/dummy/db/schema.rb", "" + end + + def teardown + FileUtils.rm_rf(@destination_root) + end + + def test_rerun_snippet_is_relative_path + create_test_file "post", pass: false + + output = run_test_command("test/post_test.rb") + expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/post_test\.rb:6\]:\nwups!\n\nrails test test/post_test\.rb:4} + assert_match expect, output + end + + private + def plugin_path + "#{@destination_root}/bukkits" + end + + def run_test_command(arguments) + Dir.chdir(plugin_path) { `bin/rails test #{arguments}` } + end +end diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index 31a575749a..f98c1f78f7 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -1,7 +1,8 @@ -require 'generators/generators_test_helper' -require 'rails/generators/rails/model/model_generator' -require 'rails/generators/test_unit/model/model_generator' -require 'minitest/mock' +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/model/model_generator" +require "rails/generators/test_unit/model/model_generator" class GeneratorsTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -9,65 +10,66 @@ class GeneratorsTest < Rails::Generators::TestCase def setup @path = File.expand_path("lib", Rails.root) $LOAD_PATH.unshift(@path) - @mock_generator = MiniTest::Mock.new end def teardown $LOAD_PATH.delete(@path) - @mock_generator.verify end def test_simple_invoke - assert File.exist?(File.join(@path, 'generators', 'model_generator.rb')) - @mock_generator.expect(:call, nil, [["Account"],{}]) - TestUnit::Generators::ModelGenerator.stub(:start, @mock_generator) do + assert File.exist?(File.join(@path, "generators", "model_generator.rb")) + assert_called_with(TestUnit::Generators::ModelGenerator, :start, [["Account"], {}]) do Rails::Generators.invoke("test_unit:model", ["Account"]) end end def test_invoke_when_generator_is_not_found name = :unknown - output = capture(:stdout){ Rails::Generators.invoke name } + output = capture(:stdout) { Rails::Generators.invoke name } assert_match "Could not find generator '#{name}'", output assert_match "`rails generate --help`", output end def test_generator_suggestions name = :migrationz - output = capture(:stdout){ Rails::Generators.invoke name } - assert_match "Maybe you meant 'migration'", output + output = capture(:stdout) { Rails::Generators.invoke name } + assert_match 'Maybe you meant "migration"?', output end - def test_generator_multiple_suggestions + def test_generator_suggestions_except_en_locale + orig_available_locales = I18n.available_locales + orig_default_locale = I18n.default_locale + I18n.available_locales = :ja + I18n.default_locale = :ja name = :tas - output = capture(:stdout){ Rails::Generators.invoke name } - assert_match "Maybe you meant 'task', 'job' or", output + output = capture(:stdout) { Rails::Generators.invoke name } + assert_match 'Maybe you meant "task"?', output + ensure + I18n.available_locales = orig_available_locales + I18n.default_locale = orig_default_locale end def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments - output = capture(:stdout){ Rails::Generators.invoke :model, [] } + output = capture(:stdout) { Rails::Generators.invoke :model, [] } assert_match(/Description:/, output) end def test_should_give_higher_preference_to_rails_generators - assert File.exist?(File.join(@path, 'generators', 'model_generator.rb')) - @mock_generator.expect(:call, nil, [["Account"],{}]) - Rails::Generators::ModelGenerator.stub(:start, @mock_generator) do - warnings = capture(:stderr){ Rails::Generators.invoke :model, ["Account"] } - assert warnings.empty? + assert File.exist?(File.join(@path, "generators", "model_generator.rb")) + assert_called_with(Rails::Generators::ModelGenerator, :start, [["Account"], {}]) do + warnings = capture(:stderr) { Rails::Generators.invoke :model, ["Account"] } + assert_empty warnings end end def test_invoke_with_default_values - @mock_generator.expect(:call, nil, [["Account"],{}]) - Rails::Generators::ModelGenerator.stub(:start, @mock_generator) do + assert_called_with(Rails::Generators::ModelGenerator, :start, [["Account"], {}]) do Rails::Generators.invoke :model, ["Account"] end end def test_invoke_with_config_values - @mock_generator.expect(:call, nil, [["Account"],{behavior: :skip}]) - Rails::Generators::ModelGenerator.stub(:start, @mock_generator) do + assert_called_with(Rails::Generators::ModelGenerator, :start, [["Account"], behavior: :skip]) do Rails::Generators.invoke :model, ["Account"], behavior: :skip end end @@ -115,15 +117,14 @@ class GeneratorsTest < Rails::Generators::TestCase def test_invoke_with_nested_namespaces model_generator = Minitest::Mock.new model_generator.expect(:start, nil, [["Account"], {}]) - @mock_generator.expect(:call, model_generator, ['namespace', 'my:awesome']) - Rails::Generators.stub(:find_by_namespace, @mock_generator) do - Rails::Generators.invoke 'my:awesome:namespace', ["Account"] + assert_called_with(Rails::Generators, :find_by_namespace, ["namespace", "my:awesome"], returns: model_generator) do + Rails::Generators.invoke "my:awesome:namespace", ["Account"] end model_generator.verify end def test_rails_generators_help_with_builtin_information - output = capture(:stdout){ Rails::Generators.help } + output = capture(:stdout) { Rails::Generators.help } assert_match(/Rails:/, output) assert_match(/^ model$/, output) assert_match(/^ scaffold_controller$/, output) @@ -131,19 +132,19 @@ class GeneratorsTest < Rails::Generators::TestCase end def test_rails_generators_help_does_not_include_app_nor_plugin_new - output = capture(:stdout){ Rails::Generators.help } - assert_no_match(/app/, output) + output = capture(:stdout) { Rails::Generators.help } + assert_no_match(/app\W/, output) assert_no_match(/[^:]plugin/, output) end def test_rails_generators_with_others_information - output = capture(:stdout){ Rails::Generators.help } + output = capture(:stdout) { Rails::Generators.help } assert_match(/Fixjour:/, output) assert_match(/^ fixjour$/, output) end def test_rails_generators_does_not_show_active_record_hooks - output = capture(:stdout){ Rails::Generators.help } + output = capture(:stdout) { Rails::Generators.help } assert_match(/ActiveRecord:/, output) assert_match(/^ active_record:fixjour$/, output) end @@ -185,8 +186,7 @@ class GeneratorsTest < Rails::Generators::TestCase def test_fallbacks_for_generators_on_invoke Rails::Generators.fallbacks[:shoulda] = :test_unit - @mock_generator.expect(:call, nil, [["Account"],{}]) - TestUnit::Generators::ModelGenerator.stub(:start, @mock_generator) do + assert_called_with(TestUnit::Generators::ModelGenerator, :start, [["Account"], {}]) do Rails::Generators.invoke "shoulda:model", ["Account"] end ensure @@ -196,8 +196,7 @@ class GeneratorsTest < Rails::Generators::TestCase def test_nested_fallbacks_for_generators Rails::Generators.fallbacks[:shoulda] = :test_unit Rails::Generators.fallbacks[:super_shoulda] = :shoulda - @mock_generator.expect(:call, nil, [["Account"],{}]) - TestUnit::Generators::ModelGenerator.stub(:start, @mock_generator) do + assert_called_with(TestUnit::Generators::ModelGenerator, :start, [["Account"], {}]) do Rails::Generators.invoke "super_shoulda:model", ["Account"] end ensure @@ -210,7 +209,7 @@ class GeneratorsTest < Rails::Generators::TestCase self.class.class_eval(<<-end_eval, __FILE__, __LINE__ + 1) class WithOptionsGenerator < Rails::Generators::Base - class_option :generate, :default => true + class_option :generate, default: true, type: :boolean end end_eval @@ -224,7 +223,7 @@ class GeneratorsTest < Rails::Generators::TestCase # Create template mkdir_p(File.dirname(template)) - File.open(template, 'w'){ |f| f.write "empty" } + File.open(template, "w") { |f| f.write "empty" } capture(:stdout) do Rails::Generators.invoke :model, ["user"], destination_root: destination_root @@ -239,18 +238,18 @@ class GeneratorsTest < Rails::Generators::TestCase def test_source_paths_for_not_namespaced_generators mspec = Rails::Generators.find_by_namespace :fixjour - assert mspec.source_paths.include?(File.join(Rails.root, "lib", "templates", "fixjour")) + assert_includes mspec.source_paths, File.join(Rails.root, "lib", "templates", "fixjour") end def test_usage_with_embedded_ruby - require File.expand_path("fixtures/lib/generators/usage_template/usage_template_generator", File.dirname(__FILE__)) - output = capture(:stdout) { Rails::Generators.invoke :usage_template, ['--help'] } + require_relative "fixtures/lib/generators/usage_template/usage_template_generator" + output = capture(:stdout) { Rails::Generators.invoke :usage_template, ["--help"] } assert_match(/:: 2 ::/, output) end def test_hide_namespace - assert !Rails::Generators.hidden_namespaces.include?("special:namespace") + assert_not_includes Rails::Generators.hidden_namespaces, "special:namespace" Rails::Generators.hide_namespace("special:namespace") - assert Rails::Generators.hidden_namespaces.include?("special:namespace") + assert_includes Rails::Generators.hidden_namespaces, "special:namespace" end end diff --git a/railties/test/initializable_test.rb b/railties/test/initializable_test.rb index ed9573453b..59fee245f9 100644 --- a/railties/test/initializable_test.rb +++ b/railties/test/initializable_test.rb @@ -1,8 +1,9 @@ -require 'abstract_unit' -require 'rails/initializable' +# frozen_string_literal: true -module InitializableTests +require "abstract_unit" +require "rails/initializable" +module InitializableTests class Foo include Rails::Initializable attr_accessor :foo, :bar @@ -174,6 +175,11 @@ module InitializableTests end end end + + test "Initializer provides context's class name" do + foo = Foo.new + assert_equal foo.class, foo.initializers.first.context_class + end end class BeforeAfter < ActiveSupport::TestCase @@ -215,8 +221,8 @@ module InitializableTests class WithArgsTest < ActiveSupport::TestCase test "running initializers with args" do $with_arg = nil - WithArgs.new.run_initializers(:default, 'foo') - assert_equal 'foo', $with_arg + WithArgs.new.run_initializers(:default, "foo") + assert_equal "foo", $with_arg end end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 65d8a55421..d4eed69a87 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -1,40 +1,53 @@ +# frozen_string_literal: true + # Note: # It is important to keep this file as light as possible # the goal for tests that require this is to test booting up -# rails from an empty state, so anything added here could +# Rails from an empty state, so anything added here could # hide potential failures # # It is also good to know what is the bare minimum to get # Rails booted up. -require 'fileutils' +require "fileutils" + +require "bundler/setup" unless defined?(Bundler) +require "active_support" +require "active_support/testing/autorun" +require "active_support/testing/stream" +require "active_support/testing/method_call_assertions" +require "active_support/test_case" +require "minitest/retry" -require 'bundler/setup' unless defined?(Bundler) -require 'active_support' -require 'active_support/testing/autorun' -require 'active_support/testing/stream' -require 'active_support/test_case' +Minitest::Retry.use!(verbose: false, retry_count: 1) -RAILS_FRAMEWORK_ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../..") +RAILS_FRAMEWORK_ROOT = File.expand_path("../../..", __dir__) # These files do not require any others and are needed # to run the tests +require "active_support/core_ext/object/blank" require "active_support/testing/isolation" require "active_support/core_ext/kernel/reporting" -require 'tmpdir' +require "tmpdir" +require "rails/secrets" module TestHelpers module Paths def app_template_path - File.join Dir.tmpdir, 'app_template' + File.join RAILS_FRAMEWORK_ROOT, "tmp/templates/app_template" end def tmp_path(*args) - @tmp_path ||= File.realpath(Dir.mktmpdir) + @tmp_path ||= File.realpath(Dir.mktmpdir(nil, File.join(RAILS_FRAMEWORK_ROOT, "tmp"))) File.join(@tmp_path, *args) end def app_path(*args) - tmp_path(*%w[app] + args) + path = tmp_path(*%w[app] + args) + if block_given? + yield path + else + path + end end def framework_path @@ -51,7 +64,9 @@ module TestHelpers old_env = ENV["RAILS_ENV"] @app ||= begin ENV["RAILS_ENV"] = env + require "#{app_path}/config/environment" + Rails.application end ensure @@ -59,8 +74,8 @@ module TestHelpers end def extract_body(response) - "".tap do |body| - response[2].each {|chunk| body << chunk } + (+"").tap do |body| + response[2].each { |chunk| body << chunk } end end @@ -69,88 +84,130 @@ module TestHelpers end def assert_welcome(resp) - assert_equal 200, resp[0] - assert_match 'text/html', resp[1]["Content-Type"] - assert_match 'charset=utf-8', resp[1]["Content-Type"] - assert extract_body(resp).match(/Welcome aboard/) - end - - def assert_success(resp) - assert_equal 202, resp[0] - end - - def assert_missing(resp) - assert_equal 404, resp[0] - end - - def assert_header(key, value, resp) - assert_equal value, resp[1][key.to_s] - end + resp = Array(resp) - def assert_body(expected, resp) - assert_equal expected, extract_body(resp) + assert_equal 200, resp[0] + assert_match "text/html", resp[1]["Content-Type"] + assert_match "charset=utf-8", resp[1]["Content-Type"] + assert extract_body(resp).match(/Yay! You.*re on Rails!/) end end module Generation # Build an application by invoking the generator and going through the whole stack. def build_app(options = {}) - @prev_rails_env = ENV['RAILS_ENV'] - ENV['RAILS_ENV'] = "development" - ENV['SECRET_KEY_BASE'] ||= SecureRandom.hex(16) + @prev_rails_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "development" FileUtils.rm_rf(app_path) FileUtils.cp_r(app_template_path, app_path) # Delete the initializers unless requested unless options[:initializers] - Dir["#{app_path}/config/initializers/*.rb"].each do |initializer| + Dir["#{app_path}/config/initializers/**/*.rb"].each do |initializer| File.delete(initializer) end end - gemfile_path = "#{app_path}/Gemfile" - if options[:gemfile].blank? && File.exist?(gemfile_path) - File.delete gemfile_path - end - routes = File.read("#{app_path}/config/routes.rb") - if routes =~ /(\n\s*end\s*)\Z/ - File.open("#{app_path}/config/routes.rb", 'w') do |f| - f.puts $` + "\nmatch ':controller(/:action(/:id))(.:format)', via: :all\n" + $1 + if routes =~ /(\n\s*end\s*)\z/ + File.open("#{app_path}/config/routes.rb", "w") do |f| + f.puts $` + "\nActiveSupport::Deprecation.silence { match ':controller(/:action(/:id))(.:format)', via: :all }\n" + $1 end end - File.open("#{app_path}/config/database.yml", "w") do |f| - f.puts <<-YAML - default: &default - adapter: sqlite3 - pool: 5 - timeout: 5000 - development: - <<: *default - database: db/development.sqlite3 - test: - <<: *default - database: db/test.sqlite3 - production: - <<: *default - database: db/production.sqlite3 - YAML + if options[:multi_db] + File.open("#{app_path}/config/database.yml", "w") do |f| + f.puts <<-YAML + default: &default + adapter: sqlite3 + pool: 5 + timeout: 5000 + development: + primary: + <<: *default + database: db/development.sqlite3 + primary_readonly: + <<: *default + database: db/development.sqlite3 + replica: true + animals: + <<: *default + database: db/development_animals.sqlite3 + migrations_paths: db/animals_migrate + animals_readonly: + <<: *default + database: db/development_animals.sqlite3 + migrations_paths: db/animals_migrate + replica: true + test: + primary: + <<: *default + database: db/test.sqlite3 + primary_readonly: + <<: *default + database: db/test.sqlite3 + replica: true + animals: + <<: *default + database: db/test_animals.sqlite3 + migrations_paths: db/animals_migrate + animals_readonly: + <<: *default + database: db/test_animals.sqlite3 + migrations_paths: db/animals_migrate + replica: true + production: + primary: + <<: *default + database: db/production.sqlite3 + primary_readonly: + <<: *default + database: db/production.sqlite3 + replica: true + animals: + <<: *default + database: db/production_animals.sqlite3 + migrations_paths: db/animals_migrate + animals_readonly: + <<: *default + database: db/production_animals.sqlite3 + migrations_paths: db/animals_migrate + readonly: true + YAML + end + else + File.open("#{app_path}/config/database.yml", "w") do |f| + f.puts <<-YAML + default: &default + adapter: sqlite3 + pool: 5 + timeout: 5000 + development: + <<: *default + database: db/development.sqlite3 + test: + <<: *default + database: db/test.sqlite3 + production: + <<: *default + database: db/production.sqlite3 + YAML + end end add_to_config <<-RUBY config.eager_load = false config.session_store :cookie_store, key: "_myapp_session" config.active_support.deprecation = :log - config.active_support.test_order = :random config.action_controller.allow_forgery_protection = false config.log_level = :info RUBY end def teardown_app - ENV['RAILS_ENV'] = @prev_rails_env if @prev_rails_env + ENV["RAILS_ENV"] = @prev_rails_env if @prev_rails_env + FileUtils.rm_rf(tmp_path) end # Make a very basic app, without creating the whole directory structure. @@ -160,22 +217,22 @@ module TestHelpers require "action_controller/railtie" require "action_view/railtie" - app = Class.new(Rails::Application) - app.config.eager_load = false - app.secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" - app.config.session_store :cookie_store, key: "_myapp_session" - app.config.active_support.deprecation = :log - app.config.active_support.test_order = :random - app.config.log_level = :info + @app = Class.new(Rails::Application) do + def self.name; "RailtiesTestApp"; end + end + @app.config.eager_load = false + @app.config.session_store :cookie_store, key: "_myapp_session" + @app.config.active_support.deprecation = :log + @app.config.log_level = :info - yield app if block_given? - app.initialize! + yield @app if block_given? + @app.initialize! - app.routes.draw do + @app.routes.draw do get "/" => "omg#index" end - require 'rack/test' + require "rack/test" extend ::Rack::Test::Methods end @@ -183,12 +240,12 @@ module TestHelpers controller :foo, <<-RUBY class FooController < ApplicationController def index - render text: "foo" + render plain: "foo" end end RUBY - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':controller(/:action)' end @@ -205,7 +262,7 @@ module TestHelpers def write(file, string) path = "#{@path}/#{file}" FileUtils.mkdir_p(File.dirname(path)) - File.open(path, "w") {|f| f.puts string } + File.open(path, "w") { |f| f.puts string } end def delete(file) @@ -218,10 +275,10 @@ module TestHelpers FileUtils.mkdir_p(dir) app = File.readlines("#{app_path}/config/application.rb") - app.insert(2, "$:.unshift(\"#{dir}/lib\")") - app.insert(3, "require #{name.inspect}") + app.insert(4, "$:.unshift(\"#{dir}/lib\")") + app.insert(5, "require #{name.inspect}") - File.open("#{app_path}/config/application.rb", 'r+') do |f| + File.open("#{app_path}/config/application.rb", "r+") do |f| f.puts app end @@ -230,16 +287,92 @@ module TestHelpers end end - def script(script) - Dir.chdir(app_path) do - `#{Gem.ruby} #{app_path}/bin/rails #{script}` + # Invoke a bin/rails command inside the app + # + # allow_failure:: true to return normally if the command exits with + # a non-zero status. By default, this method will raise. + # stderr:: true to pass STDERR output straight to the "real" STDERR. + # By default, the STDERR and STDOUT of the process will be + # combined in the returned string. + def rails(*args, allow_failure: false, stderr: false) + args = args.flatten + fork = true + + command = "bin/rails #{Shellwords.join args}#{' 2>&1' unless stderr}" + + # Don't fork if the environment has disabled it + fork = false if ENV["NO_FORK"] + + # Don't fork if the runtime isn't able to + fork = false if !Process.respond_to?(:fork) + + # Don't fork if we're re-invoking minitest + fork = false if args.first == "t" || args.grep(/\Atest(:|\z)/).any? + + if fork + out_read, out_write = IO.pipe + if stderr + err_read, err_write = IO.pipe + else + err_write = out_write + end + + pid = fork do + out_read.close + err_read.close if err_read + + $stdin.reopen(File::NULL, "r") + $stdout.reopen(out_write) + $stderr.reopen(err_write) + + at_exit do + case $! + when SystemExit + exit! $!.status + when nil + exit! 0 + else + err_write.puts "#{$!.class}: #{$!}" + exit! 1 + end + end + + Rails.instance_variable_set :@_env, nil + + $-v = $-w = false + Dir.chdir app_path unless Dir.pwd == app_path + + ARGV.replace(args) + load "./bin/rails" + + exit! 0 + end + + out_write.close + + if err_read + err_write.close + + $stderr.write err_read.read + end + + output = out_read.read + + Process.waitpid pid + + else + output = `cd #{app_path}; #{command}` end + + raise "rails command failed (#{$?.exitstatus}): #{command}\n#{output}" unless allow_failure || $?.success? + + output end def add_to_top_of_config(str) environment = File.read("#{app_path}/config/application.rb") if environment =~ /(Rails::Application\s*)/ - File.open("#{app_path}/config/application.rb", 'w') do |f| + File.open("#{app_path}/config/application.rb", "w") do |f| f.puts $` + $1 + "\n#{str}\n" + $' end end @@ -247,8 +380,8 @@ module TestHelpers def add_to_config(str) environment = File.read("#{app_path}/config/application.rb") - if environment =~ /(\n\s*end\s*end\s*)\Z/ - File.open("#{app_path}/config/application.rb", 'w') do |f| + if environment =~ /(\n\s*end\s*end\s*)\z/ + File.open("#{app_path}/config/application.rb", "w") do |f| f.puts $` + "\n#{str}\n" + $1 end end @@ -256,25 +389,34 @@ module TestHelpers def add_to_env_config(env, str) environment = File.read("#{app_path}/config/environments/#{env}.rb") - if environment =~ /(\n\s*end\s*)\Z/ - File.open("#{app_path}/config/environments/#{env}.rb", 'w') do |f| + if environment =~ /(\n\s*end\s*)\z/ + File.open("#{app_path}/config/environments/#{env}.rb", "w") do |f| f.puts $` + "\n#{str}\n" + $1 end end end def remove_from_config(str) - file = "#{app_path}/config/application.rb" + remove_from_file("#{app_path}/config/application.rb", str) + end + + def remove_from_env_config(env, str) + remove_from_file("#{app_path}/config/environments/#{env}.rb", str) + end + + def remove_from_file(file, str) contents = File.read(file) contents.sub!(/#{str}/, "") - File.open(file, "w+") { |f| f.puts contents } + File.write(file, contents) end - def app_file(path, contents, mode = 'w') - FileUtils.mkdir_p File.dirname("#{app_path}/#{path}") - File.open("#{app_path}/#{path}", mode) do |f| + def app_file(path, contents, mode = "w") + file_name = "#{app_path}/#{path}" + FileUtils.mkdir_p File.dirname(file_name) + File.open(file_name, mode) do |f| f.puts contents end + file_name end def remove_file(path) @@ -286,17 +428,28 @@ module TestHelpers end def use_frameworks(arr) - to_remove = [:actionmailer, :activerecord] - arr + to_remove = [:actionmailer, :activerecord, :activestorage, :activejob] - arr if to_remove.include?(:activerecord) - remove_from_config 'config.active_record.*' + remove_from_config "config.active_record.*" end - $:.reject! {|path| path =~ %r'/(#{to_remove.join('|')})/' } + $:.reject! { |path| path =~ %r'/(#{to_remove.join('|')})/' } end - def boot_rails - require File.expand_path('../../../../load_paths', __FILE__) + def use_postgresql + File.open("#{app_path}/config/database.yml", "w") do |f| + f.puts <<-YAML + default: &default + adapter: postgresql + pool: 5 + database: railties_test + development: + <<: *default + test: + <<: *default + YAML + end end end end @@ -306,9 +459,11 @@ class ActiveSupport::TestCase include TestHelpers::Rack include TestHelpers::Generation include ActiveSupport::Testing::Stream + include ActiveSupport::Testing::MethodCallAssertions - self.test_order = :sorted - + def frozen_error_class + Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError + end end # Create a scope and build a fixture rails app @@ -317,14 +472,42 @@ Module.new do # Build a rails app FileUtils.rm_rf(app_template_path) - FileUtils.mkdir(app_template_path) + FileUtils.mkdir_p(app_template_path) - environment = File.expand_path('../../../../load_paths', __FILE__) - require_environment = "-r #{environment}" + Dir.chdir "#{RAILS_FRAMEWORK_ROOT}/actionview" do + `yarn build` + end - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --no-rc` - File.open("#{app_template_path}/config/boot.rb", 'w') do |f| - f.puts "require '#{environment}'" + `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --skip-listen --no-rc` + File.open("#{app_template_path}/config/boot.rb", "w") do |f| f.puts "require 'rails/all'" end + + Dir.chdir(app_template_path) { `yarn add https://github.com/rails/webpacker.git` } # Use the latest version. + + # Manually install `webpack` as bin symlinks are not created for subdependencies + # in workspaces. See https://github.com/yarnpkg/yarn/issues/4964 + Dir.chdir(app_template_path) { `yarn add webpack@4.17.1 --tilde` } + Dir.chdir(app_template_path) { `yarn add webpack-cli` } + + # Fake 'Bundler.require' -- we run using the repo's Gemfile, not an + # app-specific one: we don't want to require every gem that lists. + contents = File.read("#{app_template_path}/config/application.rb") + contents.sub!(/^Bundler\.require.*/, "%w(turbolinks webpacker).each { |r| require r }") + File.write("#{app_template_path}/config/application.rb", contents) + + require "rails" + + require "active_model" + require "active_job" + require "active_record" + require "action_controller" + require "action_mailer" + require "action_view" + require "active_storage" + require "action_cable" + require "sprockets" + + require "action_view/helpers" + require "action_dispatch/routing/route_set" end unless defined?(RAILS_ISOLATED_ENGINE) diff --git a/railties/test/json_params_parsing_test.rb b/railties/test/json_params_parsing_test.rb new file mode 100644 index 0000000000..65ad9673ff --- /dev/null +++ b/railties/test/json_params_parsing_test.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "action_dispatch" +require "active_record" + +class JsonParamsParsingTest < ActionDispatch::IntegrationTest + def test_prevent_null_query + # Make sure we have data to find + klass = Class.new(ActiveRecord::Base) do + def self.name; "Foo"; end + establish_connection adapter: "sqlite3", database: ":memory:" + connection.create_table "foos" do |t| + t.string :title + t.timestamps null: false + end + end + klass.create + assert klass.first + + app = ->(env) { + request = ActionDispatch::Request.new env + params = ActionController::Parameters.new request.parameters + if params[:t] + klass.find_by_title(params[:t]) + else + nil + end + } + + assert_nil app.call(make_env("t" => nil)) + assert_nil app.call(make_env("t" => [nil])) + + [[[nil]], [[[nil]]]].each do |data| + assert_nil app.call(make_env("t" => data)) + end + ensure + klass.connection.drop_table("foos") + end + + private + def make_env(json) + data = JSON.dump json + content_length = data.length + { + "CONTENT_LENGTH" => content_length, + "CONTENT_TYPE" => "application/json", + "rack.input" => StringIO.new(data) + } + end +end diff --git a/railties/test/minitest/rails_plugin_test.rb b/railties/test/minitest/rails_plugin_test.rb new file mode 100644 index 0000000000..7c3a2022a9 --- /dev/null +++ b/railties/test/minitest/rails_plugin_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class Minitest::RailsPluginTest < ActiveSupport::TestCase + setup do + @options = Minitest.process_args [] + @output = StringIO.new("".encode("UTF-8")) + end + + test "default reporters are replaced" do + with_reporter Minitest::CompositeReporter.new do |reporter| + reporter << Minitest::SummaryReporter.new(@output, @options) + reporter << Minitest::ProgressReporter.new(@output, @options) + reporter << Minitest::Reporter.new(@output, @options) + + Minitest.plugin_rails_init({}) + + assert_equal 3, reporter.reporters.count + assert reporter.reporters.any? { |candidate| candidate.kind_of?(Minitest::SuppressedSummaryReporter) } + assert reporter.reporters.any? { |candidate| candidate.kind_of?(::Rails::TestUnitReporter) } + assert reporter.reporters.any? { |candidate| candidate.kind_of?(Minitest::Reporter) } + end + end + + test "no custom reporters are added if nothing to replace" do + with_reporter Minitest::CompositeReporter.new do |reporter| + Minitest.plugin_rails_init({}) + + assert_empty reporter.reporters + end + end + + private + def with_reporter(reporter) + old_reporter, Minitest.reporter = Minitest.reporter, reporter + + yield reporter + ensure + Minitest.reporter = old_reporter + end +end diff --git a/railties/test/path_generation_test.rb b/railties/test/path_generation_test.rb index 27e64b97b7..849b183b37 100644 --- a/railties/test/path_generation_test.rb +++ b/railties/test/path_generation_test.rb @@ -1,6 +1,8 @@ -require 'abstract_unit' -require 'active_support/core_ext/object/with_options' -require 'active_support/core_ext/object/json' +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/core_ext/object/with_options" +require "active_support/core_ext/object/json" class PathGenerationTest < ActiveSupport::TestCase attr_reader :app @@ -11,26 +13,26 @@ class PathGenerationTest < ActiveSupport::TestCase super() end - class Dispatcher < ActionDispatch::Routing::RouteSet::Dispatcher - def initialize(defaults, set, block) - super(defaults) + class Request < DelegateClass(ActionDispatch::Request) + def initialize(target, url_helpers, block) + super(target) + @url_helpers = url_helpers @block = block - @set = set end - def controller_reference(controller_param) + def controller_class + url_helpers = @url_helpers block = @block - set = @set Class.new(ActionController::Base) { - include set.url_helpers + include 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 + def make_request(env) + Request.new(super, url_helpers, @block) end end @@ -38,11 +40,11 @@ class PathGenerationTest < ActiveSupport::TestCase host = uri_or_host.host unless path path ||= uri_or_host.path - params = {'PATH_INFO' => path, - 'REQUEST_METHOD' => method, - 'HTTP_HOST' => host } + params = { "PATH_INFO" => path, + "REQUEST_METHOD" => method, + "HTTP_HOST" => host } - params['SCRIPT_NAME'] = script_name if script_name + params["SCRIPT_NAME"] = script_name if script_name status, headers, body = app.call(params) new_body = [] @@ -56,12 +58,14 @@ class PathGenerationTest < ActiveSupport::TestCase Rails.logger = Logger.new nil app = Class.new(Rails::Application) { + def self.name; "ScriptNameTestApp"; end + 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 @@ -72,11 +76,11 @@ class PathGenerationTest < ActiveSupport::TestCase 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, "/FOO") + assert_equal "/FOO/blogs", app.instance.controller.blogs_path - send_request(url, 'GET', nil) - assert_equal '/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 diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb index 12630e4d01..9f5bb37c20 100644 --- a/railties/test/paths_test.rb +++ b/railties/test/paths_test.rb @@ -1,6 +1,8 @@ -require 'abstract_unit' -require 'rails/paths' -require 'minitest/mock' +# frozen_string_literal: true + +require "abstract_unit" +require "rails/paths" +require "minitest/mock" class PathsTest < ActiveSupport::TestCase def setup @@ -62,6 +64,13 @@ class PathsTest < ActiveSupport::TestCase assert_equal ["/foo/bar/baz"], @root["app/models"].to_a end + test "absolute current path" do + @root.add "config" + @root.add "config/locales" + + assert_equal "/foo/bar/config/locales", @root["config/locales"].absolute_current + end + test "adding multiple physical paths as an array" do @root.add "app", with: ["/app", "/app2"] assert_equal ["/app", "/app2"], @root["app"].to_a @@ -95,35 +104,35 @@ class PathsTest < ActiveSupport::TestCase File.stub(:exist?, true) do @root.add "app", with: "/app" @root["app"].autoload_once! - assert @root["app"].autoload_once? - assert @root.autoload_once.include?(@root["app"].expanded.first) + assert_predicate @root["app"], :autoload_once? + assert_includes @root.autoload_once, @root["app"].expanded.first end end test "it is possible to remove a path that should be autoloaded only once" do @root["app"] = "/app" @root["app"].autoload_once! - assert @root["app"].autoload_once? + assert_predicate @root["app"], :autoload_once? @root["app"].skip_autoload_once! - assert !@root["app"].autoload_once? - assert !@root.autoload_once.include?(@root["app"].expanded.first) + assert_not_predicate @root["app"], :autoload_once? + assert_not_includes @root.autoload_once, @root["app"].expanded.first end test "it is possible to add a path without assignment and specify it should be loaded only once" do File.stub(:exist?, true) do @root.add "app", with: "/app", autoload_once: true - assert @root["app"].autoload_once? - assert @root.autoload_once.include?("/app") + assert_predicate @root["app"], :autoload_once? + assert_includes @root.autoload_once, "/app" end end test "it is possible to add multiple paths without assignment and specify it should be loaded only once" do File.stub(:exist?, true) do @root.add "app", with: ["/app", "/app2"], autoload_once: true - assert @root["app"].autoload_once? - assert @root.autoload_once.include?("/app") - assert @root.autoload_once.include?("/app2") + assert_predicate @root["app"], :autoload_once? + assert_includes @root.autoload_once, "/app" + assert_includes @root.autoload_once, "/app2" end end @@ -132,7 +141,7 @@ class PathsTest < ActiveSupport::TestCase @root["app"] = "/app" @root["app"].autoload_once! @root["app"].autoload_once! - assert_equal 1, @root.autoload_once.select {|p| p == @root["app"].expanded.first }.size + assert_equal 1, @root.autoload_once.select { |p| p == @root["app"].expanded.first }.size end end @@ -149,45 +158,45 @@ class PathsTest < ActiveSupport::TestCase File.stub(:exist?, true) do @root["app"] = "/app" @root["app"].eager_load! - assert @root["app"].eager_load? - assert @root.eager_load.include?(@root["app"].to_a.first) + assert_predicate @root["app"], :eager_load? + assert_includes @root.eager_load, @root["app"].to_a.first end end test "it is possible to skip a path from eager loading" do @root["app"] = "/app" @root["app"].eager_load! - assert @root["app"].eager_load? + assert_predicate @root["app"], :eager_load? @root["app"].skip_eager_load! - assert !@root["app"].eager_load? - assert !@root.eager_load.include?(@root["app"].to_a.first) + assert_not_predicate @root["app"], :eager_load? + assert_not_includes @root.eager_load, @root["app"].to_a.first end test "it is possible to add a path without assignment and mark it as eager" do File.stub(:exist?, true) do @root.add "app", with: "/app", eager_load: true - assert @root["app"].eager_load? - assert @root.eager_load.include?("/app") + assert_predicate @root["app"], :eager_load? + assert_includes @root.eager_load, "/app" end end test "it is possible to add multiple paths without assignment and mark them as eager" do File.stub(:exist?, true) do @root.add "app", with: ["/app", "/app2"], eager_load: true - assert @root["app"].eager_load? - assert @root.eager_load.include?("/app") - assert @root.eager_load.include?("/app2") + assert_predicate @root["app"], :eager_load? + assert_includes @root.eager_load, "/app" + assert_includes @root.eager_load, "/app2" end end test "it is possible to create a path without assignment and mark it both as eager and load once" do File.stub(:exist?, true) do @root.add "app", with: "/app", eager_load: true, autoload_once: true - assert @root["app"].eager_load? - assert @root["app"].autoload_once? - assert @root.eager_load.include?("/app") - assert @root.autoload_once.include?("/app") + assert_predicate @root["app"], :eager_load? + assert_predicate @root["app"], :autoload_once? + assert_includes @root.eager_load, "/app" + assert_includes @root.autoload_once, "/app" end end @@ -196,7 +205,7 @@ class PathsTest < ActiveSupport::TestCase @root["app"] = "/app" @root["app"].eager_load! @root["app"].eager_load! - assert_equal 1, @root.eager_load.select {|p| p == @root["app"].expanded.first }.size + assert_equal 1, @root.eager_load.select { |p| p == @root["app"].expanded.first }.size end end @@ -215,6 +224,12 @@ class PathsTest < ActiveSupport::TestCase assert_equal "*.rb", @root["app"].glob end + test "it should be possible to get extensions by glob" do + @root["app"] = "/app" + @root["app"].glob = "*.{rb,yml}" + assert_equal ["rb", "yml"], @root["app"].extensions + end + test "it should be possible to override a path's default glob without assignment" do @root.add "app", with: "/app", glob: "*.rb" assert_equal "*.rb", @root["app"].glob @@ -239,7 +254,7 @@ class PathsTest < ActiveSupport::TestCase test "a path can be added to the load path on creation" do File.stub(:exist?, true) do @root.add "app", with: "/app", load_path: true - assert @root["app"].load_path? + assert_predicate @root["app"], :load_path? assert_equal ["/app"], @root.load_paths end end @@ -256,8 +271,28 @@ class PathsTest < ActiveSupport::TestCase test "a path can be marked as autoload on creation" do File.stub(:exist?, true) do @root.add "app", with: "/app", autoload: true - assert @root["app"].autoload? + assert_predicate @root["app"], :autoload? assert_equal ["/app"], @root.autoload_paths end end end + +class PathsIntegrationTest < ActiveSupport::TestCase + test "A failed symlink is still a valid file" do + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + FileUtils.mkdir_p("foo") + File.symlink("foo/doesnotexist.rb", "foo/bar.rb") + assert_equal true, File.symlink?("foo/bar.rb") + + root = Rails::Paths::Root.new("foo") + root.add "bar.rb" + + exception = assert_raises(RuntimeError) do + root["bar.rb"].existent + end + assert_match File.expand_path("foo/bar.rb"), exception.message + end + end + end +end diff --git a/railties/test/rack_logger_test.rb b/railties/test/rack_logger_test.rb index fcc79b57fb..ac37062e6d 100644 --- a/railties/test/rack_logger_test.rb +++ b/railties/test/rack_logger_test.rb @@ -1,8 +1,10 @@ -require 'abstract_unit' -require 'active_support/testing/autorun' -require 'active_support/test_case' -require 'rails/rack/logger' -require 'logger' +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/testing/autorun" +require "active_support/test_case" +require "rails/rack/logger" +require "logger" module Rails module Rack @@ -12,15 +14,22 @@ module Rails attr_reader :logger - def initialize(logger = NULL, taggers = nil, &block) - super(->(_) { block.call; [200, {}, []] }, taggers) + def initialize(logger = NULL, app: nil, taggers: nil, &block) + app ||= ->(_) { block.call; [200, {}, []] } + super(app, taggers) @logger = logger end def development?; false; end end - class Subscriber < Struct.new(:starts, :finishes) + class TestApp < Struct.new(:response) + def call(_env) + response + end + end + + Subscriber = Struct.new(:starts, :finishes) do def initialize(starts = [], finishes = []) super end @@ -39,7 +48,7 @@ module Rails def setup @subscriber = Subscriber.new @notifier = ActiveSupport::Notifications.notifier - @subscription = notifier.subscribe 'request.action_dispatch', subscriber + @subscription = notifier.subscribe "request.action_dispatch", subscriber end def teardown @@ -49,9 +58,9 @@ module Rails def test_notification logger = TestLogger.new { } - assert_difference('subscriber.starts.length') do - assert_difference('subscriber.finishes.length') do - logger.call('REQUEST_METHOD' => 'GET').last.close + assert_difference("subscriber.starts.length") do + assert_difference("subscriber.finishes.length") do + logger.call("REQUEST_METHOD" => "GET").last.close end end end @@ -62,14 +71,25 @@ module Rails raise NotImplementedError end - assert_difference('subscriber.starts.length') do - assert_difference('subscriber.finishes.length') do + assert_difference("subscriber.starts.length") do + assert_difference("subscriber.finishes.length") do assert_raises(NotImplementedError) do - logger.call 'REQUEST_METHOD' => 'GET' + logger.call "REQUEST_METHOD" => "GET" end end end end + + def test_logger_does_not_mutate_app_return + response = [].freeze + app = TestApp.new(response) + logger = TestLogger.new(app: app) + assert_no_changes("response") do + assert_nothing_raised do + logger.call("REQUEST_METHOD" => "GET") + end + end + end end end end diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index c51503c2b7..6ab68f8333 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -1,4 +1,6 @@ -require 'abstract_unit' +# frozen_string_literal: true + +require "abstract_unit" module ActionController class Base @@ -11,8 +13,8 @@ class InfoControllerTest < ActionController::TestCase def setup Rails.application.routes.draw do - get '/rails/info/properties' => "rails/info#properties" - get '/rails/info/routes' => "rails/info#routes" + get "/rails/info/properties" => "rails/info#properties" + get "/rails/info/routes" => "rails/info#routes" end @routes = Rails.application.routes @@ -30,7 +32,7 @@ class InfoControllerTest < ActionController::TestCase test "info controller renders an error message when request was forbidden" do @request.env["REMOTE_ADDR"] = "example.org" get :properties - assert_select 'p' + assert_select "p" end test "info controller allows requests when all requests are considered local" do @@ -45,7 +47,12 @@ class InfoControllerTest < ActionController::TestCase test "info controller renders a table with properties" do get :properties - assert_select 'table' + assert_select "table" + end + + test "info controller renders json with properties" do + get :properties, format: :json + assert_equal Rails::Info.to_json, response.body end test "info controller renders with routes" do @@ -54,28 +61,34 @@ class InfoControllerTest < ActionController::TestCase end test "info controller returns exact matches" do - exact_count = -> { JSON(response.body)['exact'].size } + exact_count = -> { JSON(response.body)["exact"].size } - get :routes, params: { path: 'rails/info/route' } - assert exact_count.call == 0, 'should not match incomplete routes' + get :routes, params: { path: "rails/info/route" } + assert exact_count.call == 0, "should not match incomplete routes" - get :routes, params: { path: 'rails/info/routes' } - assert exact_count.call == 1, 'should match complete routes' + get :routes, params: { path: "rails/info/routes" } + assert exact_count.call == 1, "should match complete routes" - get :routes, params: { path: 'rails/info/routes.html' } - assert exact_count.call == 1, 'should match complete routes with optional parts' + get :routes, params: { path: "rails/info/routes.html" } + assert exact_count.call == 1, "should match complete routes with optional parts" end test "info controller returns fuzzy matches" do - fuzzy_count = -> { JSON(response.body)['fuzzy'].size } + fuzzy_count = -> { JSON(response.body)["fuzzy"].size } - get :routes, params: { path: 'rails/info' } - assert fuzzy_count.call == 2, 'should match incomplete routes' + get :routes, params: { path: "rails/info" } + assert fuzzy_count.call == 2, "should match incomplete routes" - get :routes, params: { path: 'rails/info/routes' } - assert fuzzy_count.call == 1, 'should match complete routes' + get :routes, params: { path: "rails/info/routes" } + assert fuzzy_count.call == 1, "should match complete routes" - get :routes, params: { path: 'rails/info/routes.html' } - assert fuzzy_count.call == 0, 'should match optional parts of route literally' + get :routes, params: { path: "rails/info/routes.html" } + assert fuzzy_count.call == 0, "should match optional parts of route literally" + end + + test "internal routes do not have a default params[:internal] value" do + get :properties + assert_response :success + assert_nil @controller.params[:internal] end end diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb index 92e4af25b5..d167a86e56 100644 --- a/railties/test/rails_info_test.rb +++ b/railties/test/rails_info_test.rb @@ -1,60 +1,61 @@ -require 'abstract_unit' +# frozen_string_literal: true -unless defined?(Rails) && defined?(Rails::Info) - module Rails - class Info; end - end -end - -require "active_support/core_ext/kernel/reporting" +require "abstract_unit" class InfoTest < ActiveSupport::TestCase - def setup - Rails.send :remove_const, :Info - silence_warnings { load 'rails/info.rb' } - end - def test_property_with_block_swallows_exceptions_and_ignores_property assert_nothing_raised do Rails::Info.module_eval do - property('Bogus') {raise} + property("Bogus") { raise } end end - assert !property_defined?('Bogus') + assert_not property_defined?("Bogus") end def test_property_with_string Rails::Info.module_eval do - property 'Hello', 'World' + property "Hello", "World" end - assert_property 'Hello', 'World' + assert_property "Hello", "World" end def test_property_with_block Rails::Info.module_eval do - property('Goodbye') {'World'} + property("Goodbye") { "World" } end - assert_property 'Goodbye', 'World' + assert_property "Goodbye", "World" end def test_rails_version - assert_property 'Rails version', - File.read(File.realpath('../../../RAILS_VERSION', __FILE__)).chomp + assert_property "Rails version", + File.read(File.realpath("../../RAILS_VERSION", __dir__)).chomp end def test_html_includes_middleware Rails::Info.module_eval do - property 'Middleware', ['Rack::Lock', 'Rack::Static'] + property "Middleware", ["Rack::Lock", "Rack::Static"] end html = Rails::Info.to_html - assert html.include?('<tr><td class="name">Middleware</td>') - properties.value_for('Middleware').each do |value| - assert html.include?("<li>#{CGI.escapeHTML(value)}</li>") + assert_includes html, '<tr><td class="name">Middleware</td>' + properties.value_for("Middleware").each do |value| + assert_includes html, "<li>#{CGI.escapeHTML(value)}</li>" + end + end + + def test_json_includes_middleware + Rails::Info.module_eval do + property "Middleware", ["Rack::Lock", "Rack::Static"] + end + + hash = JSON.parse(Rails::Info.to_json) + assert_includes hash.keys, "Middleware" + properties.value_for("Middleware").each do |value| + assert_includes hash["Middleware"], value end end - protected + private def properties Rails::Info.properties end diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 79bd7a8241..4ac8f8d741 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -1,10 +1,11 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" require "stringio" require "rack/test" module RailtiesTest class EngineTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Isolation include Rack::Test::Methods @@ -28,17 +29,19 @@ module RailtiesTest end def boot_rails - super require "#{app_path}/config/environment" end + def migrations + migration_root = File.expand_path(ActiveRecord::Migrator.migrations_paths.first, app_path) + ActiveRecord::MigrationContext.new(migration_root).migrations + end + test "serving sprocket's assets" do @plugin.write "app/assets/javascripts/engine.js.erb", "<%= :alert %>();" add_to_env_config "development", "config.assets.digest = false" boot_rails - require 'rack/test' - extend Rack::Test::Methods get "/assets/engine.js" assert_match "alert()", last_response.body @@ -63,56 +66,57 @@ module RailtiesTest test "copying migrations" do @plugin.write "db/migrate/1_create_users.rb", <<-RUBY - class CreateUsers < ActiveRecord::Migration + class CreateUsers < ActiveRecord::Migration::Current end RUBY @plugin.write "db/migrate/2_add_last_name_to_users.rb", <<-RUBY - class AddLastNameToUsers < ActiveRecord::Migration + class AddLastNameToUsers < ActiveRecord::Migration::Current end RUBY @plugin.write "db/migrate/3_create_sessions.rb", <<-RUBY - class CreateSessions < ActiveRecord::Migration + class CreateSessions < ActiveRecord::Migration::Current end RUBY app_file "db/migrate/1_create_sessions.rb", <<-RUBY - class CreateSessions < ActiveRecord::Migration + class CreateSessions < ActiveRecord::Migration::Current def up end end RUBY - add_to_config "ActiveRecord::Base.timestamped_migrations = false" - boot_rails Dir.chdir(app_path) do + # Install Active Storage migration file first so as not to affect test. + `bundle exec rake active_storage:install` output = `bundle exec rake bukkits:install:migrations` - assert File.exist?("#{app_path}/db/migrate/2_create_users.bukkits.rb") - assert File.exist?("#{app_path}/db/migrate/3_add_last_name_to_users.bukkits.rb") - assert_match(/Copied migration 2_create_users.bukkits.rb from bukkits/, output) - assert_match(/Copied migration 3_add_last_name_to_users.bukkits.rb from bukkits/, output) - assert_match(/NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/, output) - assert_equal 3, Dir["#{app_path}/db/migrate/*.rb"].length + ["CreateUsers", "AddLastNameToUsers", "CreateSessions"].each do |migration_name| + assert migrations.detect { |migration| migration.name == migration_name } + end + assert_match(/Copied migration \d+_create_users\.bukkits\.rb from bukkits/, output) + assert_match(/Copied migration \d+_add_last_name_to_users\.bukkits\.rb from bukkits/, output) + assert_match(/NOTE: Migration \d+_create_sessions\.rb from bukkits has been skipped/, output) - output = `bundle exec rake railties:install:migrations`.split("\n") + migrations_count = Dir["#{app_path}/db/migrate/*.rb"].length - assert_no_match(/2_create_users/, output.join("\n")) + assert_equal migrations.length, migrations_count - bukkits_migration_order = output.index(output.detect{|o| /NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/ =~ o }) - assert_not_nil bukkits_migration_order, "Expected migration to be skipped" - - migrations_count = Dir["#{app_path}/db/migrate/*.rb"].length - `bundle exec rake railties:install:migrations` + output = `bundle exec rake railties:install:migrations`.split("\n") assert_equal migrations_count, Dir["#{app_path}/db/migrate/*.rb"].length + + assert_no_match(/\d+_create_users/, output.join("\n")) + + bukkits_migration_order = output.index(output.detect { |o| /NOTE: Migration \d+_create_sessions\.rb from bukkits has been skipped/ =~ o }) + assert_not_nil bukkits_migration_order, "Expected migration to be skipped" end end - test 'respects the order of railties when installing migrations' do + test "respects the order of railties when installing migrations" do @blog = engine "blog" do |plugin| plugin.write "lib/blog.rb", <<-RUBY module Blog @@ -123,12 +127,12 @@ module RailtiesTest end @plugin.write "db/migrate/1_create_users.rb", <<-RUBY - class CreateUsers < ActiveRecord::Migration + class CreateUsers < ActiveRecord::Migration::Current end RUBY @blog.write "db/migrate/2_create_blogs.rb", <<-RUBY - class CreateBlogs < ActiveRecord::Migration + class CreateBlogs < ActiveRecord::Migration::Current end RUBY @@ -137,10 +141,10 @@ module RailtiesTest boot_rails Dir.chdir(app_path) do - output = `bundle exec rake railties:install:migrations`.split("\n") + output = `bundle exec rake railties:install:migrations`.split("\n") - assert_match(/Copied migration \d+_create_users.bukkits.rb from bukkits/, output.first) - assert_match(/Copied migration \d+_create_blogs.blog_engine.rb from blog_engine/, output.last) + assert_match(/Copied migration \d+_create_users\.bukkits\.rb from bukkits/, output.first) + assert_match(/Copied migration \d+_create_blogs\.blog_engine\.rb from blog_engine/, output.second) end end @@ -163,20 +167,22 @@ module RailtiesTest end @core.write "db/migrate/1_create_users.rb", <<-RUBY - class CreateUsers < ActiveRecord::Migration; end + class CreateUsers < ActiveRecord::Migration::Current; end RUBY @api.write "db/migrate/2_create_keys.rb", <<-RUBY - class CreateKeys < ActiveRecord::Migration; end + class CreateKeys < ActiveRecord::Migration::Current; end RUBY boot_rails Dir.chdir(app_path) do - output = `bundle exec rake railties:install:migrations`.split("\n") + # Install Active Storage migration file first so as not to affect test. + `bundle exec rake active_storage:install` + output = `bundle exec rake railties:install:migrations`.split("\n") - assert_match(/Copied migration \d+_create_users.core_engine.rb from core_engine/, output.first) - assert_match(/Copied migration \d+_create_keys.api_engine.rb from api_engine/, output.last) + assert_match(/Copied migration \d+_create_users\.core_engine\.rb from core_engine/, output.first) + assert_match(/Copied migration \d+_create_keys\.api_engine\.rb from api_engine/, output.second) end end @@ -190,7 +196,7 @@ module RailtiesTest RUBY @plugin.write "db/migrate/0_add_first_name_to_users.rb", <<-RUBY - class AddFirstNameToUsers < ActiveRecord::Migration + class AddFirstNameToUsers < ActiveRecord::Migration::Current end RUBY @@ -205,19 +211,22 @@ module RailtiesTest Dir.chdir(@plugin.path) do output = `bundle exec rake app:bukkits:install:migrations` - assert File.exist?("#{app_path}/db/migrate/0_add_first_name_to_users.bukkits.rb") - assert_match(/Copied migration 0_add_first_name_to_users.bukkits.rb from bukkits/, output) - assert_equal 1, Dir["#{app_path}/db/migrate/*.rb"].length + + migration_with_engine_path = migrations.detect { |migration| migration.name == "AddFirstNameToUsers" } + assert migration_with_engine_path + assert_match(/\/db\/migrate\/\d+_add_first_name_to_users\.bukkits\.rb/, migration_with_engine_path.filename) + assert_match(/Copied migration \d+_add_first_name_to_users\.bukkits\.rb from bukkits/, output) + assert_equal migrations.length, Dir["#{app_path}/db/migrate/*.rb"].length end end test "no rake task without migrations" do boot_rails - require 'rake' - require 'rdoc/task' - require 'rake/testtask' + require "rake" + require "rdoc/task" + require "rake/testtask" Rails.application.load_tasks - assert !Rake::Task.task_defined?('bukkits:install:migrations') + assert_not Rake::Task.task_defined?("bukkits:install:migrations") end test "puts its lib directory on load path" do @@ -322,8 +331,6 @@ module RailtiesTest RUBY boot_rails - require 'rack/test' - extend Rack::Test::Methods get "/sprokkit" assert_equal "I am a Sprokkit", last_response.body @@ -333,7 +340,7 @@ module RailtiesTest controller "foo", <<-RUBY class FooController < ActionController::Base def index - render :text => "foo" + render plain: "foo" end end RUBY @@ -347,7 +354,7 @@ module RailtiesTest @plugin.write "app/controllers/bar_controller.rb", <<-RUBY class BarController < ActionController::Base def index - render :text => "bar" + render plain: "bar" end end RUBY @@ -360,14 +367,12 @@ module RailtiesTest RUBY boot_rails - require 'rack/test' - extend Rack::Test::Methods - get '/foo' - assert_equal 'foo', last_response.body + get "/foo" + assert_equal "foo", last_response.body - get '/bar' - assert_equal 'bar', last_response.body + get "/bar" + assert_equal "bar", last_response.body end test "rake tasks lib tasks are loaded" do @@ -379,9 +384,9 @@ module RailtiesTest RUBY boot_rails - require 'rake' - require 'rdoc/task' - require 'rake/testtask' + require "rake" + require "rdoc/task" + require "rake/testtask" Rails.application.load_tasks Rake::Task[:foo].invoke assert $executed @@ -392,18 +397,18 @@ module RailtiesTest config.i18n.load_path << "#{app_path}/app/locales/en.yml" RUBY - app_file 'app/locales/en.yml', <<-YAML + app_file "app/locales/en.yml", <<-YAML en: bar: "1" YAML - app_file 'config/locales/en.yml', <<-YAML + app_file "config/locales/en.yml", <<-YAML en: foo: "2" bar: "2" YAML - @plugin.write 'config/locales/en.yml', <<-YAML + @plugin.write "config/locales/en.yml", <<-YAML en: foo: "3" YAML @@ -444,14 +449,12 @@ YAML @plugin.write "app/controllers/admin/foo/bar_controller.rb", <<-RUBY class Admin::Foo::BarController < ApplicationController def index - render text: "Rendered from namespace" + render plain: "Rendered from namespace" end end RUBY boot_rails - require 'rack/test' - extend Rack::Test::Methods get "/admin/foo/bar" assert_equal 200, last_response.status @@ -481,7 +484,7 @@ YAML end RUBY - add_to_config "config.middleware.use \"Bukkits\"" + add_to_config "config.middleware.use Bukkits" boot_rails end @@ -513,7 +516,7 @@ YAML def call(env) response = @app.call(env) - response[2].each(&:upcase!) + response[2] = response[2].collect(&:upcase) response end end @@ -546,7 +549,7 @@ YAML controller "foo", <<-RUBY class FooController < ActionController::Base def index - render text: params[:username] + render plain: params[:username] end end RUBY @@ -567,7 +570,6 @@ YAML get("/arunagw") assert_equal "arunagw", last_response.body - end test "it provides routes as default endpoint" do @@ -637,11 +639,11 @@ YAML env = Rack::MockRequest.env_for("/") Bukkits::Engine.call(env) - assert_equal Bukkits::Engine.routes, env['action_dispatch.routes'] + assert_equal Bukkits::Engine.routes, env["action_dispatch.routes"] env = Rack::MockRequest.env_for("/") Rails.application.call(env) - assert_equal Rails.application.routes, env['action_dispatch.routes'] + assert_equal Rails.application.routes, env["action_dispatch.routes"] end test "isolated engine should include only its own routes and helpers" do @@ -710,7 +712,7 @@ YAML end def show - render text: foo_path + render plain: foo_path end def from_app @@ -722,7 +724,7 @@ YAML end def polymorphic_path_without_namespace - render text: polymorphic_path(Post.new) + render plain: polymorphic_path(Post.new) end end RUBY @@ -742,7 +744,7 @@ YAML assert_equal "bukkits", Bukkits::Engine.engine_name assert_equal Bukkits.railtie_namespace, Bukkits::Engine assert ::Bukkits::MyMailer.method_defined?(:foo_url) - assert !::Bukkits::MyMailer.method_defined?(:bar_url) + assert_not ::Bukkits::MyMailer.method_defined?(:bar_url) get("/bukkits/from_app") assert_equal "false", last_response.body @@ -845,7 +847,7 @@ YAML @plugin.write "app/controllers/bukkits/awesome/foo_controller.rb", <<-RUBY class Bukkits::Awesome::FooController < ActionController::Base def index - render :text => "ok" + render plain: "ok" end end RUBY @@ -892,7 +894,17 @@ YAML end RUBY - add_to_config "isolate_namespace AppTemplate" + engine "loaded_first" do |plugin| + plugin.write "lib/loaded_first.rb", <<-RUBY + module AppTemplate + module LoadedFirst + class Engine < ::Rails::Engine + isolate_namespace(AppTemplate) + end + end + end + RUBY + end app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do end @@ -900,7 +912,7 @@ YAML boot_rails - assert_equal AppTemplate.railtie_namespace, AppTemplate::Engine + assert_equal AppTemplate::LoadedFirst::Engine, AppTemplate.railtie_namespace end test "properly reload routes" do @@ -967,14 +979,14 @@ YAML boot_rails app_generators = Rails.application.config.generators.options[:rails] - assert_equal :mongoid , app_generators[:orm] - assert_equal :liquid , app_generators[:template_engine] + assert_equal :mongoid, app_generators[:orm] + assert_equal :liquid, app_generators[:template_engine] assert_equal :test_unit, app_generators[:test_framework] generators = Bukkits::Engine.config.generators.options[:rails] assert_equal :data_mapper, generators[:orm] - assert_equal :haml , generators[:template_engine] - assert_equal :rspec , generators[:test_framework] + assert_equal :haml, generators[:template_engine] + assert_equal :rspec, generators[:test_framework] end test "engine should get default generators with ability to overwrite them" do @@ -990,10 +1002,10 @@ YAML generators = Bukkits::Engine.config.generators.options[:rails] assert_equal :active_record, generators[:orm] - assert_equal :rspec , generators[:test_framework] + assert_equal :rspec, generators[:test_framework] app_generators = Rails.application.config.generators.options[:rails] - assert_equal :test_unit , app_generators[:test_framework] + assert_equal :test_unit, app_generators[:test_framework] end test "do not create table_name_prefix method if it already exists" do @@ -1028,7 +1040,7 @@ YAML # check expanding paths engine_dir = @plugin.path.chomp("/").split("/").last - engine_path = File.join(@plugin.path, '..', engine_dir) + engine_path = File.join(@plugin.path, "..", engine_dir) assert_equal Bukkits::Engine.instance, Rails::Engine.find(engine_path) end @@ -1155,10 +1167,10 @@ YAML assert_equal "App's bar partial", last_response.body.strip get("/assets/foo.js") - assert_equal "// Bukkit's foo js", last_response.body.strip + assert_match "// Bukkit's foo js", last_response.body.strip get("/assets/bar.js") - assert_equal "// App's bar js", last_response.body.strip + assert_match "// App's bar js", last_response.body.strip # ensure that railties are not added twice railties = Rails.application.send(:ordered_railties).map(&:class) @@ -1205,7 +1217,7 @@ YAML test "engine can be properly mounted at root" do add_to_config("config.action_dispatch.show_exceptions = false") - add_to_config("config.serve_static_files = false") + add_to_config("config.public_file_server.enabled = false") @plugin.write "lib/bukkits.rb", <<-RUBY module Bukkits @@ -1230,13 +1242,12 @@ YAML fullpath: \#{request.fullpath} path: \#{request.path} TEXT - render text: text + render plain: text end end end RUBY - app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do mount Bukkits::Engine => "/" @@ -1268,7 +1279,7 @@ YAML app_file "app/controllers/bar_controller.rb", <<-RUBY class BarController < ApplicationController def index - render text: bukkits.bukkit_path + render plain: bukkits.bukkit_path end end RUBY @@ -1286,22 +1297,200 @@ YAML end RUBY + @plugin.write "app/controllers/bukkits/bukkit_controller.rb", <<-RUBY + class Bukkits::BukkitController < ActionController::Base + def index + render plain: main_app.bar_path + end + end + RUBY + + boot_rails + + get("/bukkits/bukkit", {}, { "SCRIPT_NAME" => "/foo" }) + assert_equal "/foo/bar", last_response.body + + get("/bar", {}, { "SCRIPT_NAME" => "/foo" }) + assert_equal "/foo/bukkits/bukkit", last_response.body + end + + test "paths are properly generated when application is mounted at sub-path and relative_url_root is set" do + add_to_config "config.relative_url_root = '/foo'" + + @plugin.write "lib/bukkits.rb", <<-RUBY + module Bukkits + class Engine < ::Rails::Engine + isolate_namespace Bukkits + end + end + RUBY + + app_file "app/controllers/bar_controller.rb", <<-RUBY + class BarController < ApplicationController + def index + render plain: bukkits.bukkit_path + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/bar' => 'bar#index', :as => 'bar' + mount Bukkits::Engine => "/bukkits", :as => "bukkits" + end + RUBY + + @plugin.write "config/routes.rb", <<-RUBY + Bukkits::Engine.routes.draw do + get '/bukkit' => 'bukkit#index' + end + RUBY @plugin.write "app/controllers/bukkits/bukkit_controller.rb", <<-RUBY class Bukkits::BukkitController < ActionController::Base def index - render text: main_app.bar_path + render plain: main_app.bar_path + end + end + RUBY + + boot_rails + + get("/bukkits/bukkit", {}, { "SCRIPT_NAME" => "/foo" }) + assert_equal "/foo/bar", last_response.body + + get("/bar", {}, { "SCRIPT_NAME" => "/foo" }) + assert_equal "/foo/bukkits/bukkit", last_response.body + end + + test "isolated engine can be mounted under multiple static locations" do + app_file "app/controllers/foos_controller.rb", <<-RUBY + class FoosController < ApplicationController + def through_fruits + render plain: fruit_bukkits.posts_path + end + + def through_vegetables + render plain: vegetable_bukkits.posts_path + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + scope "/fruits" do + mount Bukkits::Engine => "/bukkits", as: :fruit_bukkits + end + + scope "/vegetables" do + mount Bukkits::Engine => "/bukkits", as: :vegetable_bukkits + end + + get "/through_fruits" => "foos#through_fruits" + get "/through_vegetables" => "foos#through_vegetables" + end + RUBY + + @plugin.write "config/routes.rb", <<-RUBY + Bukkits::Engine.routes.draw do + resources :posts, only: :index + end + RUBY + + boot_rails + + get("/through_fruits") + assert_equal "/fruits/bukkits/posts", last_response.body + + get("/through_vegetables") + assert_equal "/vegetables/bukkits/posts", last_response.body + end + + test "isolated engine can be mounted under multiple dynamic locations" do + app_file "app/controllers/foos_controller.rb", <<-RUBY + class FoosController < ApplicationController + def through_fruits + render plain: fruit_bukkits.posts_path(fruit_id: 1) + end + + def through_vegetables + render plain: vegetable_bukkits.posts_path(vegetable_id: 1) + end + end + RUBY + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + resources :fruits do + mount Bukkits::Engine => "/bukkits" + end + + resources :vegetables do + mount Bukkits::Engine => "/bukkits" + end + + get "/through_fruits" => "foos#through_fruits" + get "/through_vegetables" => "foos#through_vegetables" + end + RUBY + + @plugin.write "config/routes.rb", <<-RUBY + Bukkits::Engine.routes.draw do + resources :posts, only: :index + end + RUBY + + boot_rails + + get("/through_fruits") + assert_equal "/fruits/1/bukkits/posts", last_response.body + + get("/through_vegetables") + assert_equal "/vegetables/1/bukkits/posts", last_response.body + end + + test "route helpers resolve script name correctly when called with different script name from current one" do + @plugin.write "app/controllers/posts_controller.rb", <<-RUBY + class PostsController < ActionController::Base + def index + render plain: fruit_bukkits.posts_path(fruit_id: 2) end end RUBY + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + resources :fruits do + mount Bukkits::Engine => "/bukkits" + end + end + RUBY + + @plugin.write "config/routes.rb", <<-RUBY + Bukkits::Engine.routes.draw do + resources :posts, only: :index + end + RUBY + boot_rails - get("/bukkits/bukkit", {}, {'SCRIPT_NAME' => '/foo'}) - assert_equal '/foo/bar', last_response.body + get("/fruits/1/bukkits/posts") + assert_equal "/fruits/2/bukkits/posts", last_response.body + end - get("/bar", {}, {'SCRIPT_NAME' => '/foo'}) - assert_equal '/foo/bukkits/bukkit', last_response.body + test "active_storage:install task works within engine" do + @plugin.write "Rakefile", <<-RUBY + APP_RAKEFILE = '#{app_path}/Rakefile' + load 'rails/tasks/engine.rake' + RUBY + + Dir.chdir(@plugin.path) do + output = `bundle exec rake app:active_storage:install` + assert $?.success?, output + + active_storage_migration = migrations.detect { |migration| migration.name == "CreateActiveStorageTables" } + assert active_storage_migration + end end private diff --git a/railties/test/railties/generators_test.rb b/railties/test/railties/generators_test.rb index 5f4171d44b..8383cb3050 100644 --- a/railties/test/railties/generators_test.rb +++ b/railties/test/railties/generators_test.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + RAILS_ISOLATED_ENGINE = true require "isolation/abstract_unit" -require 'generators/generators_test_helper' +require "generators/generators_test_helper" require "rails/generators/test_case" module RailtiesTests @@ -9,7 +11,7 @@ module RailtiesTests include ActiveSupport::Testing::Isolation def destination_root - tmp_path 'foo_bar' + tmp_path "foo_bar" end def tmp_path(*args) @@ -18,7 +20,7 @@ module RailtiesTests end def engine_path - tmp_path('foo_bar') + tmp_path("foo_bar") end def bundled_rails(cmd) @@ -26,14 +28,10 @@ module RailtiesTests end def rails(cmd) - environment = File.expand_path('../../../../load_paths', __FILE__) - if File.exist?("#{environment}.rb") - require_environment = "-r #{environment}" - end - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails #{cmd}` + `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails #{cmd}` end - def build_engine(is_mountable=false) + def build_engine(is_mountable = false) FileUtils.rm_rf(engine_path) FileUtils.mkdir_p(engine_path) @@ -43,7 +41,7 @@ module RailtiesTests Dir.chdir(engine_path) do File.open("Gemfile", "w") do |f| - f.write <<-GEMFILE.gsub(/^ {12}/, '') + f.write <<-GEMFILE.gsub(/^ {12}/, "") source "https://rubygems.org" gem 'rails', path: '#{RAILS_FRAMEWORK_ROOT}' diff --git a/railties/test/railties/mounted_engine_test.rb b/railties/test/railties/mounted_engine_test.rb index fb2071c7c3..48f0fbc80f 100644 --- a/railties/test/railties/mounted_engine_test.rb +++ b/railties/test/railties/mounted_engine_test.rb @@ -1,10 +1,12 @@ -require 'isolation/abstract_unit' +# frozen_string_literal: true + +require "isolation/abstract_unit" module ApplicationTests class ApplicationRoutingTest < ActiveSupport::TestCase - require 'rack/test' - include Rack::Test::Methods - include ActiveSupport::Testing::Isolation + require "rack/test" + include Rack::Test::Methods + include ActiveSupport::Testing::Isolation def setup build_app @@ -15,7 +17,7 @@ module ApplicationTests @plugin = engine "blog" @metrics_plugin = engine "metrics" - app_file 'config/routes.rb', <<-RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do mount Weblog::Engine, :at => '/', :as => 'weblog' resources :posts @@ -34,7 +36,6 @@ module ApplicationTests end RUBY - @simple_plugin.write "lib/weblog.rb", <<-RUBY module Weblog class Engine < ::Rails::Engine @@ -51,7 +52,7 @@ module ApplicationTests @simple_plugin.write "app/controllers/weblogs_controller.rb", <<-RUBY class WeblogsController < ActionController::Base def index - render text: request.url + render plain: request.url end end RUBY @@ -75,7 +76,7 @@ module ApplicationTests module Metrics class GeneratingController < ActionController::Base def generate_blog_route - render text: blog.post_path(1) + render plain: blog.post_path(1) end def generate_blog_route_in_view @@ -112,6 +113,7 @@ module ApplicationTests @plugin.write "config/routes.rb", <<-RUBY Blog::Engine.routes.draw do resources :posts + get '/different_context', to: 'posts#different_context' get '/generate_application_route', to: 'posts#generate_application_route' get '/application_route_in_view', to: 'posts#application_route_in_view' get '/engine_polymorphic_path', to: 'posts#engine_polymorphic_path' @@ -123,14 +125,18 @@ module ApplicationTests module Blog class PostsController < ActionController::Base def index - render text: blog.post_path(1) + render plain: blog.post_path(1) + end + + def different_context + render plain: blog.post_path(1, user: "ada") end def generate_application_route path = main_app.url_for(controller: "/main", action: "index", only_path: true) - render text: path + render plain: path end def application_route_in_view @@ -138,11 +144,11 @@ module ApplicationTests end def engine_polymorphic_path - render text: polymorphic_path(Post.new) + render plain: polymorphic_path(Post.new) end def engine_asset_path - render inline: "<%= asset_path 'images/foo.png' %>" + render inline: "<%= asset_path 'images/foo.png', skip_pipeline: true %>" end end end @@ -151,7 +157,7 @@ module ApplicationTests app_file "app/controllers/application_generating_controller.rb", <<-RUBY class ApplicationGeneratingController < ActionController::Base def engine_route - render text: blog.posts_path + render plain: blog.posts_path end def engine_route_in_view @@ -159,7 +165,7 @@ module ApplicationTests end def weblog_engine_route - render text: weblog.weblogs_path + render plain: weblog.weblogs_path end def weblog_engine_route_in_view @@ -167,20 +173,18 @@ module ApplicationTests end def url_for_engine_route - render text: blog.url_for(controller: "blog/posts", action: "index", user: "john", only_path: true) + render plain: blog.url_for(controller: "blog/posts", action: "index", user: "john", only_path: true) end def polymorphic_route - render text: polymorphic_url([blog, Blog::Post.new]) + render plain: polymorphic_url([blog, Blog::Post.new]) end def application_polymorphic_path - render text: polymorphic_path(Blog::Post.new) + render plain: polymorphic_path(Blog::Post.new) end end RUBY - - boot_rails end def teardown @@ -199,8 +203,12 @@ module ApplicationTests get "/john/blog/posts" assert_equal "/john/blog/posts/1", last_response.body + # test generating engine route from engine with a different context + get "/john/blog/different_context" + assert_equal "/ada/blog/posts/1", last_response.body + # test generating engine's route from engine with default_url_options - get "/john/blog/posts", {}, 'SCRIPT_NAME' => "/foo" + get "/john/blog/posts", {}, { "SCRIPT_NAME" => "/foo" } assert_equal "/foo/john/blog/posts/1", last_response.body # test generating engine's route from application @@ -214,10 +222,10 @@ module ApplicationTests assert_equal "/john/blog/posts", last_response.body # test generating engine's route from application with default_url_options - get "/engine_route", {}, 'SCRIPT_NAME' => "/foo" + get "/engine_route", {}, { "SCRIPT_NAME" => "/foo" } assert_equal "/foo/anonymous/blog/posts", last_response.body - get "/url_for_engine_route", {}, 'SCRIPT_NAME' => "/foo" + get "/url_for_engine_route", {}, { "SCRIPT_NAME" => "/foo" } assert_equal "/foo/john/blog/posts", last_response.body # test generating application's route from engine @@ -229,21 +237,20 @@ module ApplicationTests # test generating engine's route from other engine get "/metrics/generate_blog_route" - assert_equal '/anonymous/blog/posts/1', last_response.body + assert_equal "/anonymous/blog/posts/1", last_response.body get "/metrics/generate_blog_route_in_view" - assert_equal '/anonymous/blog/posts/1', last_response.body + assert_equal "/anonymous/blog/posts/1", last_response.body # test generating engine's route from other engine with default_url_options - get "/metrics/generate_blog_route", {}, 'SCRIPT_NAME' => '/foo' - assert_equal '/foo/anonymous/blog/posts/1', last_response.body - - get "/metrics/generate_blog_route_in_view", {}, 'SCRIPT_NAME' => '/foo' - assert_equal '/foo/anonymous/blog/posts/1', last_response.body + get "/metrics/generate_blog_route", {}, { "SCRIPT_NAME" => "/foo" } + assert_equal "/foo/anonymous/blog/posts/1", last_response.body + get "/metrics/generate_blog_route_in_view", {}, { "SCRIPT_NAME" => "/foo" } + assert_equal "/foo/anonymous/blog/posts/1", last_response.body # test generating application's route from engine with default_url_options - get "/someone/blog/generate_application_route", {}, 'SCRIPT_NAME' => '/foo' + get "/someone/blog/generate_application_route", {}, { "SCRIPT_NAME" => "/foo" } assert_equal "/foo/", last_response.body # test polymorphic routes diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb index 5042d628cf..7c3d1e3759 100644 --- a/railties/test/railties/railtie_test.rb +++ b/railties/test/railties/railtie_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "isolation/abstract_unit" module RailtiesTest @@ -6,7 +8,6 @@ module RailtiesTest def setup build_app - boot_rails FileUtils.rm_rf("#{app_path}/config/environments") require "rails/all" end @@ -64,7 +65,7 @@ module RailtiesTest test "railtie can add to_prepare callbacks" do $to_prepare = false class Foo < Rails::Railtie ; config.to_prepare { $to_prepare = true } ; end - assert !$to_prepare + assert_not $to_prepare require "#{app_path}/config/environment" require "rack/test" extend Rack::Test::Methods @@ -80,10 +81,17 @@ module RailtiesTest assert_equal app_path, $before_configuration end + test "before_configuration callbacks run as soon as the application constant inherits from Rails::Application" do + $before_configuration = false + class Foo < Rails::Railtie ; config.before_configuration { $before_configuration = true } ; end + class Application < Rails::Application ; end + assert $before_configuration + end + test "railtie can add after_initialize callbacks" do $after_initialize = false class Foo < Rails::Railtie ; config.after_initialize { $after_initialize = true } ; end - assert !$after_initialize + assert_not $after_initialize require "#{app_path}/config/environment" assert $after_initialize end @@ -99,10 +107,10 @@ module RailtiesTest require "#{app_path}/config/environment" - assert !$ran_block - require 'rake' - require 'rake/testtask' - require 'rdoc/task' + assert_not $ran_block + require "rake" + require "rake/testtask" + require "rdoc/task" Rails.application.load_tasks assert $ran_block @@ -124,12 +132,12 @@ module RailtiesTest require "#{app_path}/config/environment" assert_equal [], $ran_block - require 'rake' - require 'rake/testtask' - require 'rdoc/task' + require "rake" + require "rake/testtask" + require "rdoc/task" Rails.application.load_tasks - assert $ran_block.include?("my_tie") + assert_includes $ran_block, "my_tie" end test "generators block is executed when MyApp.load_generators is called" do @@ -143,7 +151,7 @@ module RailtiesTest require "#{app_path}/config/environment" - assert !$ran_block + assert_not $ran_block Rails.application.load_generators assert $ran_block end @@ -159,7 +167,7 @@ module RailtiesTest require "#{app_path}/config/environment" - assert !$ran_block + assert_not $ran_block Rails.application.load_console assert $ran_block end @@ -175,7 +183,7 @@ module RailtiesTest require "#{app_path}/config/environment" - assert !$ran_block + assert_not $ran_block Rails.application.load_runner assert $ran_block end @@ -189,7 +197,7 @@ module RailtiesTest end end - assert !$ran_block + assert_not $ran_block require "#{app_path}/config/environment" assert $ran_block end @@ -197,8 +205,8 @@ module RailtiesTest test "we can change our environment if we want to" do begin original_env = Rails.env - Rails.env = 'foo' - assert_equal('foo', Rails.env) + Rails.env = "foo" + assert_equal("foo", Rails.env) ensure Rails.env = original_env assert_equal(original_env, Rails.env) diff --git a/railties/test/secrets_test.rb b/railties/test/secrets_test.rb new file mode 100644 index 0000000000..06877bc76a --- /dev/null +++ b/railties/test/secrets_test.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rails/secrets" + +class Rails::SecretsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + setup :build_app + teardown :teardown_app + + test "setting read to false skips parsing" do + run_secrets_generator do + Rails::Secrets.write(<<-end_of_secrets) + production: + yeah_yeah: lets-walk-in-the-cool-evening-light + end_of_secrets + + add_to_env_config("production", "config.read_encrypted_secrets = false") + app("production") + + assert_not Rails.application.secrets.yeah_yeah + end + end + + test "raises when reading secrets without a key" do + run_secrets_generator do + FileUtils.rm("config/secrets.yml.key") + + assert_raises Rails::Secrets::MissingKeyError do + Rails::Secrets.key + end + end + end + + test "reading with ENV variable" do + run_secrets_generator do + begin + old_key = ENV["RAILS_MASTER_KEY"] + ENV["RAILS_MASTER_KEY"] = IO.binread("config/secrets.yml.key").strip + FileUtils.rm("config/secrets.yml.key") + + assert_match "# production:\n# external_api_key:", Rails::Secrets.read + ensure + ENV["RAILS_MASTER_KEY"] = old_key + end + end + end + + test "reading from key file" do + run_secrets_generator do + File.binwrite("config/secrets.yml.key", "00112233445566778899aabbccddeeff") + + assert_equal "00112233445566778899aabbccddeeff", Rails::Secrets.key + end + end + + test "editing" do + run_secrets_generator do + decrypted_path = nil + + Rails::Secrets.read_for_editing do |tmp_path| + decrypted_path = tmp_path + + assert_match(/# production:\n# external_api_key/, File.read(tmp_path)) + + File.write(tmp_path, "Empty streets, empty nights. The Downtown Lights.") + end + + assert_not File.exist?(decrypted_path) + assert_equal "Empty streets, empty nights. The Downtown Lights.", Rails::Secrets.read + end + end + + test "merging secrets with encrypted precedence" do + run_secrets_generator do + File.write("config/secrets.yml", <<-end_of_secrets) + production: + yeah_yeah: lets-go-walking-down-this-empty-street + end_of_secrets + + Rails::Secrets.write(<<-end_of_secrets) + production: + yeah_yeah: lets-walk-in-the-cool-evening-light + end_of_secrets + + add_to_env_config("production", "config.read_encrypted_secrets = true") + app("production") + + assert_equal "lets-walk-in-the-cool-evening-light", Rails.application.secrets.yeah_yeah + end + end + + test "refer secrets inside env config" do + run_secrets_generator do + Rails::Secrets.write(<<-end_of_yaml) + production: + some_secret: yeah yeah + end_of_yaml + + add_to_env_config "production", <<-end_of_config + config.dereferenced_secret = Rails.application.secrets.some_secret + end_of_config + + app("production") + + assert_equal "yeah yeah", Rails.application.config.dereferenced_secret + end + end + + test "do not update secrets.yml.enc when secretes do not change" do + run_secrets_generator do + Rails::Secrets.read_for_editing do |tmp_path| + File.write(tmp_path, "Empty streets, empty nights. The Downtown Lights.") + end + + FileUtils.cp("config/secrets.yml.enc", "config/secrets.yml.enc.bk") + + Rails::Secrets.read_for_editing do |tmp_path| + File.write(tmp_path, "Empty streets, empty nights. The Downtown Lights.") + end + + assert_equal File.read("config/secrets.yml.enc.bk"), File.read("config/secrets.yml.enc") + end + end + + test "can read secrets written in binary" do + run_secrets_generator do + secrets = <<-end_of_secrets + production: + api_key: 00112233445566778899aabbccddeeff… + end_of_secrets + + Rails::Secrets.write(secrets.dup.force_encoding(Encoding::ASCII_8BIT)) + + Rails::Secrets.read_for_editing do |tmp_path| + assert_match(/production:\n\s*api_key: 00112233445566778899aabbccddeeff…\n/, File.read(tmp_path)) + end + + app("production") + + assert_equal "00112233445566778899aabbccddeeff…", Rails.application.secrets.api_key + end + end + + test "can read secrets written in non-binary" do + run_secrets_generator do + secrets = <<-end_of_secrets + production: + api_key: 00112233445566778899aabbccddeeff… + end_of_secrets + + Rails::Secrets.write(secrets) + + Rails::Secrets.read_for_editing do |tmp_path| + assert_equal(secrets.dup.force_encoding(Encoding::ASCII_8BIT), IO.binread(tmp_path)) + end + + app("production") + + assert_equal "00112233445566778899aabbccddeeff…", Rails.application.secrets.api_key + end + end + + private + def run_secrets_generator + Dir.chdir(app_path) do + File.write("config/secrets.yml.key", "f731758c639da2604dfb6bf3d1025de8") + File.write("config/secrets.yml.enc", "sEB0mHxDbeP1/KdnMk00wyzPFACl9K6t0cZWn5/Mfx/YbTHvnI07vrneqHg9kaH3wOS7L6pIQteu1P077OtE4BSx/ZRc/sgQPHyWu/tXsrfHqnPNpayOF/XZqizE91JacSFItNMWpuPsp9ynbzz+7cGhoB1S4aPNIU6u0doMrzdngDbijsaAFJmsHIQh6t/QHoJx--8aMoE0PvUWmw1Iqz--ldFqnM/K0g9k17M8PKoN/Q==") + + add_to_config <<-RUBY + config.read_encrypted_secrets = true + RUBY + + yield + end + end +end diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb index 3066ba82d6..81b7ab19a1 100644 --- a/railties/test/test_unit/reporter_test.rb +++ b/railties/test/test_unit/reporter_test.rb @@ -1,5 +1,8 @@ -require 'abstract_unit' -require 'rails/test_unit/reporter' +# frozen_string_literal: true + +require "abstract_unit" +require "rails/test_unit/reporter" +require "minitest/mock" class TestUnitReporterTest < ActiveSupport::TestCase class ExampleTest < Minitest::Test @@ -8,14 +11,14 @@ class TestUnitReporterTest < ActiveSupport::TestCase setup do @output = StringIO.new - @reporter = Rails::TestUnitReporter.new @output + @reporter = Rails::TestUnitReporter.new @output, output_inline: true end test "prints rerun snippet to run a single failed test" do @reporter.record(failed_test) @reporter.report - assert_match %r{^bin/rails test .*test/test_unit/reporter_test.rb:6$}, @output.string + assert_match %r{^rails test .*test/test_unit/reporter_test\.rb:\d+$}, @output.string assert_rerun_snippet_count 1 end @@ -32,7 +35,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(passing_test) @reporter.record(skipped_test) @reporter.report - assert_no_match 'Failed tests:', @output.string + assert_no_match "Failed tests:", @output.string assert_rerun_snippet_count 0 end @@ -51,38 +54,145 @@ class TestUnitReporterTest < ActiveSupport::TestCase @reporter.record(failed_test) @reporter.report - assert_match %r{^bin/test .*test/test_unit/reporter_test.rb:6$}, @output.string + assert_match %r{^bin/test .*test/test_unit/reporter_test\.rb:\d+$}, @output.string ensure Rails::TestUnitReporter.executable = original_executable end end - private - def assert_rerun_snippet_count(snippet_count) - assert_equal snippet_count, @output.string.scan(%r{^bin/rails test }).size + test "outputs failures inline" do + @reporter.record(failed_test) + @reporter.report + + expect = %r{\AF\n\nFailure:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nboo\n\nrails test test/test_unit/reporter_test\.rb:\d+\n\n\z} + assert_match expect, @output.string + end + + test "outputs errors inline" do + @reporter.record(errored_test) + @reporter.report + + expect = %r{\AE\n\nError:\nTestUnitReporterTest::ExampleTest#woot:\nArgumentError: wups\n \n\nrails test .*test/test_unit/reporter_test\.rb:\d+\n\n\z} + assert_match expect, @output.string + end + + test "outputs skipped tests inline if verbose" do + verbose = Rails::TestUnitReporter.new @output, verbose: true, output_inline: true + verbose.record(skipped_test) + verbose.report + + expect = %r{\ATestUnitReporterTest::ExampleTest#woot = 10\.00 s = S\n\n\nSkipped:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nskipchurches, misstemples\n\nrails test test/test_unit/reporter_test\.rb:\d+\n\n\z} + assert_match expect, @output.string + end + + test "does not output rerun snippets after run" do + @reporter.record(failed_test) + @reporter.report + + assert_no_match "Failed tests:", @output.string + end + + test "fail fast interrupts run on failure" do + fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true + interrupt_raised = false + + # Minitest passes through Interrupt, catch it manually. + begin + fail_fast.record(failed_test) + rescue Interrupt + interrupt_raised = true + ensure + assert interrupt_raised, "Expected Interrupt to be raised." + end + end + + test "fail fast interrupts run on error" do + fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true + interrupt_raised = false + + # Minitest passes through Interrupt, catch it manually. + begin + fail_fast.record(errored_test) + rescue Interrupt + interrupt_raised = true + ensure + assert interrupt_raised, "Expected Interrupt to be raised." + end end - def failed_test - ft = ExampleTest.new(:woot) - ft.failures << begin - raise Minitest::Assertion, "boo" - rescue Minitest::Assertion => e - e - end - ft + test "fail fast does not interrupt run skips" do + fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true + + fail_fast.record(skipped_test) + assert_no_match "Failed tests:", @output.string end - def passing_test - ExampleTest.new(:woot) + test "outputs colored passing results" do + @output.stub(:tty?, true) do + colored = Rails::TestUnitReporter.new @output, color: true, output_inline: true + colored.record(passing_test) + + expect = %r{\e\[32m\.\e\[0m} + assert_match expect, @output.string + end + end + + test "outputs colored skipped results" do + @output.stub(:tty?, true) do + colored = Rails::TestUnitReporter.new @output, color: true, output_inline: true + colored.record(skipped_test) + + expect = %r{\e\[33mS\e\[0m} + assert_match expect, @output.string + end end - def skipped_test - st = ExampleTest.new(:woot) - st.failures << begin - raise Minitest::Skip - rescue Minitest::Assertion => e - e - end - st + test "outputs colored failed results" do + @output.stub(:tty?, true) do + colored = Rails::TestUnitReporter.new @output, color: true, output_inline: true + colored.record(errored_test) + + expected = %r{\e\[31mE\e\[0m\n\n\e\[31mError:\nTestUnitReporterTest::ExampleTest#woot:\nArgumentError: wups\n \n\e\[0m} + assert_match expected, @output.string + end end + + private + def assert_rerun_snippet_count(snippet_count) + assert_equal snippet_count, @output.string.scan(%r{^rails test }).size + end + + def failed_test + ft = Minitest::Result.from(ExampleTest.new(:woot)) + ft.failures << begin + raise Minitest::Assertion, "boo" + rescue Minitest::Assertion => e + e + end + ft + end + + def errored_test + error = ArgumentError.new("wups") + error.set_backtrace([ "some_test.rb:4" ]) + + et = Minitest::Result.from(ExampleTest.new(:woot)) + et.failures << Minitest::UnexpectedError.new(error) + et + end + + def passing_test + Minitest::Result.from(ExampleTest.new(:woot)) + end + + def skipped_test + st = Minitest::Result.from(ExampleTest.new(:woot)) + st.failures << begin + raise Minitest::Skip, "skipchurches, misstemples" + rescue Minitest::Assertion => e + e + end + st.time = 10 + st + end end diff --git a/railties/test/version_test.rb b/railties/test/version_test.rb index f270d8f0c9..17a024fe7f 100644 --- a/railties/test/version_test.rb +++ b/railties/test/version_test.rb @@ -1,4 +1,6 @@ -require 'abstract_unit' +# frozen_string_literal: true + +require "abstract_unit" class VersionTest < ActiveSupport::TestCase def test_rails_version_returns_a_string |