From db045dbbf60b53dbe013ef25554fd013baf88134 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 24 Nov 2004 01:04:44 +0000 Subject: Initial git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/lib/active_record/vendor/simple.rb | 702 ++++++++++++++++++++++++ 1 file changed, 702 insertions(+) create mode 100644 activerecord/lib/active_record/vendor/simple.rb (limited to 'activerecord/lib/active_record/vendor/simple.rb') diff --git a/activerecord/lib/active_record/vendor/simple.rb b/activerecord/lib/active_record/vendor/simple.rb new file mode 100644 index 0000000000..1bd332c882 --- /dev/null +++ b/activerecord/lib/active_record/vendor/simple.rb @@ -0,0 +1,702 @@ +# :title: Transaction::Simple +# +# == Licence +# +# 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. +#-- +# Transaction::Simple +# Simple object transaction support for Ruby +# Version 1.11 +# +# Copyright (c) 2003 Austin Ziegler +# +# $Id: simple.rb,v 1.2 2004/08/20 13:56:37 webster132 Exp $ +# +# ========================================================================== +# Revision History :: +# YYYY.MM.DD Change ID Developer +# Description +# -------------------------------------------------------------------------- +# 2003.07.29 Austin Ziegler +# Added debugging capabilities and VERSION string. +# 2003.08.21 Austin Ziegler +# Added named transactions. +# +# ========================================================================== +#++ +require 'thread' + + # The "Transaction" namespace can be used for additional transactional + # support objects and modules. +module Transaction + + # A standard exception for transactional errors. + class TransactionError < StandardError; end + # A standard exception for transactional errors involving the acquisition + # of locks for Transaction::Simple::ThreadSafe. + class TransactionThreadError < StandardError; end + + # = Transaction::Simple for Ruby + # Simple object transaction support for Ruby + # + # == Introduction + # + # Transaction::Simple provides a generic way to add active transactional + # support to objects. The transaction methods added by this module will + # work with most objects, excluding those that cannot be Marshaled + # (bindings, procedure objects, IO instances, or singleton objects). + # + # The transactions supported by Transaction::Simple are not backed + # transactions; that is, they have nothing to do with any sort of data + # store. They are "live" transactions occurring in memory and in the + # object itself. This is to allow "test" changes to be made to an object + # before making the changes permanent. + # + # Transaction::Simple can handle an "infinite" number of transactional + # levels (limited only by memory). If I open two transactions, commit the + # first, but abort the second, the object will revert to the original + # version. + # + # Transaction::Simple supports "named" transactions, so that multiple + # levels of transactions can be committed, aborted, or rewound by + # referring to the appropriate name of the transaction. Names may be any + # object *except* +nil+. + # + # Copyright:: Copyright © 2003 by Austin Ziegler + # Version:: 1.1 + # Licence:: MIT-Style + # + # Thanks to David Black for help with the initial concept that led to this + # library. + # + # == Usage + # include 'transaction/simple' + # + # v = "Hello, you." # => "Hello, you." + # v.extend(Transaction::Simple) # => "Hello, you." + # + # v.start_transaction # => ... (a Marshal string) + # v.transaction_open? # => true + # v.gsub!(/you/, "world") # => "Hello, world." + # + # v.rewind_transaction # => "Hello, you." + # v.transaction_open? # => true + # + # v.gsub!(/you/, "HAL") # => "Hello, HAL." + # v.abort_transaction # => "Hello, you." + # v.transaction_open? # => false + # + # v.start_transaction # => ... (a Marshal string) + # v.start_transaction # => ... (a Marshal string) + # + # v.transaction_open? # => true + # v.gsub!(/you/, "HAL") # => "Hello, HAL." + # + # v.commit_transaction # => "Hello, HAL." + # v.transaction_open? # => true + # v.abort_transaction # => "Hello, you." + # v.transaction_open? # => false + # + # == Named Transaction Usage + # v = "Hello, you." # => "Hello, you." + # v.extend(Transaction::Simple) # => "Hello, you." + # + # v.start_transaction(:first) # => ... (a Marshal string) + # v.transaction_open? # => true + # v.transaction_open?(:first) # => true + # v.transaction_open?(:second) # => false + # v.gsub!(/you/, "world") # => "Hello, world." + # + # v.start_transaction(:second) # => ... (a Marshal string) + # v.gsub!(/world/, "HAL") # => "Hello, HAL." + # v.rewind_transaction(:first) # => "Hello, you." + # v.transaction_open? # => true + # v.transaction_open?(:first) # => true + # v.transaction_open?(:second) # => false + # + # v.gsub!(/you/, "world") # => "Hello, world." + # v.start_transaction(:second) # => ... (a Marshal string) + # v.gsub!(/world/, "HAL") # => "Hello, HAL." + # v.transaction_name # => :second + # v.abort_transaction(:first) # => "Hello, you." + # v.transaction_open? # => false + # + # v.start_transaction(:first) # => ... (a Marshal string) + # v.gsub!(/you/, "world") # => "Hello, world." + # v.start_transaction(:second) # => ... (a Marshal string) + # v.gsub!(/world/, "HAL") # => "Hello, HAL." + # + # v.commit_transaction(:first) # => "Hello, HAL." + # v.transaction_open? # => false + # + # == Contraindications + # + # While Transaction::Simple is very useful, it has some severe limitations + # that must be understood. Transaction::Simple: + # + # * uses Marshal. Thus, any object which cannot be Marshaled cannot + # use Transaction::Simple. + # * does not manage resources. Resources external to the object and its + # instance variables are not managed at all. However, all instance + # variables and objects "belonging" to those instance variables are + # managed. If there are object reference counts to be handled, + # Transaction::Simple will probably cause problems. + # * is not inherently thread-safe. In the ACID ("atomic, consistent, + # isolated, durable") test, Transaction::Simple provides CD, but it is + # up to the user of Transaction::Simple to provide isolation and + # atomicity. Transactions should be considered "critical sections" in + # multi-threaded applications. If thread safety and atomicity is + # absolutely required, use Transaction::Simple::ThreadSafe, which uses a + # Mutex object to synchronize the accesses on the object during the + # transactional operations. + # * does not necessarily maintain Object#__id__ values on rewind or abort. + # This may change for future versions that will be Ruby 1.8 or better + # *only*. Certain objects that support #replace will maintain + # Object#__id__. + # * Can be a memory hog if you use many levels of transactions on many + # objects. + # + module Simple + VERSION = '1.1.1.0'; + + # Sets the Transaction::Simple debug object. It must respond to #<<. + # Sets the transaction debug object. Debugging will be performed + # automatically if there's a debug object. The generic transaction error + # class. + def self.debug_io=(io) + raise TransactionError, "Transaction Error: the transaction debug object must respond to #<<" unless io.respond_to?(:<<) + @tdi = io + end + + # Returns the Transaction::Simple debug object. It must respond to #<<. + def self.debug_io + @tdi + end + + # If +name+ is +nil+ (default), then returns +true+ if there is + # currently a transaction open. + # + # If +name+ is specified, then returns +true+ if there is currently a + # transaction that responds to +name+ open. + def transaction_open?(name = nil) + if name.nil? + Transaction::Simple.debug_io << "Transaction [#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" unless Transaction::Simple.debug_io.nil? + return (not @__transaction_checkpoint__.nil?) + else + Transaction::Simple.debug_io << "Transaction(#{name.inspect}) [#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" unless Transaction::Simple.debug_io.nil? + return ((not @__transaction_checkpoint__.nil?) and @__transaction_names__.include?(name)) + end + end + + # Returns the current name of the transaction. Transactions not + # explicitly named are named +nil+. + def transaction_name + raise TransactionError, "Transaction Error: No transaction open." if @__transaction_checkpoint__.nil? + Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Transaction Name: #{@__transaction_names__[-1].inspect}\n" unless Transaction::Simple.debug_io.nil? + @__transaction_names__[-1] + end + + # Starts a transaction. Stores the current object state. If a + # transaction name is specified, the transaction will be named. + # Transaction names must be unique. Transaction names of +nil+ will be + # treated as unnamed transactions. + def start_transaction(name = nil) + @__transaction_level__ ||= 0 + @__transaction_names__ ||= [] + + if name.nil? + @__transaction_names__ << nil + s = "" + else + raise TransactionError, "Transaction Error: Named transactions must be unique." if @__transaction_names__.include?(name) + @__transaction_names__ << name + s = "(#{name.inspect})" + end + + @__transaction_level__ += 1 + + Transaction::Simple.debug_io << "#{'>' * @__transaction_level__} Start Transaction#{s}\n" unless Transaction::Simple.debug_io.nil? + + @__transaction_checkpoint__ = Marshal.dump(self) + end + + # Rewinds the transaction. If +name+ is specified, then the intervening + # transactions will be aborted and the named transaction will be + # rewound. Otherwise, only the current transaction is rewound. + def rewind_transaction(name = nil) + raise TransactionError, "Transaction Error: Cannot rewind. There is no current transaction." if @__transaction_checkpoint__.nil? + if name.nil? + __rewind_this_transaction + s = "" + else + raise TransactionError, "Transaction Error: Cannot rewind to transaction #{name.inspect} because it does not exist." unless @__transaction_names__.include?(name) + s = "(#{name})" + + while @__transaction_names__[-1] != name + @__transaction_checkpoint__ = __rewind_this_transaction + Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Rewind Transaction#{s}\n" unless Transaction::Simple.debug_io.nil? + @__transaction_level__ -= 1 + @__transaction_names__.pop + end + __rewind_this_transaction + end + Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Rewind Transaction#{s}\n" unless Transaction::Simple.debug_io.nil? + self + end + + # Aborts the transaction. Resets the object state to what it was before + # the transaction was started and closes the transaction. If +name+ is + # specified, then the intervening transactions and the named transaction + # will be aborted. Otherwise, only the current transaction is aborted. + def abort_transaction(name = nil) + raise TransactionError, "Transaction Error: Cannot abort. There is no current transaction." if @__transaction_checkpoint__.nil? + if name.nil? + __abort_transaction(name) + else + raise TransactionError, "Transaction Error: Cannot abort nonexistant transaction #{name.inspect}." unless @__transaction_names__.include?(name) + + __abort_transaction(name) while @__transaction_names__.include?(name) + end + self + end + + # If +name+ is +nil+ (default), the current transaction level is closed + # out and the changes are committed. + # + # If +name+ is specified and +name+ is in the list of named + # transactions, then all transactions are closed and committed until the + # named transaction is reached. + def commit_transaction(name = nil) + raise TransactionError, "Transaction Error: Cannot commit. There is no current transaction." if @__transaction_checkpoint__.nil? + + if name.nil? + s = "" + __commit_transaction + Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil? + else + raise TransactionError, "Transaction Error: Cannot commit nonexistant transaction #{name.inspect}." unless @__transaction_names__.include?(name) + s = "(#{name})" + + while @__transaction_names__[-1] != name + Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil? + __commit_transaction + end + Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil? + __commit_transaction + end + self + end + + # Alternative method for calling the transaction methods. An optional + # name can be specified for named transaction support. + # + # #transaction(:start):: #start_transaction + # #transaction(:rewind):: #rewind_transaction + # #transaction(:abort):: #abort_transaction + # #transaction(:commit):: #commit_transaction + # #transaction(:name):: #transaction_name + # #transaction:: #transaction_open? + def transaction(action = nil, name = nil) + case action + when :start + start_transaction(name) + when :rewind + rewind_transaction(name) + when :abort + abort_transaction(name) + when :commit + commit_transaction(name) + when :name + transaction_name + when nil + transaction_open?(name) + end + end + + def __abort_transaction(name = nil) #:nodoc: + @__transaction_checkpoint__ = __rewind_this_transaction + + if name.nil? + s = "" + else + s = "(#{name.inspect})" + end + + Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Abort Transaction#{s}\n" unless Transaction::Simple.debug_io.nil? + @__transaction_level__ -= 1 + @__transaction_names__.pop + if @__transaction_level__ < 1 + @__transaction_level__ = 0 + @__transaction_names__ = [] + end + end + + TRANSACTION_CHECKPOINT = "@__transaction_checkpoint__" #:nodoc: + SKIP_TRANSACTION_VARS = [TRANSACTION_CHECKPOINT, "@__transaction_level__"] #:nodoc: + + def __rewind_this_transaction #:nodoc: + r = Marshal.restore(@__transaction_checkpoint__) + + begin + self.replace(r) if respond_to?(:replace) + rescue + nil + end + + r.instance_variables.each do |i| + next if SKIP_TRANSACTION_VARS.include?(i) + if respond_to?(:instance_variable_get) + instance_variable_set(i, r.instance_variable_get(i)) + else + instance_eval(%q|#{i} = r.instance_eval("#{i}")|) + end + end + + if respond_to?(:instance_variable_get) + return r.instance_variable_get(TRANSACTION_CHECKPOINT) + else + return r.instance_eval(TRANSACTION_CHECKPOINT) + end + end + + def __commit_transaction #:nodoc: + if respond_to?(:instance_variable_get) + @__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_variable_get(TRANSACTION_CHECKPOINT) + else + @__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_eval(TRANSACTION_CHECKPOINT) + end + + @__transaction_level__ -= 1 + @__transaction_names__.pop + if @__transaction_level__ < 1 + @__transaction_level__ = 0 + @__transaction_names__ = [] + end + end + + private :__abort_transaction, :__rewind_this_transaction, :__commit_transaction + + # = Transaction::Simple::ThreadSafe + # Thread-safe simple object transaction support for Ruby. + # Transaction::Simple::ThreadSafe is used in the same way as + # Transaction::Simple. Transaction::Simple::ThreadSafe uses a Mutex + # object to ensure atomicity at the cost of performance in threaded + # applications. + # + # Transaction::Simple::ThreadSafe will not wait to obtain a lock; if the + # lock cannot be obtained immediately, a + # Transaction::TransactionThreadError will be raised. + # + # Thanks to Mauricio Fernández for help with getting this part working. + module ThreadSafe + VERSION = '1.1.1.0'; + + include Transaction::Simple + + SKIP_TRANSACTION_VARS = Transaction::Simple::SKIP_TRANSACTION_VARS.dup #:nodoc: + SKIP_TRANSACTION_VARS << "@__transaction_mutex__" + + Transaction::Simple.instance_methods(false) do |meth| + next if meth == "transaction" + arg = "(name = nil)" unless meth == "transaction_name" + module_eval <<-EOS + def #{meth}#{arg} + if (@__transaction_mutex__ ||= Mutex.new).try_lock + result = super + @__transaction_mutex__.unlock + return result + else + raise TransactionThreadError, "Transaction Error: Cannot obtain lock for ##{meth}" + end + ensure + @__transaction_mutex__.unlock + end + EOS + end + end + end +end + +if $0 == __FILE__ + require 'test/unit' + + class Test__Transaction_Simple < Test::Unit::TestCase #:nodoc: + VALUE = "Now is the time for all good men to come to the aid of their country." + + def setup + @value = VALUE.dup + @value.extend(Transaction::Simple) + end + + def test_extended + assert_respond_to(@value, :start_transaction) + end + + def test_started + assert_equal(false, @value.transaction_open?) + assert_nothing_raised { @value.start_transaction } + assert_equal(true, @value.transaction_open?) + end + + def test_rewind + assert_equal(false, @value.transaction_open?) + assert_raises(Transaction::TransactionError) { @value.rewind_transaction } + assert_nothing_raised { @value.start_transaction } + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value.gsub!(/men/, 'women') } + assert_not_equal(VALUE, @value) + assert_nothing_raised { @value.rewind_transaction } + assert_equal(true, @value.transaction_open?) + assert_equal(VALUE, @value) + end + + def test_abort + assert_equal(false, @value.transaction_open?) + assert_raises(Transaction::TransactionError) { @value.abort_transaction } + assert_nothing_raised { @value.start_transaction } + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value.gsub!(/men/, 'women') } + assert_not_equal(VALUE, @value) + assert_nothing_raised { @value.abort_transaction } + assert_equal(false, @value.transaction_open?) + assert_equal(VALUE, @value) + end + + def test_commit + assert_equal(false, @value.transaction_open?) + assert_raises(Transaction::TransactionError) { @value.commit_transaction } + assert_nothing_raised { @value.start_transaction } + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value.gsub!(/men/, 'women') } + assert_not_equal(VALUE, @value) + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value.commit_transaction } + assert_equal(false, @value.transaction_open?) + assert_not_equal(VALUE, @value) + end + + def test_multilevel + assert_equal(false, @value.transaction_open?) + assert_nothing_raised { @value.start_transaction } + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value.gsub!(/men/, 'women') } + assert_equal(VALUE.gsub(/men/, 'women'), @value) + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value.start_transaction } + assert_nothing_raised { @value.gsub!(/country/, 'nation-state') } + assert_nothing_raised { @value.commit_transaction } + assert_equal(VALUE.gsub(/men/, 'women').gsub(/country/, 'nation-state'), @value) + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value.abort_transaction } + assert_equal(VALUE, @value) + end + + def test_multilevel_named + assert_equal(false, @value.transaction_open?) + assert_raises(Transaction::TransactionError) { @value.transaction_name } + assert_nothing_raised { @value.start_transaction(:first) } # 1 + assert_raises(Transaction::TransactionError) { @value.start_transaction(:first) } + assert_equal(true, @value.transaction_open?) + assert_equal(true, @value.transaction_open?(:first)) + assert_equal(:first, @value.transaction_name) + assert_nothing_raised { @value.start_transaction } # 2 + assert_not_equal(:first, @value.transaction_name) + assert_equal(nil, @value.transaction_name) + assert_raises(Transaction::TransactionError) { @value.abort_transaction(:second) } + assert_nothing_raised { @value.abort_transaction(:first) } + assert_equal(false, @value.transaction_open?) + assert_nothing_raised do + @value.start_transaction(:first) + @value.gsub!(/men/, 'women') + @value.start_transaction(:second) + @value.gsub!(/women/, 'people') + @value.start_transaction + @value.gsub!(/people/, 'sentients') + end + assert_nothing_raised { @value.abort_transaction(:second) } + assert_equal(true, @value.transaction_open?(:first)) + assert_equal(VALUE.gsub(/men/, 'women'), @value) + assert_nothing_raised do + @value.start_transaction(:second) + @value.gsub!(/women/, 'people') + @value.start_transaction + @value.gsub!(/people/, 'sentients') + end + assert_raises(Transaction::TransactionError) { @value.rewind_transaction(:foo) } + assert_nothing_raised { @value.rewind_transaction(:second) } + assert_equal(VALUE.gsub(/men/, 'women'), @value) + assert_nothing_raised do + @value.gsub!(/women/, 'people') + @value.start_transaction + @value.gsub!(/people/, 'sentients') + end + assert_raises(Transaction::TransactionError) { @value.commit_transaction(:foo) } + assert_nothing_raised { @value.commit_transaction(:first) } + assert_equal(VALUE.gsub(/men/, 'sentients'), @value) + assert_equal(false, @value.transaction_open?) + end + + def test_array + assert_nothing_raised do + @orig = ["first", "second", "third"] + @value = ["first", "second", "third"] + @value.extend(Transaction::Simple) + end + assert_equal(@orig, @value) + assert_nothing_raised { @value.start_transaction } + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value[1].gsub!(/second/, "fourth") } + assert_not_equal(@orig, @value) + assert_nothing_raised { @value.abort_transaction } + assert_equal(@orig, @value) + end + end + + class Test__Transaction_Simple_ThreadSafe < Test::Unit::TestCase #:nodoc: + VALUE = "Now is the time for all good men to come to the aid of their country." + + def setup + @value = VALUE.dup + @value.extend(Transaction::Simple::ThreadSafe) + end + + def test_extended + assert_respond_to(@value, :start_transaction) + end + + def test_started + assert_equal(false, @value.transaction_open?) + assert_nothing_raised { @value.start_transaction } + assert_equal(true, @value.transaction_open?) + end + + def test_rewind + assert_equal(false, @value.transaction_open?) + assert_raises(Transaction::TransactionError) { @value.rewind_transaction } + assert_nothing_raised { @value.start_transaction } + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value.gsub!(/men/, 'women') } + assert_not_equal(VALUE, @value) + assert_nothing_raised { @value.rewind_transaction } + assert_equal(true, @value.transaction_open?) + assert_equal(VALUE, @value) + end + + def test_abort + assert_equal(false, @value.transaction_open?) + assert_raises(Transaction::TransactionError) { @value.abort_transaction } + assert_nothing_raised { @value.start_transaction } + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value.gsub!(/men/, 'women') } + assert_not_equal(VALUE, @value) + assert_nothing_raised { @value.abort_transaction } + assert_equal(false, @value.transaction_open?) + assert_equal(VALUE, @value) + end + + def test_commit + assert_equal(false, @value.transaction_open?) + assert_raises(Transaction::TransactionError) { @value.commit_transaction } + assert_nothing_raised { @value.start_transaction } + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value.gsub!(/men/, 'women') } + assert_not_equal(VALUE, @value) + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value.commit_transaction } + assert_equal(false, @value.transaction_open?) + assert_not_equal(VALUE, @value) + end + + def test_multilevel + assert_equal(false, @value.transaction_open?) + assert_nothing_raised { @value.start_transaction } + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value.gsub!(/men/, 'women') } + assert_equal(VALUE.gsub(/men/, 'women'), @value) + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value.start_transaction } + assert_nothing_raised { @value.gsub!(/country/, 'nation-state') } + assert_nothing_raised { @value.commit_transaction } + assert_equal(VALUE.gsub(/men/, 'women').gsub(/country/, 'nation-state'), @value) + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value.abort_transaction } + assert_equal(VALUE, @value) + end + + def test_multilevel_named + assert_equal(false, @value.transaction_open?) + assert_raises(Transaction::TransactionError) { @value.transaction_name } + assert_nothing_raised { @value.start_transaction(:first) } # 1 + assert_raises(Transaction::TransactionError) { @value.start_transaction(:first) } + assert_equal(true, @value.transaction_open?) + assert_equal(true, @value.transaction_open?(:first)) + assert_equal(:first, @value.transaction_name) + assert_nothing_raised { @value.start_transaction } # 2 + assert_not_equal(:first, @value.transaction_name) + assert_equal(nil, @value.transaction_name) + assert_raises(Transaction::TransactionError) { @value.abort_transaction(:second) } + assert_nothing_raised { @value.abort_transaction(:first) } + assert_equal(false, @value.transaction_open?) + assert_nothing_raised do + @value.start_transaction(:first) + @value.gsub!(/men/, 'women') + @value.start_transaction(:second) + @value.gsub!(/women/, 'people') + @value.start_transaction + @value.gsub!(/people/, 'sentients') + end + assert_nothing_raised { @value.abort_transaction(:second) } + assert_equal(true, @value.transaction_open?(:first)) + assert_equal(VALUE.gsub(/men/, 'women'), @value) + assert_nothing_raised do + @value.start_transaction(:second) + @value.gsub!(/women/, 'people') + @value.start_transaction + @value.gsub!(/people/, 'sentients') + end + assert_raises(Transaction::TransactionError) { @value.rewind_transaction(:foo) } + assert_nothing_raised { @value.rewind_transaction(:second) } + assert_equal(VALUE.gsub(/men/, 'women'), @value) + assert_nothing_raised do + @value.gsub!(/women/, 'people') + @value.start_transaction + @value.gsub!(/people/, 'sentients') + end + assert_raises(Transaction::TransactionError) { @value.commit_transaction(:foo) } + assert_nothing_raised { @value.commit_transaction(:first) } + assert_equal(VALUE.gsub(/men/, 'sentients'), @value) + assert_equal(false, @value.transaction_open?) + end + + def test_array + assert_nothing_raised do + @orig = ["first", "second", "third"] + @value = ["first", "second", "third"] + @value.extend(Transaction::Simple::ThreadSafe) + end + assert_equal(@orig, @value) + assert_nothing_raised { @value.start_transaction } + assert_equal(true, @value.transaction_open?) + assert_nothing_raised { @value[1].gsub!(/second/, "fourth") } + assert_not_equal(@orig, @value) + assert_nothing_raised { @value.abort_transaction } + assert_equal(@orig, @value) + end + end +end -- cgit v1.2.3