def setUp(self):
     """Set up the test."""
     self.qc = QueueContent(home='/')
     self.handler = MementoHandler()
     self.handler.setLevel(logging.DEBUG)
     logger = logging.getLogger('magicicada.queue_content')
     logger.addHandler(self.handler)
     logger.setLevel(logging.DEBUG)
     self.addCleanup(logger.removeHandler, self.handler)
Example #2
0
    def __init__(self, dbus_class=DBusInterface):
        logger.info("SyncDaemon interface started!")

        # set up dbus and related stuff
        self.dbus = dbus_class(self)

        # attributes for GUI, definition and filling
        self.current_state = State()
        self.folders = None
        self.shares_to_me = None
        self.shares_to_others = None
        self.public_files = None
        self.queue_content = QueueContent(home=user.home)

        # callbacks for GUI to hook in
        self.status_changed_callback = NO_OP
        self.on_started_callback = NO_OP
        self.on_stopped_callback = NO_OP
        self.on_connected_callback = NO_OP
        self.on_disconnected_callback = NO_OP
        self.on_online_callback = NO_OP
        self.on_offline_callback = NO_OP
        self.on_folders_changed_callback = NO_OP
        self.on_shares_to_me_changed_callback = NO_OP
        self.on_shares_to_others_changed_callback = NO_OP
        self.on_public_files_changed_callback = NO_OP
        self.on_metadata_ready_callback = mandatory_callback(
            'on_metadata_ready_callback')
        self.on_initial_data_ready_callback = NO_OP
        self.on_initial_online_data_ready_callback = NO_OP
        self.on_share_op_error_callback = mandatory_callback(
            'on_share_op_error_callback')
        self.on_folder_op_error_callback = mandatory_callback(
            'on_folder_op_error_callback')
        self.on_public_op_error_callback = mandatory_callback(
            'on_public_op_error_callback')
        self.on_node_ops_changed_callback = NO_OP
        self.on_internal_ops_changed_callback = NO_OP
        self.on_transfers_callback = NO_OP

        # poller
        self.transfers_poller = Poller(TRANSFER_POLL_INTERVAL,
                                       self.get_current_transfers)
        self._check_started()
class GetPathTestCase(unittest.TestCase):
    """Test how we get the path from the operation."""

    def setUp(self):
        """Set up the test."""
        self.qc = QueueContent(home='/')

    def test_makefile(self):
        """Get path from makefile."""
        r = self.qc._get_path_elements('MakeFile', dict(path='foo'))
        self.assertEqual(r, ['', 'foo'])

    def test_makedir(self):
        """Get path from makedir."""
        r = self.qc._get_path_elements('MakeDir', dict(path='foo'))
        self.assertEqual(r, ['', 'foo'])

    def test_move(self):
        """Get path from Move."""
        r = self.qc._get_path_elements('Move',
                                       dict(path_from='foo', path_to='bar'))
        self.assertEqual(r, ['', 'foo'])

    def test_unlink(self):
        """Get path from unlink."""
        r = self.qc._get_path_elements('Unlink', dict(path='foo'))
        self.assertEqual(r, ['', 'foo'])

    def test_upload(self):
        """Get path from upload."""
        r = self.qc._get_path_elements('Upload', dict(path='foo'))
        self.assertEqual(r, ['', 'foo'])

    def test_download(self):
        """Get path from download."""
        r = self.qc._get_path_elements('Download', dict(path='foo'))
        self.assertEqual(r, ['', 'foo'])
Example #4
0
class SyncDaemon(object):
    """Interface to Ubuntu One's SyncDaemon."""

    def __init__(self, dbus_class=DBusInterface):
        logger.info("SyncDaemon interface started!")

        # set up dbus and related stuff
        self.dbus = dbus_class(self)

        # attributes for GUI, definition and filling
        self.current_state = State()
        self.folders = None
        self.shares_to_me = None
        self.shares_to_others = None
        self.public_files = None
        self.queue_content = QueueContent(home=user.home)

        # callbacks for GUI to hook in
        self.status_changed_callback = NO_OP
        self.on_started_callback = NO_OP
        self.on_stopped_callback = NO_OP
        self.on_connected_callback = NO_OP
        self.on_disconnected_callback = NO_OP
        self.on_online_callback = NO_OP
        self.on_offline_callback = NO_OP
        self.on_folders_changed_callback = NO_OP
        self.on_shares_to_me_changed_callback = NO_OP
        self.on_shares_to_others_changed_callback = NO_OP
        self.on_public_files_changed_callback = NO_OP
        self.on_metadata_ready_callback = mandatory_callback(
            'on_metadata_ready_callback')
        self.on_initial_data_ready_callback = NO_OP
        self.on_initial_online_data_ready_callback = NO_OP
        self.on_share_op_error_callback = mandatory_callback(
            'on_share_op_error_callback')
        self.on_folder_op_error_callback = mandatory_callback(
            'on_folder_op_error_callback')
        self.on_public_op_error_callback = mandatory_callback(
            'on_public_op_error_callback')
        self.on_node_ops_changed_callback = NO_OP
        self.on_internal_ops_changed_callback = NO_OP
        self.on_transfers_callback = NO_OP

        # poller
        self.transfers_poller = Poller(TRANSFER_POLL_INTERVAL,
                                       self.get_current_transfers)
        self._check_started()

    @defer.inlineCallbacks
    def _check_started(self):
        """Check if started and load initial data if yes."""
        # load initial data if ubuntuone-client already started
        started = yield self.dbus.is_sd_started()
        if started:
            self.current_state.set(is_started=True)
            self._get_initial_data()
        else:
            self.current_state.set(is_started=False)

    def shutdown(self):
        """Shut down the SyncDaemon."""
        logger.info("SyncDaemon interface going down")
        self.transfers_poller.run(False)
        self.dbus.shutdown()

    @defer.inlineCallbacks
    def get_current_transfers(self):
        """Get downloads and uploads."""
        uploads = yield self.dbus.get_current_uploads()
        downloads = yield self.dbus.get_current_downloads()
        self.on_transfers_callback(uploads + downloads)

    @defer.inlineCallbacks
    def _get_initial_data(self):
        """Get the initial SD data."""
        logger.info("Getting offline initial data")

        status_data = yield self.dbus.get_status()
        self._send_status_changed(*status_data)

        # queue content stuff
        shares_real_dir = yield self.dbus.get_real_shares_dir()
        shares_link_dir = yield self.dbus.get_link_shares_dir()
        self.queue_content.set_shares_dirs(shares_link_dir, shares_real_dir)
        content = yield self.dbus.get_queue_content()
        self.queue_content.set_content(content)
        self.transfers_poller.run(self.queue_content.transferring)

        self.folders = yield self.dbus.get_folders()

        self.shares_to_me = yield self.dbus.get_shares_to_me()
        self.shares_to_others = yield self.dbus.get_shares_to_others()

        # let frontend know that we have all the initial offline data
        logger.info("All initial offline data is ready")
        self.on_initial_data_ready_callback()

        logger.info("Getting online initial data")
        self.public_files = yield self.dbus.get_public_files()

        # let frontend know that we have all the initial online data
        logger.info("All initial online data is ready")
        self.on_initial_online_data_ready_callback()

    @defer.inlineCallbacks
    def on_sd_public_files_changed(self, pf=None, is_public=False):
        """Update the Public Files list."""
        data = yield self.dbus.get_public_files()
        logger.info("Got new Public Files list (%d items)", len(data))
        self.public_files = data
        self.on_public_files_changed_callback(self.public_files)

    @defer.inlineCallbacks
    def on_sd_shares_changed(self):
        """Shares changed, ask for new information."""
        logger.info("SD Shares changed")

        # to me
        new_to_me = yield self.dbus.get_shares_to_me()
        if new_to_me != self.shares_to_me:
            self.shares_to_me = new_to_me
            self.on_shares_to_me_changed_callback(new_to_me)

        # to others
        new_to_others = yield self.dbus.get_shares_to_others()
        if new_to_others != self.shares_to_others:
            self.shares_to_others = new_to_others
            self.on_shares_to_others_changed_callback(new_to_others)

    @defer.inlineCallbacks
    def on_sd_folders_changed(self):
        """Folders changed, ask for new information."""
        logger.info("SD Folders changed")
        self.folders = yield self.dbus.get_folders()
        self.on_folders_changed_callback(self.folders)

    def on_sd_status_changed(self, *status_data):
        """The Status of SD changed.."""
        logger.info("SD Status changed")
        self._send_status_changed(*status_data)

    def _send_status_changed(self, name, description, is_error, is_connected,
                             is_online, queues, connection):
        """Send status changed signal."""
        kwargs = dict(name=name, description=description,
                      is_error=is_error, is_connected=is_connected,
                      is_online=is_online, queues=queues,
                      connection=connection)

        # check status changes to call other callbacks
        if is_connected and not self.current_state.is_connected:
            self.on_connected_callback()
        if not is_connected and self.current_state.is_connected:
            self.on_disconnected_callback()
        if is_online and not self.current_state.is_online:
            self.on_online_callback()
        if not is_online and self.current_state.is_online:
            self.on_offline_callback()

        # state of SD
        if name == "SHUTDOWN":
            state = STATE_STOPPED
            self.current_state.set(is_started=False)
            self.on_stopped_callback()
        elif name in ('READY', 'WAITING'):
            if connection == 'With User With Network':
                state = STATE_CONNECTING
            else:
                state = STATE_DISCONNECTED
        elif name == 'STANDOFF':
            state = STATE_DISCONNECTED
        else:
            if is_connected:
                if name == "QUEUE_MANAGER":
                    if queues == "IDLE":
                        state = STATE_IDLE
                    else:
                        state = STATE_WORKING
                else:
                    state = STATE_CONNECTING
            else:
                state = STATE_STARTING
                # check if it's the first time we flag starting
                if self.current_state.state != STATE_STARTING:
                    self.current_state.set(is_started=True)
                    self.on_started_callback()
                    self._get_initial_data()
        kwargs['state'] = state
        xs = sorted(kwargs.iteritems())
        logger.debug("    new status: %s", ', '.join('%s=%r' % i for i in xs))

        # set current state to new values and call status changed cb
        self.current_state.set(**kwargs)
        self.status_changed_callback(**kwargs)

    def on_sd_queue_added(self, op_name, op_id, op_data):
        """A command was added to the Request Queue."""
        logger.info("Queue content: added %r [%s] %s", op_name, op_id, op_data)
        r = self.queue_content.add(op_name, op_id, op_data)
        if r == NODE_OP:
            self.on_node_ops_changed_callback(self.queue_content.node_ops)
        elif r == INTERNAL_OP:
            self.on_internal_ops_changed_callback(
                self.queue_content.internal_ops)
        self.transfers_poller.run(self.queue_content.transferring)

    def on_sd_queue_removed(self, op_name, op_id, op_data):
        """A command was removed from the Request Queue."""
        logger.info("Queue content: removed %r [%s] %s",
                    op_name, op_id, op_data)
        r = self.queue_content.remove(op_name, op_id, op_data)
        if r == NODE_OP:
            self.on_node_ops_changed_callback(self.queue_content.node_ops)
        elif r == INTERNAL_OP:
            self.on_internal_ops_changed_callback(
                self.queue_content.internal_ops)
        self.transfers_poller.run(self.queue_content.transferring)

    def start(self):
        """Start the SyncDaemon."""
        logger.info("Starting u1.SD")
        d = self.dbus.start()
        self._get_initial_data()
        return d

    def quit(self):
        """Stop the SyncDaemon and makes it quit."""
        logger.info("Stopping u1.SD")
        return self.dbus.quit()

    def connect(self):
        """Tell the SyncDaemon that the user wants it to connect."""
        logger.info("Telling u1.SD to connect")
        return self.dbus.connect()

    def disconnect(self):
        """Tell the SyncDaemon that the user wants it to disconnect."""
        logger.info("Telling u1.SD to disconnect")
        return self.dbus.disconnect()

    @defer.inlineCallbacks
    def get_metadata(self, path):
        """Get the metadata for given path."""
        resp = yield self.dbus.get_metadata(os.path.realpath(path))
        if resp == NOT_SYNCHED_PATH:
            self.on_metadata_ready_callback(path, resp)
            return

        # have data! store it in raw, and process some
        result = dict(raw_result=resp)

        # stat
        if resp['stat'] == u'None':
            stat = None
        else:
            items = re.match(".*\((.*)\)", resp['stat']).groups()[0]
            items = [x.split("=") for x in items.split(", ")]
            stat = dict((a, int(b[:-1] if b[-1] == 'L' else b))
                        for a, b in items)
        result['stat'] = stat

        # changed
        is_partial = resp['info_is_partial'] != u'False'
        if resp['local_hash'] == resp['server_hash']:
            if is_partial:
                logger.warning("Bad 'changed' values: %r", resp)
                changed = None
            else:
                changed = CHANGED_NONE
        else:
            if is_partial:
                changed = CHANGED_SERVER
            else:
                changed = CHANGED_LOCAL
        result['changed'] = changed

        # path
        processed_path = resp['path']
        if processed_path.startswith(user.home):
            processed_path = "~" + processed_path[len(user.home):]
        result['path'] = processed_path

        self.on_metadata_ready_callback(path, result)

    @defer.inlineCallbacks
    def get_free_space(self, volume_id):
        """Get the free space for a volume."""
        free_space = yield self.dbus.get_free_space(volume_id)
        defer.returnValue(int(free_space))

    def _answer_share(self, share_id, method, action_name):
        """Effectively accept or reject a share."""
        def error(failure):
            """Operation failed."""
            if failure.check(ShareOperationError):
                error = failure.value.error
                logger.info("%s share %s finished with error: %s",
                            action_name, share_id, error)
                self.on_share_op_error_callback(share_id, error)
            else:
                logger.error("Unexpected error when %s share %s: %s %s",
                             action_name.lower(), share_id,
                             failure.type, failure.value)

        def success(_):
            """Operation finished ok."""
            logger.info("%s share %s finished successfully",
                        action_name, share_id)

        logger.info("%s share %s started", action_name, share_id)
        d = method(share_id)
        d.addCallbacks(success, error)

    def accept_share(self, share_id):
        """Accept a share."""
        self._answer_share(share_id, self.dbus.accept_share, "Accepting")

    def reject_share(self, share_id):
        """Reject a share."""
        self._answer_share(share_id, self.dbus.reject_share, "Rejecting")

    def subscribe_share(self, share_id):
        """Subscribe a share."""
        self._answer_share(share_id, self.dbus.subscribe_share, "Subscribing")

    def unsubscribe_share(self, share_id):
        """Unsubscribe a share."""
        self._answer_share(share_id, self.dbus.unsubscribe_share,
                           "Unsubscribing")

    def send_share_invitation(self, path, mail_address, sh_name, access_level):
        """Send a share invitation."""
        def error(failure):
            """Operation failed."""
            if failure.check(ShareOperationError):
                error = failure.value.error
                logger.info("Sending share invitation finished with error %s "
                            "(path=%r mail_address=%s share_name=%r "
                            "access_level=%s)", error, path, mail_address,
                            sh_name, access_level)
            else:
                logger.error("Unexpected error when sending share invitation "
                             "%s %s (path=%r mail_address=%s share_name=%r "
                             "access_level=%s)", failure.type, failure.value,
                             path, mail_address, sh_name, access_level)

        def success(_):
            """Operation finished ok."""
            logger.info("Sending share invitation finished successfully "
                        "(path=%r mail_address=%s share_name=%r "
                        "access_level=%s", path, mail_address,
                        sh_name, access_level)

        logger.info("Sending share invitation: path=%r mail_address=%s "
                    "share_name=%r access_level=%s", path, mail_address,
                    sh_name, access_level)
        d = self.dbus.send_share_invitation(path, mail_address,
                                            sh_name, access_level)
        d.addCallbacks(success, error)

    @defer.inlineCallbacks
    def _folder_operation(self, operation, value, op_name):
        """Generic folder operation."""
        try:
            result = yield operation(value)
        except FolderOperationError, e:
            logger.info("%s folder (on %r) finished with error: %s",
                        op_name, value, e)
            self.on_folder_op_error_callback(e)
        else:
 def setUp(self):
     """Set up the test."""
     self.qc = QueueContent(home='/a/b')
class TransferringFlagTestCase(unittest.TestCase):
    """Tests that check the transferring flag."""

    def setUp(self):
        """Set up the test."""
        self.qc = QueueContent(home='/a/b')

    def test_init(self):
        """Of course, at start time we're not transferring."""
        self.assertFalse(self.qc.transferring)

    def test_add_non_transferring_op(self):
        """Add a non transferring op."""
        self.qc.add('ListShares', '456', {'a': 3})
        self.assertFalse(self.qc.transferring)

    def test_add_download(self):
        """Add a download."""
        self.qc.add('Download', '456', {'path': 'foo'})
        self.assertTrue(self.qc.transferring)

    def test_add_remove_download(self):
        """Add and remove a download."""
        self.qc.add('Download', '456', {'path': 'foo'})
        self.qc.remove('Download', '456', {'path': 'foo'})
        self.assertFalse(self.qc.transferring)

    def test_add_upload(self):
        """Add a download."""
        self.qc.add('Upload', '456', {'path': 'foo'})
        self.assertTrue(self.qc.transferring)

    def test_add_remove_upload(self):
        """Add and remove a download."""
        self.qc.add('Upload', '456', {'path': 'foo'})
        self.qc.remove('Upload', '456', {'path': 'foo'})
        self.assertFalse(self.qc.transferring)

    def test_add_different_combinations(self):
        """Different combinations."""
        self.qc.add('Upload', '1', {'path': 'foo'})
        self.assertTrue(self.qc.transferring)

        self.qc.add('ListShares', '2', {'a': 3})
        self.assertTrue(self.qc.transferring)

        self.qc.add('Download', '3', {'path': 'foo'})
        self.assertTrue(self.qc.transferring)

        self.qc.remove('Upload', '1', {'path': 'foo'})
        self.assertTrue(self.qc.transferring)

        self.qc.remove('Download', '3', {'path': 'foo'})
        self.assertFalse(self.qc.transferring)

        self.qc.remove('ListShares', '2', {'a': 3})
        self.assertFalse(self.qc.transferring)
class InternalStructureTestCase(unittest.TestCase):
    """Tests that we store the internal commands ok."""

    def setUp(self):
        """Set up the test."""
        self.qc = QueueContent(home='/a/b')

    def test_none(self):
        """No operations added."""
        self.assertFalse(self.qc.internal_ops)

    def test_one_added(self):
        """Add one op."""
        self.qc.add('ListShares', '456', {'a': 3})
        data = self.qc.internal_ops[0]
        self.assertTrue(isinstance(data.timestamp, float))
        self.assertEqual(data.op_name, 'ListShares')
        self.assertEqual(data.op_id, '456')
        self.assertEqual(data.op_data, {'a': 3})
        self.assertEqual(data.action, ACTION_ADDED)

    def test_one_removed(self):
        """Remove one op."""
        self.qc.remove('ListShares', '456', {'a': 3})
        data = self.qc.internal_ops[0]
        self.assertTrue(isinstance(data.timestamp, float))
        self.assertEqual(data.op_name, 'ListShares')
        self.assertEqual(data.op_id, '456')
        self.assertEqual(data.op_data, {'a': 3})
        self.assertEqual(data.action, ACTION_REMOVED)

    def test_same_added_removed(self):
        """Add and remove the same op."""
        self.qc.add('ListShares', '456', {'a': 3})
        self.qc.remove('ListShares', '456', {'a': 3})

        data = self.qc.internal_ops[0]
        op_added_tstamp = data.timestamp
        self.assertTrue(isinstance(data.timestamp, float))
        self.assertEqual(data.op_name, 'ListShares')
        self.assertEqual(data.op_id, '456')
        self.assertEqual(data.op_data, {'a': 3})
        self.assertEqual(data.action, ACTION_ADDED)

        data = self.qc.internal_ops[1]
        self.assertTrue(data.timestamp > op_added_tstamp)
        self.assertTrue(isinstance(data.timestamp, float))
        self.assertEqual(data.op_name, 'ListShares')
        self.assertEqual(data.op_id, '456')
        self.assertEqual(data.op_data, {'a': 3})
        self.assertEqual(data.action, ACTION_REMOVED)

    def test_several_mixed(self):
        """Add and remove several operations."""
        self.qc.add('ListShares', '456', {'a': 3})
        self.qc.add('GetDelta', '789', {'b': 5})
        self.qc.remove('ListShares', '123', {'c': 7})

        data = self.qc.internal_ops[0]
        self.assertTrue(isinstance(data.timestamp, float))
        self.assertEqual(data.op_name, 'ListShares')
        self.assertEqual(data.op_id, '456')
        self.assertEqual(data.op_data, {'a': 3})
        self.assertEqual(data.action, ACTION_ADDED)

        data = self.qc.internal_ops[1]
        self.assertTrue(isinstance(data.timestamp, float))
        self.assertEqual(data.op_name, 'GetDelta')
        self.assertEqual(data.op_id, '789')
        self.assertEqual(data.op_data, {'b': 5})
        self.assertEqual(data.action, ACTION_ADDED)

        data = self.qc.internal_ops[2]
        self.assertTrue(isinstance(data.timestamp, float))
        self.assertEqual(data.op_name, 'ListShares')
        self.assertEqual(data.op_id, '123')
        self.assertEqual(data.op_data, {'c': 7})
        self.assertEqual(data.action, ACTION_REMOVED)
 def setUp(self):
     """Set up the test."""
     self.qc = QueueContent(home='/a/b')
     self.qc.set_shares_dirs('/a/b/link', '/a/b/real')
class DeliverNodeDataTestCase(unittest.TestCase):
    """Send the node data without the home."""

    def setUp(self):
        """Set up the test."""
        self.qc = QueueContent(home='/a/b')
        self.qc.set_shares_dirs('/a/b/link', '/a/b/real')

    def test_share_link_inside_home(self):
        """Assure the share link is inside home."""
        self.assertRaises(ValueError, self.qc.set_shares_dirs,
                          share_link='/a/k', share_real='/a/b/r')

    def test_share_real_inside_home(self):
        """Assure the share real is inside home."""
        self.assertRaises(ValueError, self.qc.set_shares_dirs,
                          share_link='/a/b/r', share_real='/a/k')

    def test_none(self):
        """Test getting with nothing."""
        self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME)
        self.assertEqual(self.qc.node_ops[0][1], {})

    def test_one_home(self):
        """Test getting with one in home."""
        self.qc.add('MakeFile', '67', {'path': '/a/b/foo/fighters'})
        self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME)
        node = self.qc.node_ops[0][1]['foo']
        self.assertEqual(node.last_modified, None)
        self.assertEqual(node.kind, KIND_DIR)
        self.assertEqual(node.operations, [])
        self.assertEqual(node.done, None)
        self.assertEqual(len(node.children), 1)

    def test_two_home(self):
        """Test getting with two in home."""
        self.qc.set_content([('MakeFile', '67',
                             {'path': '/a/b/foo/fighters'})])
        self.qc.set_content([('MakeFile', '99', {'path': '/a/b/bar'})])
        self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME)
        self.assertEqual(len(self.qc.node_ops[0][1]), 2)

        # first node
        node = self.qc.node_ops[0][1]['foo']
        self.assertEqual(node.last_modified, None)
        self.assertEqual(node.kind, KIND_DIR)
        self.assertEqual(node.operations, [])
        self.assertEqual(node.done, None)
        self.assertEqual(len(node.children), 1)

        # second node
        node = self.qc.node_ops[0][1]['bar']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_FILE)
        expected = [('99', 'MakeFile',
                     {'path': '/a/b/bar', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

    def test_one_share(self):
        """Test getting with one share."""
        self.qc.set_content([('MakeFile', '12', {'path': '/a/b/real/foo'})])
        self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME)

        node = self.qc.node_ops[0][1]['link']
        self.assertEqual(node.last_modified, None)
        self.assertEqual(node.kind, KIND_DIR)
        self.assertEqual(node.operations, [])
        self.assertEqual(node.done, None)
        self.assertEqual(len(node.children), 1)

        node = node.children['foo']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_FILE)
        expected = [('12', 'MakeFile',
                     {'path': '/a/b/real/foo', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

    def test_several_mixed(self):
        """Test mixing two nodes in the same share, other share, and home."""
        self.qc.set_content([('MakeDir', '0', {'path': '/a/b/j'})])
        self.qc.set_content([('MakeDir', '1', {'path': '/a/b/real/foo/bar1'})])
        self.qc.set_content([('MakeDir', '2', {'path': '/a/b/real/foo/bar2'})])
        self.qc.set_content([('MakeDir', '3', {'path': '/a/b/real/othr/baz'})])
        self.assertEqual(self.qc.node_ops[0][0], ROOT_HOME)
        self.assertIn('j', self.qc.node_ops[0][1])
        link_dir = self.qc.node_ops[0][1]['link']
        self.assertIn('othr', link_dir.children)
        foo_dir = link_dir.children['foo']
        self.assertIn('bar1', foo_dir.children)
        self.assertIn('bar2', foo_dir.children)
class ClearNodesTestCase(unittest.TestCase):
    """Clear the nodes on request."""

    def setUp(self):
        """Set up the test."""
        self.qc = QueueContent(home='/')

    def test_empty_structure(self):
        """Empty structure."""
        self.qc.clear()
        self.assertFalse(self.qc._node_ops)

    def test_nothing_to_clear(self):
        """Commands that are not done."""
        self.qc.add('MakeFile', '12', {'path': '/a'})
        self.qc.clear()
        self.assertIn('a', self.qc._node_ops[''].children)

    def test_one_to_clear(self):
        """Commands that can be removed."""
        self.qc.add('MakeFile', '12', {'path': '/a'})
        self.qc.remove('MakeFile', '12', {'path': '/a'})
        self.qc.clear()
        self.assertNotIn('', self.qc._node_ops)
        self.assertFalse(self.qc._node_ops)

    def test_one_to_alter(self):
        """Commands that are not done."""
        self.qc.add('MakeDir', '12', {'path': '/a'})
        self.qc.add('MakeFile', '23', {'path': '/a/b'})
        self.qc.remove('MakeDir', '12', {'path': '/a'})

        node = self.qc._node_ops[''].children['a']
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('12', 'MakeDir', {'path': '/a', '__done__': True})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, True)
        self.assertEqual(len(node.children), 1)

        node = node.children['b']
        self.assertEqual(node.kind, KIND_FILE)
        expected = [('23', 'MakeFile', {'path': '/a/b', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)

        # clear and check again
        self.qc.clear()
        node = self.qc._node_ops[''].children['a']
        self.assertEqual(node.kind, KIND_DIR)
        self.assertEqual(node.operations, [])
        self.assertEqual(node.done, None)
        self.assertEqual(len(node.children), 1)

        node = node.children['b']
        self.assertEqual(node.kind, KIND_FILE)
        expected = [('23', 'MakeFile', {'path': '/a/b', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)

    def test_one_fixed_and_two_to_clear(self):
        """Commands that can be removed."""
        # create them  all
        self.qc.add('MakeDir', '12', {'path': '/a/b'})
        self.qc.add('MakeDir', '23', {'path': '/a/h'})
        self.qc.add('MakeFile', '45', {'path': '/a/b/c'})

        node_a = self.qc._node_ops[''].children['a']
        self.assertEqual(node_a.done, None)

        node_b = node_a.children['b']
        self.assertEqual(node_b.done, False)

        node_h = node_a.children['h']
        self.assertEqual(node_h.done, False)

        node_c = node_b.children['c']
        self.assertEqual(node_c.done, False)

        # remove the makedir and makefile
        self.qc.remove('MakeDir', '12', {'path': '/a/b'})
        self.qc.remove('MakeFile', '45', {'path': '/a/b/c'})
        self.assertEqual(node_b.done, True)
        self.assertEqual(node_c.done, True)

        # clear and check
        self.qc.clear()

        node_a = self.qc._node_ops[''].children['a']
        self.assertEqual(node_a.done, None)

        node_h = node_a.children['h']
        self.assertEqual(node_h.done, False)

        self.assertNotIn('b', node_a.children)
class TwoStructuresTestCase(unittest.TestCase):
    """Tests that we change the corresponding structure."""

    def setUp(self):
        """Set up the test."""
        self.qc = QueueContent(home='/a/b')

    def test_set_empty(self):
        """Set nothing."""
        self.qc.set_content([])
        self.assertFalse(self.qc._node_ops)
        self.assertFalse(self.qc.internal_ops)

    def test_set_one_node_makefile(self):
        """Set one node with a file op."""
        self.qc.set_content([('MakeFile', '123', {'path': '/a/b/foo'})])
        self.assertTrue(self.qc._node_ops)
        self.assertFalse(self.qc.internal_ops)

    def test_set_one_node_makedir(self):
        """Set one node with a file op."""
        self.qc.set_content([('MakeDir', '123', {'path': '/a/b/foo'})])
        self.assertTrue(self.qc._node_ops)
        self.assertFalse(self.qc.internal_ops)

    def test_set_one_node_unlink(self):
        """Set one node with a file op."""
        self.qc.set_content([('Unlink', '123', {'path': '/a/b/foo'})])
        self.assertTrue(self.qc._node_ops)
        self.assertFalse(self.qc.internal_ops)

    def test_set_one_node_move(self):
        """Set one node with a file op."""
        self.qc.set_content([('Move', '123', {'path_from': '/a/b/foo'})])
        self.assertTrue(self.qc._node_ops)
        self.assertFalse(self.qc.internal_ops)

    def test_set_one_node_upload(self):
        """Set one node with a file op."""
        self.qc.set_content([('Upload', '123', {'path': '/a/b/foo'})])
        self.assertTrue(self.qc._node_ops)
        self.assertFalse(self.qc.internal_ops)

    def test_set_one_node_download(self):
        """Set one node with a file op."""
        self.qc.set_content([('Download', '123', {'path': '/a/b/foo'})])
        self.assertTrue(self.qc._node_ops)
        self.assertFalse(self.qc.internal_ops)

    def test_set_one_internal(self):
        """Set one internal op."""
        self.qc.set_content([('ListShares', '456', {})])
        self.assertFalse(self.qc._node_ops)
        self.assertTrue(self.qc.internal_ops)

    def test_set_mixed_ops(self):
        """Set a couple of node and internal ops."""
        self.qc.set_content([('MakeFile', '123', {'path': 'foo'}),
                             ('ListShares', '456', {})])
        self.assertTrue(self.qc._node_ops)
        self.assertTrue(self.qc.internal_ops)

    def test_set_is_several_adds(self):
        """Check that one set are several adds."""
        called = []
        self.qc.add = lambda *a: called.append(a)
        self.qc.set_content([('MakeFile', '123', {'path': 'foo'}),
                             ('ListShares', '456', {})])
        self.assertTrue(called[0], ('MakeFile', '123', {'path': 'foo'}))
        self.assertTrue(called[1], ('ListShares', '456', {}))

    def test_add_one_node(self):
        """Add one node op."""
        r = self.qc.add('Unlink', '456', {'path': 'foo'})
        self.assertTrue(self.qc._node_ops)
        self.assertFalse(self.qc.internal_ops)
        self.assertEqual(r, NODE_OP)

    def test_add_one_internal(self):
        """Add one internal op."""
        r = self.qc.add('ListShares', '456', {})
        self.assertFalse(self.qc._node_ops)
        self.assertTrue(self.qc.internal_ops)
        self.assertEqual(r, INTERNAL_OP)

    def test_remove_one_node(self):
        """Remove one node op."""
        self.qc.add('Unlink', '456', {'path': 'foo'})
        r = self.qc.remove('Unlink', '456', {'path': 'foo'})
        self.assertTrue(self.qc._node_ops)
        self.assertFalse(self.qc.internal_ops)
        self.assertEqual(r, NODE_OP)

    def test_remove_one_internal(self):
        """Remove one internal op."""
        self.qc.add('ListShares', '456', {})
        r = self.qc.remove('ListShares', '456', {})
        self.assertFalse(self.qc._node_ops)
        self.assertTrue(self.qc.internal_ops)
        self.assertEqual(r, INTERNAL_OP)
class NodeStructureTestCase(unittest.TestCase):
    """Tests that we store the node commands ok."""

    def setUp(self):
        """Set up the test."""
        self.qc = QueueContent(home='/')
        self.handler = MementoHandler()
        self.handler.setLevel(logging.DEBUG)
        logger = logging.getLogger('magicicada.queue_content')
        logger.addHandler(self.handler)
        logger.setLevel(logging.DEBUG)
        self.addCleanup(logger.removeHandler, self.handler)

    def test_one_node_file(self):
        """Add one node with a file op."""
        self.qc.set_content([('MakeFile', '123', {'path': '/a/b/foo'})])
        self.assertEqual(len(self.qc._node_ops), 1)

        node = self.qc._node_ops[''].children['a']
        self.assertEqual(node.last_modified, None)
        self.assertEqual(node.kind, KIND_DIR)
        self.assertEqual(node.operations, [])
        self.assertEqual(node.done, None)
        self.assertEqual(len(node.children), 1)

        node = node.children['b']
        self.assertEqual(node.last_modified, None)
        self.assertEqual(node.kind, KIND_DIR)
        self.assertEqual(node.operations, [])
        self.assertEqual(node.done, None)
        self.assertEqual(len(node.children), 1)

        node = node.children['foo']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_FILE)
        expected = [('123', 'MakeFile',
                     {'path': '/a/b/foo', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

    def test_one_node_dir(self):
        """Add one node with a dir op."""
        self.qc.set_content([('MakeDir', '123', {'path': '/a/boo'})])
        self.assertEqual(len(self.qc._node_ops), 1)

        node = self.qc._node_ops[''].children['a']
        self.assertEqual(node.last_modified, None)
        self.assertEqual(node.kind, KIND_DIR)
        self.assertEqual(node.operations, [])
        self.assertEqual(node.done, None)
        self.assertEqual(len(node.children), 1)

        node = node.children['boo']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('123', 'MakeDir', {'path': '/a/boo', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

    def test_one_node_unknown(self):
        """Add one node with a unknown op."""
        self.qc.set_content([('Unlink', '123', {'path': '/a/boo'})])
        self.assertEqual(len(self.qc._node_ops), 1)

        node = self.qc._node_ops[''].children['a']
        self.assertEqual(node.last_modified, None)
        self.assertEqual(node.kind, KIND_DIR)
        self.assertEqual(node.operations, [])
        self.assertEqual(node.done, None)
        self.assertEqual(len(node.children), 1)

        node = node.children['boo']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_UNKNOWN)
        expected = [('123', 'Unlink', {'path': '/a/boo', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

    def test_one_node_known_unknown(self):
        """Add one node known op with a later unknown op."""
        self.qc.set_content([('MakeFile', '123', {'path': '/a'})])
        self.assertEqual(len(self.qc._node_ops), 1)

        node = self.qc._node_ops[''].children['a']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_FILE)
        expected = [('123', 'MakeFile', {'path': '/a', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

        self.qc.set_content([('Unlink', '456', {'path': '/a'})])
        self.assertEqual(len(self.qc._node_ops), 1)

        node = self.qc._node_ops[''].children['a']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_FILE)
        expected = [('123', 'MakeFile', {'path': '/a', '__done__': False}),
                    ('456', 'Unlink', {'path': '/a', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

    def test_one_node_unknown_known(self):
        """Add one node unknown op with a later known op."""
        self.qc.set_content([('Unlink', '123', {'path': '/a'})])
        self.assertEqual(len(self.qc._node_ops), 1)

        node = self.qc._node_ops[''].children['a']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_UNKNOWN)
        expected = [('123', 'Unlink', {'path': '/a', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

        self.qc.set_content([('MakeDir', '456', {'path': '/a'})])
        self.assertEqual(len(self.qc._node_ops), 1)

        node = self.qc._node_ops[''].children['a']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('123', 'Unlink', {'path': '/a', '__done__': False}),
                    ('456', 'MakeDir', {'path': '/a', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

    def test_several_nodes_mixed(self):
        """Add some nodes with different combinations."""
        # add /a/b
        self.qc.set_content([('MakeDir', '12', {'path': '/a/b'})])
        self.assertEqual(len(self.qc._node_ops), 1)

        node = self.qc._node_ops[''].children['a']
        self.assertEqual(node.last_modified, None)
        self.assertEqual(node.kind, KIND_DIR)
        self.assertEqual(node.operations, [])
        self.assertEqual(node.done, None)
        self.assertEqual(len(node.children), 1)

        node = node.children['b']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('12', 'MakeDir', {'path': '/a/b', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

        # add /a/b/foo
        self.qc.set_content([('MakeDir', '34', {'path': '/a/b/foo'})])
        node = self.qc._node_ops[''].children['a']

        node = node.children['b']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('12', 'MakeDir', {'path': '/a/b', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 1)

        node = node.children['foo']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('34', 'MakeDir', {'path': '/a/b/foo', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

        # add /a/b/bar
        self.qc.set_content([('MakeDir', '45', {'path': '/a/b/bar'})])
        node = self.qc._node_ops[''].children['a']

        node = node.children['b']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('12', 'MakeDir', {'path': '/a/b', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 2)

        node = node.children['bar']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('45', 'MakeDir', {'path': '/a/b/bar', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

        # add /a/b/foo/fighters
        self.qc.set_content([('MakeFile', '67',
                              {'path': '/a/b/foo/fighters'})])
        node = self.qc._node_ops[''].children['a']

        node = node.children['b']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('12', 'MakeDir', {'path': '/a/b', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 2)

        node = node.children['foo']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('34', 'MakeDir', {'path': '/a/b/foo', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 1)

        node = node.children['fighters']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_FILE)
        expected = [('67', 'MakeFile',
                     {'path': '/a/b/foo/fighters', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

        # other /a/b/foo/fighters
        self.qc.set_content([('Unlink', '89', {'path': '/a/b/foo/fighters'})])
        node = self.qc._node_ops[''].children['a']

        node = node.children['b']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('12', 'MakeDir', {'path': '/a/b', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 2)

        node = node.children['foo']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('34', 'MakeDir', {'path': '/a/b/foo', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 1)

        node = node.children['fighters']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_FILE)
        p = '/a/b/foo/fighters'
        expected = [('67', 'MakeFile', {'path': p, '__done__': False}),
                    ('89', 'Unlink', {'path': p, '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

    def test_finishing_nothing(self):
        """Finish something that is not there."""
        r = self.qc.remove('MakeDir', '34', {'path': '/a/bar'})
        self.assertEqual(r, None)
        self.assertTrue(self.handler.check_warning(
                        "Element ''", "['', 'a', 'bar']", 'not in children'))

    def test_operation_error_nothing(self):
        """Finish an operation that is not there."""
        # create a node and break it on purpose
        self.qc.add('MakeDir', '12', {'path': '/a'})
        self.assertEqual(len(self.qc._node_ops), 1)
        node = self.qc._node_ops[''].children['a']
        node.operations = []

        # remove the operation and check
        r = self.qc.remove('MakeDir', '12', {'path': '/a'})
        self.assertEqual(r, None)
        self.assertTrue(self.handler.check_error(
                        "found 0 times", "MakeDir", "12"))

    def test_operation_error_several(self):
        """Finish an operation that is more than once."""
        # create a node and break it on purpose
        self.qc.add('MakeDir', '12', {'path': '/a'})
        self.assertEqual(len(self.qc._node_ops), 1)
        node = self.qc._node_ops[''].children['a']
        node.operations = node.operations * 2

        # remove the operation and check
        r = self.qc.remove('MakeDir', '12', {'path': '/a'})
        self.assertEqual(r, None)
        self.assertTrue(self.handler.check_error(
                        "found 2 times", "MakeDir", "12"))

    def test_two_ops_finishing_one(self):
        """Add some nodes with different combinations."""
        # create two dirs
        self.qc.set_content([('MakeDir', '12', {'path': '/a/foo'}),
                             ('MakeDir', '34', {'path': '/a/bar'})])
        self.assertEqual(len(self.qc._node_ops), 1)

        # all inited properly
        root = self.qc._node_ops[''].children['a']
        self.assertEqual(root.last_modified, None)
        self.assertEqual(root.kind, KIND_DIR)
        self.assertEqual(root.operations, [])
        self.assertEqual(root.done, None)
        self.assertEqual(len(root.children), 2)

        node = root.children['foo']
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('12', 'MakeDir', {'path': '/a/foo', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

        node = root.children['bar']
        bar_created_timestamp = node.last_modified
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('34', 'MakeDir', {'path': '/a/bar', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

        # finish the second make dir and check again
        self.qc.remove('MakeDir', '34', {'path': '/a/bar'})
        self.assertTrue(node.last_modified > bar_created_timestamp)
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_DIR)
        expected = [('34', 'MakeDir', {'path': '/a/bar', '__done__': True})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, True)
        self.assertEqual(len(node.children), 0)

    def test_two_ops_finishing_both(self):
        """Add two ops to the same node and finish both."""
        # create two dirs
        self.qc.set_content([('MakeFile', '12', {'path': '/a'}),
                             ('Upload', '34', {'path': '/a'})])
        self.assertEqual(len(self.qc._node_ops), 1)

        # all inited properly
        node = self.qc._node_ops[''].children['a']
        node_created_tstamp = node.last_modified
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_FILE)
        expected = [('12', 'MakeFile', {'path': '/a', '__done__': False}),
                    ('34', 'Upload', {'path': '/a', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

        # finish one
        self.qc.remove('MakeFile', '12', {'path': '/a'})
        node_changed_tstamp = node.last_modified
        self.assertTrue(node.last_modified > node_created_tstamp)
        expected = [('12', 'MakeFile', {'path': '/a', '__done__': True}),
                    ('34', 'Upload', {'path': '/a', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)

        # finish the second
        self.qc.remove('Upload', '34', {'path': '/a'})
        self.assertTrue(node.last_modified > node_changed_tstamp)
        expected = [('12', 'MakeFile', {'path': '/a', '__done__': True}),
                    ('34', 'Upload', {'path': '/a', '__done__': True})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, True)

    def test_one_op_finishes_startsagain(self):
        """Add an op, finish it, add another one."""
        self.qc.add('MakeFile', '12', {'path': '/a'})
        self.assertEqual(len(self.qc._node_ops), 1)

        # all inited properly
        node = self.qc._node_ops[''].children['a']
        node_created_tstamp = node.last_modified
        self.assertTrue(isinstance(node.last_modified, float))
        self.assertEqual(node.kind, KIND_FILE)
        expected = [('12', 'MakeFile', {'path': '/a', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)
        self.assertEqual(len(node.children), 0)

        # finish the op
        self.qc.remove('MakeFile', '12', {'path': '/a'})
        node_changed_tstamp = node.last_modified
        self.assertTrue(node.last_modified > node_created_tstamp)
        expected = [('12', 'MakeFile', {'path': '/a', '__done__': True})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, True)

        # send other one to the same node
        self.qc.add('Upload', '34', {'path': '/a'})
        self.assertTrue(node.last_modified > node_changed_tstamp)
        expected = [('34', 'Upload', {'path': '/a', '__done__': False})]
        self.assertEqual(node.operations, expected)
        self.assertEqual(node.done, False)