Attached please find a patch that is r4747 of the coverage-poker-network
branch.  I plan to commit this patch to trunk tomorrow around 09:00
US/Eastern.

It fixes a few minor bugs in pokercashier.py, and adds a number of tests
to test-pokercashier.py.in using database mock-ups.  There is also a
full coverage set for the cashier locking mechanism.

Please tell me via email, on list, or on #pokersource if you have
concerns about committing this patch to trunk.

This patch is licensed under AGPLv3-or-later.

diff --git a/poker-network/ChangeLog b/poker-network/ChangeLog
index 5035c6c..b720675 100644
--- a/poker-network/ChangeLog
+++ b/poker-network/ChangeLog
@@ -1,3 +1,66 @@
+2008-10-19  Bradley M. Kuhn  <[EMAIL PROTECTED]>
+
+	* tests/run.in (DEFAULT_COVERAGE_FILES): Added
+	../pokernetwork/pokercashier.py
+
+	* tests/test-pokercashier.py.in (PokerCashierLockUnlockTestCase):
+	Created class.
+	(PokerCashierLockUnlockTestCase.test01_unlockNonExistent): Wrote
+	test.
+	(PokerCashierLockUnlockTestCase.test02_lockCreateTwice): Wrote
+	test.
+	(PokerCashierLockUnlockTestCase.test04_unlockTwice): Wrote test.
+
+	* pokernetwork/pokercashier.py (PokerCashier.cashOutBreakNote):
+	'message' variable was not set in error condition.
+
+	* tests/test-pokercashier.py.in
+	(PokerCashierFakeDBTestCase.test09_cashOutUpdateCounter_forceRaiseOnNotesOrder):
+	Wrot test.
+	(PokerCashierFakeDBTestCase.test10_cashOutBreakNote_DeferredFromCommit):
+	Wrote test.
+	(PokerCashierFakeDBTestCase.test11_cashOutBreakNote_multirowForSerial):
+	Wrote test.
+
+2008-10-18  Bradley M. Kuhn  <[EMAIL PROTECTED]>
+
+	* tests/test-pokercashier.py.in
+	(PokerCashierFakeDBTestCase.test08_cashOutUpdateCounter_various):
+	Wrote test.
+
+	* pokernetwork/pokercashier.py
+	(PokerCashier.cashOutUpdateCounter): Fixed typo in message.
+
+	* tests/test-pokercashier.py.in
+	(PokerCashierFakeDBTestCase.test06_cashOutUpdateSafe_forceFakeFalsePacket):
+	Wrote test.
+	(PokerCashierFakeDBTestCase.cashOutUpdateCounter_weirdLen): Wrote
+	method.
+	(PokerCashierFakeDBTestCase.test07_cashOutUpdateCounter_weirdLen_1):
+	Wrote test.
+	(PokerCashierFakeDBTestCase.test07_cashOutUpdateCounter_weirdLen_3):
+	Wrote test.
+
+	* pokernetwork/pokercashier.py (PokerCashier.unlock): Fixed
+	misspelling in verbose output.
+
+	* tests/test-pokercashier.py.in
+	(PokerCashierFakeDBTestCase.test04_cashOutUpdateSafe_twoNullPackets):
+	Wrote test.
+
+2008-10-16  Bradley M. Kuhn  <[EMAIL PROTECTED]>
+
+	* tests/test-pokercashier.py.in (PokerCashierFakeDBTestCase):
+	Created class.
+	(PokerCashierFakeDBTestCase.test01_ForceExceptionOnExecute): Wrote
+	test.
+	(PokerCashierFakeDBTestCase.test01_ForceExceptionOnRowCount):
+	Renamed test.
+	(PokerCashierFakeDBTestCase.test02_forceExceptionOnExecute): Wrote
+	test.
+	(PokerCashierTestCase.test07_getCurrencySerial): Improved checking
+	for getCurrencySerial()
+
 2008-10-13  Bradley M. Kuhn  <[EMAIL PROTECTED]>
 
 	* tests/test-clientserver.py.in (MockPingTimer): Moved class from
diff --git a/poker-network/pokernetwork/pokercashier.py b/poker-network/pokernetwork/pokercashier.py
index 44e3d27..eb822f2 100644
--- a/poker-network/pokernetwork/pokercashier.py
+++ b/poker-network/pokernetwork/pokercashier.py
@@ -88,6 +88,8 @@ class PokerCashier:
                 cursor.execute(sql, url)
                 if cursor.rowcount == 1:
                     currency_serial = cursor.lastrowid
+                else:
+                    raise Exception("SQL command '%s' failed without raising exception.  Underlying DB may be severely hosed" % sql)
             except Exception, e:
                 cursor.close()
                 if e[0] == ER.DUP_ENTRY and reentrant:
@@ -311,7 +313,7 @@ class PokerCashier:
         return deferred
         
     def cashOutUpdateCounter(self, new_notes, packet):
-        if self.verbose > 2: self.message("cashOuUpdateCounter: new_notes = " + str(new_notes) + " packet = " + str(packet))
+        if self.verbose > 2: self.message("cashOutUpdateCounter: new_notes = " + str(new_notes) + " packet = " + str(packet))
         cursor = self.db.cursor()
         if len(new_notes) != 2:
             raise PacketError(other_type = PACKET_POKER_CASH_OUT,
@@ -370,7 +372,8 @@ class PokerCashier:
                 if self.verbose > 2: self.message(sql)
                 cursor.execute(sql)
                 if cursor.rowcount != 1:
-                    self.error(sql + " found " + str(cursor.rowcount) + " records instead of exactly 1")
+                    message = sql + " found " + str(cursor.rowcount) + " records instead of exactly 1"
+                    self.error(message)
                     raise PacketError(other_type = PACKET_POKER_CASH_OUT,
                                       code = PacketPokerCashOut.SAFE,
                                       message = message)
@@ -393,10 +396,10 @@ class PokerCashier:
     def unlock(self, currency_serial):
         name = self.getLockName(currency_serial)
         if not self.locks.has_key(name):
-            if self.verbose: self.error("cashInUnlock: unpexected missing " + name + " in locks (ignored)")
+            if self.verbose: self.error("cashInUnlock: unexpected missing " + name + " in locks (ignored)")
             return
         if not self.locks[name].isAlive():
-            if self.verbose: self.error("cashInUnlock: unpexected dead " + name + " pokerlock (ignored)")
+            if self.verbose: self.error("cashInUnlock: unexpected dead " + name + " pokerlock (ignored)")
             return
         self.locks[name].release(name)
 
diff --git a/poker-network/tests/run.in b/poker-network/tests/run.in
index bff5212..45cb163 100644
--- a/poker-network/tests/run.in
+++ b/poker-network/tests/run.in
@@ -78,6 +78,7 @@ COVERAGE_100_PERCENT="
 ../pokernetwork/protocol
 ../pokernetwork/server
 ../pokernetwork/client
+../pokernetwork/pokercashier
 ../pokernetwork/pokerauthmysql
 ../pokernetwork/user
 ../pokernetwork/userstats
diff --git a/poker-network/tests/test-pokercashier.py.in b/poker-network/tests/test-pokercashier.py.in
index 1ff5352..8abed5c 100644
--- a/poker-network/tests/test-pokercashier.py.in
+++ b/poker-network/tests/test-pokercashier.py.in
@@ -1,14 +1,14 @@
 [EMAIL PROTECTED]@
-# -*- mode: python -*-
+# -*- py-indent-offset: 4; coding: iso-8859-1; mode: python -*-
 #
-# Copyright (C) 2006, 2007, 2008 Loic Dachary <[EMAIL PROTECTED]>
-# Copyright (C) 2008 Bradley M. Kuhn <[EMAIL PROTECTED]> 
-# Copyright (C) 2006 Mekensleep
+# Note: this file is copyrighted by multiple entities; some license their
+# copyrights under GPLv3-or-later and some under AGPLv3-or-later.  Read
+# below for details.
 #
-# Mekensleep
-# 24 rue vieille du temple
-# 75004 Paris
-#       [EMAIL PROTECTED]
+# Copyright (C) 2006, 2007, 2008 Loic Dachary <[EMAIL PROTECTED]>
+# Copyright (C) 2006             Mekensleep
+#                                24 rue vieille du temple 75004 Paris
+#                                <[EMAIL PROTECTED]>
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -24,9 +24,26 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301, USA.
 #
+# Copyright (C)             2008 Bradley M. Kuhn <[EMAIL PROTECTED]>
+#
+# This program gives you software freedom; you can copy, convey,
+# propogate, redistribute and/or modify this program under the terms of
+# the GNU Affero General Public License (AGPL) as published by the Free
+# Software Foundation, either version 3 of the License, or (at your
+# option) any later version of the AGPL.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program in a file in the toplevel directory called
+# "AGPLv3".  If not, see <http://www.gnu.org/licenses/>.
+#
 # Authors:
 #  Loic Dachary <[EMAIL PROTECTED]>
-#
+#  Bradley M. Kuhn <[EMAIL PROTECTED]>
 
 import sys, os
 sys.path.insert(0, "@srcdir@/..")
@@ -42,7 +59,7 @@ from twisted.internet import reactor, defer
 
 twisted.internet.base.DelayedCall.debug = True
 
-from tests.testmessages import silence_all_messages
+from tests.testmessages import silence_all_messages, get_messages, clear_all_messages
 verbose = int(os.environ.get('VERBOSE_T', '-1'))
 if verbose < 0: silence_all_messages()
 
@@ -365,9 +382,17 @@ class PokerCashierTestCase(unittest.TestCase):
 
     # --------------------------------------------------------
     def test07_getCurrencySerial(self):
+        clear_all_messages()
         self.cashier.parameters['user_create'] = 'no';
 
-        self.failUnlessRaises(PacketError, self.cashier.getCurrencySerial, 'http://fake')
+        pe = self.failUnlessRaises(PacketError, self.cashier.getCurrencySerial, 'http://fake')
+        self.assertEquals(pe.type, PACKET_ERROR)
+        self.assertEquals(pe.other_type, PACKET_POKER_CASH_IN)
+        self.assertEquals(pe.message, 
+                        'Invalid currency http://fake and user_create = no in settings.')
+        self.assertEquals(pe.code, PacketPokerCashIn.REFUSED)
+        self.assertEquals(get_messages(),
+                          ["SELECT serial FROM currencies WHERE url = 'http://fake'"])
     # --------------------------------------------------------
     def test08_forcecashInUpdateSafeFail(self):
         self.value = 100
@@ -521,11 +546,787 @@ class PokerCashierTestCase(unittest.TestCase):
         self.cashier.cashOutCollect = origCashOutCollect
         return True
 # --------------------------------------------------------
+# Following tests use a MockDB rather than the real MySQL database
+class PokerCashierFakeDBTestCase(unittest.TestCase):
+    def destroyDb(self):
+        if len("@MYSQL_TEST_DBROOT_PASSWORD@") > 0:
+            os.system("@MYSQL@ -u @MYSQL_TEST_DBROOT@ --password='@MYSQL_TEST_DBROOT_PASSWORD@' -h '@MYSQL_TEST_DBHOST@' -e 'DROP DATABASE IF EXISTS pokernetworktest'")
+        else:
+            os.system("@MYSQL@ -u @MYSQL_TEST_DBROOT@ -h '@MYSQL_TEST_DBHOST@' -e 'DROP DATABASE IF EXISTS pokernetworktest'")
+        
+    # --------------------------------------------------------
+    def setUp(self):
+        self.destroyDb()
+        self.settings = pokernetworkconfig.Config([])
+        self.settings.doc = libxml2.parseMemory(settings_xml, len(settings_xml))
+        self.settings.header = self.settings.doc.xpathNewContext()
+        self.cashier = pokercashier.PokerCashier(self.settings)
+        self.user_serial = 5050
+        self.user1_serial = 6060
+        self.user2_serial = 7070
+        self.user3_serial = 8080
+        self.users_serial = range(9000, 9010)
+
+    # --------------------------------------------------------
+    def tearDown(self):
+        self.cashier.close()
+#        self.destroyDb()
+    # --------------------------------------------------------
+    def test01_getCurrencySerial_forceExceptionOnRowCount(self):
+        clear_all_messages()
+        self.cashier.parameters['user_create'] = 'yes'
+        class MockCursor:
+            def __init__(cursorSelf):
+                cursorSelf.rowcount = 0
+            def close(cursorSelf): pass
+            def execute(*args):
+                self = args[0]
+                sql = args[1]
+                if sql.find('SELECT') >= 0:
+                    self.rowcount = 0
+                elif sql.find('INSERT') >= 0:
+                    self.rowcount = 0
+                else:
+                    self.failIf(True)  # Should not be reached.
+        class MockDatabase:
+            def __init__(dbSelf):
+                class MockInternalDatabase:
+                    def literal(intDBSelf, *args):
+                        return args[0]
+                dbSelf.db = MockInternalDatabase()
+            def cursor(dbSelf):
+                return MockCursor()
+        
+        self.cashier.setDb(MockDatabase())
+
+        caughtExeption = False
+        try:
+            self.cashier.getCurrencySerial("http://example.org";)
+            self.failIf(True)
+        except Exception, e:
+            caughtExeption = True
+            self.assertEquals(e.__str__(), "SQL command 'INSERT INTO currencies (url) VALUES (%s)' failed without raising exception.  Underlying DB may be severely hosed")
+        self.failUnless(caughtExeption)
+
+        self.assertEquals(get_messages(), ['SELECT serial FROM currencies WHERE url = http://example.org', 'INSERT INTO currencies (url) VALUES (http://example.org)'])
+    # --------------------------------------------------------
+    def test02_getCurrencySerial_forceExceptionOnExecute(self):
+        clear_all_messages()
+        from MySQLdb.constants import ER
+        self.cashier.parameters['user_create'] = 'yes'
+        class MockCursor:
+            def __init__(cursorSelf):
+                cursorSelf.rowcount = 0
+            def close(cursorSelf): pass
+            def execute(*args):
+                self = args[0]
+                sql = args[1]
+                if sql.find('SELECT') >= 0:
+                    self.rowcount = 0
+                elif sql.find('INSERT INTO') >= 0:
+                    raise Exception(ER.DUP_ENTRY)
+                else:
+                    self.failIf(True)  # Should not be reached.
+        class MockDatabase:
+            def __init__(dbSelf):
+                class MockInternalDatabase:
+                    def literal(intDBSelf, *args):
+                        return args[0]
+                dbSelf.db = MockInternalDatabase()
+            def cursor(dbSelf):
+                return MockCursor()
+        
+        self.cashier.setDb(MockDatabase())
+
+        caughtExeption = False
+        try:
+            self.cashier.getCurrencySerial("http://example.org";, reentrant = False)
+            self.failIf(True)
+        except Exception, e:
+            caughtExeption = True
+            self.assertEquals(len(e.args), 1)
+            self.assertEquals(e[0], ER.DUP_ENTRY)
+        self.failUnless(caughtExeption)
+
+        self.assertEquals(get_messages(), ['SELECT serial FROM currencies WHERE url = http://example.org', 'INSERT INTO currencies (url) VALUES (http://example.org)'])
+    # --------------------------------------------------------
+    def test03_getCurrencySerial_forceRecursionWithNoResolution(self):
+        clear_all_messages()
+        from MySQLdb.constants import ER
+        self.cashier.parameters['user_create'] = 'yes'
+        class MockCursor:
+            def __init__(cursorSelf):
+                cursorSelf.rowcount = 0
+                cursorSelf.selectCount = 0
+                cursorSelf.insertCount = 0
+            def close(cursorSelf): pass
+            def execute(*args):
+                cursorSelf = args[0]
+                sql = args[1]
+                if sql.find('SELECT') >= 0:
+                    self.rowcount = 0
+                    cursorSelf.selectCount += 1
+                elif sql.find('INSERT INTO') >= 0:
+                    cursorSelf.insertCount += 1
+                    raise Exception(ER.DUP_ENTRY)
+                else:
+                    self.failIf(True)  # Should not be reached.
+        class MockDatabase:
+            def __init__(dbSelf):
+                class MockInternalDatabase:
+                    def literal(intDBSelf, *args):
+                        return args[0]
+                dbSelf.db = MockInternalDatabase()
+                dbSelf.cursorValue = MockCursor()
+            def cursor(dbSelf):
+                return dbSelf.cursorValue
+        
+        db = MockDatabase()
+        self.cashier.setDb(db)
+
+        caughtExeption = False
+        try:
+            self.cashier.getCurrencySerial("http://example.org";)
+            self.failIf(True)
+        except Exception, e:
+            caughtExeption = True
+            self.assertEquals(len(e.args), 1)
+            self.assertEquals(e[0], ER.DUP_ENTRY)
+        self.failUnless(caughtExeption)
+        self.assertEquals(db.cursor().selectCount, 2)
+        self.assertEquals(db.cursor().insertCount, 2)
+
+        self.assertEquals(get_messages(), ['SELECT serial FROM currencies WHERE url = http://example.org', 'INSERT INTO currencies (url) VALUES (http://example.org)', 'SELECT serial FROM currencies WHERE url = http://example.org', 'INSERT INTO currencies (url) VALUES (http://example.org)'])
+    # --------------------------------------------------------
+    def test04_cashOutUpdateSafe_twoNullPackets(self):
+        """test04_cashOutUpdateSafe_forceFallThrough
+        This test is handling the case where cashOutCollect() twice
+        returns an empty packet in a row.  The code in cashOutUpdateSafe()
+        does not actually check what is returned on the second call, but
+        is wrapped in a try/except, so we catch that and make sure the
+        operations."""
+
+        clear_all_messages()
+        from MySQLdb.constants import ER
+        self.cashier.parameters['user_create'] = 'yes'
+        class MockCursor:
+            def __init__(cursorSelf):
+                cursorSelf.rowcount = 0
+                cursorSelf.counts = {}
+                cursorSelf.acceptedStatements = [ 'SELECT', 'INSERT INTO', 'UPDATE',
+                                                  'DELETE', 'START TRANSACTION',
+                                                  'COMMIT', 'ROLLBACK' ]
+                for cntType in cursorSelf.acceptedStatements:
+                    cursorSelf.counts[cntType] = 0 
+            def close(cursorSelf): pass
+            def execute(*args):
+                cursorSelf = args[0]
+                sql = args[1]
+                found = False
+                for str in cursorSelf.acceptedStatements:
+                    if sql[:len(str)] == str:
+                        cursorSelf.counts[str] += 1
+                        cursorSelf.rowcount = 0
+                        if str == "DELETE" or str == 'UPDATE':
+                            cursorSelf.rowcount = 1
+                        found = True
+                        break
+                self.failUnless(found)
+                return cursorSelf.rowcount
+        class MockDatabase:
+            def __init__(dbSelf):
+                class MockInternalDatabase:
+                    def literal(intDBSelf, *args):
+                        return args[0]
+                dbSelf.db = MockInternalDatabase()
+                dbSelf.cursorValue = MockCursor()
+            def cursor(dbSelf):
+                return dbSelf.cursorValue
+        
+        db = MockDatabase()
+        self.cashier.setDb(db)
+
+        caughtExeption = False
+        try:
+            packet = self.cashier.cashOutUpdateSafe("IGNORED", 5, 8)
+            self.failIf(True)
+        except Exception, e:
+            caughtExeption = True
+            self.assertEquals(len(e.args), 1)
+            self.assertEquals(e[0], "'NoneType' object has no attribute 'value'")
+
+        self.assertEquals(caughtExeption, True)
+        self.assertEquals(db.cursor().counts['SELECT'], 2)
+        self.assertEquals(db.cursor().counts['DELETE'], 2)
+        self.assertEquals(db.cursor().counts['INSERT INTO'], 1)
+        self.assertEquals(db.cursor().counts['ROLLBACK'], 1)
+        self.assertEquals(db.cursor().counts['COMMIT'], 0)
+    # --------------------------------------------------------
+    def test05_cashOutUpdateSafe_secondPacketGood(self):
+        """test05_cashOutUpdateSafe_secondPacketGood
+        On the second call to cashOutCollect(), we return a valid row.
+        This causes us to get back a valid packet.  But still an (ignored)
+        error on the lock() not existing."""
+        clear_all_messages()
+        from MySQLdb.constants import ER
+        self.cashier.parameters['user_create'] = 'yes'
+        class MockCursor:
+            def __init__(cursorSelf):
+                cursorSelf.rowcount = 0
+                cursorSelf.counts = {}
+                cursorSelf.acceptedStatements = [ 'SELECT', 'INSERT INTO', 'UPDATE',
+                                                  'DELETE', 'START TRANSACTION',
+                                                  'COMMIT', 'ROLLBACK' ]
+                cursorSelf.row = ()
+                for cntType in cursorSelf.acceptedStatements:
+                    cursorSelf.counts[cntType] = 0 
+            def close(cursorSelf): pass
+            def execute(*args):
+                cursorSelf = args[0]
+                sql = args[1]
+                found = False
+                for str in cursorSelf.acceptedStatements:
+                    if sql[:len(str)] == str:
+                        cursorSelf.counts[str] += 1
+                        cursorSelf.rowcount = 0
+                        if str == "DELETE" or str == 'UPDATE':
+                            cursorSelf.rowcount = 1
+                        found = True
+                        break
+                self.failUnless(found)
+                # The second time cashOutCollect() is called, we want to
+                # return a valid set of values.
+                if str == "SELECT" and cursorSelf.counts[str] == 2:
+                    cursorSelf.rowcount = 1
+                    cursorSelf.row = (5, "http://example.org";, 5, "example", 10, "")
+                return cursorSelf.rowcount
+            def fetchone(cursorSelf): return cursorSelf.row
+        class MockDatabase:
+            def __init__(dbSelf):
+                class MockInternalDatabase:
+                    def literal(intDBSelf, *args):
+                        return args[0]
+                dbSelf.db = MockInternalDatabase()
+                dbSelf.cursorValue = MockCursor()
+            def cursor(dbSelf):
+                return dbSelf.cursorValue
+        
+        db = MockDatabase()
+        self.cashier.setDb(db)
+
+        packet = self.cashier.cashOutUpdateSafe("IGNORED", 5, 8)
+        self.assertEquals(packet.type, PACKET_POKER_CASH_OUT)
+        self.assertEquals(packet.serial, 5)
+        self.assertEquals(packet.url, "http://example.org";)
+        self.assertEquals(packet.name, "example")
+        self.assertEquals(packet.bserial, 5)
+        self.assertEquals(packet.value, 10)
+        self.assertEquals(db.cursor().counts['SELECT'], 2)
+        self.assertEquals(db.cursor().counts['DELETE'], 2)
+        self.assertEquals(db.cursor().counts['INSERT INTO'], 1)
+        self.assertEquals(db.cursor().counts['ROLLBACK'], 0)
+        self.assertEquals(db.cursor().counts['COMMIT'], 1)
+        msgs = get_messages()
+        self.assertEquals(msgs[len(msgs)-1], '*ERROR* cashInUnlock: unexpected missing cash_5 in locks (ignored)')
+    # --------------------------------------------------------
+    def test06_cashOutUpdateSafe_forceFakeFalsePacket(self):
+        """test06_cashOutUpdateSafe_forceFakeFalsePacket
+        We override the second call to cashOutCollect(), so we return a
+        valid row.  We force the packet returned to always be false, to
+        force the final error code to operate.  """
+        clear_all_messages()
+        from MySQLdb.constants import ER
+        self.cashier.parameters['user_create'] = 'yes'
+        class MockCursor:
+            def __init__(cursorSelf):
+                cursorSelf.rowcount = 0
+                cursorSelf.counts = {}
+                cursorSelf.acceptedStatements = [ 'SELECT', 'INSERT INTO', 'UPDATE',
+                                                  'DELETE', 'START TRANSACTION',
+                                                  'COMMIT', 'ROLLBACK' ]
+                cursorSelf.row = ()
+                for cntType in cursorSelf.acceptedStatements:
+                    cursorSelf.counts[cntType] = 0 
+            def close(cursorSelf): pass
+            def execute(*args):
+                cursorSelf = args[0]
+                sql = args[1]
+                found = False
+                for str in cursorSelf.acceptedStatements:
+                    if sql[:len(str)] == str:
+                        cursorSelf.counts[str] += 1
+                        cursorSelf.rowcount = 0
+                        if str == "DELETE" or str == 'UPDATE':
+                            cursorSelf.rowcount = 1
+                        found = True
+                        break
+                self.failUnless(found)
+                return cursorSelf.rowcount
+        class MockDatabase:
+            def __init__(dbSelf):
+                class MockInternalDatabase:
+                    def literal(intDBSelf, *args):
+                        return args[0]
+                dbSelf.db = MockInternalDatabase()
+                dbSelf.cursorValue = MockCursor()
+            def cursor(dbSelf):
+                return dbSelf.cursorValue
+        
+        db = MockDatabase()
+        self.cashier.setDb(db)
+
+        global calledCount
+        calledCount = 0
+
+        class MockPacket():
+            def __init__(mockPacketSelf):
+                mockPacketSelf.serial  = 5
+                mockPacketSelf.value  = 10
+            def __nonzero__(mockPacketSelf): return False
+            
+        def mockedcashOutCollect(currencySerial, transactionId):
+            return MockPacket()
+
+        self.cashier.cashOutCollect = mockedcashOutCollect
+
+        packet = self.cashier.cashOutUpdateSafe("IGNORED", 5, 8)
+
+        self.assertEquals(packet.type, PACKET_ERROR)
+        self.assertEquals(packet.message, 'no currency note to be collected for currency 5')
+        self.assertEquals(packet.other_type, PACKET_POKER_CASH_OUT)
+        self.assertEquals(packet.code, PacketPokerCashOut.EMPTY)
+
+        self.assertEquals(db.cursor().counts['SELECT'], 0)
+        self.assertEquals(db.cursor().counts['DELETE'], 2)
+        self.assertEquals(db.cursor().counts['INSERT INTO'], 1)
+        self.assertEquals(db.cursor().counts['ROLLBACK'], 0)
+        self.assertEquals(db.cursor().counts['COMMIT'], 1)
+        msgs = get_messages()
+        self.assertEquals(msgs[len(msgs)-1], '*ERROR* cashInUnlock: unexpected missing cash_5 in locks (ignored)')
+    # --------------------------------------------------------
+    def cashOutUpdateCounter_weirdLen(self, new_notes, message):
+        clear_all_messages()
+        class MockDatabase():
+            def cursor(dbSelf): return MockCursor()
+        class MockCursor(): pass
+        db = MockDatabase()
+        self.cashier.setDb(db)
+
+        caughtIt = False
+        try:
+            self.cashier.cashOutUpdateCounter(new_notes, "dummy packet")
+            self.failIf(True)
+        except PacketError, pe:
+            caughtIt = True
+            self.assertEquals(pe.type, PACKET_ERROR)
+            self.assertEquals(pe.other_type, PACKET_POKER_CASH_OUT)
+            self.assertEquals(pe.code, PacketPokerCashOut.BREAK_NOTE)
+            self.assertEquals(pe.message, "breaking dummy packet resulted in %d notes (%s) instead of 2"
+                              % (len(new_notes), message))
+            self.assertEquals(get_messages(),
+                              ["cashOutUpdateCounter: new_notes = %s packet = dummy packet" % message])
+        self.failUnless(caughtIt)
+    # --------------------------------------------------------
+    def test07_cashOutUpdateCounter_weirdLen_1(self):
+        self.cashOutUpdateCounter_weirdLen(['a'], "['a']")
+    # --------------------------------------------------------
+    def test07_cashOutUpdateCounter_weirdLen_3(self):
+        self.cashOutUpdateCounter_weirdLen(['a', 'b', 'c'], "['a', 'b', 'c']")
+    # --------------------------------------------------------
+    def test08_cashOutUpdateCounter_various(self):
+        """test08_cashOutUpdateCounter_various
+        This is a somewhat goofy test in that it is covering a bunch of
+        oddball situations in cashOutUpdateCounter().  First, it's
+        checking for the case where the new_notes args are in order [
+        user, server].  Second, it checks that when server_note's value is
+        zero, only one INSERT is done.  Third, it's handling the case when
+        an Exception is thrown by the execute causing a rollback. """
+        self.cashier.parameters['user_create'] = 'yes'
+        class MockException(Exception):
+            pass
+        class MockCursor:
+            def __init__(cursorSelf):
+                cursorSelf.rowcount = 0
+                cursorSelf.counts = {}
+                cursorSelf.acceptedStatements = [ 'INSERT INTO', 'START TRANSACTION',
+                                                  'COMMIT', 'ROLLBACK' ]
+                cursorSelf.row = ()
+                for cntType in cursorSelf.acceptedStatements:
+                    cursorSelf.counts[cntType] = 0 
+            def close(cursorSelf): pass
+            def execute(*args):
+                cursorSelf = args[0]
+                sql = args[1]
+                found = False
+                for str in cursorSelf.acceptedStatements:
+                    if sql[:len(str)] == str:
+                        cursorSelf.counts[str] += 1
+                        cursorSelf.rowcount = 0
+                        found = True
+                        break
+                self.failUnless(found)
+                if sql[:len(str)] == "INSERT INTO": raise MockException()
+                return cursorSelf.rowcount
+        class MockDatabase:
+            def __init__(dbSelf):
+                class MockInternalDatabase:
+                    def literal(intDBSelf, *args):
+                        return args[0]
+                dbSelf.db = MockInternalDatabase()
+                dbSelf.cursorValue = MockCursor()
+            def cursor(dbSelf):
+                return dbSelf.cursorValue
+        class MockPacket():
+            def __init__(mockPacketSelf):
+                mockPacketSelf.value  = 55
+                mockPacketSelf.currency_serial = 5
+                mockPacketSelf.serial = 1
+                mockPacketSelf.application_data = "application"
+            def __str__(mockPacketSelf): return "MOCK PACKET"
+
+        db = MockDatabase()
+        self.cashier.setDb(db)
+
+        caughtIt = False
+        clear_all_messages()
+        try:
+            self.cashier.cashOutUpdateCounter([ (0, 5, "joe", 55), (0, 0, "server", 0)],
+                                              MockPacket())
+            self.failIf(True)
+        except MockException, me:
+            caughtIt = True
+            self.failUnless(isinstance(me, MockException))
+
+        self.failUnless(caughtIt)
+        self.assertEquals(db.cursor().counts['INSERT INTO'], 1)
+        self.assertEquals(db.cursor().counts['ROLLBACK'], 1)
+        self.assertEquals(db.cursor().counts['COMMIT'], 0)
+        self.assertEquals(db.cursor().counts['START TRANSACTION'], 1)
+        self.assertEquals(get_messages(), 
+                          ["cashOutUpdateCounter: new_notes = [(0, 5, 'joe', 55), (0, 0, 'server', 0)] packet = MOCK PACKET"])
+    # --------------------------------------------------------
+    def test09_cashOutUpdateCounter_forceRaiseOnNotesOrder(self):
+        """test09_cashOutUpdateCounter_forceRaiseOnNotesOrder
+        This test handles the case where new_notes do not match what is in
+        the packet sent to cashOutUpdateCounter() """
+        caughtIt = False
+        clear_all_messages()
+        class MockPacket():
+            def __init__(mockPacketSelf): mockPacketSelf.value  = 43
+            def __str__(mockPacketSelf):  return "MOCK PACKET"
+        class MockDatabase():
+            def cursor(mockDBSelf): return "MOCK CURSOR"
+        db = MockDatabase()
+        self.cashier.setDb(db)
+        try:
+            self.cashier.cashOutUpdateCounter([ (0, 5, "joe", 57), (0, 0, "server", 59)],
+                                              MockPacket())
+            self.failIf(True)
+        except PacketError, pe:
+            caughtIt = True
+            self.failUnless(isinstance(pe, PacketError))
+            self.assertEquals(pe.other_type, PACKET_POKER_CASH_OUT)
+            self.assertEquals(pe.code, PacketPokerCashOut.BREAK_NOTE)
+            self.assertEquals(pe.message,
+                              "breaking MOCK PACKET did not provide a note with the proper value (notes are [(0, 5, 'joe', 57), (0, 0, 'server', 59)])")
+        self.failUnless(caughtIt)
+    # --------------------------------------------------------
+    def test10_cashOutBreakNote_DeferredFromCommit(self):
+        """test10_cashOutBreakNote_DeferredFromCommit
+        Handle situation where the currency_serial already has an entry in
+        counter table, and causes a deferred to be returned from
+        cashOutCurrencyCommit()."""
+        class MockCursor:
+            def __init__(cursorSelf):
+                cursorSelf.rowcount = 0
+                cursorSelf.counts = {}
+                cursorSelf.acceptedStatements = [ 'SELECT transaction_id', 
+                                                  "SELECT serial FROM currencies",
+                                                  "SELECT counter.user_serial"]
+                cursorSelf.row = ()
+                for cntType in cursorSelf.acceptedStatements:
+                    cursorSelf.counts[cntType] = 0 
+            def close(cursorSelf): pass
+            def execute(*args):
+                cursorSelf = args[0]
+                sql = args[1]
+                found = False
+                for str in cursorSelf.acceptedStatements:
+                    if sql[:len(str)] == str:
+                        cursorSelf.counts[str] += 1
+                        cursorSelf.rowcount = 0
+                        cursorSelf.row = ()
+                        found = True
+                        break
+                self.failUnless(found)
+                if sql[:len(str)] == 'SELECT transaction_id':
+                    cursorSelf.rowcount = 1
+                    cursorSelf.row = (777,)
+                elif sql[:len(str)] == "SELECT serial FROM currencies":
+                    cursorSelf.rowcount = 1
+                    cursorSelf.row = (6,)
+                elif sql[:len(str)] == "SELECT counter.user_serial":
+                    cursorSelf.rowcount = 1
+                    cursorSelf.row = ( 6, "http://example.org";, 9, "joe", 100, "application" )
+                return cursorSelf.rowcount
+            def fetchone(cursorSelf): return cursorSelf.row
+        class MockDatabase:
+            def __init__(dbSelf):
+                class MockInternalDatabase:
+                    def literal(intDBSelf, *args): return args[0]
+                dbSelf.db = MockInternalDatabase()
+                dbSelf.cursorValue = MockCursor()
+            def cursor(dbSelf): return dbSelf.cursorValue
+        class MockPacket():
+            def __init__(mockPacketSelf):
+                mockPacketSelf.url  = "http://cashier.example.org";
+                mockPacketSelf.currency_serial = 12
+            def __str__(mockPacketSelf): return "MOCK PACKET"
+        class MockCurrencyClient():
+            def commit(ccSelf, url, transactionId):
+                self.assertEquals(url, "http://cashier.example.org";)
+                self.assertEquals(transactionId, 777)
+                return defer.Deferred()
+
+        db = MockDatabase()
+        self.cashier.setDb(db)
+        self.cashier.currency_client = MockCurrencyClient()
+
+        clear_all_messages()
+        d = self.cashier.cashOutBreakNote("MEANINGLESS ARG", MockPacket())
+
+        for key in db.cursor().counts.keys():
+            if key in [ 'SELECT transaction_id', "SELECT serial FROM currencies" ]:
+                self.assertEquals(db.cursor().counts[key], 1)
+            else:
+                self.assertEquals(db.cursor().counts[key], 0)
+        self.assertEquals(get_messages(), ['SELECT transaction_id FROM counter WHERE  currency_serial = 12', 'cashOutCurrencyCommit', 'SELECT serial FROM currencies WHERE url = http://cashier.example.org'])
+        clear_all_messages()
+
+        self.assertEquals(d.callback(True), None)
+        pack = d.result
+        self.assertEquals(pack.type, PACKET_POKER_CASH_OUT)
+        self.assertEquals(pack.serial,  6)
+        self.assertEquals(pack.url, 'http://example.org')
+        self.assertEquals(pack.name, 'joe')
+        self.assertEquals(pack.bserial, 9)
+        self.assertEquals(pack.value, 100)
+        self.assertEquals(pack.application_data, 'application')
+        [self.assertEquals(db.cursor().counts[key], 1) for key in db.cursor().counts.keys()]
+        msgs = get_messages()
+        self.assertEquals(len(msgs), 4)
+        self.assertEquals(msgs[0], 'cashOutUpdateSafe: 6 777')
+        self.assertEquals(msgs[3], '*ERROR* cashInUnlock: unexpected missing cash_6 in locks (ignored)')
+    # --------------------------------------------------------
+    def test11_cashOutBreakNote_multirowForSerial(self):
+        """test11_cashOutBreakNote_multirowForSerial
+
+        """
+        class MockCursor:
+            def __init__(cursorSelf):
+                cursorSelf.rowcount = 0
+                cursorSelf.counts = {}
+                cursorSelf.acceptedStatements = [ 'SELECT transaction_id', 
+                                                  'SELECT name']
+                cursorSelf.row = ()
+                for cntType in cursorSelf.acceptedStatements:
+                    cursorSelf.counts[cntType] = 0 
+            def close(cursorSelf): pass
+            def execute(*args):
+                cursorSelf = args[0]
+                sql = args[1]
+                found = False
+                for str in cursorSelf.acceptedStatements:
+                    if sql[:len(str)] == str:
+                        cursorSelf.counts[str] += 1
+                        cursorSelf.rowcount = 0
+                        cursorSelf.row = ()
+                        found = True
+                        break
+                self.failUnless(found)
+                return cursorSelf.rowcount
+        class MockDatabase:
+            def __init__(dbSelf):
+                dbSelf.cursorValue = MockCursor()
+            def cursor(dbSelf): return dbSelf.cursorValue
+        class MockPacket():
+            def __init__(mockPacketSelf):
+                mockPacketSelf.url  = "http://cashier.example.org";
+                mockPacketSelf.currency_serial = 12
+            def __str__(mockPacketSelf): return "MOCK PACKET"
+        db = MockDatabase()
+        self.cashier.setDb(db)
+
+        clear_all_messages()
+        caughtIt = False
+        failMsg = 'SELECT name, serial, value FROM safe WHERE currency_serial = 12 found 0 records instead of exactly 1'
+        try:
+            self.cashier.cashOutBreakNote("MEANINGLESS ARG", MockPacket())
+            self.failIf(True)  # Should not be reached
+        except PacketError, pe:
+            caughtIt = True
+            self.assertEquals(pe.other_type, PACKET_POKER_CASH_OUT)
+            self.assertEquals(pe.type, PACKET_ERROR)
+            self.assertEquals(pe.code, PacketPokerCashOut.SAFE)
+            self.assertEquals(pe.message, failMsg)
+        self.failUnless(caughtIt)
+        msgs = get_messages()
+        self.assertEquals(len(msgs), 3)
+        self.assertEquals(msgs[2], "*ERROR* " + failMsg)
+# --------------------------------------------------------
+# Following tests are for the lock/unlock mechanism and do not need any
+# database at all.
+class PokerCashierLockUnlockTestCase(unittest.TestCase):
+    # --------------------------------------------------------
+    def setUp(self):
+        from pokernetwork import pokerlock
+        class MockLock():
+            def __init__(lockSelf, params):
+                lockSelf.alive = False
+                lockSelf.started = False
+                lockSelf.acquireCounts = {}
+            def isAlive(lockSelf): return lockSelf.alive
+            def close(lockSelf):
+                lockSelf.alive = False
+                lockSelf.started = False
+            def release(lockSelf, name):
+                lockSelf.alive = False
+                lockSelf.acquireCounts[name] -= 1
+            def start(lockSelf):
+                lockSelf.alive = True
+                lockSelf.started = True
+            def acquire(lockSelf, name, value):
+                self.assertEquals(value, 5)
+                if lockSelf.acquireCounts.has_key(name):
+                    lockSelf.acquireCounts[name] += 1
+                else:
+                    lockSelf.acquireCounts[name] = 1
+                return "ACQUIRED %s: %d" % (name, lockSelf.acquireCounts[name])
+
+        pokerlock.PokerLock = MockLock
+        self.settings = pokernetworkconfig.Config([])
+        self.settings.doc = libxml2.parseMemory(settings_xml, len(settings_xml))
+        self.settings.header = self.settings.doc.xpathNewContext()
+        self.cashier = pokercashier.PokerCashier(self.settings)
+    # --------------------------------------------------------
+    def tearDown(self):
+        pass
+    # --------------------------------------------------------
+    def test01_unlockNonExistent(self):
+        """test01_unlockNonExistent
+        Tests when unlock is called on a serial that does not exist."""
+        self.assertEquals(self.cashier.locks, {})
+        clear_all_messages()
+        self.assertEquals(self.cashier.unlock(5), None)
+        self.assertEquals(self.cashier.locks, {})
+        self.assertEquals(get_messages(), ['*ERROR* cashInUnlock: unexpected missing cash_5 in locks (ignored)'])
+    # --------------------------------------------------------
+    def test02_lockCreateTwice(self):
+        """test02_lockCreateTwice
+        Testing creation of the lock twice."""
+        self.assertEquals(self.cashier.locks, {})
+        clear_all_messages()
+        self.assertEquals(self.cashier.lock(2), "ACQUIRED cash_2: 1")
+        self.assertEquals(self.cashier.locks.keys(), ['cash_2'])
+        cash2Lock = self.cashier.locks['cash_2']
+        self.failUnless(cash2Lock.alive)
+        self.failUnless(cash2Lock.started)
+        self.assertEquals(cash2Lock.acquireCounts.keys(), [ 'cash_2' ])
+        self.assertEquals(cash2Lock.acquireCounts['cash_2'], 1)
+    # --------------------------------------------------------
+    def test03_lockCreateTwiceWhenUnalive(self):
+        """test03_lockCreateTwiceWhenUnalive
+        Testing creation of the lock again after the activity is turned
+        off."""
+        self.assertEquals(self.cashier.locks, {})
+        clear_all_messages()
+        self.assertEquals(self.cashier.lock(2), "ACQUIRED cash_2: 1")
+        self.assertEquals(self.cashier.locks.keys(), ['cash_2'])
+        cash2Lock = self.cashier.locks['cash_2']
+        self.failUnless(cash2Lock.alive)
+        self.failUnless(cash2Lock.started)
+        self.assertEquals(cash2Lock.acquireCounts.keys(), [ 'cash_2' ])
+        self.assertEquals(cash2Lock.acquireCounts['cash_2'], 1)
+        self.assertEquals(get_messages(), ['get lock cash_2'])
+
+        clear_all_messages()
+        cash2Lock.alive = False
+
+        self.assertEquals(self.cashier.lock(2), "ACQUIRED cash_2: 1")
+        self.assertEquals(self.cashier.locks.keys(), ['cash_2'])
+        cash2newLock = self.cashier.locks['cash_2']
+        self.failUnless(cash2newLock.alive)
+        self.failUnless(cash2newLock.started)
+        self.assertEquals(cash2newLock.acquireCounts.keys(), [ 'cash_2' ])
+        self.assertEquals(cash2newLock.acquireCounts['cash_2'], 1)
+        self.assertEquals(get_messages(), ['get lock cash_2'])
+    # --------------------------------------------------------
+    def test04_unlockTwice(self):
+        """test03_unlockTwice
+        try to unlock a lock twice"""
+        self.assertEquals(self.cashier.locks, {})
+        clear_all_messages()
+        self.assertEquals(self.cashier.lock(2), "ACQUIRED cash_2: 1")
+        self.assertEquals(self.cashier.locks.keys(), ['cash_2'])
+        cash2Lock = self.cashier.locks['cash_2']
+        self.failUnless(cash2Lock.alive)
+        self.failUnless(cash2Lock.started)
+        self.assertEquals(cash2Lock.acquireCounts.keys(), [ 'cash_2' ])
+        self.assertEquals(cash2Lock.acquireCounts['cash_2'], 1)
+        self.assertEquals(get_messages(), ['get lock cash_2'])
+
+        clear_all_messages()
+
+        self.assertEquals(self.cashier.unlock(2), None)
+        self.assertEquals(self.cashier.locks.keys(), ['cash_2'])
+        cash2newLock = self.cashier.locks['cash_2']
+        self.failIf(cash2newLock.alive)
+        self.failUnless(cash2newLock.started)
+        self.assertEquals(cash2newLock.acquireCounts.keys(), [ 'cash_2' ])
+        self.assertEquals(cash2newLock.acquireCounts['cash_2'], 0)
+        self.assertEquals(get_messages(), [])
+
+        self.assertEquals(self.cashier.unlock(2), None)
+        self.assertEquals(self.cashier.locks.keys(), ['cash_2'])
+        cash2newLock = self.cashier.locks['cash_2']
+        self.failIf(cash2newLock.alive)
+        self.failUnless(cash2newLock.started)
+        self.assertEquals(cash2newLock.acquireCounts.keys(), [ 'cash_2' ])
+        self.assertEquals(cash2newLock.acquireCounts['cash_2'], 0)
+        self.assertEquals(get_messages(),
+                          ['*ERROR* cashInUnlock: unexpected dead cash_2 pokerlock (ignored)'])
+    # --------------------------------------------------------
+    def test05_lockCreateTwiceWhenAliven(self):
+        """test05_lockCreateTwiceWhenAlive
+        relock after lock leaving it alive"""
+        self.assertEquals(self.cashier.locks, {})
+        clear_all_messages()
+        self.assertEquals(self.cashier.lock(2), "ACQUIRED cash_2: 1")
+        self.assertEquals(self.cashier.locks.keys(), ['cash_2'])
+        cash2Lock = self.cashier.locks['cash_2']
+        self.failUnless(cash2Lock.alive)
+        self.failUnless(cash2Lock.started)
+        self.assertEquals(cash2Lock.acquireCounts.keys(), [ 'cash_2' ])
+        self.assertEquals(cash2Lock.acquireCounts['cash_2'], 1)
+        self.assertEquals(get_messages(), ['get lock cash_2'])
+
+        clear_all_messages()
+
+        self.assertEquals(self.cashier.lock(2), "ACQUIRED cash_2: 2")
+        self.assertEquals(self.cashier.locks.keys(), ['cash_2'])
+        cash2newLock = self.cashier.locks['cash_2']
+        self.failUnless(cash2newLock.alive)
+        self.failUnless(cash2newLock.started)
+        self.assertEquals(cash2newLock.acquireCounts.keys(), [ 'cash_2' ])
+        self.assertEquals(cash2newLock.acquireCounts['cash_2'], 2)
+        self.assertEquals(get_messages(), ['get lock cash_2'])
+# --------------------------------------------------------
 def GetTestSuite():
     suite = runner.TestSuite(PokerCashierTestCase)
     suite.addTest(unittest.makeSuite(PokerCashierTestCase))
+    suite.addTest(unittest.makeSuite(PokerCashierFakeDBTestCase))
+    suite.addTest(unittest.makeSuite(PokerCashierLockUnlockTestCase))
     return suite
-
 # --------------------------------------------------------
 def GetTestedModule():
     return pokerengineconfig
@@ -533,8 +1334,11 @@ def GetTestedModule():
 # --------------------------------------------------------
 def Run():
     loader = runner.TestLoader()
-#    loader.methodPrefix = "test14"
-    suite = loader.loadClass(PokerCashierTestCase)
+#    loader.methodPrefix = "test11"
+    suite = loader.suiteFactory()
+    suite.addTest(loader.loadClass(PokerCashierTestCase))
+    suite.addTest(loader.loadClass(PokerCashierFakeDBTestCase))
+    suite.addTest(loader.loadClass(PokerCashierLockUnlockTestCase))
     return runner.TrialRunner(reporter.VerboseTextReporter,
 #                              tracebackFormat='verbose',
                               tracebackFormat='default',
-- 

   -- bkuhn
_______________________________________________
Pokersource-users mailing list
[email protected]
https://mail.gna.org/listinfo/pokersource-users

Reply via email to