Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit to dm-transactions.

  • Loading branch information...
commit 6fb9d54413ce74c547b2f7824c179b90e7658ab3 0 parents
@snusnu snusnu authored
5 .document
@@ -0,0 +1,5 @@
+README.rdoc
+lib/**/*.rb
+bin/*
+features/**/*.feature
+LICENSE
25 .gitignore
@@ -0,0 +1,25 @@
+## MAC OS
+.DS_Store
+
+## TEXTMATE
+*.tmproj
+tmtags
+
+## EMACS
+*~
+\#*
+.\#*
+
+## VIM
+*.swp
+
+## PROJECT::GENERAL
+coverage
+rdoc
+pkg
+.gem
+
+## PROJECT::SPECIFIC
+.bundle
+spec/db
+
83 Gemfile
@@ -0,0 +1,83 @@
+source 'http://rubygems.org'
+
+group :runtime do
+
+ # We bundle both AS and extlib while extlib compatibility needs to be kept around.
+ # require 'dm-core' will ensure that only one is activated at any time though.
+ # This is done by trying to require AS components and fallback to requiring
+ # extlib in case a LoadError was rescued when requiring AS code.
+ #
+ # Due to bundle exec activating all groups in the Gemfile, it's recommended to run
+ #
+ # bundle install --without quality
+ #
+ # to have a development environment that is able to run the specs. The problem is that
+ # metric_fu activates active_support=2.2.3 if we comment out the gem 'activesupport'
+ # declaration - have a look below for why we would want to do that (and a bit later, for
+ # why that's actually not *strictly* necessary, but recommended)
+ #
+ # To run the specs using AS, leave this Gemfile as it is and just run
+ #
+ # bundle install --without qality
+ # ADAPTERS=sqlite3 bundle exec rake spec # or whatever adapter
+ #
+ # To run the specs using extlib, comment out the: gem 'activesupport' line and run
+ #
+ # bundle install --without quality
+ # ADAPTERS=sqlite3 bundle exec rake spec # or whatever adapter
+ #
+ # If you want to run the quality tasks as provided by metric_fu and related gems,
+ # you have to run
+ #
+ # bundle install
+ # bundle exec rake metrics:all
+ #
+ # Switch back to a bundle without quality gems before trying to run the specs again
+ #
+ # bundle install --without quality
+ # ADAPTERS=sqlite3 bundle exec rake spec # or whatever adapter
+ #
+ # It was mentioned above that all this is not *strictly* necessary, and this is true.
+ # Currently dm-core does the following as the first require when checking for AS
+ #
+ # require 'active_support/core_ext/object/singleton_class'
+ #
+ # Because this method is not present in activesupport <= 3.0.0.beta, dm-core's feature
+ # detection will actually do the "right thing" and fall back to extlib. However, since
+ # this is not the case for all dm-more gems as well, the safest thing to do is to respect
+ # the more tedious workflow for now, as it will at least be guaranteed to work the same
+ # for both dm-core and dm-more.
+ #
+ # Note that this won't be an issue anymore once we dropped support for extlib completely,
+ # or bundler folks decide to support something like "bundle exec --without=foo rake spec"
+ # (which probably is not going to happen anytime soon).
+ #
+
+ if ENV['EXTLIB']
+ gem 'extlib', '~> 0.9.15', :git => 'git://github.com/datamapper/extlib.git'
+ else
+ gem 'activesupport', '~> 3.0.0.beta1', :git => 'git://github.com/rails/rails.git', :require => nil
+ end
+
+ gem 'dm-core', '~> 0.10.3', :git => 'git://github.com/datamapper/dm-core.git'
+
+end
+
+group :development do
+ gem 'rake', '~> 0.8.7'
+ gem 'rspec', '~> 1.3'
+ gem 'yard', '~> 0.5'
+ gem 'rcov', '~> 0.9.7'
+ gem 'jeweler', '~> 1.4'
+ gem 'data_objects', '~> 0.10.1'
+ gem 'do_sqlite3', '~> 0.10.1'
+ gem 'do_mysql', '~> 0.10.1'
+ gem 'do_postgres', '~> 0.10.1'
+end
+
+group :quality do
+ gem 'yardstick', '~> 0.1'
+ gem 'metric_fu', '~> 1.3'
+ gem 'reek', '~> 1.2.7'
+ gem 'roodi', '~> 2.1'
+end
20 LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2009 snusnu
+
+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.
23 README.rdoc
@@ -0,0 +1,23 @@
+= dm-transactions
+
+This gem adds transaction support for datamapper. The currently supported adapters are
+
+* postgres
+* mysql
+* sqlite3
+* oracle
+* sqlserver
+
+== Note on Patches/Pull Requests
+
+* Fork the project.
+* Make your feature addition or bug fix.
+* Add tests for it. This is important so I don't break it in a
+ future version unintentionally.
+* Commit, do not mess with rakefile, version, or history.
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
+* Send me a pull request. Bonus points for topic branches.
+
+== Copyright
+
+Copyright (c) 2010 snusnu. See LICENSE for details.
27 Rakefile
@@ -0,0 +1,27 @@
+require 'rubygems'
+require 'rake'
+
+begin
+
+ require 'jeweler'
+
+ Jeweler::Tasks.new do |gem|
+ gem.name = "dm-transactions"
+ gem.summary = %Q{Adds transaction support to datamapper}
+ gem.description = %Q{Makes transaction support available for adapters that support them}
+ gem.email = "gamsnjaga@gmail.com"
+ gem.homepage = "http://github.com/datamapper/dm-transactions"
+ gem.authors = ["Dirkjan Bussink (dbussink)", "Dan Kubb (dkubb)"]
+
+ gem.add_dependency 'dm-core', '~> 0.10.2'
+ gem.add_development_dependency 'rspec', '~> 1.3'
+
+ end
+
+ Jeweler::GemcutterTasks.new
+
+ FileList['tasks/**/*.rake'].each { |task| import task }
+
+rescue LoadError
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
+end
1  VERSION
@@ -0,0 +1 @@
+0.10.3
64 dm-transactions.gemspec
@@ -0,0 +1,64 @@
+# Generated by jeweler
+# DO NOT EDIT THIS FILE DIRECTLY
+# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
+# -*- encoding: utf-8 -*-
+
+Gem::Specification.new do |s|
+ s.name = %q{dm-transactions}
+ s.version = "0.10.3"
+
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+ s.authors = ["Dirkjan Bussink (dbussink)", "Dan Kubb (dkubb)"]
+ s.date = %q{2010-04-01}
+ s.description = %q{Makes transaction support available for adapters that support them}
+ s.email = %q{gamsnjaga@gmail.com}
+ s.extra_rdoc_files = [
+ "LICENSE",
+ "README.rdoc"
+ ]
+ s.files = [
+ ".document",
+ ".gitignore",
+ "Gemfile",
+ "LICENSE",
+ "README.rdoc",
+ "Rakefile",
+ "VERSION",
+ "lib/dm-transactions.rb",
+ "spec/dm-transactions_spec.rb",
+ "spec/rcov.opts",
+ "spec/spec.opts",
+ "spec/spec_helper.rb",
+ "tasks/ci.rake",
+ "tasks/metrics.rake",
+ "tasks/spec.rake",
+ "tasks/yard.rake",
+ "tasks/yardstick.rake"
+ ]
+ s.homepage = %q{http://github.com/datamapper/dm-transactions}
+ s.rdoc_options = ["--charset=UTF-8"]
+ s.require_paths = ["lib"]
+ s.rubygems_version = %q{1.3.6}
+ s.summary = %q{Adds transaction support to datamapper}
+ s.test_files = [
+ "spec/dm-transactions_spec.rb",
+ "spec/spec_helper.rb"
+ ]
+
+ if s.respond_to? :specification_version then
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
+ s.specification_version = 3
+
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
+ s.add_runtime_dependency(%q<dm-core>, ["~> 0.10.2"])
+ s.add_development_dependency(%q<rspec>, ["~> 1.3"])
+ else
+ s.add_dependency(%q<dm-core>, ["~> 0.10.2"])
+ s.add_dependency(%q<rspec>, ["~> 1.3"])
+ end
+ else
+ s.add_dependency(%q<dm-core>, ["~> 0.10.2"])
+ s.add_dependency(%q<rspec>, ["~> 1.3"])
+ end
+end
+
506 lib/dm-transactions.rb
@@ -0,0 +1,506 @@
+require 'dm-core'
+
+module DataMapper
+ class Transaction
+ extend Chainable
+
+ # @api private
+ attr_accessor :state
+
+ # @api private
+ def none?
+ state == :none
+ end
+
+ # @api private
+ def begin?
+ state == :begin
+ end
+
+ # @api private
+ def rollback?
+ state == :rollback
+ end
+
+ # @api private
+ def commit?
+ state == :commit
+ end
+
+ # Create a new Transaction
+ #
+ # @see Transaction#link
+ #
+ # In fact, it just calls #link with the given arguments at the end of the
+ # constructor.
+ #
+ # @api public
+ def initialize(*things)
+ @transaction_primitives = {}
+ self.state = :none
+ @adapters = {}
+ link(*things)
+ if block_given?
+ warn "Passing block to #{self.class.name}.new is deprecated (#{caller[0]})"
+ commit { |*block_args| yield(*block_args) }
+ end
+ end
+
+ # Associate this Transaction with some things.
+ #
+ # @param [Object] things
+ # the things you want this Transaction associated with:
+ #
+ # Adapters::AbstractAdapter subclasses will be added as
+ # adapters as is.
+ # Arrays will have their elements added.
+ # Repository will have it's own @adapters added.
+ # Resource subclasses will have all the repositories of all
+ # their properties added.
+ # Resource instances will have all repositories of all their
+ # properties added.
+ #
+ # @param [Proc] block
+ # a block (taking one argument, the Transaction) to execute within
+ # this transaction. The transaction will begin and commit around
+ # the block, and rollback if an exception is raised.
+ #
+ # @api private
+ def link(*things)
+ unless none?
+ raise "Illegal state for link: #{state}"
+ end
+
+ things.each do |thing|
+ case thing
+ when DataMapper::Adapters::AbstractAdapter
+ @adapters[thing] = :none
+ when DataMapper::Repository
+ link(thing.adapter)
+ when DataMapper::Model
+ link(*thing.repositories)
+ when DataMapper::Resource
+ link(thing.model)
+ when Array
+ link(*thing)
+ else
+ raise "Unknown argument to #{self.class}#link: #{thing.inspect} (#{thing.class})"
+ end
+ end
+
+ if block_given?
+ commit { |*block_args| yield(*block_args) }
+ else
+ self
+ end
+ end
+
+ # Begin the transaction
+ #
+ # Before #begin is called, the transaction is not valid and can not be used.
+ #
+ # @api private
+ def begin
+ unless none?
+ raise "Illegal state for begin: #{state}"
+ end
+
+ each_adapter(:connect_adapter, [:log_fatal_transaction_breakage])
+ each_adapter(:begin_adapter, [:rollback_and_close_adapter_if_begin, :close_adapter_if_none])
+ self.state = :begin
+ end
+
+ # Commit the transaction
+ #
+ # If no block is given, it will simply commit any changes made since the
+ # Transaction did #begin.
+ #
+ # @param block<Block> a block (taking the one argument, the Transaction) to
+ # execute within this transaction. The transaction will begin and commit
+ # around the block, and roll back if an exception is raised.
+ #
+ # @api private
+ def commit
+ if block_given?
+ unless none?
+ raise "Illegal state for commit with block: #{state}"
+ end
+
+ begin
+ self.begin
+ rval = within { |*block_args| yield(*block_args) }
+ rescue Exception => exception
+ if begin?
+ rollback
+ end
+ raise exception
+ ensure
+ unless exception
+ if begin?
+ commit
+ end
+ return rval
+ end
+ end
+ else
+ unless begin?
+ raise "Illegal state for commit without block: #{state}"
+ end
+ each_adapter(:commit_adapter, [:log_fatal_transaction_breakage])
+ each_adapter(:close_adapter, [:log_fatal_transaction_breakage])
+ self.state = :commit
+ end
+ end
+
+ # Rollback the transaction
+ #
+ # Will undo all changes made during the transaction.
+ #
+ # @api private
+ def rollback
+ unless begin?
+ raise "Illegal state for rollback: #{state}"
+ end
+ each_adapter(:rollback_adapter_if_begin, [:rollback_and_close_adapter_if_begin, :close_adapter_if_none])
+ each_adapter(:close_adapter_if_open, [:log_fatal_transaction_breakage])
+ self.state = :rollback
+ end
+
+ # Execute a block within this Transaction.
+ #
+ # No #begin, #commit or #rollback is performed in #within, but this
+ # Transaction will pushed on the per thread stack of transactions for each
+ # adapter it is associated with, and it will ensures that it will pop the
+ # Transaction away again after the block is finished.
+ #
+ # @param block<Block> the block of code to execute.
+ #
+ # @api private
+ def within
+ unless block_given?
+ raise 'No block provided'
+ end
+
+ unless begin?
+ raise "Illegal state for within: #{state}"
+ end
+
+ adapters = @adapters
+
+ adapters.each_key do |adapter|
+ adapter.push_transaction(self)
+ end
+
+ begin
+ yield self
+ ensure
+ adapters.each_key do |adapter|
+ adapter.pop_transaction
+ end
+ end
+ end
+
+ # @api private
+ def method_missing(method, *args, &block)
+ first_arg = args.first
+
+ return super unless args.size == 1 && first_arg.kind_of?(Adapters::AbstractAdapter)
+ return super unless match = method.to_s.match(/\A(.*)_(if|unless)_(none|begin|rollback|commit)\z/)
+
+ action, condition, expected_state = match.captures
+ return super unless respond_to?(action, true)
+
+ state = state_for(first_arg).to_s
+ execute = (condition == 'if') == (state == expected_state)
+
+ send(action, first_arg) if execute
+ end
+
+ # @api private
+ def primitive_for(adapter)
+ unless @adapters.include?(adapter)
+ raise "Unknown adapter #{adapter}"
+ end
+
+ unless @transaction_primitives.include?(adapter)
+ raise "No primitive for #{adapter}"
+ end
+
+ @transaction_primitives[adapter]
+ end
+
+ private
+
+ # @api private
+ def validate_primitive(primitive)
+ [:close, :begin, :rollback, :commit].each do |meth|
+ unless primitive.respond_to?(meth)
+ raise "Invalid primitive #{primitive}: doesnt respond_to?(#{meth.inspect})"
+ end
+ end
+
+ primitive
+ end
+
+ # @api private
+ def each_adapter(method, on_fail)
+ adapters = @adapters
+ begin
+ adapters.each_key do |adapter|
+ send(method, adapter)
+ end
+ rescue Exception => exception
+ adapters.each_key do |adapter|
+ on_fail.each do |fail_handler|
+ begin
+ send(fail_handler, adapter)
+ rescue Exception => inner_exception
+ DataMapper.logger.fatal("#{self}#each_adapter(#{method.inspect}, #{on_fail.inspect}) failed with #{exception.inspect}: #{exception.backtrace.join("\n")} - and when sending #{fail_handler} to #{adapter} we failed again with #{inner_exception.inspect}: #{inner_exception.backtrace.join("\n")}")
+ end
+ end
+ end
+ raise exception
+ end
+ end
+
+ # @api private
+ def state_for(adapter)
+ unless @adapters.include?(adapter)
+ raise "Unknown adapter #{adapter}"
+ end
+
+ @adapters[adapter]
+ end
+
+ # @api private
+ def do_adapter(adapter, what, prerequisite)
+ unless @transaction_primitives.include?(adapter)
+ raise "No primitive for #{adapter}"
+ end
+
+ state = state_for(adapter)
+
+ unless state == prerequisite
+ raise "Illegal state for #{what}: #{state}"
+ end
+
+ DataMapper.logger.debug("#{adapter.name}: #{what}")
+ @transaction_primitives[adapter].send(what)
+ @adapters[adapter] = what
+ end
+
+ # @api private
+ def log_fatal_transaction_breakage(adapter)
+ DataMapper.logger.fatal("#{self} experienced a totally broken transaction execution. Presenting member #{adapter.inspect}.")
+ end
+
+ # @api private
+ def connect_adapter(adapter)
+ if @transaction_primitives.key?(adapter)
+ raise "Already a primitive for adapter #{adapter}"
+ end
+
+ @transaction_primitives[adapter] = validate_primitive(adapter.transaction_primitive)
+ end
+
+ # @api private
+ def close_adapter_if_open(adapter)
+ if @transaction_primitives.include?(adapter)
+ close_adapter(adapter)
+ end
+ end
+
+ # @api private
+ def close_adapter(adapter)
+ unless @transaction_primitives.include?(adapter)
+ raise 'No primitive for adapter'
+ end
+
+ @transaction_primitives[adapter].close
+ @transaction_primitives.delete(adapter)
+ end
+
+ # @api private
+ def begin_adapter(adapter)
+ do_adapter(adapter, :begin, :none)
+ end
+
+ # @api private
+ def commit_adapter(adapter)
+ do_adapter(adapter, :commit, :begin)
+ end
+
+ # @api private
+ def rollback_adapter(adapter)
+ do_adapter(adapter, :rollback, :begin)
+ end
+
+ # @api private
+ def rollback_and_close_adapter(adapter)
+ rollback_adapter(adapter)
+ close_adapter(adapter)
+ end
+
+ module Adapter
+ extend Chainable
+
+ # @api private
+ def self.included(base)
+ [ :Repository, :Model, :Resource ].each do |name|
+ DataMapper.const_get(name).send(:include, Transaction.const_get(name))
+ end
+ end
+
+ # Produces a fresh transaction primitive for this Adapter
+ #
+ # Used by Transaction to perform its various tasks.
+ #
+ # @return [Object]
+ # a new Object that responds to :close, :begin, :commit,
+ # and :rollback,
+ #
+ # @api private
+ def transaction_primitive
+ DataObjects::Transaction.create_for_uri(normalized_uri)
+ end
+
+ # Pushes the given Transaction onto the per thread Transaction stack so
+ # that everything done by this Adapter is done within the context of said
+ # Transaction.
+ #
+ # @param [Transaction] transaction
+ # a Transaction to be the 'current' transaction until popped.
+ #
+ # @return [Array(Transaction)]
+ # the stack of active transactions for the current thread
+ #
+ # @api private
+ def push_transaction(transaction)
+ transactions << transaction
+ end
+
+ # Pop the 'current' Transaction from the per thread Transaction stack so
+ # that everything done by this Adapter is no longer necessarily within the
+ # context of said Transaction.
+ #
+ # @return [Transaction]
+ # the former 'current' transaction.
+ #
+ # @api private
+ def pop_transaction
+ transactions.pop
+ end
+
+ # Retrieve the current transaction for this Adapter.
+ #
+ # Everything done by this Adapter is done within the context of this
+ # Transaction.
+ #
+ # @return [Transaction]
+ # the 'current' transaction for this Adapter.
+ #
+ # @api private
+ def current_transaction
+ transactions.last
+ end
+
+ chainable do
+ protected
+
+ # @api semipublic
+ def open_connection
+ current_connection || super
+ end
+
+ # @api semipublic
+ def close_connection(connection)
+ super unless current_connection.equal?(connection)
+ end
+ end
+
+ private
+
+ # @api private
+ def transactions
+ Thread.current[:dm_transactions] ||= []
+ end
+
+ # Retrieve the current connection for this Adapter.
+ #
+ # @return [Transaction]
+ # the 'current' connection for this Adapter.
+ #
+ # @api private
+ def current_connection
+ if transaction = current_transaction
+ transaction.primitive_for(self).connection
+ end
+ end
+ end # module Adapter
+
+ # alias the MySQL, PostgreSQL, Sqlite3 and Oracle adapters to use transactions
+ MysqlAdapter = PostgresAdapter = Sqlite3Adapter = OracleAdapter = SqlserverAdapter = Adapter
+
+ module Repository
+
+ # Produce a new Transaction for this Repository
+ #
+ # @return [Adapters::Transaction]
+ # a new Transaction (in state :none) that can be used
+ # to execute code #with_transaction
+ #
+ # @api public
+ def transaction
+ Transaction.new(self)
+ end
+ end # module Repository
+
+ module Model
+ # @api private
+ def self.included(mod)
+ mod.descendants.each { |model| model.extend self }
+ end
+
+ # Produce a new Transaction for this Resource class
+ #
+ # @return <Adapters::Transaction
+ # a new Adapters::Transaction with all Repositories
+ # of the class of this Resource added.
+ #
+ # @api public
+ def transaction
+ transaction = Transaction.new(self)
+ transaction.commit { |block_args| yield(*block_args) }
+ end
+ end # module Model
+
+ module Resource
+
+ # Produce a new Transaction for the class of this Resource
+ #
+ # @return [Adapters::Transaction]
+ # a new Adapters::Transaction for the Repository
+ # of the class of this Resource added.
+ #
+ # @api public
+ def transaction
+ model.transaction { |*block_args| yield(*block_args) }
+ end
+ end # module Resource
+ end # class Transaction
+
+ module Adapters
+ extendable do
+
+ # @api private
+ def const_added(const_name)
+ if Transaction.const_defined?(const_name)
+ adapter = const_get(const_name)
+ adapter.send(:include, Transaction.const_get(const_name))
+ end
+
+ super
+ end
+ end
+ end # module Adapters
+end # module DataMapper
153 spec/dm-transactions_spec.rb
@@ -0,0 +1,153 @@
+require 'spec_helper'
+
+describe DataMapper::Resource, 'Transactions' do
+ before :all do
+ module ::Blog
+ class User
+ include DataMapper::Resource
+
+ property :name, String, :key => true
+ property :age, Integer
+ property :summary, Text
+ property :description, Text
+ property :admin, Boolean, :accessor => :private
+
+ belongs_to :parent, self, :required => false
+ has n, :children, self, :inverse => :parent
+
+ belongs_to :referrer, self, :required => false
+ has n, :comments
+
+ # FIXME: figure out a different approach than stubbing things out
+ def comment=(*)
+ # do nothing with comment
+ end
+ end
+
+ class Author < User; end
+
+ class Comment
+ include DataMapper::Resource
+
+ property :id, Serial
+ property :body, Text
+
+ belongs_to :user
+ end
+
+ class Article
+ include DataMapper::Resource
+
+ property :id, Serial
+ property :body, Text
+
+ has n, :paragraphs
+ end
+
+ class Paragraph
+ include DataMapper::Resource
+
+ property :id, Serial
+ property :text, String
+
+ belongs_to :article
+ end
+ end
+
+ class ::Default
+ include DataMapper::Resource
+
+ property :name, String, :key => true, :default => 'a default value'
+ end
+
+ @user_model = Blog::User
+ @author_model = Blog::Author
+ @comment_model = Blog::Comment
+ @article_model = Blog::Article
+ @paragraph_model = Blog::Paragraph
+ end
+
+ supported_by :postgres, :mysql, :sqlite3, :oracle, :sqlserver do
+ before :all do
+ user = @user_model.create(:name => 'dbussink', :age => 25, :description => 'Test')
+
+ @user = @user_model.get(*user.key)
+ end
+
+ before do
+ # --- Temporary private api use to get around rspec limitations ---
+ @repository.scope do |repository|
+ transaction = DataMapper::Transaction.new(repository)
+ transaction.begin
+ repository.adapter.push_transaction(transaction)
+ end
+ end
+
+ after do
+ while @repository.adapter.current_transaction
+ @repository.adapter.pop_transaction.rollback
+ end
+ end
+
+ it_should_behave_like 'A public Resource'
+ it_should_behave_like 'A Resource supporting Strategic Eager Loading'
+ end
+
+ supported_by :postgres, :mysql, :sqlite3, :oracle, :sqlserver do
+ describe '#transaction' do
+ before do
+ @user_model.all.destroy!
+ end
+
+ it 'should have access to resources presisted before the transaction' do
+ @user_model.create(:name => 'carllerche')
+ @user_model.transaction do
+ @user_model.first.name.should == 'carllerche'
+ end
+ end
+
+ it 'should have access to resources persisted in the transaction' do
+ @user_model.transaction do
+ @user_model.create(:name => 'carllerche')
+ @user_model.first.name.should == 'carllerche'
+ end
+ end
+
+ it 'should not mark saved resources as new records' do
+ @user_model.transaction do
+ @user_model.create(:name => 'carllerche').should_not be_new
+ end
+ end
+
+ it 'should rollback when an error is thrown in a transaction' do
+ @user_model.all.should have(0).entries
+ lambda {
+ @user_model.transaction do
+ @user_model.create(:name => 'carllerche')
+ raise 'I love coffee'
+ end
+ }.should raise_error('I love coffee')
+ @user_model.all.should have(0).entries
+ end
+
+ it 'should close the transaction if return is called within the closure' do
+ @txn = nil
+
+ def doit
+ @user_model.transaction do
+ @txn = Thread.current[:dm_transactions].last
+ return
+ end
+ end
+ doit
+
+ @txn.instance_variable_get(:@state).should == :commit
+ @txn = nil
+ end
+
+ it 'should return the last statement in the transaction block' do
+ @user_model.transaction { 1 }.should == 1
+ end
+ end
+ end
+end
6 spec/rcov.opts
@@ -0,0 +1,6 @@
+--exclude "spec"
+--sort coverage
+--callsites
+--xrefs
+--profile
+--text-summary
1  spec/spec.opts
@@ -0,0 +1 @@
+--color
26 spec/spec_helper.rb
@@ -0,0 +1,26 @@
+require 'dm-transactions'
+
+require 'dm-core/spec/lib/counter_adapter'
+require 'dm-core/spec/lib/pending_helpers'
+require 'dm-core/spec/lib/adapter_helpers'
+
+require 'dm-core/spec/resource_shared_spec'
+require 'dm-core/spec/sel_shared_spec'
+
+ENV['ADAPTERS'] ||= 'sqlite3'
+
+# create sqlite3_fs directory if it doesn't exist
+temp_db_dir = Pathname(File.expand_path('../db', __FILE__))
+temp_db_dir.mkpath
+
+DataMapper::Spec::AdapterHelpers.temp_db_dir = temp_db_dir
+
+adapters = ENV['ADAPTERS'].split(' ').map { |adapter_name| adapter_name.strip.downcase }.uniq
+adapters = DataMapper::Spec::AdapterHelpers.primary_adapters.keys if adapters.include?('all')
+
+DataMapper::Spec::AdapterHelpers.setup_adapters(adapters)
+
+Spec::Runner.configure do |config|
+ config.extend(DataMapper::Spec::AdapterHelpers)
+ config.include(DataMapper::Spec::PendingHelpers)
+end
1  tasks/ci.rake
@@ -0,0 +1 @@
+task :ci => [ :verify_measurements, 'metrics:all' ]
36 tasks/metrics.rake
@@ -0,0 +1,36 @@
+begin
+ require 'metric_fu'
+rescue LoadError
+ namespace :metrics do
+ task :all do
+ abort 'metric_fu is not available. In order to run metrics:all, you must: gem install metric_fu'
+ end
+ end
+end
+
+begin
+ require 'reek/adapters/rake_task'
+
+ Reek::RakeTask.new do |t|
+ t.fail_on_error = true
+ t.verbose = false
+ t.source_files = 'lib/**/*.rb'
+ end
+rescue LoadError
+ task :reek do
+ abort 'Reek is not available. In order to run reek, you must: gem install reek'
+ end
+end
+
+begin
+ require 'roodi'
+ require 'roodi_task'
+
+ RoodiTask.new do |t|
+ t.verbose = false
+ end
+rescue LoadError
+ task :roodi do
+ abort 'Roodi is not available. In order to run roodi, you must: gem install roodi'
+ end
+end
38 tasks/spec.rake
@@ -0,0 +1,38 @@
+spec_defaults = lambda do |spec|
+ spec.pattern = 'spec/**/*_spec.rb'
+ spec.libs << 'lib' << 'spec'
+ spec.spec_opts << '--options' << 'spec/spec.opts'
+end
+
+begin
+ require 'spec/rake/spectask'
+
+ Spec::Rake::SpecTask.new(:spec, &spec_defaults)
+rescue LoadError
+ task :spec do
+ abort 'rspec is not available. In order to run spec, you must: gem install rspec'
+ end
+end
+
+begin
+ require 'rcov'
+ require 'spec/rake/verify_rcov'
+
+ Spec::Rake::SpecTask.new(:rcov) do |rcov|
+ spec_defaults.call(rcov)
+ rcov.rcov = true
+ rcov.rcov_opts = File.read('spec/rcov.opts').split(/\s+/)
+ end
+
+ RCov::VerifyTask.new(:verify_rcov => :rcov) do |rcov|
+ rcov.threshold = 100
+ end
+rescue LoadError
+ %w[ rcov verify_rcov ].each do |name|
+ task name do
+ abort "rcov is not available. In order to run #{name}, you must: gem install rcov"
+ end
+ end
+end
+
+task :default => :spec
9 tasks/yard.rake
@@ -0,0 +1,9 @@
+begin
+ require 'yard'
+
+ YARD::Rake::YardocTask.new
+rescue LoadError
+ task :yard do
+ abort 'YARD is not available. In order to run yard, you must: gem install yard'
+ end
+end
19 tasks/yardstick.rake
@@ -0,0 +1,19 @@
+begin
+ require 'pathname'
+ require 'yardstick/rake/measurement'
+ require 'yardstick/rake/verify'
+
+ # yardstick_measure task
+ Yardstick::Rake::Measurement.new
+
+ # verify_measurements task
+ Yardstick::Rake::Verify.new do |verify|
+ verify.threshold = 100
+ end
+rescue LoadError
+ %w[ yardstick_measure verify_measurements ].each do |name|
+ task name.to_s do
+ abort "Yardstick is not available. In order to run #{name}, you must: gem install yardstick"
+ end
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.