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_order_items__provenance_cycle(self, isfile):
        """Verify that if a provenance cycle is detected we raise an error"""

        isfile.return_value = True

        items = [
            _SyncUploadItem(
                File(path='/tmp/1', parentId='syn123'),
                ['/tmp/2'],  # used
                [],
                {},  # annotations
            ),
            _SyncUploadItem(
                File(path='/tmp/2', parentId='syn123'),
                [],  # used
                ['/tmp/1'],  # executed
                {},  # annotations
            ),
        ]

        with pytest.raises(RuntimeError) as cm_ex:
            _SyncUploader._order_items(items)
        assert 'cyclic' in str(cm_ex.value)
    def test_order_items__provenance_file_not_uploaded(self, isfile):
        """Verify that if one file depends on another for provenance but that file
        is not included in the upload we raise an error."""

        isfile.return_value = True

        items = [
            _SyncUploadItem(
                File(path='/tmp/1', parentId='syn123'),
                ['/tmp/2'],  # used
                [],
                {},  # annotations
            ),
        ]

        with pytest.raises(ValueError) as cm_ex:
            _SyncUploader._order_items(items)
        assert 'not being uploaded' in str(cm_ex.value)
    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
    def test_order_items(self, mock_isfile):
        """Verfy that items are properly ordered according to their provenance."""
        def isfile(path):
            return path.startswith('/tmp')

        mock_isfile.side_effect = isfile

        # dependencies flow down
        #
        #                       /tmp/6
        #                         |
        #                  _______|_______
        #                  |             |
        #                  V             |
        #  /tmp/4        /tmp/5          |
        #    |_____________|             |
        #           |                    |
        #           V                    |
        #         /tmp/3                 |
        #           |                    |
        #           V                    V
        #         /tmp/1               /tmp/2

        item_1 = _SyncUploadItem(
            File(path='/tmp/1', parentId='syn123'),
            [],  # used
            [],  # executed
            {},  # annotations
        )
        item_2 = _SyncUploadItem(
            File(path='/tmp/2', parentId='syn123'),
            [],  # used
            [],  # executed
            {},  # annotations
        )
        item_3 = _SyncUploadItem(
            File(path='/tmp/3', parentId='syn123'),
            ['/tmp/1'],  # used
            [],  # executed
            {},  # annotations
        )
        item_4 = _SyncUploadItem(
            File(path='/tmp/4', parentId='syn123'),
            [],  # used
            ['/tmp/3'],  # executed
            {},  # annotations
        )
        item_5 = _SyncUploadItem(
            File(path='/tmp/5', parentId='syn123'),
            ['/tmp/3'],  # used
            [],  # executed
            {},  # annotations
        )
        item_6 = _SyncUploadItem(
            File(path='/tmp/6', parentId='syn123'),
            ['/tmp/5'],  # used
            ['/tmp/2'],  # executed
            {},  # annotations
        )

        items = [
            item_5,
            item_6,
            item_2,
            item_3,
            item_1,
            item_4,
        ]

        random.shuffle(items)

        ordered = _SyncUploader._order_items(items)

        seen = set()
        for i in ordered:
            assert all(p in seen for p in (i.used + i.executed))
            seen.add(i.entity.path)