diff --git a/Changelog.md b/Changelog.md index f9237c5142..01469b40ca 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,15 @@ +### 3.2.2 / 2015-06-03 + +Bug Fixes: + +* Fix auto-including of generic `Helper` object for view specs sitting in the + `app/views` root (David Daniell, #1289) +* Remove pre-loading of ActionMailer in the Railtie (Aaron Kromer, #1327) +* Fix undefined method `need_auto_run=` error when using Ruby 2.1 and Rails 3.2 + without the test-unit gem (Orien Madgwick, #1350) +* Fix load order issued which causes an undefined method `fixture_path` error + when loading rspec-rails after a spec has been created. (Aaron Kromer, #1372) + ### 3.2.1 / 2015-02-23 [Full Changelog](http://github.com/rspec/rspec-rails/compare/v3.2.0...v3.2.1) diff --git a/example_app_generator/generate_action_mailer_specs.rb b/example_app_generator/generate_action_mailer_specs.rb new file mode 100644 index 0000000000..2e220b5771 --- /dev/null +++ b/example_app_generator/generate_action_mailer_specs.rb @@ -0,0 +1,32 @@ +require 'active_support' +require 'active_support/core_ext/module' + +# We need to copy this method from Thor for older Rails versions +def comment_lines(path, flag, *args) + flag = flag.respond_to?(:source) ? flag.source : flag + gsub_file(path, /^(\s*)([^#|\n]*#{flag})/, '\1# \2', *args) +end + +using_source_path(File.expand_path('..', __FILE__)) do + # Comment out the default mailer stuff + comment_lines 'config/environments/development.rb', /action_mailer/ + comment_lines 'config/environments/test.rb', /action_mailer/ + + initializer 'action_mailer.rb', <<-CODE + if ENV['DEFAULT_URL'] + Rails.application.configure do + config.action_mailer.default_url_options = { :host => ENV['DEFAULT_URL'] } + end + end + CODE + + copy_file 'spec/support/default_preview_path' + chmod 'spec/support/default_preview_path', 0755 + gsub_file 'spec/support/default_preview_path', + /ExampleApp/, + Rails.application.class.parent.to_s + if skip_active_record? + comment_lines 'spec/support/default_preview_path', /active_record/ + end + copy_file 'spec/verify_mailer_preview_path_spec.rb' +end diff --git a/example_app_generator/generate_stuff.rb b/example_app_generator/generate_stuff.rb index fb647bfafc..c52d178186 100644 --- a/example_app_generator/generate_stuff.rb +++ b/example_app_generator/generate_stuff.rb @@ -1,11 +1,11 @@ require 'rspec/rails/feature_check' -module ExampleAppHooks - DEFAULT_SOURCE_PATH = File.expand_path('..', __FILE__) +DEFAULT_SOURCE_PATH = File.expand_path('..', __FILE__) +module ExampleAppHooks module AR def source_paths - [DEFAULT_SOURCE_PATH] + @__source_paths__ ||= [DEFAULT_SOURCE_PATH] end def setup_tasks @@ -19,11 +19,15 @@ def final_tasks run('bin/rake db:migrate RAILS_ENV=test') end end + + def skip_active_record? + false + end end module NoAR def source_paths - [File.join(DEFAULT_SOURCE_PATH, 'no_active_record')] + @__source_paths__ ||= [File.join(DEFAULT_SOURCE_PATH, 'no_active_record')] end def setup_tasks @@ -40,6 +44,10 @@ def setup_tasks def final_tasks copy_file 'spec/verify_no_active_record_spec.rb' end + + def skip_active_record? + true + end end def self.environment_hooks @@ -56,6 +64,14 @@ def generate(*) $?.success? || abort end +def using_source_path(path) + source_paths.unshift path + yield +ensure + # Remove our path munging + source_paths.shift +end + # Generally polluting `main` is bad as it monkey patches all objects. In this # context, `self` is an _instance_ of a `Rails::Generators::AppGenerator`. So # this won't pollute anything. @@ -68,32 +84,7 @@ def generate(*) generate('controller welcome index') # singular generate('integration_test widgets') generate('mailer Notifications signup') -if ::RSpec::Rails::FeatureCheck.has_action_mailer_preview? - create_file "spec/support/default_preview_path.rb", <<-EOS.strip_heredoc - ENV['RAILS_ENV'] = 'development' - - CONFIG_PATH = File.expand_path('../../../config', __FILE__) - APP_PATH = File.expand_path(File.join(CONFIG_PATH, 'application')) - # Default rails setup - require File.join(CONFIG_PATH, 'boot') - require 'rails/all' - require File.join(CONFIG_PATH, 'environment') - - puts Rails.application.config.action_mailer.preview_path - EOS - create_file "spec/verify_mailer_preview_path_spec.rb", <<-EOS.strip_heredoc - RSpec.describe 'Verifying the railtie sets the preview path' do - it 'is set to the rspec path' do - exec_script = File.expand_path( - File.join(__FILE__, '../support/default_preview_path.rb') - ) - expect(%x(ruby #\{exec_script\}).chomp).to eq( - "#\{::Rails.root\}/spec/mailers/previews" - ) - end - end - EOS -end + generate('model thing name:string') generate('helper things') generate('scaffold widget name:string category:string instock:boolean foo_id:integer bar_id:integer --force') @@ -113,6 +104,13 @@ def generate(*) "This is a template for a custom action.", :force => true +# Use the absolute path so we can load it without active record too +apply File.join(DEFAULT_SOURCE_PATH, 'generate_action_mailer_specs.rb') +using_source_path(File.expand_path('..', __FILE__)) do + # rspec-core loads files alphabetically, so we want this to be the first one + copy_file 'spec/__verify_fixture_load_order_spec.rb' +end + gsub_file 'spec/spec_helper.rb', /^=(begin|end)/, '' # Warnings are too noisy in the sample apps diff --git a/example_app_generator/run_specs.rb b/example_app_generator/run_specs.rb index e03a027f88..8df4862d90 100644 --- a/example_app_generator/run_specs.rb +++ b/example_app_generator/run_specs.rb @@ -1,4 +1,8 @@ run('bin/rspec spec -cfdoc') || abort +# Ensure we test the issue in-case this isn't the first spec file loaded +run( + 'bin/rspec --backtrace -cfdoc spec/__verify_fixture_load_order_spec.rb' +) || abort run('bin/rake --backtrace spec') || abort run('bin/rake --backtrace spec:requests') || abort run('bin/rake --backtrace spec:models') || abort diff --git a/example_app_generator/spec/__verify_fixture_load_order_spec.rb b/example_app_generator/spec/__verify_fixture_load_order_spec.rb new file mode 100644 index 0000000000..c6e857628e --- /dev/null +++ b/example_app_generator/spec/__verify_fixture_load_order_spec.rb @@ -0,0 +1,7 @@ +# This spec needs to be run before `rails_helper` is loaded to check the issue +RSpec.describe "Verify issue rspec/rspec-rails#1355" do + it "passes" do + expect(1).to eq 1 + end +end +require 'rails_helper' diff --git a/example_app_generator/spec/support/default_preview_path b/example_app_generator/spec/support/default_preview_path new file mode 100755 index 0000000000..8eae336d7d --- /dev/null +++ b/example_app_generator/spec/support/default_preview_path @@ -0,0 +1,60 @@ +#!/usr/bin/env ruby +# Transparent helper to simply document code sections +def require_file_stub(name) + yield +end + +ENV['RAILS_ENV'] ||= 'development' + +require_file_stub 'config/environment' do + # Load the Rails application. + require_file_stub 'config/application' do + require_file_stub 'config/boot' do + # Set up gems listed in the Gemfile. + ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) + require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) + end + + # Pick the frameworks you want: + require "active_record/railtie" + require "action_controller/railtie" + require "action_mailer/railtie" unless ENV['NO_ACTION_MAILER'] + require "action_view/railtie" + + # Require the gems listed in Gemfile, including any gems + # you've limited to :test, :development, or :production. + if Rails::VERSION::STRING.to_f < 3.1 + Bundler.require(:default, Rails.env) + else + Bundler.require(*Rails.groups) + end + + module ExampleApp + class Application < Rails::Application + config.eager_load = false + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false unless ENV['NO_ACTION_MAILER'] + + if ENV['CUSTOM_PREVIEW_PATH'] + config.action_mailer.preview_path = ENV['CUSTOM_PREVIEW_PATH'] + end + if ENV['SHOW_PREVIEWS'] + config.action_mailer.show_previews = (ENV['SHOW_PREVIEWS'] == 'true') + end + end + end + + I18n.enforce_available_locales = true if I18n.respond_to?(:enforce_available_locales) + end + + # Initialize the Rails application. + Rails.application.initialize! +end + +exit if ENV['NO_ACTION_MAILER'] +if ENV['DEFAULT_URL'] + puts ActionMailer::Base.default_url_options[:host] +elsif defined?(::ActionMailer::Preview) + puts Rails.application.config.action_mailer.preview_path +end diff --git a/example_app_generator/spec/verify_mailer_preview_path_spec.rb b/example_app_generator/spec/verify_mailer_preview_path_spec.rb new file mode 100644 index 0000000000..e554212837 --- /dev/null +++ b/example_app_generator/spec/verify_mailer_preview_path_spec.rb @@ -0,0 +1,183 @@ +require 'rails_helper' +require 'rspec/rails/feature_check' + +RSpec.describe 'Action Mailer railtie hook' do + CaptureExec = Struct.new(:io, :exit_status) do + def ==(str) + io == str + end + end + + def capture_exec(*ops) + io = if RUBY_VERSION.to_f < 1.9 + IO.popen(ops.join(' ')) + else + ops << { :err => [:child, :out] } + IO.popen(ops) + end + # Necessary to ignore warnings from Rails code base + out = io.readlines.reject { |line| + line =~ /warning: circular argument reference/ + }.join.chomp + CaptureExec.new(out, $?.exitstatus) + end + + def have_no_preview + have_attributes(:io => be_blank, :exit_status => 0) + end + + let(:exec_script) { + File.expand_path(File.join(__FILE__, '../support/default_preview_path')) + } + + if RSpec::Rails::FeatureCheck.has_action_mailer_show_preview? + context 'in the development environment' do + let(:custom_env) { { 'RAILS_ENV' => rails_env } } + let(:rails_env) { 'development' } + + it 'sets the preview path to the default rspec path' do + expect(capture_exec(custom_env, exec_script)).to eq( + "#{::Rails.root}/spec/mailers/previews" + ) + end + + it 'respects the setting from `show_previews`' do + expect( + capture_exec( + custom_env.merge('SHOW_PREVIEWS' => 'false'), + exec_script + ) + ).to have_no_preview + end + + it 'respects a custom `preview_path`' do + expect( + capture_exec( + custom_env.merge('CUSTOM_PREVIEW_PATH' => '/custom/path'), + exec_script + ) + ).to eq('/custom/path') + end + + it 'allows initializers to set options' do + expect( + capture_exec( + custom_env.merge('DEFAULT_URL' => 'test-host'), + exec_script + ) + ).to eq('test-host') + end + + it 'handles action mailer not being available' do + expect( + capture_exec( + custom_env.merge('NO_ACTION_MAILER' => 'true'), + exec_script + ) + ).to have_no_preview + end + end + + context 'in a non-development environment' do + let(:custom_env) { { 'RAILS_ENV' => rails_env } } + let(:rails_env) { 'test' } + + it 'does not set the preview path by default' do + expect(capture_exec(custom_env, exec_script)).to have_no_preview + end + + it 'respects the setting from `show_previews`' do + expect( + capture_exec(custom_env.merge('SHOW_PREVIEWS' => 'true'), exec_script) + ).to eq("#{::Rails.root}/spec/mailers/previews") + end + + it 'allows initializers to set options' do + expect( + capture_exec( + custom_env.merge('DEFAULT_URL' => 'test-host'), + exec_script + ) + ).to eq('test-host') + end + + it 'handles action mailer not being available' do + expect( + capture_exec( + custom_env.merge('NO_ACTION_MAILER' => 'true'), + exec_script + ) + ).to have_no_preview + end + end + elsif RSpec::Rails::FeatureCheck.has_action_mailer_preview? + context 'in the development environment', 'without `show_previews`' do + let(:custom_env) { { 'RAILS_ENV' => rails_env } } + let(:rails_env) { 'development' } + + it 'sets the preview path to the default rspec path' do + expect(capture_exec(custom_env, exec_script)).to eq( + "#{::Rails.root}/spec/mailers/previews" + ) + end + + it 'respects a custom `preview_path`' do + expect( + capture_exec( + custom_env.merge('CUSTOM_PREVIEW_PATH' => '/custom/path'), + exec_script + ) + ).to eq('/custom/path') + end + + it 'allows initializers to set options' do + expect( + capture_exec( + custom_env.merge('DEFAULT_URL' => 'test-host'), + exec_script + ) + ).to eq('test-host') + end + + it 'handles action mailer not being available' do + expect( + capture_exec( + custom_env.merge('NO_ACTION_MAILER' => 'true'), + exec_script + ) + ).to have_no_preview + end + end + + context 'in a non-development environment', 'without `show_previews`' do + let(:custom_env) { { 'RAILS_ENV' => rails_env } } + let(:rails_env) { 'test' } + + it 'does not set the preview path by default' do + expect(capture_exec(custom_env, exec_script)).to have_no_preview + end + + it 'respects a custom `preview_path`' do + expect( + capture_exec( + custom_env.merge('CUSTOM_PREVIEW_PATH' => '/custom/path'), + exec_script + ) + ).to eq('/custom/path') + end + + it 'handles action mailer not being available' do + expect( + capture_exec( + custom_env.merge('NO_ACTION_MAILER' => 'true'), + exec_script + ) + ).to have_no_preview + end + end + else + it 'handles no action mailer preview' do + expect(capture_exec(exec_script)).to have_no_preview + end + end +end diff --git a/lib/rspec-rails.rb b/lib/rspec-rails.rb index bbbfc34e8e..4c519deaa0 100644 --- a/lib/rspec-rails.rb +++ b/lib/rspec-rails.rb @@ -23,28 +23,45 @@ class Railtie < ::Rails::Railtie # sets the default for the `preview_path` initializer "rspec_rails.action_mailer", :before => "action_mailer.set_configs" do |app| - if ::RSpec::Rails::FeatureCheck.has_action_mailer_preview? - options = app.config.action_mailer - # Rails 4.1 does not have `show_previews` - if ::ActionMailer::Base.respond_to?(:show_previews=) - options.show_previews ||= ::Rails.env.development? - set_preview_path = options.show_previews - else - set_preview_path = ::Rails.env.development? - end - - if set_preview_path - rspec_preview_path = "#{::Rails.root}/spec/mailers/previews" - config_preview_path = options.preview_path - if config_preview_path.blank? - options.preview_path = rspec_preview_path - elsif config_preview_path != rspec_preview_path - warn "Action Mailer `preview_path` is not the RSpec default. " \ - "Preview path is set to: #{config_preview_path}" - end - end + setup_preview_path(app) + end + + private + + def setup_preview_path(app) + # If the action mailer railtie isn't loaded the config will not respond + return unless supports_action_mailer_previews?(app.config) + options = app.config.action_mailer + config_default_preview_path(options) if config_preview_path?(options) + end + + def config_preview_path?(options) + # This string version check avoids loading the ActionMailer class, as + # would happen using `defined?`. This is necessary because the + # ActionMailer class only loads it's settings once, at load time. If we + # load the class now any settings declared in a config block in an + # initializer will be ignored. + # + # We cannot use `respond_to?(:show_previews)` here as it will always + # return `true`. + if ::Rails::VERSION::STRING < '4.2' + ::Rails.env.development? + elsif options.show_previews.nil? + options.show_previews = ::Rails.env.development? + else + options.show_previews end end + + def config_default_preview_path(options) + return unless options.preview_path.blank? + options.preview_path = "#{::Rails.root}/spec/mailers/previews" + end + + def supports_action_mailer_previews?(config) + config.respond_to?(:action_mailer) && + config.action_mailer.respond_to?(:preview_path) + end end end end diff --git a/lib/rspec/rails/adapters.rb b/lib/rspec/rails/adapters.rb index 3350d51347..4e7fa1a1d3 100644 --- a/lib/rspec/rails/adapters.rb +++ b/lib/rspec/rails/adapters.rb @@ -5,6 +5,19 @@ module RSpec module Rails + def self.disable_testunit_autorun + # `Test::Unit::AutoRunner.need_auto_run=` was introduced to the test-unit + # gem in version 2.4.9. Previous to this version `Test::Unit.run=` was + # used. The implementation of test-unit included with Ruby has neither + # method. + if defined?(Test::Unit::AutoRunner.need_auto_run = ()) + Test::Unit::AutoRunner.need_auto_run = false + elsif defined?(Test::Unit.run = ()) + Test::Unit.run = false + end + end + private_class_method :disable_testunit_autorun + if ::Rails::VERSION::STRING >= '4.1.0' if defined?(Kernel.gem) gem 'minitest' @@ -37,7 +50,7 @@ module Rails # date). If so, we turn the auto runner off. require 'test/unit' require 'test/unit/assertions' - Test::Unit::AutoRunner.need_auto_run = false if defined?(Test::Unit::AutoRunner) + disable_testunit_autorun rescue LoadError => e raise LoadError, <<-ERR.squish, e.backtrace Ruby 2.2+ has removed test/unit from the core library. Rails @@ -62,7 +75,7 @@ module Rails require 'test/unit/assertions' end # Turn off test unit's auto runner for those using the gem - Test::Unit::AutoRunner.need_auto_run = false if defined?(Test::Unit::AutoRunner) + disable_testunit_autorun # Constant aliased to either Minitest or TestUnit, depending on what is # loaded. Assertions = Test::Unit::Assertions diff --git a/lib/rspec/rails/configuration.rb b/lib/rspec/rails/configuration.rb index 9c3e7f63a0..d86f1a308c 100644 --- a/lib/rspec/rails/configuration.rb +++ b/lib/rspec/rails/configuration.rb @@ -56,12 +56,21 @@ def self.initialize_configuration(config) config.add_setting :infer_base_class_for_anonymous_controllers, :default => true # fixture support - config.include RSpec::Rails::FixtureSupport config.add_setting :use_transactional_fixtures, :alias_with => :use_transactional_examples config.add_setting :use_instantiated_fixtures config.add_setting :global_fixtures config.add_setting :fixture_path + # TODO: We'll need to create a deprecated module in order to properly + # report to gems / projects which are relying on this being loaded + # globally. + # + # See rspec/rspec-rails#1355 for history + # + # @deprecated Include `RSpec::Rails::RailsExampleGroup` or + # `RSpec::Rails::FixtureSupport` directly instead + config.include RSpec::Rails::FixtureSupport + # This allows us to expose `render_views` as a config option even though it # breaks the convention of other options by using `render_views` as a # command (i.e. `render_views = true`), where it would normally be used diff --git a/lib/rspec/rails/example/view_example_group.rb b/lib/rspec/rails/example/view_example_group.rb index 6606d1c451..f6d3502aff 100644 --- a/lib/rspec/rails/example/view_example_group.rb +++ b/lib/rspec/rails/example/view_example_group.rb @@ -14,7 +14,7 @@ module ViewExampleGroup module ClassMethods def _default_helper base = metadata[:description].split('/')[0..-2].join('/') - (base.camelize + 'Helper').constantize if base + (base.camelize + 'Helper').constantize unless base.to_s.empty? rescue NameError nil end diff --git a/lib/rspec/rails/feature_check.rb b/lib/rspec/rails/feature_check.rb index 04da0cdd32..6a66d53a69 100644 --- a/lib/rspec/rails/feature_check.rb +++ b/lib/rspec/rails/feature_check.rb @@ -38,6 +38,11 @@ def has_action_mailer_preview? has_action_mailer? && defined?(::ActionMailer::Preview) end + def has_action_mailer_show_preview? + has_action_mailer_preview? && + ::ActionMailer::Base.respond_to?(:show_previews=) + end + def has_1_9_hash_syntax? ::Rails::VERSION::STRING > '4.0' end diff --git a/lib/rspec/rails/version.rb b/lib/rspec/rails/version.rb index 6cb5f79bab..cf977c1ac2 100644 --- a/lib/rspec/rails/version.rb +++ b/lib/rspec/rails/version.rb @@ -3,7 +3,7 @@ module Rails # Version information for RSpec Rails. module Version # Current version of RSpec Rails, in semantic versioning format. - STRING = '3.2.1' + STRING = '3.2.2' end end end diff --git a/spec/rspec/rails/example/view_example_group_spec.rb b/spec/rspec/rails/example/view_example_group_spec.rb index 94e3f2a50e..9dbcd7713d 100644 --- a/spec/rspec/rails/example/view_example_group_spec.rb +++ b/spec/rspec/rails/example/view_example_group_spec.rb @@ -34,6 +34,15 @@ module ::Namespaced; module ThingsHelper; end; end }.not_to raise_error end + it 'operates normally when the view has no path and there is a Helper class defined' do + class ::Helper; end + expect { + RSpec::Core::ExampleGroup.describe 'show.html.erb' do + include ViewExampleGroup + end + }.not_to raise_error + end + context 'application helper exists' do before do if !Object.const_defined? 'ApplicationHelper'