Index: Lib/test/test_mailbox.py =================================================================== --- Lib/test/test_mailbox.py (revision 53478) +++ Lib/test/test_mailbox.py (working copy) @@ -121,6 +121,38 @@ self.assert_(len(self._box) == 1) self.assertRaises(KeyError, lambda: self._box[key0]) + def test_double_shorten(self): + # Check that flush() can shorten the mailbox twice + self._test_remove_two_of_three(broken_locking=False) + + def test_remove_with_broken_locking(self): + # Check that a (broken) application releasing the lock and + # then removing messages using the existing keys does not + # delete the wrong messages. + self._test_remove_two_of_three(broken_locking=True) + + def _test_remove_two_of_three(self, broken_locking=False): + self._box.lock() + key0 = self._box.add(self._template % 0) + key1 = self._box.add(self._template % 1) + key2 = self._box.add(self._template % 2) + self._box.flush() + self._box.remove(key0) + self._box.flush() + if broken_locking: + # As the name suggests, code that does this is broken + # (releasing the lock invalidates the keys, in general), + # but ideally mailbox.py should not break it further. + self._box.unlock() + self._box.lock() + self._box.remove(key1) + self._box.flush() + self._box.unlock() + self._box.close() + self._box = self._factory(self._path) + self.assert_(len(self._box) == 1) + self.assert_(self._box.itervalues().next().get_payload() == '2') + def test_get(self): # Retrieve messages using get() key0 = self._box.add(self._template % 0) @@ -416,6 +448,68 @@ self.assertRaises(TypeError, lambda: self._box._dump_message(None, output)) + def test_concurrent_add(self): + # Simple test of concurrent addition to a mailbox. + # This exercises the add() and flush() methods, based on bug #1599254. + # This bug affected only the classes based on _singlefileMailbox + # (mbox, MMDF, Babyl), but this test can apply to any mailbox type. + + # If fork isn't available, skip this test. + # XXX Could someone please port this to Windows? + if not hasattr(os, 'fork'): + return + + def random_message (): + # Generate a random message body + import random + body = "" + for i in range(random.randint(1, 10)): + line = "a" * random.randint(0, 75) + '\n' + body += line + + return body + + def add_25_messages (): + "Helper function to add 25 messages to a mailbox." + mbox = self._box + + for i in range(25): + msg = """Subject: %i, pid %i +From: sender@example.com + +Content goes here. +%s""" % (i, os.getpid(), random_message()) + while True: + try: + mbox.lock() + except mailbox.ExternalClashError: + # In case of conflict, wait a bit and try again. + time.sleep(0.01) + else: + break + mbox.add(msg) + mbox.flush() + mbox.unlock() + + # Fire off a subprocess that will add 25 messages to a mailbox + # file, locking and unlocking it each time. The parent process + # will do the same. The resulting mailbox should contain 50 messages. + pid = os.fork() + if pid == 0: + try: + add_25_messages() + finally: + os._exit(0) + + # Add our 25 messages, and wait for the child to exit. + add_25_messages() + os.wait() + + # We expect the mailbox to contain 50 messages. + self._box.lock() + self.assert_(len(self._box) == 50) + self._box.unlock() + def _get_lock_path(self): # Return the path of the dot lock file. May be overridden. return self._path + '.lock'