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)
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)
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}
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)))
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
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)
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
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)