def test_upload__error(self, syn):
        """Verify that if an item upload fails the error is raised in the main thread
        and any running Futures are cancelled"""

        item_1 = _SyncUploadItem(File(path='/tmp/foo', parentId='syn123'), [],
                                 [], {})
        item_2 = _SyncUploadItem(File(path='/tmp/bar', parentId='syn123'), [],
                                 [], {})
        items = [item_1, item_2]

        def syn_store_side_effect(entity, *args, **kwargs):
            if entity.path == entity.path:
                raise ValueError()
            return Mock()

        uploader = _SyncUploader(syn, get_executor())
        original_abort = uploader._abort

        def abort_side_effect(futures):
            return original_abort(futures)

        with patch.object(syn, 'store') as mock_syn_store, \
                patch.object(uploader, '_abort') as mock_abort:

            mock_syn_store.side_effect = syn_store_side_effect
            mock_abort.side_effect = abort_side_effect
            with pytest.raises(ValueError):
                uploader.upload(items)

            # it would be aborted with Futures
            mock_abort.assert_called_once_with([ANY])
            isinstance(mock_abort.call_args_list[0][0], Future)
    def test_upload_item_success(self, syn):
        """Test successfully uploading an item"""

        uploader = _SyncUploader(syn, Mock())

        used = ['foo']
        executed = ['bar']
        item = _SyncUploadItem(
            File(path='/tmp/file', parentId='syn123'),
            used,
            executed,
            {'forceVersion': True},
        )

        finished_items = {}
        mock_condition = create_autospec(threading.Condition())
        pending_provenance = _PendingProvenance()
        pending_provenance.update(set([item.entity.path]))
        abort_event = threading.Event()
        progress = CumulativeTransferProgress('Test Upload')

        mock_stored_entity = Mock()

        with patch.object(syn, 'store') as mock_store, \
                patch.object(uploader._file_semaphore, 'release') as mock_release:

            mock_store.return_value = mock_stored_entity

            uploader._upload_item(
                item,
                used,
                executed,
                finished_items,
                pending_provenance,
                mock_condition,
                abort_event,
                progress,
            )

        mock_store.assert_called_once_with(item.entity,
                                           used=used,
                                           executed=executed,
                                           **item.store_kwargs)

        # item should be finished and removed from pending provenance
        assert mock_stored_entity == finished_items[item.entity.path]
        assert len(pending_provenance._pending) == 0

        # should have notified the condition to let anything waiting on this provenance to continue
        mock_condition.notifyAll.assert_called_once_with()

        assert not abort_event.is_set()

        mock_release.assert_called_once_with()
    def test_upload_item__failure(self, syn):
        """Verify behavior if an item upload fails.
        Exception should be raised, and appropriate threading controls should be released/notified."""

        uploader = _SyncUploader(syn, Mock())

        item = _SyncUploadItem(
            File(path='/tmp/file', parentId='syn123'),
            [],
            [],
            {'forceVersion': True},
        )

        finished_items = {}
        mock_condition = create_autospec(threading.Condition())
        pending_provenance = set([item.entity.path])
        abort_event = threading.Event()
        progress = CumulativeTransferProgress('Test Upload')

        with pytest.raises(ValueError), \
                patch.object(syn, 'store') as mock_store, \
                patch.object(uploader._file_semaphore, 'release') as mock_release:

            mock_store.side_effect = ValueError('Falure during upload')

            uploader._upload_item(
                item,
                item.used,
                item.executed,
                finished_items,
                pending_provenance,
                mock_condition,
                abort_event,
                progress,
            )

        # abort event should have been raised and we shoudl have released threading locks
        assert abort_event.is_set()
        mock_release.assert_called_once_with()
        mock_condition.notifyAll.assert_called_once_with()
    def test_upload(self, mock_os_isfile, syn):
        """Ensure that an upload including multiple items which depend on each other through
        provenance are all uploaded and in the expected order."""
        mock_os_isfile.return_value = True

        item_1 = _SyncUploadItem(
            File(path='/tmp/foo', parentId='syn123'),
            [],  # used
            [],  # executed
            {},  # annotations
        )
        item_2 = _SyncUploadItem(
            File(path='/tmp/bar', parentId='syn123'),
            ['/tmp/foo'],  # used
            [],  # executed
            {},  # annotations
        )
        item_3 = _SyncUploadItem(
            File(path='/tmp/baz', parentId='syn123'),
            ['/tmp/bar'],  # used
            [],  # executed
            {},  # annotations
        )

        items = [
            item_1,
            item_2,
            item_3,
        ]

        convert_provenance_calls = 2 * len(items)
        convert_provenance_condition = threading.Condition()

        mock_stored_entities = {
            item_1.entity.path: Mock(),
            item_2.entity.path: Mock(),
            item_3.entity.path: Mock(),
        }

        uploader = _SyncUploader(syn, get_executor())

        convert_provenance_original = uploader._convert_provenance

        def patched_convert_provenance(provenance, finished_items):
            # we hack the convert_provenance method as a way of ensuring that
            # the first item doesn't finish storing until the items that depend on it
            # have finished one trip through the wait loop. that way we ensure that
            # our locking logic is being exercised.
            nonlocal convert_provenance_calls
            with convert_provenance_condition:
                convert_provenance_calls -= 1

                if convert_provenance_calls == 0:
                    convert_provenance_condition.notifyAll()

            return convert_provenance_original(provenance, finished_items)

        def syn_store_side_effect(entity, *args, **kwargs):
            if entity.path == item_1.entity.path:
                with convert_provenance_condition:
                    if convert_provenance_calls > 0:
                        convert_provenance_condition.wait_for(
                            lambda: convert_provenance_calls == 0)

            return mock_stored_entities[entity.path]

        with patch.object(uploader, '_convert_provenance') as mock_convert_provenance, \
                patch.object(syn, 'store') as mock_syn_store:

            mock_convert_provenance.side_effect = patched_convert_provenance
            mock_syn_store.side_effect = syn_store_side_effect

            uploader.upload(items)

        # all three of our items should have been stored
        stored = [args[0][0].path for args in mock_syn_store.call_args_list]
        assert [i.entity.path for i in items] == stored