Repository: grosser/test_after_commit Branch: master Commit: a3cbb3624b0e Files: 17 Total size: 19.6 KB Directory structure: gitextract_de18ncmi/ ├── .travis.yml ├── Gemfile ├── MIT-LICENSE ├── Rakefile ├── Readme.md ├── gemfiles/ │ ├── 32.gemfile │ ├── 40.gemfile │ ├── 41.gemfile │ └── 42.gemfile ├── lib/ │ ├── test_after_commit/ │ │ ├── database_statements.rb │ │ ├── version.rb │ │ └── with_transaction_state.rb │ └── test_after_commit.rb ├── spec/ │ ├── database.rb │ ├── spec_helper.rb │ └── test_after_commit_spec.rb └── test_after_commit.gemspec ================================================ FILE CONTENTS ================================================ ================================================ FILE: .travis.yml ================================================ language: ruby sudo: false cache: bundler bundler_args: '--path vendor/bundle' gemfile: - gemfiles/32.gemfile - gemfiles/40.gemfile - gemfiles/41.gemfile - gemfiles/42.gemfile rvm: - 2.0.0 - 2.1.8 - 2.2.4 env: - REAL=1 - TEST=1 script: bundle exec rake spec ================================================ FILE: Gemfile ================================================ source "https://rubygems.org" gemspec gem 'rails-observers' ================================================ FILE: MIT-LICENSE ================================================ Copyright (c) 2014 Michael Grosser MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Rakefile ================================================ require 'bundler/setup' require 'bundler/gem_tasks' require 'bump/tasks' require 'wwtd/tasks' task :spec do sh "rspec spec/" end task :default => "wwtd:local" ================================================ FILE: Readme.md ================================================ Make after_commit callbacks fire in tests for Rails 3+ with transactional_fixtures = true. **Deprecation** this is no longer needed on rails 5.0+ https://github.com/rails/rails/pull/18458 Install ======= gem install test_after_commit # Gemfile (never include in :development !) gem 'test_after_commit', :group => :test Usage ===== Test that the methods get called or the side-effect of the methods, something like: ```Ruby class Car < ActiveRecord::Base after_commit :foo, :on => :update def foo $foo = 1 end end ... it "sets $foo on commit" do $foo.should == nil car.save! $foo.should == 1 end ``` ### Temporary disable after commit hooks In your test_helper, you can specify the default ``` TestAfterCommit.enabled = true ``` Then use blocks in your tests to change the behavior: ``` TestAfterCommit.with_commits(true) do my_tests end TestAfterCommit.with_commits(false) do my_tests end ``` TIPS ==== - hooks do not re-raise errors (with or without this gem) use [after_commit_exception_notification](https://github.com/grosser/after_commit_exception_notification) Author ====== Inspired by https://gist.github.com/1305285 ### [Contributors](https://github.com/grosser/test_after_commit/contributors) - [James Le Cuirot](https://github.com/chewi) - [emirose](https://github.com/emirose) - [Brad Gessler](https://github.com/bradgessler) - [Rohan McGovern](https://github.com/rohanpm) - [lsylvester](https://github.com/lsylvester) - [Tony Novak](https://github.com/afn) - [Brian Palmer](https://github.com/codekitchen) - [Oleg Dashevskii](https://github.com/be9) - [Jonathan Spies](https://github.com/jspies) - [Nick Sieger](https://github.com/nicksieger) [Michael Grosser](http://grosser.it)
michael@grosser.it
License: MIT
[![Build Status](https://travis-ci.org/grosser/test_after_commit.png)](https://travis-ci.org/grosser/test_after_commit) ================================================ FILE: gemfiles/32.gemfile ================================================ source "https://rubygems.org" gemspec :path=>"../" gem "activerecord", "~> 3.2.18" gem "i18n", "~> 0.6.9" # 0.7 does not support 1.9 gem "bump", "0.5.2" # after that it's 1.9.3 only ================================================ FILE: gemfiles/40.gemfile ================================================ source "https://rubygems.org" gemspec :path=>"../" gem "activerecord", "~> 4.0.6" gem "rails-observers" ================================================ FILE: gemfiles/41.gemfile ================================================ source "https://rubygems.org" gemspec :path=>"../" gem "activerecord", "~> 4.1.2" gem "rails-observers" ================================================ FILE: gemfiles/42.gemfile ================================================ source "https://rubygems.org" gemspec :path=>"../" gem "activerecord", "~> 4.2.0.beta1" gem "rails-observers" ================================================ FILE: lib/test_after_commit/database_statements.rb ================================================ module TestAfterCommit::DatabaseStatements def transaction(*) @test_open_transactions ||= 0 skip_emulation = ActiveRecord::Base.connection.open_transactions.zero? run_callbacks = false result = nil rolled_back = false super do begin @test_open_transactions += 1 if ActiveRecord::VERSION::MAJOR == 3 @_current_transaction_records.push([]) if @_current_transaction_records.empty? end result = yield rescue Exception rolled_back = true raise ensure @test_open_transactions -= 1 if @test_open_transactions == 0 && !rolled_back && !skip_emulation if TestAfterCommit.enabled run_callbacks = true elsif ActiveRecord::VERSION::MAJOR == 3 @_current_transaction_records.clear end end end end ensure test_commit_records if run_callbacks result end def test_commit_records if ActiveRecord::VERSION::MAJOR == 3 commit_transaction_records else # To avoid an infinite loop, we need to copy the transaction locally, and clear out # `records` on the copy that stays in the AR stack. Otherwise new # transactions inside a commit callback will cause an infinite loop. # # This is because we're re-using the transaction on the stack, before # it's been popped off and re-created by the AR code. original = @transaction || @transaction_manager.current_transaction return unless original.respond_to?(:records) transaction = original.dup transaction.instance_variable_set(:@records, transaction.records.dup) # deep clone of records array original.records.clear # so that this clear doesn't clear out both copies transaction.commit_records end end end ================================================ FILE: lib/test_after_commit/version.rb ================================================ module TestAfterCommit VERSION = '1.2.2' end ================================================ FILE: lib/test_after_commit/with_transaction_state.rb ================================================ # disable parts of the sync code that starts looping module TestAfterCommit module WithTransactionState def sync_with_transaction_state @reflects_state ||= [] @reflects_state[0] = true super end end end ================================================ FILE: lib/test_after_commit.rb ================================================ require 'test_after_commit/version' if ActiveRecord::VERSION::MAJOR >= 5 raise 'after_commit testing is baked into rails 5, you no longer need test_after_commit gem' end if ActiveRecord::VERSION::MAJOR >= 4 require 'test_after_commit/with_transaction_state' ActiveRecord::Base.send(:prepend, TestAfterCommit::WithTransactionState) end require 'test_after_commit/database_statements' ActiveRecord::ConnectionAdapters::AbstractAdapter.send(:prepend, TestAfterCommit::DatabaseStatements) module TestAfterCommit @enabled = true class << self attr_accessor :enabled def with_commits(value = true) old = enabled self.enabled = value yield ensure self.enabled = old end end end ================================================ FILE: spec/database.rb ================================================ # setup database require 'active_record' if ActiveRecord::VERSION::MAJOR > 3 require "rails/observers/activerecord/active_record" end if ActiveRecord::VERSION::STRING >= "4.2.0" ActiveRecord::Base.raise_in_transactional_callbacks = true end ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ':memory:' ) ActiveRecord::Migration.verbose = false ActiveRecord::Schema.define(:version => 1) do create_table "cars", :force => true do |t| t.integer :counter, :default => 0, :null => false t.integer :car_id t.timestamps :null => false end create_table "addresses", :force => true do |t| t.integer :number_of_residents, :default => 0, :null => false t.timestamps :null => false end create_table "people", :force => true do |t| t.belongs_to :address t.timestamps :null => false end create_table "fu_bears", :force => true do |t| t.string :name t.timestamps :null => false end end module Called def called(x=nil) @called ||= [] if x @called << x else @called end end end class Car < ActiveRecord::Base extend Called has_many :cars after_commit :simple_after_commit after_commit :simple_after_commit_on_create, :on => :create after_commit :save_once, :on => :create, :if => :do_after_create_save after_commit :simple_after_commit_on_update, :on => :update after_commit :maybe_raise_errors after_commit :save_open_transactions_count after_save :trigger_rollback attr_accessor :make_rollback, :raise_error, :do_after_create_save def trigger_rollback raise ActiveRecord::Rollback if make_rollback end def self.returning_method_with_transaction Car.transaction do return Car.create end end attr_reader :open_transactions private def save_open_transactions_count @open_transactions = ActiveRecord::Base.connection.open_transactions end def save_once update_attributes(:counter => 3) unless counter == 3 self.class.called :save_once end def maybe_raise_errors if raise_error # puts "MAYBE RAISE" # just debugging, but it really does not work ... raise "Expected error" end end def simple_after_commit self.class.called :always end def simple_after_commit_on_create self.class.called :create end def simple_after_commit_on_update self.class.called :update end end class CarObserver < ActiveRecord::Observer cattr_accessor :recording cattr_accessor :callback [:after_commit, :after_rollback].each do |action| define_method action do |record| return unless recording Car.called << "observed_#{action}".to_sym Untracked.create! callback.call() if callback end end end Car.observers = :car_observer Car.instantiate_observers class Bar < ActiveRecord::Base self.table_name = "cars" has_many :bars, :foreign_key => :car_id end class MultiBar < ActiveRecord::Base extend Called self.table_name = "cars" after_commit :one, :on => :create after_commit :two, :on => :create def one self.class.called << :one end def two self.class.called << :two end end class Address < ActiveRecord::Base has_many :people after_commit :create_residents, :on => :create def create_residents if ActiveRecord::VERSION::MAJOR == 3 # stupid hack because nested after_commit is broken on rails 3 and loops return if @create_residents @create_residents = true end Person.create!(:address => self) Person.create!(:address => self) end end class Person < ActiveRecord::Base belongs_to :address after_commit :update_number_of_residents_on_address, :on => :create def update_number_of_residents_on_address address.update_attributes(:number_of_residents => address.number_of_residents + 1) end end class Untracked < ActiveRecord::Base self.table_name = "cars" end class FuBear < ActiveRecord::Base extend Called self.table_name = "fu_bears" validates_presence_of :name after_commit :simple_after_commit def simple_after_commit self.class.called :always end end ================================================ FILE: spec/spec_helper.rb ================================================ require 'bundler/setup' require File.expand_path '../database', __FILE__ I18n.enforce_available_locales = false def rails3? ActiveRecord::VERSION::MAJOR == 3 end def rails4? ActiveRecord::VERSION::MAJOR >= 4 end def rails42? rails4? && ActiveRecord::VERSION::MINOR >= 2 end require 'test_after_commit' if ENV['REAL'] puts 'using real transactions' TestAfterCommit.enabled = false end module ConnectionFinder def connection @connection ||= if rails4? ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection).first else ActiveRecord::Base.connection_handler.connection_pools.values.map(&:connection).first end end end RSpec.configure do |config| config.include ConnectionFinder unless ENV['REAL'] config.around do |example| # open a transaction without using .transaction as activerecord use_transactional_fixtures does if ActiveRecord::VERSION::MAJOR > 3 connection.begin_transaction :joinable => false else connection.increment_open_transactions connection.transaction_joinable = false connection.begin_db_transaction end example.call connection.rollback_db_transaction if ActiveRecord::VERSION::MAJOR == 3 connection.decrement_open_transactions end end end config.expect_with(:rspec) { |c| c.syntax = :should } config.mock_with(:rspec) { |c| c.syntax = :should } end ================================================ FILE: spec/test_after_commit_spec.rb ================================================ require 'spec_helper' describe TestAfterCommit do before do CarObserver.recording = false Car.called.clear end after do TestAfterCommit.enabled = true unless ENV["REAL"] end it "has a VERSION" do TestAfterCommit::VERSION.should =~ /^[\.\da-z]+$/ end it "fires on create" do Car.create Car.called.should == [:create, :always] end it "runs callback outside of transaction" do car = Car.create car.open_transactions.should == ActiveRecord::Base.connection.open_transactions end it "works outside of transaction" do car = described_class.with_commits(true) { Car.create } car.destroy end if ENV["REAL"] it "fires on update" do car = Car.create Car.called.clear car.save! Car.called.should == [:update, :always] end it "fires on update_attribute" do car = Car.create Car.called.clear car.update_attribute :counter, 123 Car.called.should == [:update, :always] end it "does not fire on rollback" do car = Car.new car.make_rollback = true car.save.should == nil Car.called.should == [] end it "does not fire on ActiveRecord::RecordInvalid" do lambda { FuBear.create! }.should raise_exception(ActiveRecord::RecordInvalid) FuBear.called.should == [] end it "does not fire multiple times in nested transactions" do Car.transaction do Car.transaction do Car.create! Car.called.should == [] end Car.called.should == [] end Car.called.should == [:create, :always] end it "fires when transaction block returns from method" do Car.returning_method_with_transaction Car.called.should == [:create, :always] end if rails42? it "raises errors" do car = Car.new car.raise_error = true lambda { car.save! }.should raise_error(RuntimeError) end else it "does not raises errors" do car = Car.new car.raise_error = true car.save! end end if rails42? context "with config.active_record.raise_in_transactional_callbacks" do around do |test| old = ActiveRecord::Base.raise_in_transactional_callbacks ActiveRecord::Base.raise_in_transactional_callbacks = true begin test.call ensure ActiveRecord::Base.raise_in_transactional_callbacks = old end end it "keeps working after an exception is raised" do car = Car.new car.raise_error = true lambda { car.save! }.should raise_error(RuntimeError) car = Car.new car.save! Car.called.should include(:always) end end end it "can do 1 save in after_commit" do car = Car.new car.do_after_create_save = true car.save! expected = if rails4? [:save_once, :create, :always, :save_once, :always] else [:save_once, :create, :always, :save_once, :create, :always] end Car.called.should == expected car.counter.should == 3 end it "returns on create and on create of associations" do Car.create!.class.should == Car Car.create!.cars.create.class.should == Car unless rails4? end it "returns on create and on create of associations without after_commit" do Bar.create!.class.should == Bar Bar.create!.bars.create.class.should == Bar unless rails4? end it "calls callbacks in correct order" do MultiBar.create! MultiBar.called.should == [:two, :one] end context "Observer" do before do CarObserver.recording = true end it "should record commits" do Car.transaction do Car.create end Car.called.should == [:observed_after_commit, :create, :always] end it "should record rollbacks caused by ActiveRecord::Rollback" do Car.transaction do Car.create raise ActiveRecord::Rollback end Car.called.should == [:observed_after_rollback] end it "should record rollbacks caused by any type of exception" do begin Car.transaction do car = Car.create raise Exception, 'simulated error' end rescue Exception => e e.message.should == 'simulated error' end Car.called.should == [:observed_after_rollback] end it "should see the correct number of open transactions during callbacks" do skip if ENV["REAL"] begin open_txn = nil CarObserver.callback = proc { open_txn = Car.connection.instance_variable_get(:@test_open_transactions) } Car.transaction do Car.create end open_txn.should == 0 ensure CarObserver.callback = nil end end end context "block behavior" do it "does not fire if turned off" do TestAfterCommit.enabled = false Car.create Car.called.should == [] end it "always fires with when enabled by a block" do TestAfterCommit.enabled = false TestAfterCommit.with_commits(true) do Car.create Car.called.should == [:create, :always] end end it "defaults to with commits" do TestAfterCommit.with_commits do Car.create Car.called.should == [:create, :always] end end it "does not fire with without commits" do TestAfterCommit.with_commits(false) do Car.create Car.called.should == [] end end end unless ENV["REAL"] context "nested after_commit" do it 'is executed' do skip if rails4? # infinite loop in REAL and fails in TEST and lots of noise when left as pending @address = Address.create! lambda { Person.create!(:address => @address) }.should change(@address, :number_of_residents).by(1) # one from the line above and two from the after_commit @address.people.count.should == 3 @address.number_of_residents.should == 3 end end end if rails3? && !ENV["REAL"] describe TestAfterCommit, "with mixed TAC enabled specs" do before do TestAfterCommit.enabled = false Car.called.clear end context "and a test with TAC disabled" do it "creates a record" do Car.new.save! Car.called.should == [] end it "verifies that records are empty before each test 1" do connection.instance_variable_get(:@_current_transaction_records).should be_empty end end context "and a test with TAC enabled" do before { TestAfterCommit.enabled = true } it "creates a record and fires commit callbacks" do Car.new.save! Car.called.should == [:create, :always] end it "verifies that records are empty before each test 2" do connection.instance_variable_get(:@_current_transaction_records).should be_empty end end end end ================================================ FILE: test_after_commit.gemspec ================================================ name = "test_after_commit" require "./lib/#{name}/version" Gem::Specification.new name, TestAfterCommit::VERSION do |s| s.summary = "makes after_commit callbacks testable in Rails 3+ with transactional_fixtures" s.authors = ["Michael Grosser"] s.email = "michael@grosser.it" s.homepage = "https://github.com/grosser/#{name}" s.files = `git ls-files lib Readme.md MIT-LICENSE`.split("\n") s.license = 'MIT' s.required_ruby_version = '>= 2.0.0' s.add_runtime_dependency "activerecord", [">= 3.2", "< 5.0"] s.add_development_dependency "wwtd" s.add_development_dependency "bump" s.add_development_dependency "rake" s.add_development_dependency "sqlite3" s.add_development_dependency "rspec" end