Exemple #1
0
class TestUndoableActionLog(TestCase):

    def setUp(self):
        self.log = UndoableActionLog()
        self._connectToUndoableActionLog(self.log)
        self.signals = []

    def tearDown(self):
        self._disconnectFromUndoableActionLog(self.log)

    def _undoActionLogSignalCb(self, log, *args):
        args = list(args)
        signalName = args.pop(-1)
        self.signals.append((signalName, args))

    def _connectToUndoableActionLog(self, log):
        for signalName in ("begin", "push", "rollback", "commit", "move"):
            log.connect(signalName, self._undoActionLogSignalCb, signalName)

    def _disconnectFromUndoableActionLog(self, log):
        self.log.disconnect_by_func(self._undoActionLogSignalCb)

    def testRollbackWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.rollback)

    def testCommitWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.commit, "")

    def testPushWrongState(self):
        # no error in this case
        self.log.push(None)

    def testUndoWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.undo)

    def testRedoWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.redo)

    def testCheckpoint(self):
        self.log.begin("meh")
        self.log.push(mock.Mock(spec=UndoableAction))
        self.assertRaises(UndoWrongStateError, self.log.checkpoint)
        self.log.rollback()
        self.log.checkpoint()
        self.assertNotEqual(self.log._checkpoint, None)

    def testDirty(self):
        self.assertFalse(self.log.dirty())
        self.log.begin("meh")
        self.log.push(mock.Mock(spec=UndoableAction))
        self.log.commit("meh")
        self.assertTrue(self.log.dirty())
        self.log.checkpoint()
        self.assertFalse(self.log.dirty())
        self.log.undo()
        self.assertTrue(self.log.dirty())
        self.log.redo()
        self.assertFalse(self.log.dirty())

    def testCommit(self):
        """
        Commit a stack.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack,) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.assertEqual(self.log.undo_stacks, [])
        self.log.push(mock.Mock(spec=UndoableAction))
        self.log.commit("meh")
        self.assertEqual(len(self.signals), 3)
        name, (stack, action) = self.signals[1]
        self.assertEqual(name, "push")
        name, (stack,) = self.signals[2]
        self.assertEqual(name, "commit")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def test_commit_proper(self):
        self.log.begin("meh")
        self.assertRaises(UndoWrongStateError, self.log.commit, "notmeh")

    def testNestedCommit(self):
        """
        Do two nested commits.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack,) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("nested")
        self.assertEqual(len(self.signals), 2)
        name, (stack,) = self.signals[1]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.assertEqual(self.log.undo_stacks, [])
        self.log.push(mock.Mock(spec=UndoableAction))
        self.log.commit("nested")
        self.assertEqual(len(self.signals), 4)
        name, (stack, action) = self.signals[2]
        self.assertEqual(name, "push")
        name, (stack,) = self.signals[3]
        self.assertEqual(name, "commit")
        self.assertTrue(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

        self.assertEqual(self.log.undo_stacks, [])
        self.log.commit("meh")
        self.assertEqual(len(self.signals), 5)
        name, (stack,) = self.signals[4]
        self.assertEqual(name, "commit")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def test_finalizing_action(self):
        action1 = mock.Mock()
        action2 = mock.Mock()
        with self.log.started("one", finalizing_action=action1):
            self.log.push(mock.Mock(spec=UndoableAction))
            with self.log.started("two", finalizing_action=action2):
                self.log.push(mock.Mock(spec=UndoableAction))
        action1.do.assert_called_once_with()
        # For now, we call the finalizing action only for the top stack.
        action2.do.assert_not_called()

    def testRollback(self):
        """
        Test a rollback.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack,) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.log.rollback()
        self.assertEqual(len(self.signals), 2)
        name, (stack,) = self.signals[1]
        self.assertEqual(name, "rollback")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def testNestedRollback(self):
        """
        Test two nested rollbacks.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack,) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("nested")
        self.assertEqual(len(self.signals), 2)
        name, (stack,) = self.signals[1]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.log.rollback()
        self.assertEqual(len(self.signals), 3)
        name, (stack,) = self.signals[2]
        self.assertEqual(name, "rollback")
        self.assertTrue(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

        self.log.rollback()
        self.assertEqual(len(self.signals), 4)
        name, (stack,) = self.signals[3]
        self.assertEqual(name, "rollback")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def testUndoRedo(self):
        """
        Try an undo() redo() sequence.
        """
        # begin
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack,) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        # push two actions
        action1 = mock.Mock(spec=UndoableAction)
        action1.expand.return_value = False
        self.log.push(action1)
        self.assertEqual(len(self.signals), 2)
        name, (stack, signalAction) = self.signals[1]
        self.assertEqual(name, "push")
        self.assertTrue(action1 is signalAction)

        action2 = mock.Mock(spec=UndoableAction)
        self.log.push(action2)
        self.assertEqual(len(self.signals), 3)
        name, (stack, signalAction) = self.signals[2]
        self.assertEqual(name, "push")
        self.assertTrue(action2 is signalAction)

        # commit
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.commit("meh")
        self.assertEqual(len(self.signals), 4)
        name, (stack,) = self.signals[3]
        self.assertEqual(name, "commit")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.assertEqual(action1.do.call_count, 0)
        self.assertEqual(action1.undo.call_count, 0)
        self.assertEqual(action2.do.call_count, 0)
        self.assertEqual(action2.undo.call_count, 0)

        # undo what we just committed
        self.log.undo()
        self.assertEqual(len(self.signals), 5)
        name, stack = self.signals[4]
        self.assertEqual(name, "move")
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 1)
        self.assertEqual(action1.do.call_count, 0)
        self.assertEqual(action1.undo.call_count, 1)
        self.assertEqual(action2.do.call_count, 0)
        self.assertEqual(action2.undo.call_count, 1)

        # redo
        self.log.redo()
        self.assertEqual(len(self.signals), 6)
        name, stack = self.signals[5]
        self.assertEqual(name, "move")
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.assertEqual(action1.do.call_count, 1)
        self.assertEqual(action1.undo.call_count, 1)
        self.assertEqual(action2.do.call_count, 1)
        self.assertEqual(action2.undo.call_count, 1)

    def testOrder(self):
        """
        Test that actions are undone and redone in the correct order.
        """
        order = mock.Mock()
        order.action1 = mock.Mock(spec=UndoableAction)
        order.action1.expand.return_value = False
        order.action2 = mock.Mock(spec=UndoableAction)
        order.action2.expand.return_value = False
        order.action3 = mock.Mock(spec=UndoableAction)
        order.action3.expand.return_value = False

        with self.log.started("meh"):
            self.log.push(order.action1)
            with self.log.started("nested"):
                self.log.push(order.action2)
            self.log.push(order.action3)

        self.log.undo()
        order.assert_has_calls([mock.call.action3.undo(),
                                mock.call.action2.undo(),
                                mock.call.action1.undo()])

        self.log.redo()
        order.assert_has_calls([mock.call.action1.do(),
                                mock.call.action2.do(),
                                mock.call.action3.do()])

        self.log.undo()
        order.assert_has_calls([mock.call.action3.undo(),
                                mock.call.action2.undo(),
                                mock.call.action1.undo()])

    def test_toplevel_operation(self):
        """Checks the toplevel operations nesting."""
        self.log.begin("one", toplevel=False)
        self.log.commit("one")

        self.log.begin("two", toplevel=True)
        self.log.commit("two")

        self.log.begin("three")
        self.assertRaises(UndoWrongStateError,
                          self.log.begin,
                          "four", toplevel=True)
        self.log.begin("nested1")
        self.log.begin("nested2", toplevel=False)
class TestUndoableActionLog(TestCase):
    def setUp(self):
        self.log = UndoableActionLog()
        self._connectToUndoableActionLog(self.log)
        self.signals = []

    def tearDown(self):
        self._disconnectFromUndoableActionLog(self.log)

    def _undoActionLogSignalCb(self, log, *args):
        args = list(args)
        signalName = args.pop(-1)
        self.signals.append((signalName, args))

    def _connectToUndoableActionLog(self, log):
        for signalName in ("begin", "push", "rollback", "commit", "undo",
                           "redo"):
            log.connect(signalName, self._undoActionLogSignalCb, signalName)

    def _disconnectFromUndoableActionLog(self, log):
        self.log.disconnect_by_func(self._undoActionLogSignalCb)

    def testRollbackWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.rollback)

    def testCommitWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.commit)

    def testPushWrongState(self):
        # no error in this case
        self.log.push(None)

    def testUndoWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.undo)

    def testRedoWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.redo)

    def testCheckpoint(self):
        self.log.begin("meh")
        self.log.push(DummyUndoableAction())
        self.assertRaises(UndoWrongStateError, self.log.checkpoint)
        self.log.rollback()
        self.log.checkpoint()
        self.assertNotEqual(self.log._checkpoint, None)

    def testDirty(self):
        self.assertFalse(self.log.dirty())
        self.log.begin("meh")
        self.log.push(DummyUndoableAction())
        self.log.commit()
        self.assertTrue(self.log.dirty())
        self.log.checkpoint()
        self.assertFalse(self.log.dirty())
        self.log.undo()
        self.assertTrue(self.log.dirty())
        self.log.redo()
        self.assertFalse(self.log.dirty())

    def testCommit(self):
        """
        Commit a stack.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, nested) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertFalse(nested)

        self.assertEqual(self.log.undo_stacks, [])
        self.log.commit()
        self.assertEqual(len(self.signals), 2)
        name, (stack, nested) = self.signals[1]
        self.assertEqual(name, "commit")
        self.assertFalse(nested)
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def testNestedCommit(self):
        """
        Do two nested commits.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, nested) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertFalse(nested)

        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("nested")
        self.assertEqual(len(self.signals), 2)
        name, (stack, nested) = self.signals[1]
        self.assertEqual(name, "begin")
        self.assertTrue(nested)

        self.assertEqual(self.log.undo_stacks, [])
        self.log.commit()
        self.assertEqual(len(self.signals), 3)
        name, (stack, nested) = self.signals[2]
        self.assertEqual(name, "commit")
        self.assertTrue(nested)
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

        self.assertEqual(self.log.undo_stacks, [])
        self.log.commit()
        self.assertEqual(len(self.signals), 4)
        name, (stack, nested) = self.signals[3]
        self.assertEqual(name, "commit")
        self.assertFalse(nested)
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def testRollback(self):
        """
        Test a rollback.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, nested) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertFalse(nested)

        self.log.rollback()
        self.assertEqual(len(self.signals), 2)
        name, (stack, nested) = self.signals[1]
        self.assertEqual(name, "rollback")
        self.assertFalse(nested)
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def testNestedRollback(self):
        """
        Test two nested rollbacks.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, nested) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertFalse(nested)

        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("nested")
        self.assertEqual(len(self.signals), 2)
        name, (stack, nested) = self.signals[1]
        self.assertEqual(name, "begin")
        self.assertTrue(nested)

        self.log.rollback()
        self.assertEqual(len(self.signals), 3)
        name, (stack, nested) = self.signals[2]
        self.assertEqual(name, "rollback")
        self.assertTrue(nested)
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

        self.log.rollback()
        self.assertEqual(len(self.signals), 4)
        name, (stack, nested) = self.signals[3]
        self.assertEqual(name, "rollback")
        self.assertFalse(nested)
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def testUndoRedo(self):
        """
        Try an undo() redo() sequence.
        """
        # begin
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, nested) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertFalse(nested)

        # push two actions
        action1 = DummyUndoableAction()
        self.log.push(action1)
        self.assertEqual(len(self.signals), 2)
        name, (stack, signalAction) = self.signals[1]
        self.assertEqual(name, "push")
        self.assertTrue(action1 is signalAction)

        action2 = DummyUndoableAction()
        self.log.push(action2)
        self.assertEqual(len(self.signals), 3)
        name, (stack, signalAction) = self.signals[2]
        self.assertEqual(name, "push")
        self.assertTrue(action2 is signalAction)

        # commit
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.commit()
        self.assertEqual(len(self.signals), 4)
        name, (stack, nested) = self.signals[3]
        self.assertEqual(name, "commit")
        self.assertFalse(nested)
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)

        self.assertTrue(action1.done_)
        self.assertTrue(action2.done_)

        # undo what we just committed
        self.log.undo()
        self.assertEqual(len(self.signals), 5)
        name, stack = self.signals[4]
        self.assertEqual(name, "undo")
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 1)

        self.assertFalse(action1.done_)
        self.assertFalse(action2.done_)

        # redo
        self.log.redo()
        self.assertEqual(len(self.signals), 6)
        name, stack = self.signals[5]
        self.assertEqual(name, "redo")
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)

        self.assertTrue(action1.done_)
        self.assertTrue(action2.done_)

    def testOrder(self):
        """
        Test that actions are undone and redone in the correct order.
        """
        call_sequence = []

        class Action(UndoableAction):
            def __init__(self, n):
                UndoableAction.__init__(self)
                self.n = n

            def do(self):
                call_sequence.append("do%s" % self.n)
                self._done()

            def undo(self):
                call_sequence.append("undo%s" % self.n)
                self._undone()

        action1 = Action(1)
        action2 = Action(2)
        action3 = Action(3)

        self.log.begin("meh")
        self.log.push(action1)
        self.log.begin("nested")
        self.log.push(action2)
        self.log.commit()
        self.log.push(action3)
        self.log.commit()

        self.log.undo()
        self.assertEqual(call_sequence, ["undo3", "undo2", "undo1"])

        call_sequence[:] = []
        self.log.redo()
        self.assertEqual(call_sequence, ["do1", "do2", "do3"])

        call_sequence[:] = []
        self.log.undo()
        self.assertEqual(call_sequence, ["undo3", "undo2", "undo1"])
Exemple #3
0
class TestUndoableActionLog(TestCase):
    def setUp(self):
        self.log = UndoableActionLog()
        self._connectToUndoableActionLog(self.log)
        self.signals = []

    def tearDown(self):
        self._disconnectFromUndoableActionLog(self.log)

    def _undoActionLogSignalCb(self, log, *args):
        args = list(args)
        signalName = args.pop(-1)
        self.signals.append((signalName, args))

    def _connectToUndoableActionLog(self, log):
        for signalName in ("begin", "push", "rollback", "commit", "move"):
            log.connect(signalName, self._undoActionLogSignalCb, signalName)

    def _disconnectFromUndoableActionLog(self, log):
        self.log.disconnect_by_func(self._undoActionLogSignalCb)

    def testRollbackWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.rollback)

    def testCommitWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.commit, "")

    def testPushWrongState(self):
        # no error in this case
        self.log.push(None)

    def testUndoWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.undo)

    def testRedoWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.redo)

    def testCheckpoint(self):
        self.log.begin("meh")
        self.log.push(mock.Mock(spec=UndoableAction))
        self.assertRaises(UndoWrongStateError, self.log.checkpoint)
        self.log.rollback()
        self.log.checkpoint()
        self.assertNotEqual(self.log._checkpoint, None)

    def testDirty(self):
        self.assertFalse(self.log.dirty())
        self.log.begin("meh")
        self.log.push(mock.Mock(spec=UndoableAction))
        self.log.commit("meh")
        self.assertTrue(self.log.dirty())
        self.log.checkpoint()
        self.assertFalse(self.log.dirty())
        self.log.undo()
        self.assertTrue(self.log.dirty())
        self.log.redo()
        self.assertFalse(self.log.dirty())

    def testCommit(self):
        """
        Commit a stack.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, ) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.assertEqual(self.log.undo_stacks, [])
        self.log.push(mock.Mock())
        self.log.commit("meh")
        self.assertEqual(len(self.signals), 3)
        name, (stack, action) = self.signals[1]
        self.assertEqual(name, "push")
        name, (stack, ) = self.signals[2]
        self.assertEqual(name, "commit")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def test_commit_proper(self):
        self.log.begin("meh")
        self.assertRaises(UndoWrongStateError, self.log.commit, "notmeh")

    def testNestedCommit(self):
        """
        Do two nested commits.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, ) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("nested")
        self.assertEqual(len(self.signals), 2)
        name, (stack, ) = self.signals[1]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.assertEqual(self.log.undo_stacks, [])
        self.log.push(mock.Mock())
        self.log.commit("nested")
        self.assertEqual(len(self.signals), 4)
        name, (stack, action) = self.signals[2]
        self.assertEqual(name, "push")
        name, (stack, ) = self.signals[3]
        self.assertEqual(name, "commit")
        self.assertTrue(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

        self.assertEqual(self.log.undo_stacks, [])
        self.log.commit("meh")
        self.assertEqual(len(self.signals), 5)
        name, (stack, ) = self.signals[4]
        self.assertEqual(name, "commit")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def test_finalizing_action(self):
        action1 = mock.Mock()
        action2 = mock.Mock()
        with self.log.started("one", finalizing_action=action1):
            self.log.push(mock.Mock())
            with self.log.started("two", finalizing_action=action2):
                self.log.push(mock.Mock())
        action1.do.assert_called_once_with()
        # For now, we call the finalizing action only for the top stack.
        action2.do.assert_not_called()

    def testRollback(self):
        """
        Test a rollback.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, ) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.log.rollback()
        self.assertEqual(len(self.signals), 2)
        name, (stack, ) = self.signals[1]
        self.assertEqual(name, "rollback")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def testNestedRollback(self):
        """
        Test two nested rollbacks.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, ) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("nested")
        self.assertEqual(len(self.signals), 2)
        name, (stack, ) = self.signals[1]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.log.rollback()
        self.assertEqual(len(self.signals), 3)
        name, (stack, ) = self.signals[2]
        self.assertEqual(name, "rollback")
        self.assertTrue(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

        self.log.rollback()
        self.assertEqual(len(self.signals), 4)
        name, (stack, ) = self.signals[3]
        self.assertEqual(name, "rollback")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def testUndoRedo(self):
        """
        Try an undo() redo() sequence.
        """
        # begin
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, ) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        # push two actions
        action1 = mock.Mock(spec=UndoableAction)
        self.log.push(action1)
        self.assertEqual(len(self.signals), 2)
        name, (stack, signalAction) = self.signals[1]
        self.assertEqual(name, "push")
        self.assertTrue(action1 is signalAction)

        action2 = mock.Mock(spec=UndoableAction)
        self.log.push(action2)
        self.assertEqual(len(self.signals), 3)
        name, (stack, signalAction) = self.signals[2]
        self.assertEqual(name, "push")
        self.assertTrue(action2 is signalAction)

        # commit
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.commit("meh")
        self.assertEqual(len(self.signals), 4)
        name, (stack, ) = self.signals[3]
        self.assertEqual(name, "commit")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.assertEqual(action1.do.call_count, 0)
        self.assertEqual(action1.undo.call_count, 0)
        self.assertEqual(action2.do.call_count, 0)
        self.assertEqual(action2.undo.call_count, 0)

        # undo what we just committed
        self.log.undo()
        self.assertEqual(len(self.signals), 5)
        name, stack = self.signals[4]
        self.assertEqual(name, "move")
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 1)
        self.assertEqual(action1.do.call_count, 0)
        self.assertEqual(action1.undo.call_count, 1)
        self.assertEqual(action2.do.call_count, 0)
        self.assertEqual(action2.undo.call_count, 1)

        # redo
        self.log.redo()
        self.assertEqual(len(self.signals), 6)
        name, stack = self.signals[5]
        self.assertEqual(name, "move")
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.assertEqual(action1.do.call_count, 1)
        self.assertEqual(action1.undo.call_count, 1)
        self.assertEqual(action2.do.call_count, 1)
        self.assertEqual(action2.undo.call_count, 1)

    def testOrder(self):
        """
        Test that actions are undone and redone in the correct order.
        """
        order = mock.Mock()
        order.action1 = mock.Mock(spec=UndoableAction)
        order.action2 = mock.Mock(spec=UndoableAction)
        order.action3 = mock.Mock(spec=UndoableAction)

        with self.log.started("meh"):
            self.log.push(order.action1)
            with self.log.started("nested"):
                self.log.push(order.action2)
            self.log.push(order.action3)

        self.log.undo()
        order.assert_has_calls([
            mock.call.action3.undo(),
            mock.call.action2.undo(),
            mock.call.action1.undo()
        ])

        self.log.redo()
        order.assert_has_calls([
            mock.call.action1.do(),
            mock.call.action2.do(),
            mock.call.action3.do()
        ])

        self.log.undo()
        order.assert_has_calls([
            mock.call.action3.undo(),
            mock.call.action2.undo(),
            mock.call.action1.undo()
        ])
Exemple #4
0
class TestUndoableActionLog(common.TestCase):
    """Tests for the UndoableActionLog class."""

    def setUp(self):
        self.log = UndoableActionLog()
        self._connect_to_undoable_action_log(self.log)
        self.signals = []

    def tearDown(self):
        self._disconnect_from_undoable_action_log()

    def check_signals(self, *expected_signals):
        signals = [item[0] for item in self.signals]
        self.assertListEqual(signals, list(expected_signals))

    def _undo_action_log_signal_cb(self, log, *args):
        args = list(args)
        signal_name = args.pop(-1)
        self.signals.append((signal_name, args))

    def _connect_to_undoable_action_log(self, log):
        for signal_name in ("begin", "push", "rollback", "commit", "move"):
            log.connect(signal_name, self._undo_action_log_signal_cb, signal_name)

    def _disconnect_from_undoable_action_log(self):
        self.log.disconnect_by_func(self._undo_action_log_signal_cb)

    def test_rollback_wrong_state(self):
        self.assertRaises(UndoWrongStateError, self.log.rollback)

    def test_commit_wrong_state(self):
        self.assertRaises(UndoWrongStateError, self.log.commit, "")

    def test_push_wrong_state(self):
        # no error in this case
        self.log.push(None)

    def test_undo_wrong_state(self):
        self.assertRaises(UndoWrongStateError, self.log.undo)

    def test_redo_wrong_state(self):
        self.assertRaises(UndoWrongStateError, self.log.redo)

    def test_checkpoint(self):
        self.log.begin("meh")
        self.log.push(mock.Mock(spec=UndoableAction))
        self.assertRaises(UndoWrongStateError, self.log.checkpoint)
        self.log.rollback()
        self.log.checkpoint()
        self.assertNotEqual(self.log._checkpoint, None)

    def test_dirty(self):
        self.assertFalse(self.log.dirty())
        self.log.begin("meh")
        self.log.push(mock.Mock(spec=UndoableAction))
        self.log.commit("meh")
        self.assertTrue(self.log.dirty())
        self.log.checkpoint()
        self.assertFalse(self.log.dirty())
        self.log.undo()
        self.assertTrue(self.log.dirty())
        self.log.redo()
        self.assertFalse(self.log.dirty())

    def test_commit(self):
        """Checks committing a stack."""
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (_stack,) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.assertEqual(self.log.undo_stacks, [])
        self.log.push(mock.Mock(spec=UndoableAction))
        self.log.commit("meh")
        self.assertEqual(len(self.signals), 3)
        name, (_stack, _action) = self.signals[1]
        self.assertEqual(name, "push")
        name, (_stack,) = self.signals[2]
        self.assertEqual(name, "commit")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def test_commit_proper(self):
        self.log.begin("meh")
        self.assertRaises(UndoWrongStateError, self.log.commit, "notmeh")

    def test_nested_commit(self):
        """Checks two nested commits."""
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (_stack,) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("nested")
        self.assertEqual(len(self.signals), 2)
        name, (_stack,) = self.signals[1]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.assertEqual(self.log.undo_stacks, [])
        self.log.push(mock.Mock(spec=UndoableAction))
        self.log.commit("nested")
        self.assertEqual(len(self.signals), 4)
        name, (_stack, _action) = self.signals[2]
        self.assertEqual(name, "push")
        name, (_stack,) = self.signals[3]
        self.assertEqual(name, "commit")
        self.assertTrue(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

        self.assertEqual(self.log.undo_stacks, [])
        self.log.commit("meh")
        self.assertEqual(len(self.signals), 5)
        name, (_stack,) = self.signals[4]
        self.assertEqual(name, "commit")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def test_finalizing_action(self):
        action1 = mock.Mock()
        action2 = mock.Mock()
        with self.log.started("one", finalizing_action=action1):
            self.log.push(mock.Mock(spec=UndoableAction))
            with self.log.started("two", finalizing_action=action2):
                self.log.push(mock.Mock(spec=UndoableAction))
        action1.do.assert_called_once_with()
        # For now, we call the finalizing action only for the top stack.
        action2.do.assert_not_called()

    def test_rollback(self):
        """Checks a rollback."""
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.check_signals("begin")
        name, (_stack,) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        action = mock.Mock(spec=UndoableAction)
        self.log.push(action)

        self.log.rollback()

        action.undo.assert_called_once_with()

        self.check_signals("begin", "push", "rollback")
        name, (_stack,) = self.signals[2]
        self.assertEqual(name, "rollback")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def test_rollback_noop(self):
        """Checks a rollback which does not act."""
        self.log.begin("meh")

        action = mock.Mock(spec=UndoableAction)
        self.log.push(action)

        self.log.rollback(undo=False)
        action.undo.assert_not_called()

    def test_nested_rollback(self):
        """Checks two nested rollbacks."""
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (_stack,) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("nested")
        self.assertEqual(len(self.signals), 2)
        name, (_stack,) = self.signals[1]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        self.log.rollback()
        self.assertEqual(len(self.signals), 3)
        name, (_stack,) = self.signals[2]
        self.assertEqual(name, "rollback")
        self.assertTrue(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

        self.log.rollback()
        self.assertEqual(len(self.signals), 4)
        name, (_stack,) = self.signals[3]
        self.assertEqual(name, "rollback")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def test_undo_redo(self):
        """Tries an undo() redo() sequence."""
        # begin
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (_stack,) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertTrue(self.log.is_in_transaction())

        # push two actions
        action1 = mock.Mock(spec=UndoableAction)
        action1.expand.return_value = False
        self.log.push(action1)
        self.assertEqual(len(self.signals), 2)
        name, (_stack, signal_action) = self.signals[1]
        self.assertEqual(name, "push")
        self.assertTrue(action1 is signal_action)

        action2 = mock.Mock(spec=UndoableAction)
        self.log.push(action2)
        self.assertEqual(len(self.signals), 3)
        name, (_stack, signal_action) = self.signals[2]
        self.assertEqual(name, "push")
        self.assertTrue(action2 is signal_action)

        # commit
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.commit("meh")
        self.assertEqual(len(self.signals), 4)
        name, (_stack,) = self.signals[3]
        self.assertEqual(name, "commit")
        self.assertFalse(self.log.is_in_transaction())
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.assertEqual(action1.do.call_count, 0)
        self.assertEqual(action1.undo.call_count, 0)
        self.assertEqual(action2.do.call_count, 0)
        self.assertEqual(action2.undo.call_count, 0)

        # undo what we just committed
        self.log.undo()
        self.assertEqual(len(self.signals), 5)
        name, _args = self.signals[4]
        self.assertEqual(name, "move")
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 1)
        self.assertEqual(action1.do.call_count, 0)
        self.assertEqual(action1.undo.call_count, 1)
        self.assertEqual(action2.do.call_count, 0)
        self.assertEqual(action2.undo.call_count, 1)

        # redo
        self.log.redo()
        self.assertEqual(len(self.signals), 6)
        name, _args = self.signals[5]
        self.assertEqual(name, "move")
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.assertEqual(action1.do.call_count, 1)
        self.assertEqual(action1.undo.call_count, 1)
        self.assertEqual(action2.do.call_count, 1)
        self.assertEqual(action2.undo.call_count, 1)

    def test_order(self):
        """Checks actions are undone and redone in the correct order."""
        order = mock.Mock()
        order.action1 = mock.Mock(spec=UndoableAction)
        order.action1.expand.return_value = False
        order.action2 = mock.Mock(spec=UndoableAction)
        order.action2.expand.return_value = False
        order.action3 = mock.Mock(spec=UndoableAction)
        order.action3.expand.return_value = False

        with self.log.started("meh"):
            self.log.push(order.action1)
            with self.log.started("nested"):
                self.log.push(order.action2)
            self.log.push(order.action3)

        self.log.undo()
        order.assert_has_calls([mock.call.action3.undo(),
                                mock.call.action2.undo(),
                                mock.call.action1.undo()])

        self.log.redo()
        order.assert_has_calls([mock.call.action1.do(),
                                mock.call.action2.do(),
                                mock.call.action3.do()])

        self.log.undo()
        order.assert_has_calls([mock.call.action3.undo(),
                                mock.call.action2.undo(),
                                mock.call.action1.undo()])

    def test_toplevel_operation(self):
        """Checks the toplevel operations nesting."""
        self.log.begin("one", toplevel=False)
        self.log.commit("one")

        self.log.begin("two", toplevel=True)
        self.log.commit("two")

        self.log.begin("three")
        self.assertRaises(UndoWrongStateError,
                          self.log.begin,
                          "four", toplevel=True)
        self.log.begin("nested1")
        self.log.begin("nested2", toplevel=False)

    def test_failing_operation_rollback(self):
        """Checks that failing operations are rolled back."""
        action = mock.Mock(spec=UndoableAction)

        class WatchingError(Exception):
            pass

        with self.assertRaises(WatchingError):
            with self.log.started("failing_op"):
                self.log.push(action)
                raise WatchingError()

        # Check the rollback happened
        self.assertEqual(action.do.call_count, 0)
        self.assertEqual(action.undo.call_count, 1)
        # Check the undo and redo stacks are empty
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def test_merging(self):
        with self.log.started("one", mergeable=False):
            action = mock.Mock(spec=UndoableAction)
            action.expand.side_effect = Exception("should not have been called")
            self.log.push(action)
        self.assertEqual(len(self.log.undo_stacks), 1)

        with self.log.started("one", mergeable=True):
            action = mock.Mock(spec=UndoableAction)
            action.expand.return_value = False
            self.log.push(action)
        self.assertEqual(len(self.log.undo_stacks), 2)

        with self.log.started("one", mergeable=True):
            action = mock.Mock(spec=UndoableAction)
            action.expand.return_value = True
            self.log.push(action)
        self.assertEqual(len(self.log.undo_stacks), 3)

        with self.log.started("one", mergeable=True):
            action = mock.Mock(spec=UndoableAction)
            action.expand.return_value = True
            self.log.push(action)
        self.assertEqual(len(self.log.undo_stacks), 3)

        with self.log.started("one", mergeable=False):
            action = mock.Mock(spec=UndoableAction)
            action.expand.side_effect = Exception("should not have been called")
            self.log.push(action)
        self.assertEqual(len(self.log.undo_stacks), 4)
Exemple #5
0
class TestUndoableActionLog(TestCase):
    def setUp(self):
        self.log = UndoableActionLog()
        self._connectToUndoableActionLog(self.log)
        self.signals = []

    def tearDown(self):
        self._disconnectFromUndoableActionLog(self.log)

    def _undoActionLogSignalCb(self, log, *args):
        args = list(args)
        signalName = args.pop(-1)
        self.signals.append((signalName, args))

    def _connectToUndoableActionLog(self, log):
        for signalName in ("begin", "push", "rollback", "commit",
                    "undo", "redo"):
            log.connect(signalName, self._undoActionLogSignalCb, signalName)

    def _disconnectFromUndoableActionLog(self, log):
        self.log.disconnect_by_func(self._undoActionLogSignalCb)

    def testRollbackWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.rollback)

    def testCommitWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.commit)

    def testPushWrongState(self):
        # no error in this case
        self.log.push(None)

    def testUndoWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.undo)

    def testRedoWrongState(self):
        self.assertRaises(UndoWrongStateError, self.log.redo)

    def testCheckpoint(self):
        self.log.begin("meh")
        self.log.push(DummyUndoableAction())
        self.assertRaises(UndoWrongStateError, self.log.checkpoint)
        self.log.rollback()
        self.log.checkpoint()
        self.assertNotEqual(self.log._checkpoint, None)

    def testDirty(self):
        self.assertFalse(self.log.dirty())
        self.log.begin("meh")
        self.log.push(DummyUndoableAction())
        self.log.commit()
        self.assertTrue(self.log.dirty())
        self.log.checkpoint()
        self.assertFalse(self.log.dirty())
        self.log.undo()
        self.assertTrue(self.log.dirty())
        self.log.redo()
        self.assertFalse(self.log.dirty())

    def testCommit(self):
        """
        Commit a stack.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, nested) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertFalse(nested)

        self.assertEqual(self.log.undo_stacks, [])
        self.log.commit()
        self.assertEqual(len(self.signals), 2)
        name, (stack, nested) = self.signals[1]
        self.assertEqual(name, "commit")
        self.assertFalse(nested)
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def testNestedCommit(self):
        """
        Do two nested commits.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, nested) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertFalse(nested)

        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("nested")
        self.assertEqual(len(self.signals), 2)
        name, (stack, nested) = self.signals[1]
        self.assertEqual(name, "begin")
        self.assertTrue(nested)

        self.assertEqual(self.log.undo_stacks, [])
        self.log.commit()
        self.assertEqual(len(self.signals), 3)
        name, (stack, nested) = self.signals[2]
        self.assertEqual(name, "commit")
        self.assertTrue(nested)
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

        self.assertEqual(self.log.undo_stacks, [])
        self.log.commit()
        self.assertEqual(len(self.signals), 4)
        name, (stack, nested) = self.signals[3]
        self.assertEqual(name, "commit")
        self.assertFalse(nested)
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def testRollback(self):
        """
        Test a rollback.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, nested) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertFalse(nested)

        self.log.rollback()
        self.assertEqual(len(self.signals), 2)
        name, (stack, nested) = self.signals[1]
        self.assertEqual(name, "rollback")
        self.assertFalse(nested)
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def testNestedRollback(self):
        """
        Test two nested rollbacks.
        """
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, nested) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertFalse(nested)

        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.begin("nested")
        self.assertEqual(len(self.signals), 2)
        name, (stack, nested) = self.signals[1]
        self.assertEqual(name, "begin")
        self.assertTrue(nested)

        self.log.rollback()
        self.assertEqual(len(self.signals), 3)
        name, (stack, nested) = self.signals[2]
        self.assertEqual(name, "rollback")
        self.assertTrue(nested)
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

        self.log.rollback()
        self.assertEqual(len(self.signals), 4)
        name, (stack, nested) = self.signals[3]
        self.assertEqual(name, "rollback")
        self.assertFalse(nested)
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)

    def testUndoRedo(self):
        """
        Try an undo() redo() sequence.
        """
        # begin
        self.log.begin("meh")
        self.assertEqual(len(self.signals), 1)
        name, (stack, nested) = self.signals[0]
        self.assertEqual(name, "begin")
        self.assertFalse(nested)

        # push two actions
        action1 = DummyUndoableAction()
        self.log.push(action1)
        self.assertEqual(len(self.signals), 2)
        name, (stack, signalAction) = self.signals[1]
        self.assertEqual(name, "push")
        self.assertTrue(action1 is signalAction)

        action2 = DummyUndoableAction()
        self.log.push(action2)
        self.assertEqual(len(self.signals), 3)
        name, (stack, signalAction) = self.signals[2]
        self.assertEqual(name, "push")
        self.assertTrue(action2 is signalAction)

        # commit
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 0)
        self.log.commit()
        self.assertEqual(len(self.signals), 4)
        name, (stack, nested) = self.signals[3]
        self.assertEqual(name, "commit")
        self.assertFalse(nested)
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)

        self.assertTrue(action1.done_)
        self.assertTrue(action2.done_)

        # undo what we just committed
        self.log.undo()
        self.assertEqual(len(self.signals), 5)
        name, stack = self.signals[4]
        self.assertEqual(name, "undo")
        self.assertEqual(len(self.log.undo_stacks), 0)
        self.assertEqual(len(self.log.redo_stacks), 1)

        self.assertFalse(action1.done_)
        self.assertFalse(action2.done_)

        # redo
        self.log.redo()
        self.assertEqual(len(self.signals), 6)
        name, stack = self.signals[5]
        self.assertEqual(name, "redo")
        self.assertEqual(len(self.log.undo_stacks), 1)
        self.assertEqual(len(self.log.redo_stacks), 0)

        self.assertTrue(action1.done_)
        self.assertTrue(action2.done_)

    def testOrder(self):
        """
        Test that actions are undone and redone in the correct order.
        """
        call_sequence = []

        class Action(UndoableAction):
            def __init__(self, n):
                self.n = n

            def do(self):
                call_sequence.append("do%s" % self.n)
                self._done()

            def undo(self):
                call_sequence.append("undo%s" % self.n)
                self._undone()

        action1 = Action(1)
        action2 = Action(2)
        action3 = Action(3)

        self.log.begin("meh")
        self.log.push(action1)
        self.log.begin("nested")
        self.log.push(action2)
        self.log.commit()
        self.log.push(action3)
        self.log.commit()

        self.log.undo()
        self.assertEqual(call_sequence, ["undo3", "undo2", "undo1"])

        call_sequence[:] = []
        self.log.redo()
        self.assertEqual(call_sequence, ["do1", "do2", "do3"])

        call_sequence[:] = []
        self.log.undo()
        self.assertEqual(call_sequence, ["undo3", "undo2", "undo1"])