class FSOfflineUser(user_fs_online_state_machine):
        Workspaces = Bundle("workspace")

        @initialize()
        async def init(self):
            await self.reset_all()
            self.device = alice
            self.oracle_fs = oracle_fs_with_sync_factory()
            self.workspace = None

            await self.start_backend()
            await self.restart_user_fs(self.device)

        @rule()
        async def restart(self):
            await self.restart_user_fs(self.device)

        @rule()
        async def reset(self):
            await self.reset_user_fs(self.device)
            await self.user_fs.sync()
            self.oracle_fs.reset()

        async def stat(self):
            expected = self.oracle_fs.stat("/")
            path_info = await self.workspace.path_info("/")
            assert path_info["type"] == expected["type"]
            # TODO: oracle's `base_version` is broken (synchronization
            # strategy with parent placeholder make it complex to get right)
            # assert path_info["base_version"] == expected["base_version"]
            if not path_info["need_sync"]:
                assert path_info["base_version"] > 0
            assert path_info["is_placeholder"] == expected["is_placeholder"]
            assert path_info["need_sync"] == expected["need_sync"]

        @rule(target=Workspaces, name=st_entry_name)
        async def create_workspace(self, name):
            self.oracle_fs.create_workspace(f"/{name}")
            wid = await self.user_fs.workspace_create(name)
            self.workspace = self.user_fs.get_workspace(wid)
            await self.user_fs.sync()
            return wid, name

        @rule(target=Workspaces, workspace=Workspaces, new_name=st_entry_name)
        async def rename_workspace(self, workspace, new_name):
            wid, workspace = workspace
            src = f"/{workspace}"
            dst = f"/{new_name}"
            expected_status = self.oracle_fs.rename_workspace(src, dst)
            if expected_status == "ok":
                await self.user_fs.workspace_rename(wid, new_name)
            else:
                with pytest.raises(FSWorkspaceNotFoundError):
                    await self.user_fs.workspace_rename(workspace, new_name)
            return wid, new_name

        @rule(workspace=Workspaces)
        async def sync(self, workspace):
            await self.user_fs.sync()
            self.oracle_fs.sync()
    class WorkspaceFSReencrytionNeed(TrioAsyncioRuleBasedStateMachine):
        Users = Bundle("user")

        async def start_user_fs(self):
            try:
                await self.user_fs_controller.stop()
            except AttributeError:
                pass

            async def _user_fs_controlled_cb(started_cb):
                async with user_fs_factory(device=self.device) as user_fs:
                    await started_cb(user_fs=user_fs)

            self.user_fs_controller = await self.get_root_nursery().start(
                call_with_control, _user_fs_controlled_cb)

        async def start_backend(self):
            async def _backend_controlled_cb(started_cb):
                async with backend_factory() as backend:
                    async with server_factory(backend.handle_client) as server:
                        await started_cb(backend=backend, server=server)

            self.backend_controller = await self.get_root_nursery().start(
                call_with_control, _backend_controlled_cb)

        def _oracle_give_or_change_role(self, user):
            current_role = self.user_roles.get(user.user_id)
            new_role = RealmRole.MANAGER if current_role != RealmRole.MANAGER else RealmRole.READER
            self.since_reencryption_role_revoked.discard(user.user_id)
            self.user_roles[user.user_id] = new_role
            return new_role

        def _oracle_revoke_role(self, user):
            if user.user_id in self.user_roles:
                self.since_reencryption_role_revoked.add(user.user_id)
                self.user_roles.pop(user.user_id, None)
                return True
            else:
                return False

        def _oracle_revoke_user(self, user):
            if user.user_id in self.user_roles:
                self.since_reencryption_user_revoked.add(user.user_id)

        async def _update_role(self, author, user, role=RealmRole.MANAGER):
            now = pendulum_now()
            certif = RealmRoleCertificateContent(
                author=author.device_id,
                timestamp=now,
                realm_id=RealmID(self.wid.uuid),
                user_id=user.user_id,
                role=role,
            ).dump_and_sign(author.signing_key)
            await self.backend.realm.update_roles(
                author.organization_id,
                RealmGrantedRole(
                    certificate=certif,
                    realm_id=RealmID(self.wid.uuid),
                    user_id=user.user_id,
                    role=role,
                    granted_by=author.device_id,
                    granted_on=now,
                ),
            )
            return certif

        @property
        def user_fs(self):
            return self.user_fs_controller.user_fs

        @property
        def backend(self):
            return self.backend_controller.backend

        @initialize()
        async def init(self):
            await reset_testbed()

            self.user_roles = {}
            self.since_reencryption_user_revoked = set()
            self.since_reencryption_role_revoked = set()

            await self.start_backend()
            self.device = self.backend_controller.server.correct_addr(alice)
            self.backend_data_binder = backend_data_binder_factory(
                self.backend)

            await self.start_user_fs()
            self.wid = await self.user_fs.workspace_create(EntryName("w"))
            await self.user_fs.sync()
            self.workspacefs = self.user_fs.get_workspace(self.wid)

        @rule(target=Users)
        async def give_role(self):
            new_user = local_device_factory()
            await self.backend_data_binder.bind_device(new_user)
            new_role = self._oracle_give_or_change_role(new_user)
            await self._update_role(alice, new_user, role=new_role)
            return new_user

        @rule(user=Users)
        async def change_role(self, user):
            new_role = self._oracle_give_or_change_role(user)
            await self._update_role(alice, user, role=new_role)

        @rule(user=Users)
        async def revoke_role(self, user):
            if self._oracle_revoke_role(user):
                await self._update_role(alice, user, role=None)

        @rule(user=consumes(Users))
        async def revoke_user(self, user):
            await self.backend_data_binder.bind_revocation(user.user_id, alice)
            self._oracle_revoke_user(user)

        @rule()
        async def reencrypt(self):
            job = await self.user_fs.workspace_start_reencryption(self.wid)
            while True:
                total, done = await job.do_one_batch()
                if total <= done:
                    break
            self.since_reencryption_user_revoked.clear()
            self.since_reencryption_role_revoked.clear()
            # Needed to keep encryption revision up to date
            await self.user_fs.process_last_messages()

        @invariant()
        async def check_reencryption_need(self):
            if not hasattr(self, "workspacefs"):
                return
            need = await self.workspacefs.get_reencryption_need()
            assert set(
                need.role_revoked) == self.since_reencryption_role_revoked
            assert (set(
                need.user_revoked) == self.since_reencryption_user_revoked -
                    self.since_reencryption_role_revoked)
Beispiel #3
0
    class FSOnlineConcurrentTreeAndSync(TrioAsyncioRuleBasedStateMachine):
        Files = Bundle("file")
        Folders = Bundle("folder")
        FSs = Bundle("fs")

        async def start_fs(self, device):
            async def _user_fs_controlled_cb(started_cb):
                async with user_fs_factory(device=device) as fs:
                    await started_cb(fs=fs)

            return await self.get_root_nursery().start(call_with_control,
                                                       _user_fs_controlled_cb)

        async def start_backend(self, devices):
            async def _backend_controlled_cb(started_cb):
                async with backend_factory() as backend:
                    async with server_factory(backend.handle_client,
                                              backend_addr) as server:
                        await started_cb(backend=backend, server=server)

            return await self.get_root_nursery().start(call_with_control,
                                                       _backend_controlled_cb)

        @property
        def user_fs1(self):
            return self.user_fs1_controller.fs

        @property
        def user_fs2(self):
            return self.user_fs2_controller.fs

        @initialize(target=Folders)
        async def init(self):
            await reset_testbed()
            self.device1 = alice
            self.device2 = alice2

            self.backend_controller = await self.start_backend(
                [self.device1, self.device2])
            self.user_fs1_controller = await self.start_fs(self.device1)
            self.user_fs2_controller = await self.start_fs(self.device2)

            self.wid = await self.user_fs1.workspace_create("w")
            workspace = self.user_fs1.get_workspace(self.wid)
            await workspace.sync()
            await self.user_fs1.sync()
            await self.user_fs2.sync()

            return "/"

        @initialize(target=FSs, force_after_init=Folders)
        async def register_user_fs1(self, force_after_init):
            return self.user_fs1

        @initialize(target=FSs, force_after_init=Folders)
        async def register_user_fs2(self, force_after_init):
            return self.user_fs2

        @rule(target=Files, fs=FSs, parent=Folders, name=st_entry_name)
        async def create_file(self, fs, parent, name):
            path = f"{parent}/{name}"
            workspace = fs.get_workspace(self.wid)
            try:
                await workspace.touch(path=path)
            except OSError:
                pass
            return path

        @rule(target=Folders, fs=FSs, parent=Folders, name=st_entry_name)
        async def create_folder(self, fs, parent, name):
            path = f"{parent}/{name}"
            workspace = fs.get_workspace(self.wid)
            try:
                await workspace.mkdir(path=path)
            except OSError:
                pass
            return path

        @rule(fs=FSs, path=Files)
        async def update_file(self, fs, path):
            workspace = fs.get_workspace(self.wid)
            try:
                await workspace.write_bytes(path, data=b"a")
            except OSError:
                pass

        @rule(fs=FSs, path=Files)
        async def delete_file(self, fs, path):
            workspace = fs.get_workspace(self.wid)
            try:
                await workspace.unlink(path=path)
            except OSError:
                pass
            return path

        @rule(fs=FSs, path=Folders)
        async def delete_folder(self, fs, path):
            workspace = fs.get_workspace(self.wid)
            try:
                await workspace.rmdir(path=path)
            except OSError:
                pass
            return path

        @rule(target=Files,
              fs=FSs,
              src=Files,
              dst_parent=Folders,
              dst_name=st_entry_name)
        async def move_file(self, fs, src, dst_parent, dst_name):
            dst = f"{dst_parent}/{dst_name}"
            workspace = fs.get_workspace(self.wid)
            try:
                await workspace.move(src, dst)
            except OSError:
                pass
            return dst

        @rule(target=Folders,
              fs=FSs,
              src=Folders,
              dst_parent=Folders,
              dst_name=st_entry_name)
        async def move_folder(self, fs, src, dst_parent, dst_name):
            dst = f"{dst_parent}/{dst_name}"
            workspace = fs.get_workspace(self.wid)
            try:
                await workspace.move(src, dst)
            except OSError:
                pass
            return dst

        @rule()
        async def sync_all_the_files(self):
            # Less than 4 retries causes the following test case to fail:
            # ```python
            # state = FSOnlineConcurrentTreeAndSync()
            # async def steps():
            #     v1 = await state.init()
            #     v2 = await state.register_user_fs1(force_after_init=v1)
            #     v3 = await state.register_user_fs2(force_after_init=v1)
            #     v4 = await state.create_file(fs=v3, name='b', parent=v1)
            #     await state.sync_all_the_files()
            #     await state.update_file(fs=v3, path=v4)
            #     await state.update_file(fs=v2, path=v4)
            #     v5 = await state.create_file(fs=v3, name='a', parent=v1)
            #     v6 = await state.create_file(fs=v3, name='a', parent=v1)
            #     v7 = await state.move_file(dst_name='a', dst_parent=v1, fs=v2, src=v4)
            #     await state.sync_all_the_files()
            #     await state.teardown()
            # state.trio_run(steps)
            # ```
            # for fs in [self.user_fs1, self.user_fs2]:
            #     workspace = fs.get_workspace(self.wid)
            retries = 4
            workspace1 = self.user_fs1.get_workspace(self.wid)
            workspace2 = self.user_fs2.get_workspace(self.wid)
            for _ in range(retries):
                await workspace1.sync()
                await workspace2.sync()
                await self.user_fs1.sync()
                await self.user_fs2.sync()

            user_fs_dump_1 = await workspace1.dump()
            user_fs_dump_2 = await workspace2.dump()
            compare_fs_dumps(user_fs_dump_1, user_fs_dump_2)
Beispiel #4
0
    class ShuffleRoles(TrioAsyncioRuleBasedStateMachine):
        realm_role_strategy = st.one_of(st.just(x) for x in RealmRole)
        User = Bundle("user")

        async def start_backend(self):
            async def _backend_controlled_cb(started_cb):
                async with backend_factory(populated=False) as backend:
                    async with server_factory(backend.handle_client, backend_addr) as server:
                        await started_cb(backend=backend, server=server)

            return await self.get_root_nursery().start(call_with_control, _backend_controlled_cb)

        @property
        def backend(self):
            return self.backend_controller.backend

        @initialize(target=User)
        async def init(self):
            await reset_testbed()
            self.backend_controller = await self.start_backend()

            # Create organization and first user
            self.backend_data_binder = backend_data_binder_factory(self.backend)
            await self.backend_data_binder.bind_organization(coolorg, alice)

            # Create realm
            self.realm_id = await realm_factory(self.backend, alice)
            self.current_roles = {alice.user_id: RealmRole.OWNER}
            self.certifs = [ANY]

            self.socks = {}
            return alice

        async def get_sock(self, device):
            try:
                return self.socks[device.user_id]
            except KeyError:
                pass

            async def _start_sock(device, *, task_status=trio.TASK_STATUS_IGNORED):
                async with backend_sock_factory(self.backend, device) as sock:
                    task_status.started(sock)
                    await trio.sleep_forever()

            sock = await self.get_root_nursery().start(_start_sock, device)
            self.socks[device.user_id] = sock
            return sock

        @rule(target=User, author=User, role=realm_role_strategy)
        async def give_role_to_new_user(self, author, role):
            # Create new user/device
            new_device = local_device_factory()
            await self.backend_data_binder.bind_device(new_device)
            self.current_roles[new_device.user_id] = None
            # Assign role
            author_sock = await self.get_sock(author)
            if await self._give_role(author_sock, author, new_device, role):
                return new_device
            else:
                return multiple()

        @rule(author=User, recipient=User, role=realm_role_strategy)
        async def change_role_for_existing_user(self, author, recipient, role):
            author_sock = await self.get_sock(author)
            await self._give_role(author_sock, author, recipient, role)

        async def _give_role(self, author_sock, author, recipient, role):
            author_sock = await self.get_sock(author)

            certif = RealmRoleCertificateContent(
                author=author.device_id,
                timestamp=pendulum_now(),
                realm_id=self.realm_id,
                user_id=recipient.user_id,
                role=role,
            ).dump_and_sign(author.signing_key)
            rep = await realm_update_roles(author_sock, certif, check_rep=False)
            if author.user_id == recipient.user_id:
                assert rep == {
                    "status": "invalid_data",
                    "reason": "Realm role certificate cannot be self-signed.",
                }

            else:
                owner_only = (RealmRole.OWNER,)
                owner_or_manager = (RealmRole.OWNER, RealmRole.MANAGER)
                existing_recipient_role = self.current_roles[recipient.user_id]
                if existing_recipient_role in owner_or_manager or role in owner_or_manager:
                    allowed_roles = owner_only
                else:
                    allowed_roles = owner_or_manager

                if self.current_roles[author.user_id] in allowed_roles:
                    # print(f"+ {author.user_id} -{role.value}-> {recipient.user_id}")
                    if existing_recipient_role != role:
                        assert rep == {"status": "ok"}
                        self.current_roles[recipient.user_id] = role
                        self.certifs.append(certif)
                    else:
                        assert rep == {"status": "already_granted"}
                else:
                    # print(f"- {author.user_id} -{role.value}-> {recipient.user_id}")
                    assert rep == {"status": "not_allowed"}

            return rep["status"] == "ok"

        @rule(author=User)
        async def get_role_certificates(self, author):
            sock = await self.get_sock(author)
            rep = await realm_get_role_certificates(sock, self.realm_id)
            if self.current_roles.get(author.user_id) is not None:
                assert rep["status"] == "ok"
                assert rep["certificates"] == self.certifs
            else:
                assert rep == {}

        @invariant()
        async def check_current_roles(self):
            try:
                backend = self.backend
            except AttributeError:
                return
            roles = await backend.realm.get_current_roles(alice.organization_id, self.realm_id)
            assert roles == {k: v for k, v in self.current_roles.items() if v is not None}
Beispiel #5
0
    class SyncMonitorStateful(TrioAsyncioRuleBasedStateMachine):
        SharedWorkspaces = Bundle("shared_workspace")
        SyncedFiles = Bundle("synced_files")

        def __init__(self):
            super().__init__()
            # Core's sync and message monitors must be kept frozen
            mock_clock = trio.testing.MockClock(rate=0, autojump_threshold=0)
            self.set_clock(mock_clock)
            self.file_count = 0
            self.data_count = 0
            self.workspace_count = 0

        def get_next_file_path(self):
            self.file_count = self.file_count + 1
            return f"/file-{self.file_count}.txt"

        def get_next_data(self):
            self.data_count = self.data_count + 1
            return f"data {self.data_count}".encode()

        def get_next_workspace_name(self):
            self.workspace_count = self.workspace_count + 1
            return f"w{self.workspace_count}"

        def get_workspace(self, device_id, wid):
            return self.user_fs_per_device[device_id].get_workspace(wid)

        async def start_alice_client(self):
            async def _client_controlled_cb(started_cb):
                async with client_factory(alice) as client:
                    await started_cb(client=client)

            self.alice_client_controller = await self.get_root_nursery().start(
                call_with_control, _client_controlled_cb)
            return self.alice_client_controller.client

        async def start_bob_user_fs(self):
            async def _user_fs_controlled_cb(started_cb):
                async with user_fs_factory(device=bob) as user_fs:
                    await started_cb(user_fs=user_fs)

            self.bob_user_fs_controller = await self.get_root_nursery().start(
                call_with_control, _user_fs_controlled_cb)
            return self.bob_user_fs_controller.user_fs

        async def start_backend(self):
            async def _backend_controlled_cb(started_cb):
                async with backend_factory() as backend:
                    async with server_factory(backend.handle_client,
                                              backend_addr) as server:
                        await started_cb(backend=backend, server=server)

            self.backend_controller = await self.get_root_nursery().start(
                call_with_control, _backend_controlled_cb)

        @initialize()
        async def init(self):
            await reset_testbed()

            await self.start_backend()
            self.bob_user_fs = await self.start_bob_user_fs()
            self.alice_client = await self.start_alice_client()
            self.user_fs_per_device = {
                alice.device_id: self.alice_client.user_fs,
                bob.device_id: self.bob_user_fs,
            }
            self.synced_files = set()
            self.alice_workspaces_role = {}

        @rule(
            target=SharedWorkspaces,
            role=st.one_of(st.just(WorkspaceRole.CONTRIBUTOR),
                           st.just(WorkspaceRole.READER)),
        )
        async def create_sharing(self, role):
            wname = self.get_next_workspace_name()
            wid = await self.bob_user_fs.workspace_create(wname)
            await self.bob_user_fs.workspace_share(wid, alice.user_id, role)
            self.alice_workspaces_role[wid] = role
            return wid

        @rule(
            wid=SharedWorkspaces,
            new_role=st.one_of(st.just(WorkspaceRole.CONTRIBUTOR),
                               st.just(WorkspaceRole.READER), st.just(None)),
        )
        async def update_sharing(self, wid, new_role):
            await self.bob_user_fs.workspace_share(wid, alice.user_id,
                                                   new_role)
            self.alice_workspaces_role[wid] = new_role

        @rule(author=st.one_of(st.just(alice.device_id),
                               st.just(bob.device_id)),
              wid=SharedWorkspaces)
        async def create_file(self, author, wid):
            file_path = self.get_next_file_path()
            if author == bob.device_id:
                await self._bob_update_file(wid, file_path, create_file=True)
            else:
                await self._alice_update_file(wid, file_path, create_file=True)

        @rule(author=st.one_of(st.just(alice.device_id),
                               st.just(bob.device_id)),
              file=SyncedFiles)
        async def update_file(self, author, file):
            wid, file_path = file
            if author == bob.device_id:
                await self._bob_update_file(wid, file_path)
            else:
                await self._alice_update_file(wid, file_path)

        async def _bob_update_file(self, wid, file_path, create_file=False):
            wfs = self.get_workspace(bob.device_id, wid)
            if create_file:
                await wfs.touch(file_path)
            else:
                data = self.get_next_data()
                await wfs.write_bytes(file_path, data)
            await wfs.sync()

        async def _alice_update_file(self, wid, file_path, create_file=False):
            try:
                wfs = self.get_workspace(alice.device_id, wid)
            except FSWorkspaceNotFoundError:
                return
            if create_file:
                try:
                    await wfs.touch(file_path)
                except (FSWorkspaceNoAccess, OSError):
                    return
            else:
                data = self.get_next_data()
                try:
                    await wfs.write_bytes(file_path, data)
                except (FSWorkspaceNoAccess, OSError):
                    return

        @rule(target=SyncedFiles)
        async def let_client_monitors_process_changes(self):
            # Wait for alice client to settle down
            await trio.sleep(300)
            # Bob get back alice's changes
            await self.bob_user_fs.sync()
            for bob_workspace_entry in self.bob_user_fs.get_user_manifest(
            ).workspaces:
                bob_w = self.bob_user_fs.get_workspace(bob_workspace_entry.id)
                await bob_w.sync()
            # Alice get back possible changes from bob's sync
            await trio.sleep(300)

            # Now alice and bob should have agreed on the data
            new_synced_files = []
            for alice_workspace_entry in self.alice_client.user_fs.get_user_manifest(
            ).workspaces:
                alice_w = self.alice_client.user_fs.get_workspace(
                    alice_workspace_entry.id)
                bob_w = self.bob_user_fs.get_workspace(
                    alice_workspace_entry.id)

                if alice_workspace_entry.role is None:
                    # No access, workspace can only diverge from bob's
                    continue

                bob_dump = await bob_w.dump()
                alice_dump = await alice_w.dump()
                if self.alice_workspaces_role[
                        alice_workspace_entry.id] == WorkspaceRole.READER:
                    # Synced with bob, but we can have local changes that cannot be synced
                    recursive_compare_fs_dumps(alice_dump,
                                               bob_dump,
                                               ignore_need_sync=True)
                else:
                    # Fully synced with bob
                    recursive_compare_fs_dumps(alice_dump,
                                               bob_dump,
                                               ignore_need_sync=False)

                for child_name in bob_dump["children"].keys():
                    key = (alice_workspace_entry.id, f"/{child_name}")
                    if key not in self.synced_files:
                        new_synced_files.append(key)

            self.synced_files.update(new_synced_files)
            return multiple(*(sorted(new_synced_files)))
Beispiel #6
0
    class FSOnlineIdempotentSync(TrioAsyncioRuleBasedStateMachine):
        BadPath = Bundle("bad_path")
        GoodFilePath = Bundle("good_file_path")
        GoodFolderPath = Bundle("good_folder_path")
        GoodPath = st.one_of(GoodFilePath, GoodFolderPath)

        async def start_user_fs(self, device):
            async def _user_fs_controlled_cb(started_cb):
                async with user_fs_factory(device=device) as user_fs:
                    await started_cb(user_fs=user_fs)

            return await self.get_root_nursery().start(call_with_control, _user_fs_controlled_cb)

        async def start_backend(self):
            async def _backend_controlled_cb(started_cb):
                async with backend_factory() as backend:
                    async with server_factory(backend.handle_client, backend_addr) as server:
                        await started_cb(backend=backend, server=server)

            return await self.get_root_nursery().start(call_with_control, _backend_controlled_cb)

        @property
        def user_fs(self):
            return self.user_fs_controller.user_fs

        @initialize(target=BadPath)
        async def init(self):
            await reset_testbed()
            self.backend_controller = await self.start_backend()
            self.device = alice
            self.user_fs_controller = await self.start_user_fs(alice)

            wid = await self.user_fs.workspace_create("w")
            self.workspace = self.user_fs.get_workspace(wid)
            await self.workspace.touch("/good_file.txt")
            await self.workspace.mkdir("/good_folder")
            await self.workspace.touch("/good_folder/good_sub_file.txt")
            await self.workspace.sync()
            await self.user_fs.sync()

            self.initial_fs_dump = await self.workspace.dump()
            check_fs_dump(self.initial_fs_dump)

            return "/dummy"

        @initialize(target=GoodFolderPath)
        async def init_good_folder_pathes(self):
            return multiple("/", "/good_folder/")

        @initialize(target=GoodFilePath)
        async def init_good_file_pathes(self):
            return multiple("/good_file.txt", "/good_folder/good_sub_file.txt")

        @rule(target=BadPath, type=st_entry_type, bad_parent=BadPath, name=st_entry_name)
        async def try_to_create_bad_path(self, type, bad_parent, name):
            path = f"{bad_parent}/{name}"
            with pytest.raises(FileNotFoundError):
                if type == "file":
                    await self.workspace.touch(path)
                else:
                    await self.workspace.mkdir(path)
            return path

        @rule(type=st_entry_type, path=GoodPath)
        async def try_to_create_already_exists(self, type, path):
            if type == "file":
                if str(path) == "/":
                    with pytest.raises(PermissionError):
                        await self.workspace.mkdir(path)
                else:
                    with pytest.raises(FileExistsError):
                        await self.workspace.mkdir(path)

        @rule(path=BadPath)
        async def try_to_update_file(self, path):
            with pytest.raises(OSError):
                async with await self.workspace.open_file(path, "r+") as f:
                    await f.write(b"a")

        @rule(path=BadPath)
        async def try_to_delete(self, path):
            with pytest.raises(FileNotFoundError):
                await self.workspace.unlink(path)
            with pytest.raises(FileNotFoundError):
                await self.workspace.rmdir(path)

        @rule(src=BadPath, dst_name=st_entry_name)
        async def try_to_move_bad_src(self, src, dst_name):
            dst = "/%s" % dst_name
            with pytest.raises(OSError):
                await self.workspace.rename(src, dst)

        @rule(src=GoodPath, dst=GoodFolderPath)
        async def try_to_move_bad_dst(self, src, dst):
            # TODO: why so much special cases ?
            if src == dst and src != "/":
                await self.workspace.rename(src, dst)
            else:
                with pytest.raises(OSError):
                    await self.workspace.rename(src, dst)

        @rule(path=GoodPath)
        async def sync(self, path):
            entry_id = await self.workspace.path_id(path)
            await self.workspace.sync_by_id(entry_id)

        @invariant()
        async def check_fs(self):
            try:
                fs_dump = await self.workspace.dump()
            except AttributeError:
                # FS not yet initialized
                pass
            else:
                assert fs_dump == self.initial_fs_dump
Beispiel #7
0
    class FSOnlineTreeAndSync(user_fs_online_state_machine):
        Files = Bundle("file")
        Folders = Bundle("folder")

        @property
        def workspace(self):
            return self.user_fs.get_workspace(self.wid)

        @initialize(target=Folders)
        async def init(self):
            await self.reset_all()
            self.oracle_fs = oracle_fs_with_sync_factory()
            self.oracle_fs.create_workspace("/w")
            self.device = alice

            await self.start_backend()
            await self.restart_user_fs(self.device)
            self.wid = await self.user_fs.workspace_create("w")
            workspace = self.user_fs.get_workspace(self.wid)
            await workspace.sync()
            await self.user_fs.sync()

            return "/w"

        @rule()
        async def restart(self):
            await self.restart_user_fs(self.device)

        @rule()
        async def reset(self):
            await self.reset_user_fs(self.device)
            self.oracle_fs.reset()
            self.oracle_fs.create_workspace("/w")
            await self.user_fs.sync()

        @rule()
        async def sync_root(self):
            await self.workspace.sync()
            await self.user_fs.sync()
            self.oracle_fs.sync()

        # TODO: really complex to implement...
        #         @rule(path=st.one_of(Folders, Files))
        #         def sync(self, path):
        #             rep = await self.core_cmd({"cmd": "synchronize", "path": path})
        #             note(rep)
        #             expected_status = self.oracle_fs.sync(path)
        #             assert rep["status"] == expected_status

        @rule(target=Files, parent=Folders, name=st_entry_name)
        async def create_file(self, parent, name):
            path = f"{parent}/{name}"
            expected_status = self.oracle_fs.create_file(path)
            if expected_status == "ok":
                await self.workspace.touch(path=get_path(path), exist_ok=False)
            else:
                with pytest.raises((FileExistsError, FileNotFoundError, NotADirectoryError)):
                    await self.workspace.touch(path=get_path(path), exist_ok=False)
            return path

        @rule(target=Folders, parent=Folders, name=st_entry_name)
        async def create_folder(self, parent, name):
            path = f"{parent}/{name}"
            expected_status = self.oracle_fs.create_folder(path)
            if expected_status == "ok":
                await self.workspace.mkdir(path=get_path(path), exist_ok=False)
            else:
                with pytest.raises((FileExistsError, FileNotFoundError, NotADirectoryError)):
                    await self.workspace.mkdir(path=get_path(path), exist_ok=False)
            return path

        @rule(path=Files)
        async def delete_file(self, path):
            expected_status = self.oracle_fs.unlink(path)
            if expected_status == "ok":
                await self.workspace.unlink(path=get_path(path))
            else:
                with pytest.raises(OSError):
                    await self.workspace.unlink(path=get_path(path))
            return path

        @rule(path=Folders)
        async def delete_folder(self, path):
            expected_status = self.oracle_fs.rmdir(path)
            if expected_status == "ok":
                await self.workspace.rmdir(path=get_path(path))
            else:
                with pytest.raises(OSError):
                    await self.workspace.rmdir(path=get_path(path))
            return path

        @rule(target=Files, src=Files, dst_parent=Folders, dst_name=st_entry_name)
        async def move_file(self, src, dst_parent, dst_name):
            dst = f"{dst_parent}/{dst_name}"
            expected_status = self.oracle_fs.move(src, dst)
            if expected_status == "ok":
                await self.workspace.rename(get_path(src), get_path(dst))
            else:
                with pytest.raises(OSError):
                    await self.workspace.rename(get_path(src), get_path(dst))
            return dst

        @rule(target=Folders, src=Folders, dst_parent=Folders, dst_name=st_entry_name)
        async def move_folder(self, src, dst_parent, dst_name):
            dst = f"{dst_parent}/{dst_name}"
            expected_status = self.oracle_fs.move(src, dst)
            if expected_status == "ok":
                await self.workspace.rename(get_path(src), get_path(dst))
            else:
                with pytest.raises(OSError):
                    await self.workspace.rename(get_path(src), get_path(dst))
            return dst

        async def _stat(self, path):
            expected = self.oracle_fs.stat(path)
            if expected["status"] != "ok":
                if path == "/w":
                    await self.workspace.path_info(get_path(path))
                else:
                    with pytest.raises(OSError):
                        await self.workspace.path_info(get_path(path))
            else:
                path_info = await self.workspace.path_info(get_path(path))
                assert path_info["type"] == expected["type"]
                # TODO: oracle's `base_version` is broken (synchronization
                # strategy with parent placeholder make it complex to get right)
                # assert stat["base_version"] == expected["base_version"]
                if not path_info["need_sync"]:
                    assert path_info["base_version"] > 0
                    if path == "/w":
                        assert not path_info["is_placeholder"]
                    else:
                        assert path_info["is_placeholder"] == expected["is_placeholder"]

        @rule(path=Files)
        async def stat_file(self, path):
            await self._stat(path)

        @rule(path=Folders)
        async def stat_folder(self, path):
            await self._stat(path)
Beispiel #8
0
    class EntryTransactionsStateMachine(TrioAsyncioRuleBasedStateMachine):
        Files = Bundle("file")
        Folders = Bundle("folder")

        async def start_transactions(self):
            async def _transactions_controlled_cb(started_cb):
                async with WorkspaceStorage.run(alice, Path("/dummy"), EntryID()) as local_storage:
                    entry_transactions = await entry_transactions_factory(
                        self.device, alice_backend_cmds, local_storage=local_storage
                    )
                    file_transactions = await file_transactions_factory(
                        self.device, alice_backend_cmds, local_storage=local_storage
                    )
                    await started_cb(
                        entry_transactions=entry_transactions, file_transactions=file_transactions
                    )

            self.transactions_controller = await self.get_root_nursery().start(
                call_with_control, _transactions_controlled_cb
            )

        @initialize(target=Folders)
        async def init_root(self):
            nonlocal tentative
            tentative += 1
            await reset_testbed()

            self.last_step_id_to_path = set()
            self.device = alice
            await self.start_transactions()
            self.entry_transactions = self.transactions_controller.entry_transactions
            self.file_transactions = self.transactions_controller.file_transactions

            self.folder_oracle = Path(tmpdir / f"oracle-test-{tentative}")
            self.folder_oracle.mkdir()
            oracle_root = self.folder_oracle / "root"
            oracle_root.mkdir()
            self.folder_oracle.chmod(0o500)  # Root oracle can no longer be removed this way
            return PathElement("/", oracle_root)

        @rule(target=Files, parent=Folders, name=st_entry_name)
        async def touch(self, parent, name):
            path = parent / name

            expected_exc = None
            try:
                path.to_oracle().touch(exist_ok=False)
            except OSError as exc:
                expected_exc = exc

            with expect_raises(expected_exc):
                _, fd = await self.entry_transactions.file_create(path.to_guardata())
                await self.file_transactions.fd_close(fd)
            return path

        @rule(target=Folders, parent=Folders, name=st_entry_name)
        async def mkdir(self, parent, name):
            path = parent / name

            expected_exc = None
            try:
                path.to_oracle().mkdir(exist_ok=False)
            except OSError as exc:
                expected_exc = exc

            with expect_raises(expected_exc):
                await self.entry_transactions.folder_create(path.to_guardata())

            return path

        @rule(path=Files)
        async def unlink(self, path):
            expected_exc = None
            try:
                path.to_oracle().unlink()
            except OSError as exc:
                expected_exc = exc

            with expect_raises(expected_exc):
                await self.entry_transactions.file_delete(path.to_guardata())

        @rule(path=Files, length=st.integers(min_value=0, max_value=32))
        async def resize(self, path, length):
            expected_exc = None
            try:
                os.truncate(path.to_oracle(), length)
            except OSError as exc:
                expected_exc = exc

            with expect_raises(expected_exc):
                await self.entry_transactions.file_resize(path.to_guardata(), length)

        @rule(path=Folders)
        async def rmdir(self, path):
            expected_exc = None
            try:
                path.to_oracle().rmdir()
            except OSError as exc:
                expected_exc = exc

            with expect_raises(expected_exc):
                await self.entry_transactions.folder_delete(path.to_guardata())

        async def _rename(self, src, dst_parent, dst_name):
            dst = dst_parent / dst_name

            expected_exc = None
            try:
                oracle_rename(src.to_oracle(), dst.to_oracle())
            except OSError as exc:
                expected_exc = exc

            with expect_raises(expected_exc):
                await self.entry_transactions.entry_rename(src.to_guardata(), dst.to_guardata())

            return dst

        @rule(target=Files, src=Files, dst_parent=Folders, dst_name=st_entry_name)
        async def rename_file(self, src, dst_parent, dst_name):
            return await self._rename(src, dst_parent, dst_name)

        @rule(target=Folders, src=Folders, dst_parent=Folders, dst_name=st_entry_name)
        async def rename_folder(self, src, dst_parent, dst_name):
            return await self._rename(src, dst_parent, dst_name)

        @invariant()
        async def check_access_to_path_unicity(self):
            try:
                self.entry_transactions
            except AttributeError:
                return

            local_storage = self.entry_transactions.local_storage
            root_entry_id = self.entry_transactions.get_workspace_entry().id
            new_id_to_path = set()

            async def _recursive_build_id_to_path(entry_id, parent_id):
                new_id_to_path.add((entry_id, parent_id))
                manifest = await local_storage.get_manifest(entry_id)
                if is_folder_manifest(manifest):
                    for child_name, child_entry_id in manifest.children.items():
                        await _recursive_build_id_to_path(child_entry_id, entry_id)

            await _recursive_build_id_to_path(root_entry_id, None)

            added_items = new_id_to_path - self.last_step_id_to_path
            for added_id, added_parent in added_items:
                for old_id, old_parent in self.last_step_id_to_path:
                    if old_id == added_id and added_parent != old_parent.parent:
                        raise AssertionError(
                            f"Same id ({old_id}) but different parent: {old_parent} -> {added_parent}"
                        )

            self.last_step_id_to_path = new_id_to_path
    class FSOfflineRestartAndTree(user_fs_offline_state_machine):
        Files = Bundle("file")
        Folders = Bundle("folder")

        @initialize(target=Folders)
        async def init(self):
            await self.reset_all()
            self.device = alice
            await self.restart_user_fs(self.device)
            self.wid = await self.user_fs.workspace_create("w")
            self.workspace = self.user_fs.get_workspace(self.wid)

            self.oracle_fs = oracle_fs_factory()
            self.oracle_fs.create_workspace("/w")
            return "/w"

        @rule()
        async def restart(self):
            await self.restart_user_fs(self.device)
            self.workspace = self.user_fs.get_workspace(self.wid)

        @rule(target=Files, parent=Folders, name=st_entry_name)
        async def create_file(self, parent, name):
            path = f"{parent}/{name}"
            expected_status = self.oracle_fs.create_file(path)
            if expected_status == "ok":
                await self.workspace.touch(path=get_path(path), exist_ok=False)
            else:
                with pytest.raises(
                    (FileExistsError, FileNotFoundError, NotADirectoryError)):
                    await self.workspace.touch(path=get_path(path),
                                               exist_ok=False)
            return path

        @rule(target=Folders, parent=Folders, name=st_entry_name)
        async def create_folder(self, parent, name):
            path = f"{parent}/{name}"
            expected_status = self.oracle_fs.create_folder(path)
            if expected_status == "ok":
                await self.workspace.mkdir(path=get_path(path), exist_ok=False)
            else:
                with pytest.raises(
                    (FileExistsError, FileNotFoundError, NotADirectoryError)):
                    await self.workspace.mkdir(path=get_path(path),
                                               exist_ok=False)
            return path

        @rule(path=Files)
        async def delete_file(self, path):
            expected_status = self.oracle_fs.unlink(path)
            if expected_status == "ok":
                await self.workspace.unlink(path=get_path(path))
            else:
                with pytest.raises(OSError):
                    await self.workspace.unlink(path=get_path(path))
            return path

        @rule(path=Folders)
        async def delete_folder(self, path):
            expected_status = self.oracle_fs.rmdir(path)
            if expected_status == "ok":
                await self.workspace.rmdir(path=get_path(path))
            else:
                with pytest.raises(OSError):
                    await self.workspace.rmdir(path=get_path(path))
            return path

        @rule(target=Files,
              src=Files,
              dst_parent=Folders,
              dst_name=st_entry_name)
        async def move_file(self, src, dst_parent, dst_name):
            dst = f"{dst_parent}/{dst_name}"
            expected_status = self.oracle_fs.move(src, dst)
            if expected_status == "ok":
                await self.workspace.rename(get_path(src), get_path(dst))
            else:
                with pytest.raises(OSError):
                    await self.workspace.rename(get_path(src), get_path(dst))
            return dst

        @rule(target=Folders,
              src=Folders,
              dst_parent=Folders,
              dst_name=st_entry_name)
        async def move_folder(self, src, dst_parent, dst_name):
            dst = f"{dst_parent}/{dst_name}"
            expected_status = self.oracle_fs.move(src, dst)
            if expected_status == "ok":
                await self.workspace.rename(get_path(src), get_path(dst))
            else:
                with pytest.raises(OSError):
                    await self.workspace.rename(get_path(src), get_path(dst))
            return dst
Beispiel #10
0
    class FSOnlineConcurrentUser(TrioAsyncioRuleBasedStateMachine):
        Workspaces = Bundle("workspace")
        FSs = Bundle("fs")

        async def start_user_fs(self, device):
            async def _user_fs_controlled_cb(started_cb):
                async with user_fs_factory(device=device) as fs:
                    await started_cb(fs=fs)

            return await self.get_root_nursery().start(call_with_control,
                                                       _user_fs_controlled_cb)

        async def start_backend(self):
            async def _backend_controlled_cb(started_cb):
                async with backend_factory() as backend:
                    async with server_factory(backend.handle_client) as server:
                        await started_cb(backend=backend, server=server)

            return await self.get_root_nursery().start(call_with_control,
                                                       _backend_controlled_cb)

        @property
        def user_fs1(self):
            return self.user_fs1_controller.fs

        @property
        def user_fs2(self):
            return self.user_fs2_controller.fs

        @initialize(target=Workspaces)
        async def init(self):
            await reset_testbed()
            self.backend_controller = await self.start_backend()

            self.device1 = self.backend_controller.server.correct_addr(alice)
            self.device2 = self.backend_controller.server.correct_addr(alice2)
            self.user_fs1_controller = await self.start_user_fs(self.device1)
            self.user_fs2_controller = await self.start_user_fs(self.device2)

            self.wid = await self.user_fs1.workspace_create(EntryName("w"))
            workspace = self.user_fs1.get_workspace(self.wid)
            await workspace.sync()
            await self.user_fs1.sync()
            await self.user_fs2.sync()

            return self.wid, "w"

        @initialize(target=FSs, force_after_init=Workspaces)
        async def register_user_fs1(self, force_after_init):
            return self.user_fs1

        @initialize(target=FSs, force_after_init=Workspaces)
        async def register_user_fs2(self, force_after_init):
            return self.user_fs2

        @rule(target=Workspaces, fs=FSs, name=st_entry_name)
        async def create_workspace(self, fs, name):
            try:
                wid = await fs.workspace_create(EntryName(name))
                workspace = fs.get_workspace(wid)
                await workspace.sync()
            except AssertionError:
                return "wrong", name
            return wid, name

        @rule(target=Workspaces,
              fs=FSs,
              src=Workspaces,
              dst_name=st_entry_name)
        async def rename_workspace(self, fs, src, dst_name):
            wid, _ = src
            if wid == "wrong":
                return src[0], src[1]
            try:
                await fs.workspace_rename(wid, EntryName(dst_name))
            except FSWorkspaceNotFoundError:
                pass
            return wid, dst_name

        @rule()
        async def sync(self):
            # Send two syncs in a row given file conflict results are not synced
            # once created

            workspace1 = self.user_fs1.get_workspace(self.wid)
            workspace2 = self.user_fs2.get_workspace(self.wid)

            # Sync 1
            await workspace1.sync()
            await self.user_fs1.sync()
            await self.user_fs1.sync()

            # Sync 2
            await workspace2.sync()
            await self.user_fs2.sync()
            await self.user_fs2.sync()

            # Sync 1
            await workspace1.sync()
            await self.user_fs1.sync()

            fs_dump_1 = await workspace1.dump()
            fs_dump_2 = await workspace2.dump()
            compare_fs_dumps(fs_dump_1, fs_dump_2)