async def test_access_not_loaded_entry(running_backend, alice_entry_transactions): entry_transactions = alice_entry_transactions entry_id = entry_transactions.get_workspace_entry().id manifest = await entry_transactions.local_storage.get_manifest(entry_id) async with entry_transactions.local_storage.lock_entry_id(entry_id): await entry_transactions.local_storage.clear_manifest(entry_id) with pytest.raises(FSRemoteManifestNotFound): await entry_transactions.entry_info(FsPath("/")) async with entry_transactions.local_storage.lock_entry_id(entry_id): await entry_transactions.local_storage.set_manifest(entry_id, manifest) entry_info = await entry_transactions.entry_info(FsPath("/")) assert entry_info == { "type": "folder", "id": entry_id, "created": datetime(2000, 1, 1), "updated": datetime(2000, 1, 1), "base_version": 0, "is_placeholder": True, "need_sync": True, "children": [], "confinement_point": None, }
async def test_file_create_delete(alice_entry_transactions, alice_sync_transactions): entry_transactions = alice_entry_transactions sync_transactions = alice_sync_transactions # Create and delete a foo file foo_id, fd = await entry_transactions.file_create(FsPath("/foo"), open=False) assert fd is None assert await entry_transactions.file_delete(FsPath("/foo")) == foo_id # The file is not synced manifest = await entry_transactions.local_storage.get_manifest(foo_id) assert manifest.need_sync # Create and sync a bar file bar_id, fd = await entry_transactions.file_create(FsPath("/bar"), open=False) remote = await sync_transactions.synchronization_step(bar_id) assert await sync_transactions.synchronization_step(bar_id, remote) is None # Remove the bar file, the manifest is synced assert await entry_transactions.file_delete(FsPath("/bar")) == bar_id manifest = await entry_transactions.local_storage.get_manifest(bar_id) assert not manifest.need_sync
async def test_mountpoint_iterdir_with_many_files(n, base_path, base_mountpoint, alice_user_fs, event_bus): wid = await alice_user_fs.workspace_create(EntryName("w")) workspace = alice_user_fs.get_workspace(wid) await workspace.mkdir(base_path, parents=True, exist_ok=True) names = [f"some_file_{i:03d}.txt" for i in range(n)] path_list = [FsPath(f"{base_path}/{name}") for name in names] for path in path_list: await workspace.touch(path) # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) test_path = mountpoint_manager.get_path_in_mountpoint( wid, FsPath(base_path)) # Work around trio issue #1308 (https://github.com/python-trio/trio/issues/1308) items = await trio.to_thread.run_sync( lambda: [path.name for path in Path(test_path).iterdir()]) assert items == names for path in path_list: item_path = mountpoint_manager.get_path_in_mountpoint(wid, path) assert await trio.Path(item_path).exists()
async def test_file_create(alice_entry_transactions, alice_file_transactions): entry_transactions = alice_entry_transactions file_transactions = alice_file_transactions with freeze_time("2000-01-02"): access_id, fd = await entry_transactions.file_create(FsPath("/foo.txt") ) await file_transactions.fd_close(fd) assert fd == 1 root_stat = await entry_transactions.entry_info(FsPath("/")) assert root_stat == { "type": "folder", "id": entry_transactions.workspace_id, "base_version": 0, "is_placeholder": True, "need_sync": True, "created": datetime(2000, 1, 1), "updated": datetime(2000, 1, 2), "children": [EntryName("foo.txt")], "confinement_point": None, } foo_stat = await entry_transactions.entry_info(FsPath("/foo.txt")) assert foo_stat == { "type": "file", "id": access_id, "base_version": 0, "is_placeholder": True, "need_sync": True, "created": datetime(2000, 1, 2), "updated": datetime(2000, 1, 2), "size": 0, "confinement_point": None, }
async def test_root_entry_info(alice_workspace_t2, alice_workspace_t4): stat2 = await alice_workspace_t2.transactions.entry_info(FsPath("/")) assert stat2 == { "type": "folder", "id": alice_workspace_t4.transactions.workspace_id, "base_version": 1, "is_placeholder": False, "need_sync": False, "created": datetime(1999, 12, 31), "updated": datetime(1999, 12, 31), "children": [EntryName("foo")], "confinement_point": None, } stat4 = await alice_workspace_t4.transactions.entry_info(FsPath("/")) assert stat4 == { "type": "folder", "id": alice_workspace_t4.transactions.workspace_id, "base_version": 2, "is_placeholder": False, "need_sync": False, "created": datetime(1999, 12, 31), "updated": datetime(2000, 1, 4), "children": [EntryName("files"), EntryName("foo")], "confinement_point": None, }
async def test_get_path_in_mountpoint(base_mountpoint, alice_user_fs, event_bus): # Populate a bit the fs first... wid = await alice_user_fs.workspace_create(EntryName("mounted_wksp")) wid2 = await alice_user_fs.workspace_create(EntryName("not_mounted_wksp")) workspace1 = alice_user_fs.get_workspace(wid) workspace2 = alice_user_fs.get_workspace(wid2) await workspace1.touch("/bar.txt") await workspace2.touch("/foo.txt") # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) bar_path = mountpoint_manager.get_path_in_mountpoint( wid, FsPath("/bar.txt")) assert isinstance(bar_path, PurePath) # Windows uses drives, not base_mountpoint if sys.platform != "win32": expected = base_mountpoint / "mounted_wksp" / "bar.txt" assert str(bar_path) == str(expected.absolute()) assert await trio.Path(bar_path).exists() with pytest.raises(MountpointNotMounted): mountpoint_manager.get_path_in_mountpoint(wid2, FsPath("/foo.txt"))
async def test_mountpoint_access_unicode(base_mountpoint, alice_user_fs, event_bus): weird_name = "ÉŸ奇怪😀🔫🐍" wid = await alice_user_fs.workspace_create(EntryName(weird_name)) workspace = alice_user_fs.get_workspace(wid) await workspace.touch(f"/{weird_name}") # Now we can start fuse async with mountpoint_manager_factory( alice_user_fs, event_bus, base_mountpoint) as mountpoint_manager: await mountpoint_manager.mount_workspace(wid) root_path = mountpoint_manager.get_path_in_mountpoint( wid, FsPath(f"/")) # Work around trio issue #1308 (https://github.com/python-trio/trio/issues/1308) items = await trio.to_thread.run_sync( lambda: [path.name for path in Path(root_path).iterdir()]) assert items == [weird_name] item_path = mountpoint_manager.get_path_in_mountpoint( wid, FsPath(f"/{weird_name}")) assert await trio.Path(item_path).exists()
async def test_version_non_existing_directory(alice_workspace, alice): version_lister = alice_workspace.get_version_lister() versions, version_list_is_complete = await version_lister.list( FsPath("/moved")) assert version_list_is_complete is True assert len(versions) == 2 assert versions[0][1:] == ( 5, _day(9), _day(10), alice.device_id, _day(8), True, None, FsPath("/files"), None, ) assert versions[1][1:] == ( 6, _day(10), _day(11), alice.device_id, _day(10), True, None, None, FsPath("/files"), )
async def test_path_info_remote_loader_exceptions(monkeypatch, alice_workspace, alice): manifest, _ = await alice_workspace.transactions._get_manifest_from_path( FsPath("/foo/bar")) async with alice_workspace.local_storage.lock_entry_id(manifest.id): await alice_workspace.local_storage.clear_manifest(manifest.id) vanilla_file_manifest_deserialize = BaseRemoteManifest._deserialize def mocked_file_manifest_deserialize(*args, **kwargs): return vanilla_file_manifest_deserialize( *args, **kwargs).evolve(**manifest_modifiers) monkeypatch.setattr(BaseRemoteManifest, "_deserialize", mocked_file_manifest_deserialize) manifest_modifiers = {"id": EntryID.new()} with pytest.raises(FSError) as exc: await alice_workspace.path_info(FsPath("/foo/bar")) assert f"Invalid entry ID: expected `{manifest.id}`, got `{manifest_modifiers['id']}`" in str( exc.value) manifest_modifiers = {"version": 4} with pytest.raises(FSError) as exc: await alice_workspace.path_info(FsPath("/foo/bar")) assert "Invalid version: expected `1`, got `4`" in str(exc.value) manifest_modifiers = {"author": DeviceID("mallory@pc1")} with pytest.raises(FSError) as exc: await alice_workspace.path_info(FsPath("/foo/bar")) assert "Invalid author: expected `alice@dev1`, got `mallory@pc1`" in str( exc.value)
async def test_upsert_file(alice_workspace): _update_file_mock = AsyncMock(spec=mock.Mock()) _create_path_mock = AsyncMock(spec=mock.Mock()) path = FsPath("/test") workspace_path = FsPath("/path_in_workspace") entry_id = EntryID.new() with mock.patch("parsec.core.cli.rsync._create_path", _create_path_mock): with mock.patch("parsec.core.cli.rsync._update_file", _update_file_mock): await rsync._upsert_file(entry_id, alice_workspace, path, workspace_path) _update_file_mock.assert_called_once_with(alice_workspace, entry_id, path, workspace_path) _create_path_mock.assert_not_called() _update_file_mock.reset_mock() entry_id = None with mock.patch("parsec.core.cli.rsync._create_path", _create_path_mock): with mock.patch("parsec.core.cli.rsync._update_file", _update_file_mock): await rsync._upsert_file(None, alice_workspace, path, workspace_path) _update_file_mock.assert_not_called() _create_path_mock.assert_called_once_with(alice_workspace, False, path, workspace_path)
async def test_mountpoint_remote_error_event(aqtbot, running_backend, logged_gui, snackbar_catcher): c_w = logged_gui.test_get_central_widget() async with aqtbot.wait_signal(c_w.new_notification): c_w.event_bus.send( CoreEvent.MOUNTPOINT_REMOTE_ERROR, exc=FSWorkspaceNoReadAccess("Cannot get workspace roles: no read access"), path=FsPath("/bar"), mountpoint=Path("/foo"), operation="open", workspace_id=EntryID.new(), timestamp=None, ) assert "Read access to " in snackbar_catcher.snackbars[0][1] snackbar_catcher.reset() async with aqtbot.wait_signal(c_w.new_notification): c_w.event_bus.send( CoreEvent.MOUNTPOINT_UNHANDLED_ERROR, exc=RuntimeError("D'Oh !"), path=FsPath("/bar"), mountpoint=Path("/foo"), operation="unlink", workspace_id=EntryID.new(), ) assert "Error with the mountpoint " in snackbar_catcher.snackbars[0][1] snackbar_catcher.reset()
async def test_listdir(alice_workspace): lst = await alice_workspace.listdir("/") assert lst == [FsPath("/foo")] lst = await alice_workspace.listdir("/foo") assert lst == [FsPath("/foo/bar"), FsPath("/foo/baz")] with pytest.raises(NotADirectoryError): await alice_workspace.listdir("/foo/bar") with pytest.raises(FileNotFoundError): await alice_workspace.listdir("/baz")
async def test_iterdir(alice_workspace): lst = [child async for child in alice_workspace.iterdir("/")] assert lst == [FsPath("/foo")] lst = [child async for child in alice_workspace.iterdir("/foo")] assert lst == [FsPath("/foo/bar"), FsPath("/foo/baz")] with pytest.raises(NotADirectoryError): async for child in alice_workspace.iterdir("/foo/bar"): assert False, child with pytest.raises(FileNotFoundError): async for child in alice_workspace.iterdir("/baz"): assert False, child
async def test_get_minimal_remote_manifest(alice, alice_sync_transactions): sync_transactions = alice_sync_transactions # Prepare w_id = sync_transactions.workspace_id a_id, fd = await sync_transactions.file_create(FsPath("/a")) await sync_transactions.fd_write(fd, b"abc", 0) await sync_transactions.fd_close(fd) b_id = await sync_transactions.folder_create(FsPath("/b")) c_id = await sync_transactions.folder_create(FsPath("/b/c")) # Workspace manifest minimal = await sync_transactions.get_minimal_remote_manifest(w_id) local = await sync_transactions.local_storage.get_manifest(w_id) expected = local.to_remote(author=alice.device_id, timestamp=minimal.timestamp).evolve( children={}, updated=local.created) assert minimal == expected await sync_transactions.synchronization_step(w_id, minimal) assert await sync_transactions.get_minimal_remote_manifest(w_id) is None # File manifest minimal = await sync_transactions.get_minimal_remote_manifest(a_id) local = await sync_transactions.local_storage.get_manifest(a_id) expected = local.evolve(blocks=(), updated=local.created, size=0).to_remote(author=alice.device_id, timestamp=minimal.timestamp) assert minimal == expected await sync_transactions.file_reshape(a_id) await sync_transactions.synchronization_step(a_id, minimal) assert await sync_transactions.get_minimal_remote_manifest(a_id) is None # Folder manifest minimal = await sync_transactions.get_minimal_remote_manifest(b_id) local = await sync_transactions.local_storage.get_manifest(b_id) expected = local.to_remote(author=alice.device_id, timestamp=minimal.timestamp).evolve( children={}, updated=local.created) assert minimal == expected await sync_transactions.synchronization_step(b_id, minimal) assert await sync_transactions.get_minimal_remote_manifest(b_id) is None # Empty folder manifest minimal = await sync_transactions.get_minimal_remote_manifest(c_id) local = await sync_transactions.local_storage.get_manifest(c_id) expected = local.to_remote(author=alice.device_id, timestamp=minimal.timestamp) assert minimal == expected await sync_transactions.synchronization_step(c_id, minimal) assert await sync_transactions.get_minimal_remote_manifest(c_id) is None
async def test_access_not_loaded_entry(alice_workspace_t4): entry_id = alice_workspace_t4.transactions.get_workspace_entry().id alice_workspace_t4.transactions.local_storage._cache.clear() with pytest.raises(FSLocalMissError): await alice_workspace_t4.transactions.local_storage.get_manifest( entry_id) await alice_workspace_t4.transactions.entry_info(FsPath("/"))
async def test_versions_not_enough_download_permited(alice_workspace, alice): version_lister = alice_workspace.get_version_lister() version_lister = alice_workspace.get_version_lister() versions, version_list_is_complete = await version_lister.list( FsPath("/files/renamed"), skip_minimal_sync=False, max_manifest_queries=1) assert version_list_is_complete is False versions, version_list_is_complete = await version_lister.list( FsPath("/files/renamed"), skip_minimal_sync=False) assert version_list_is_complete is True versions, version_list_is_complete = await version_lister.list( FsPath("/files/renamed"), skip_minimal_sync=False, max_manifest_queries=1) assert version_list_is_complete is True
async def test_clear_path(alice_workspace): is_dir_mock = AsyncMock(spec=mock.Mock, side_effect=lambda x: True) alice_workspace.is_dir = is_dir_mock rmtree_mock = AsyncMock(spec=mock.Mock) alice_workspace.rmtree = rmtree_mock unlink_mock = AsyncMock(spec=mock.Mock) alice_workspace.unlink = unlink_mock sync_mock = AsyncMock(spec=mock.Mock) alice_workspace.sync = sync_mock path = FsPath("/path_in_workspace/test") await rsync._clear_path(alice_workspace, path) is_dir_mock.assert_called_once_with(path) rmtree_mock.assert_called_once_with(path) unlink_mock.assert_not_called() sync_mock.assert_called_once_with() alice_workspace.is_dir.side_effect = lambda x: False is_dir_mock.reset_mock() rmtree_mock.reset_mock() sync_mock.reset_mock() await rsync._clear_path(alice_workspace, path) is_dir_mock.assert_called_once_with(path) rmtree_mock.assert_not_called() unlink_mock.assert_called_once_with(path) sync_mock.assert_called_once_with()
async def _root_manifest_parent( path_destination: FsPath, workspace_fs: WorkspaceFS, workspace_manifest: WorkspaceManifest ) -> Tuple[Union[WorkspaceManifest, FolderManifest], FsPath]: root_manifest = workspace_manifest parent = FsPath("/") if path_destination: for p in path_destination.parts: parent = FsPath(parent / p) entry_id = root_manifest.children.get(p) root_manifest = await _get_or_create_directory( entry_id, workspace_fs, parent, parent) assert isinstance(root_manifest, FolderManifest) return root_manifest, parent
def test_stringify(path, sanitized_path): # Don't test '//' because according to POSIX path resolution: # http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap04.html#tag_04_11 # "A pathname that begins with two successive slashes may be # interpreted in an implementation-defined manner, although more # than two leading slashes shall be treated as a single slash". obj = FsPath(path) assert str(obj) == sanitized_path
async def test_operations_on_file(alice_workspace_t4, alice_workspace_t5): _, fd4 = await alice_workspace_t4.transactions.file_open( FsPath("/files/content"), write_mode=False ) assert isinstance(fd4, int) transactions_t4 = alice_workspace_t4.transactions _, fd5 = await alice_workspace_t5.transactions.file_open( FsPath("/files/content"), write_mode=False ) assert isinstance(fd5, int) transactions_t5 = alice_workspace_t5.transactions data = await transactions_t4.fd_read(fd4, 1, 0) assert data == b"a" data = await transactions_t4.fd_read(fd4, 3, 1) assert data == b"bcd" data = await transactions_t4.fd_read(fd4, 100, 4) assert data == b"e" data = await transactions_t4.fd_read(fd4, 4, 0) assert data == b"abcd" data = await transactions_t4.fd_read(fd4, -1, 0) assert data == b"abcde" with pytest.raises(FSError): # if removed from local_storage, no write right error?.. await alice_workspace_t4.transactions.fd_write(fd4, b"hello ", 0) data = await transactions_t5.fd_read(fd5, 100, 0) assert data == b"fghij" data = await transactions_t5.fd_read(fd5, 1, 1) assert data == b"g" data = await transactions_t4.fd_read(fd4, 1, 2) assert data == b"c" await transactions_t5.fd_close(fd5) with pytest.raises(FSInvalidFileDescriptor): data = await transactions_t5.fd_read(fd5, 1, 0) data = await transactions_t4.fd_read(fd4, 1, 3) assert data == b"d" _, fd5 = await alice_workspace_t5.transactions.file_open( FsPath("/files/content"), write_mode=False ) data = await transactions_t5.fd_read(fd5, 3, 0) assert data == b"fgh"
async def test_flush_file(alice_workspace_t4): _, fd4 = await alice_workspace_t4.transactions.file_open( FsPath("/files/content"), write_mode=False ) assert isinstance(fd4, int) transactions_t4 = alice_workspace_t4.transactions await transactions_t4.fd_flush(fd4)
async def test_sync_directory(alice_workspace): _get_or_create_directory_mock = AsyncMock( spec=mock.Mock(), side_effect=lambda *x: "folder_manifest_mock") _sync_directory_content_mock = AsyncMock(spec=mock.Mock()) _clear_directory_mock = AsyncMock(spec=mock.Mock()) entry_id = EntryID.new() path = FsPath("/test") workspace_path = FsPath("/path_in_workspace") with mock.patch("parsec.core.cli.rsync._get_or_create_directory", _get_or_create_directory_mock): with mock.patch("parsec.core.cli.rsync._sync_directory_content", _sync_directory_content_mock): with mock.patch("parsec.core.cli.rsync._clear_directory", _clear_directory_mock): await rsync._sync_directory(entry_id, alice_workspace, path, workspace_path) _get_or_create_directory_mock.assert_called_once_with( entry_id, alice_workspace, path, workspace_path) _sync_directory_content_mock.assert_called_once_with( workspace_path, path, alice_workspace, "folder_manifest_mock") _clear_directory_mock.assert_called_once_with( workspace_path, path, alice_workspace, "folder_manifest_mock") _get_or_create_directory_mock.reset_mock() _sync_directory_content_mock.reset_mock() _clear_directory_mock.reset_mock() with mock.patch("parsec.core.cli.rsync._get_or_create_directory", _get_or_create_directory_mock): with mock.patch("parsec.core.cli.rsync._sync_directory_content", _sync_directory_content_mock): with mock.patch("parsec.core.cli.rsync._clear_directory", _clear_directory_mock): await rsync._sync_directory(None, alice_workspace, path, workspace_path) _get_or_create_directory_mock.assert_called_once_with( None, alice_workspace, path, workspace_path) _sync_directory_content_mock.assert_called_once_with( workspace_path, path, alice_workspace, "folder_manifest_mock") _clear_directory_mock.assert_not_called()
def open_workspace_file(self, workspace_fs, file_name): file_name = FsPath("/", file_name) if file_name else FsPath("/") try: path = self.core.mountpoint_manager.get_path_in_mountpoint( workspace_fs.workspace_id, file_name, workspace_fs.timestamp if isinstance(workspace_fs, WorkspaceFSTimestamped) else None, ) self.jobs_ctx.submit_job(self.file_open_success, self.file_open_error, desktop.open_files_job, [path]) except MountpointNotMounted: # The mountpoint has been umounted in our back, nothing left to do show_error( self, _("TEXT_FILE_OPEN_ERROR_file").format(file=str(file_name)))
async def test_versions_non_existing_file_remove_minimal_synced( alice_workspace, alice, skip_minimal_sync): version_lister = alice_workspace.get_version_lister() versions, version_list_is_complete = await version_lister.list( FsPath("/moved/renamed"), skip_minimal_sync=skip_minimal_sync) assert version_list_is_complete is True assert len(versions) == 1 assert versions[0][1:] == ( 2, _day(9), _day(11), alice.device_id, _day(8), False, 6, FsPath("/files/renamed"), FsPath("/files/renamed"), )
async def _do_rename(workspace_fs, paths): new_names = {} for (old_path, new_path, entry_id) in paths: try: await workspace_fs.rename(old_path, new_path) new_names[entry_id] = FsPath(new_path).name except FileExistsError as exc: raise JobResultError("already-exists", multi=len(paths) > 1) from exc except OSError as exc: raise JobResultError("not-empty", multi=len(paths) > 1) from exc
def _parse_destination( core: LoggedCore, destination: str) -> Tuple[WorkspaceEntry, Optional[FsPath]]: try: workspace_name, path = destination.split(":") except ValueError: workspace_name = destination path = None if path: try: path = FsPath(path) except ValueError: path = FsPath(f"/{path}") for workspace in core.user_fs.get_user_manifest().workspaces: if workspace.name == workspace_name: break else: raise SystemExit(f"Unknown workspace ({destination})") return workspace, path
def rename(self, path: FsPath, destination: str): destination = FsPath(destination) if not path.parent.is_root() and re.match(r".*\.sb-\w*-\w*", str(path.parent.name)): # This shouldn't happen with the defer_permission option in the FUSE function, # but if there ever is a permission error while saving with Apple softwares, # this is a fallback to avoid any unintended behavior related to this format # of temporary files. See https://github.com/Scille/parsec-cloud/pull/2211 self.fs_access.workspace_move(path, destination) else: self.fs_access.entry_rename(path, destination, overwrite=True) return 0
async def test_unlink(alice_workspace): await alice_workspace.unlink("/foo/bar") lst = await alice_workspace.listdir("/foo") assert lst == [FsPath("/foo/baz")] with pytest.raises(FileNotFoundError): await alice_workspace.unlink("/foo/bar") with pytest.raises(IsADirectoryError): await alice_workspace.unlink("/foo") # TODO: should this be a `IsADirectoryError`? with pytest.raises(PermissionError): await alice_workspace.unlink("/")
async def _clear_directory( workspace_directory_path: FsPath, local_path: AnyPath, workspace_fs: WorkspaceFS, folder_manifest: FolderManifest, ) -> None: local_children_keys = [p.name for p in await local_path.iterdir()] for name in folder_manifest.children.keys(): if name not in local_children_keys: absolute_path = FsPath(workspace_directory_path / name) print("delete %s" % absolute_path) await _clear_path(workspace_fs, absolute_path)
async def test_root_manifest_parent(alice_workspace): workspace_manifest = mock.Mock(spec=WorkspaceManifest) _get_or_create_directory_mock = AsyncMock(spec=mock.Mock) with mock.patch("parsec.core.cli.rsync._get_or_create_directory", _get_or_create_directory_mock): root_manifest, parent = await rsync._root_manifest_parent( None, alice_workspace, workspace_manifest) _get_or_create_directory_mock.assert_not_called() assert root_manifest is workspace_manifest assert parent == FsPath("/") workspace_manifest.children = {"test": "id1"} workspace_test_save_manifest = mock.Mock(spec=FolderManifest) workspace_test_save_manifest.children = {} workspace_test_manifest = mock.Mock(spec=FolderManifest) _get_or_create_directory_mock.side_effect = [ workspace_test_manifest, workspace_test_save_manifest, ] with mock.patch("parsec.core.cli.rsync._get_or_create_directory", _get_or_create_directory_mock): root_manifest, parent = await rsync._root_manifest_parent( FsPath("/path_in_workspace"), alice_workspace, workspace_manifest) assert root_manifest == workspace_test_manifest assert parent == FsPath("/path_in_workspace") _get_or_create_directory_mock.side_effect = [ workspace_test_manifest, workspace_test_save_manifest, ] with mock.patch("parsec.core.cli.rsync._get_or_create_directory", _get_or_create_directory_mock): root_manifest, parent = await rsync._root_manifest_parent( FsPath("/path_in_workspace/save"), alice_workspace, workspace_manifest) assert root_manifest == workspace_test_save_manifest assert parent == FsPath("/path_in_workspace/save")