diff --git a/syft/sync/connections/connection_router.py b/syft/sync/connections/connection_router.py index 619379bdbfd..65bec3b9101 100644 --- a/syft/sync/connections/connection_router.py +++ b/syft/sync/connections/connection_router.py @@ -61,13 +61,18 @@ def transport(self) -> Transport: return self._transport @classmethod - def from_configs(cls, email: str, connection_configs: List[ConnectionConfig]): + def from_configs( + cls, connection_configs: List[ConnectionConfig], peer_store: PeerStore + ) -> "ConnectionRouter": + """The one router of a manager. ``SyftboxManager.from_config`` builds + it and hands it to every part, so they share one connection, one + transport and one peer store.""" return cls( connections=[ SyftboxPlatformConnection.from_config(config) for config in connection_configs ], - peer_store=PeerStore(email=email), + peer_store=peer_store, ) def add_connection(self, connection: SyftboxPlatformConnection): diff --git a/syft/sync/syftbox_manager.py b/syft/sync/syftbox_manager.py index 5664e35f9df..2623754ce06 100644 --- a/syft/sync/syftbox_manager.py +++ b/syft/sync/syftbox_manager.py @@ -12,6 +12,7 @@ from syft.sync.callback_mixin import BaseModelCallbackMixin from syft.sync.connections.base_connection import ( + ConnectionConfig, SyftboxPlatformConnection, ) from syft.sync.connections.connection_router import ConnectionRouter @@ -54,6 +55,7 @@ random_email, random_syftbox_folder_for_testing, ) +from syft.sync.peers.peer_store import PeerStore from syft.sync.version.peer_manager import ( PeerManager, PeerManagerConfig, @@ -122,6 +124,10 @@ class SyftboxManagerConfig(BaseModel): # log, rolling state, checkpoints. Off for datasites that can never # restore (see syft-enclave's EnclaveSettings). persist_owner_state: bool = True + # The backends to connect to. Only the manager reads them: it builds one + # connection router from them and shares it with every part. Empty for + # tests that add a mock connection afterwards (see _add_connection). + connection_configs: List[ConnectionConfig] = [] datasite_owner_syncer_config: DatasiteOwnerSyncerConfig peer_manager_config: PeerManagerConfig @@ -163,7 +169,6 @@ def for_colab( syftbox_folder=syftbox_folder, collections_folder=collections_folder, collection_specs=collection_specs, - connection_configs=connection_configs, cache_config=DataSiteOwnerEventCacheConfig( email=email, use_in_memory_cache=use_in_memory_cache, @@ -175,19 +180,16 @@ def for_colab( datasite_watcher_syncer_config = DatasiteWatcherSyncerConfig( syftbox_folder=syftbox_folder, email=email, - connection_configs=connection_configs, datasite_watcher_cache_config=DataSiteWatcherCacheConfig( email=email, use_in_memory_cache=use_in_memory_cache, syftbox_folder=syftbox_folder, collection_specs=collection_specs, - connection_configs=connection_configs, ), ) peer_manager_config = PeerManagerConfig( syftbox_folder=syftbox_folder, email=email, - connection_configs=connection_configs, has_do_role=has_do_role, has_ds_role=has_ds_role, encryption_policy=EncryptionPolicy.from_bool(encryption), @@ -197,9 +199,9 @@ def for_colab( return cls( email=email, syftbox_folder=syftbox_folder, + connection_configs=connection_configs, has_ds_role=has_ds_role, has_do_role=has_do_role, - connection_configs=connection_configs, use_in_memory_cache=False, persist_owner_state=persist_owner_state, datasite_owner_syncer_config=datasite_owner_syncer_config, @@ -246,32 +248,27 @@ def for_jupyter( syftbox_folder=syftbox_folder, collections_folder=collections_folder, collection_specs=collection_specs, - connection_configs=connection_configs, cache_config=DataSiteOwnerEventCacheConfig( email=email, use_in_memory_cache=False, syftbox_folder=syftbox_folder, collections_folder=collections_folder, collection_variants=collection_variants, - connection_configs=connection_configs, ), ) datasite_watcher_syncer_config = DatasiteWatcherSyncerConfig( syftbox_folder=syftbox_folder, email=email, - connection_configs=connection_configs, datasite_watcher_cache_config=DataSiteWatcherCacheConfig( email=email, use_in_memory_cache=False, syftbox_folder=syftbox_folder, collection_specs=collection_specs, - connection_configs=connection_configs, ), ) peer_manager_config = PeerManagerConfig( syftbox_folder=syftbox_folder, email=email, - connection_configs=connection_configs, has_do_role=has_do_role, has_ds_role=has_ds_role, encryption_policy=EncryptionPolicy.from_bool(encryption), @@ -281,6 +278,7 @@ def for_jupyter( return cls( email=email, syftbox_folder=syftbox_folder, + connection_configs=connection_configs, has_ds_role=has_ds_role, has_do_role=has_do_role, use_in_memory_cache=False, @@ -346,7 +344,6 @@ def _base_config_for_testing( peer_manager_config = PeerManagerConfig( syftbox_folder=syftbox_folder, email=email, - connection_configs=[], # Empty for in-memory, connections added later n_threads=2, # Use fewer threads for testing force_ignore_peer_version=not check_versions, has_do_role=has_do_role, @@ -402,7 +399,6 @@ def for_google_drive_testing_connection( syftbox_folder=syftbox_folder, collections_folder=collections_folder, collection_specs=collection_specs, - connection_configs=connection_configs, cache_config=DataSiteOwnerEventCacheConfig( email=email, use_in_memory_cache=use_in_memory_cache, @@ -414,20 +410,17 @@ def for_google_drive_testing_connection( datasite_watcher_syncer_config = DatasiteWatcherSyncerConfig( syftbox_folder=syftbox_folder, email=email, - connection_configs=connection_configs, datasite_watcher_cache_config=DataSiteWatcherCacheConfig( email=email, use_in_memory_cache=use_in_memory_cache, syftbox_folder=syftbox_folder, collection_specs=collection_specs, - connection_configs=connection_configs, ), ) peer_manager_config = PeerManagerConfig( syftbox_folder=syftbox_folder, email=email, - connection_configs=connection_configs, force_ignore_peer_version=not check_versions, has_do_role=has_do_role, has_ds_role=has_ds_role, @@ -435,6 +428,7 @@ def for_google_drive_testing_connection( return cls( email=email, syftbox_folder=syftbox_folder, + connection_configs=connection_configs, write_files=write_files, persist_owner_state=persist_owner_state, datasite_owner_syncer_config=datasite_owner_syncer_config, @@ -466,7 +460,6 @@ class SyftboxManager(BaseModelCallbackMixin): _executor: ThreadPoolExecutor = PrivateAttr( default_factory=lambda: ThreadPoolExecutor(max_workers=10) ) - _peer_store: object = PrivateAttr(default=None) _PUBLIC_API = ( "email", @@ -524,6 +517,20 @@ def peers(self) -> PeerList: @classmethod def from_config(cls, config: SyftboxManagerConfig): + """Build a manager and its parts from ``config``. + + Configs hold plain values only (strings, paths, flags), never live + objects or state. A live object that several parts share is built here, + once, and passed to each part's ``from_config`` next to its config: + the peer store and the connection router that holds it, and through + the router one connection and one transport for the whole manager. + """ + peer_store = PeerManager.peer_store_from_config( + config.peer_manager_config, email=config.email + ) + connection_router = ConnectionRouter.from_configs( + config.connection_configs, peer_store + ) file_writer = FileWriter( base_path=config.syftbox_folder, write_files=config.write_files ) @@ -534,18 +541,18 @@ def from_config(cls, config: SyftboxManagerConfig): if config.has_do_role: datasite_owner_syncer = DatasiteOwnerSyncer.from_config( - config.datasite_owner_syncer_config + config.datasite_owner_syncer_config, connection_router ) job_file_change_handler = JobFileChangeHandler() if config.has_ds_role: datasite_watcher_syncer = DatasiteWatcherSyncer.from_config( - config.datasite_watcher_syncer_config + config.datasite_watcher_syncer_config, connection_router ) peer_manager = PeerManager.from_config( - config.peer_manager_config, email=config.email + config.peer_manager_config, connection_router ) manager_res = cls( @@ -560,30 +567,8 @@ def from_config(cls, config: SyftboxManagerConfig): has_ds_role=config.has_ds_role, config=config, ) - - # PeerManager.from_config built the peer store from peer_manager_config; - # share that single store across every syncer's router so all sync paths - # see the same peers, peer policies and keys. - manager_res._set_peer_store(peer_manager.peer_store) - return manager_res - def _set_peer_store(self, peer_store) -> None: - """Wire shared peer_store into all connection routers.""" - from syft.sync.peers.peer_store import PeerStore - - if not isinstance(peer_store, PeerStore): - return - self._peer_store = peer_store - if self.peer_manager: - self.peer_manager.peer_store = peer_store - self.peer_manager.connection_router.peer_store = peer_store - if self.datasite_owner_syncer: - self.datasite_owner_syncer.connection_router.peer_store = peer_store - if self.datasite_watcher_syncer: - self.datasite_watcher_syncer.connection_router.peer_store = peer_store - self.datasite_watcher_syncer.datasite_watcher_cache.connection_router.peer_store = peer_store - @classmethod def for_colab( cls, @@ -1088,16 +1073,7 @@ def _add_connection(self, connection: SyftboxPlatformConnection): "Only MockDriveService connections can be added to the manager" ) - if self.datasite_owner_syncer is not None: - self.datasite_owner_syncer.connection_router.add_connection(connection) - if self.datasite_watcher_syncer is not None: - self.datasite_watcher_syncer.connection_router.add_connection(connection) - self.datasite_watcher_syncer.datasite_watcher_cache.connection_router.add_connection( - connection - ) - - # Add connection to version manager's router - self.peer_manager.connection_router.add_connection(connection) + self._connection_router.add_connection(connection) def _send_file_change(self, path: str | Path, content: str): self.file_writer.write_file(path, content) @@ -1109,22 +1085,17 @@ def _get_all_accepted_events_do(self) -> List[FileChangeEventsMessage]: @property def _connection_router(self) -> ConnectionRouter: - # for DOs we have a syncer, for DSs we have a watcher syncer - if self.datasite_owner_syncer is not None: - return self.datasite_owner_syncer.connection_router - else: - return self.datasite_watcher_syncer.connection_router + # Every part holds the same router (see from_config); the peer manager + # is the one part every manager has. + return self.peer_manager.connection_router + + @property + def _peer_store(self) -> PeerStore: + return self._connection_router.peer_store def reset_all_connection_caches(self): - """Reset GDrive caches on all connection router instances.""" - if self.peer_manager: - self.peer_manager.connection_router.reset_caches() - if self.datasite_owner_syncer: - self.datasite_owner_syncer.connection_router.reset_caches() - if self.datasite_watcher_syncer: - self.datasite_watcher_syncer.connection_router.reset_caches() - if hasattr(self.datasite_watcher_syncer, "datasite_watcher_cache"): - self.datasite_watcher_syncer.datasite_watcher_cache.connection_router.reset_caches() + """Reset the Drive caches of the manager's one connection router.""" + self._connection_router.reset_caches() def _clear_caches(self): if self.datasite_owner_syncer is not None: diff --git a/syft/sync/sync/caches/datasite_watcher_cache.py b/syft/sync/sync/caches/datasite_watcher_cache.py index 3d9b96865b8..fb18ebb64c4 100644 --- a/syft/sync/sync/caches/datasite_watcher_cache.py +++ b/syft/sync/sync/caches/datasite_watcher_cache.py @@ -10,7 +10,6 @@ FileChangeEventsMessage, ) from syft.sync.connections.connection_router import ConnectionRouter -from syft.sync.connections.base_connection import ConnectionConfig from syft.sync.sync.caches.cache_file_writer_connection import ( CacheFileConnection, InMemoryCacheFileConnection, @@ -58,7 +57,6 @@ class DataSiteWatcherCacheConfig(BaseModel): use_in_memory_cache: bool = True syftbox_folder: Path | None = None events_base_path: Path | None = None - connection_configs: List[ConnectionConfig] = [] # Collections to sync down (prefix + local subpath per collection type) collection_specs: list[CollectionSyncSpec] = [] @@ -90,15 +88,18 @@ class DataSiteWatcherCache(BaseModel): pre_write_filter: Callable[[str, bool], bool] | None = None @classmethod - def from_config(cls, config: DataSiteWatcherCacheConfig): + def from_config( + cls, config: DataSiteWatcherCacheConfig, connection_router: ConnectionRouter + ): + """``config`` holds plain values only. ``connection_router`` is live and + shared with the other parts of the manager, so the caller builds it + once and passes it in next to the config (see + ``SyftboxManager.from_config``).""" if config.use_in_memory_cache: res = cls( events_connection=InMemoryCacheFileConnection[FileChangeEvent](), file_connection=InMemoryCacheFileConnection[str](), - connection_router=ConnectionRouter.from_configs( - email=config.email, - connection_configs=config.connection_configs, - ), + connection_router=connection_router, syftbox_folder=config.syftbox_folder, collection_specs=config.collection_specs, ) @@ -116,10 +117,7 @@ def from_config(cls, config: DataSiteWatcherCacheConfig): base_dir=events_folder, dtype=FileChangeEventsMessage ), file_connection=FSFileConnection(base_dir=config.syftbox_folder), - connection_router=ConnectionRouter.from_configs( - email=config.email, - connection_configs=config.connection_configs, - ), + connection_router=connection_router, syftbox_folder=config.syftbox_folder, collection_specs=config.collection_specs, ) diff --git a/syft/sync/sync/datasite_owner_syncer.py b/syft/sync/sync/datasite_owner_syncer.py index bbf919d93ed..3d558ae6e06 100644 --- a/syft/sync/sync/datasite_owner_syncer.py +++ b/syft/sync/sync/datasite_owner_syncer.py @@ -23,10 +23,7 @@ RollingState, raise_for_later_version, ) -from syft.sync.connections.base_connection import ( - ConnectionConfig, - FileCollection, -) +from syft.sync.connections.base_connection import FileCollection from syft.sync.connections.connection_router import ConnectionRouter from syft.sync.events.file_change_event import ( FileChangeEvent, @@ -153,7 +150,6 @@ class DatasiteOwnerSyncerConfig(BaseModel): cache_config: DataSiteOwnerEventCacheConfig = Field( default_factory=DataSiteOwnerEventCacheConfig ) - connection_configs: List[ConnectionConfig] = [] class OutgoingMessage(NamedTuple): @@ -216,7 +212,13 @@ class DatasiteOwnerSyncer(BaseModelCallbackMixin): _events_since_rolling_state_upload: int = PrivateAttr(default=0) @classmethod - def from_config(cls, config: DatasiteOwnerSyncerConfig): + def from_config( + cls, config: DatasiteOwnerSyncerConfig, connection_router: ConnectionRouter + ): + """``config`` holds plain values only. ``connection_router`` is live and + shared with the other parts of the manager, so the caller builds it + once and passes it in next to the config (see + ``SyftboxManager.from_config``).""" # Ensure cache config has the same collections_folder (both are now full paths) if config.collections_folder is not None: config.cache_config.collections_folder = config.collections_folder @@ -228,9 +230,7 @@ def from_config(cls, config: DatasiteOwnerSyncerConfig): event_cache=DataSiteOwnerEventCache.from_config(config.cache_config), write_files=config.write_files, persist_owner_state=config.persist_owner_state, - connection_router=ConnectionRouter.from_configs( - config.email, config.connection_configs - ), + connection_router=connection_router, email=config.email, syftbox_folder=config.syftbox_folder, perm_context=SyftPermContext(datasite=datasite), diff --git a/syft/sync/sync/datasite_watcher_syncer.py b/syft/sync/sync/datasite_watcher_syncer.py index 8cf587a42c9..359fa769250 100644 --- a/syft/sync/sync/datasite_watcher_syncer.py +++ b/syft/sync/sync/datasite_watcher_syncer.py @@ -5,7 +5,6 @@ from pydantic import BaseModel, Field, PrivateAttr -from syft.sync.connections.base_connection import ConnectionConfig from syft.sync.connections.connection_router import ConnectionRouter from syft.sync.callback_mixin import BaseModelCallbackMixin from syft.sync.events.file_change_event import FileChangeEventsMessage @@ -23,7 +22,6 @@ class DatasiteWatcherSyncerConfig(BaseModel): syftbox_folder: Path | None = None email: str | None = None - connection_configs: List[ConnectionConfig] = [] datasite_watcher_cache_config: DataSiteWatcherCacheConfig = Field( default_factory=DataSiteWatcherCacheConfig ) @@ -46,15 +44,19 @@ class Config: ) @classmethod - def from_config(cls, config: DatasiteWatcherSyncerConfig): + def from_config( + cls, config: DatasiteWatcherSyncerConfig, connection_router: ConnectionRouter + ): + """``config`` holds plain values only. ``connection_router`` is live and + shared with the other parts of the manager, so the caller builds it + once and passes it in next to the config (see + ``SyftboxManager.from_config``).""" return cls( syftbox_folder=config.syftbox_folder, email=config.email, - connection_router=ConnectionRouter.from_configs( - config.email, config.connection_configs - ), + connection_router=connection_router, datasite_watcher_cache=DataSiteWatcherCache.from_config( - config.datasite_watcher_cache_config + config.datasite_watcher_cache_config, connection_router ), ) diff --git a/syft/sync/version/peer_manager.py b/syft/sync/version/peer_manager.py index 427ffe546cb..225c3d09f8b 100644 --- a/syft/sync/version/peer_manager.py +++ b/syft/sync/version/peer_manager.py @@ -11,7 +11,6 @@ from typing import Dict, List, Optional from pydantic import BaseModel, ConfigDict, PrivateAttr, model_validator -from syft.sync.connections.base_connection import ConnectionConfig from syft.sync.connections.connection_router import ConnectionRouter from syft.sync.encryption_policy import EncryptionPolicy from syft.sync.peers.peer import Peer, PeerNotReadyError, PeerSetupError, PeerState @@ -106,7 +105,6 @@ class PeerManagerConfig(BaseModel): syftbox_folder: Path email: str = "" - connection_configs: List[ConnectionConfig] = [] # Proceeds with a peer whose version is unknown or whose client version is # incompatible, instead of skipping it. force_ignore_peer_version: bool = False @@ -184,29 +182,34 @@ def syncable_peers(self) -> List[Peer]: """Get all peers we can sync with (DS side).""" return self.peer_store.syncable_peers - @classmethod - def from_config(cls, config: PeerManagerConfig, email: str = "") -> "PeerManager": - """Create a PeerManager from a config. + @staticmethod + def peer_store_from_config(config: PeerManagerConfig, email: str = "") -> PeerStore: + """The peer store ``config`` describes. The manager builds it before the + connection router, which holds it for every part. ``email`` is a fallback for configs built without one; the config's own ``email`` takes precedence so the key path matches the datasite. """ - resolved_email = config.email or email - peer_store = PeerStore.create( - email=resolved_email, + return PeerStore.create( + email=config.email or email, encryption_policy=resolve_encryption_policy( config.settings_path, config.encryption_policy ), keys_path=config.crypto_keys_path, ) - connection_router = ConnectionRouter.from_configs( - resolved_email, config.connection_configs - ) - connection_router.peer_store = peer_store + + @classmethod + def from_config( + cls, config: PeerManagerConfig, connection_router: ConnectionRouter + ) -> "PeerManager": + """``config`` holds plain values only. ``connection_router`` is live and + shared with the other parts of the manager, so the caller builds it + once and passes it in next to the config (see + ``SyftboxManager.from_config``). The peer store is the router's.""" return cls( syftbox_folder=config.syftbox_folder, connection_router=connection_router, - peer_store=peer_store, + peer_store=connection_router.peer_store, force_ignore_peer_version=config.force_ignore_peer_version, suppress_version_warnings=config.suppress_version_warnings, n_threads=config.n_threads, diff --git a/tests/unit/test_cache_aware_sync.py b/tests/unit/test_cache_aware_sync.py index 4b1e1041054..663cecfe993 100644 --- a/tests/unit/test_cache_aware_sync.py +++ b/tests/unit/test_cache_aware_sync.py @@ -10,6 +10,7 @@ from syft.sync.sync.collection_spec import CollectionSyncSpec from tests.unit.utils import ( + router_without_connections, TEST_COLLECTION_PREFIX, TEST_COLLECTION_SUBPATH, get_mock_events_messages, @@ -244,9 +245,10 @@ def test_ds_cache_handles_deletions_correctly(): collection_specs=[ CollectionSyncSpec.public(TEST_COLLECTION_PREFIX, TEST_COLLECTION_SUBPATH) ], - connection_configs=[], ) - new_cache = DataSiteWatcherCache.from_config(config) + new_cache = DataSiteWatcherCache.from_config( + config, router_without_connections(do_manager.email) + ) # Verify deleted file is not in reloaded cache assert expected_path not in new_cache.file_hashes, ( diff --git a/tests/unit/test_encryption_policy.py b/tests/unit/test_encryption_policy.py index 978f8128bff..4973f394661 100644 --- a/tests/unit/test_encryption_policy.py +++ b/tests/unit/test_encryption_policy.py @@ -279,7 +279,7 @@ def start(policy): config = PeerManagerConfig( syftbox_folder=tmp_path, email="alice@example.com", encryption_policy=policy ) - return PeerManager.from_config(config).peer_store.encryption_policy + return PeerManager.peer_store_from_config(config).encryption_policy assert start(REQUIRED) == REQUIRED assert start(None) == REQUIRED diff --git a/tests/unit/test_shared_router.py b/tests/unit/test_shared_router.py new file mode 100644 index 00000000000..bd454416773 --- /dev/null +++ b/tests/unit/test_shared_router.py @@ -0,0 +1,54 @@ +"""A manager builds one connection router and every part shares it, so the +whole manager has one connection, one transport and one peer store.""" + +from syft.sync.connections.base_connection import SyftboxPlatformConnection +from syft.sync.connections.drive.grdrive_config import GdriveConnectionConfig +from syft.sync.connections.drive.mock_drive_service import pair_with_mock_service +from syft.sync.syftbox_manager import SyftboxManager, SyftboxManagerConfig + + +def _config_with_both_roles() -> SyftboxManagerConfig: + return SyftboxManagerConfig._base_config_for_testing( + has_do_role=True, has_ds_role=True + ) + + +def test_every_part_of_manager_shares_one_router(): + manager = SyftboxManager.from_config(_config_with_both_roles()) + router = manager._connection_router + + assert manager.peer_manager.connection_router is router + assert manager.datasite_owner_syncer.connection_router is router + assert manager.datasite_watcher_syncer.connection_router is router + watcher_cache = manager.datasite_watcher_syncer.datasite_watcher_cache + assert watcher_cache.connection_router is router + assert manager.peer_manager.peer_store is router.peer_store + assert manager._peer_store is router.peer_store + + +def test_manager_builds_one_connection_from_its_config(monkeypatch): + config = _config_with_both_roles() + config.connection_configs = [ + GdriveConnectionConfig(email=config.email, token_path=None) + ] + built = [] + + def build_mock_connection(connection_config): + connection, _ = pair_with_mock_service(config.email, "peer@example.com") + built.append(connection) + return connection + + monkeypatch.setattr( + SyftboxPlatformConnection, "from_config", staticmethod(build_mock_connection) + ) + manager = SyftboxManager.from_config(config) + + assert manager._connection_router.connections == built + assert len(built) == 1 + + +def test_added_mock_connection_is_added_once_and_backs_the_transport(): + _, do_manager = SyftboxManager.pair_with_mock_drive_service_connection() + [connection] = do_manager._connection_router.connections + + assert do_manager._connection_router.transport.p2p._conn is connection diff --git a/tests/unit/test_watcher_event_datasite_check.py b/tests/unit/test_watcher_event_datasite_check.py index 22fad435094..9a6ba6e596b 100644 --- a/tests/unit/test_watcher_event_datasite_check.py +++ b/tests/unit/test_watcher_event_datasite_check.py @@ -19,6 +19,7 @@ ) from syft.sync.syftbox_manager import SyftboxManager from syft.sync.utils.syftbox_utils import get_event_hash_from_content +from tests.unit.utils import router_without_connections VICTIM = "victim@example.com" PEER = "peer@example.com" @@ -41,7 +42,8 @@ def _event( def _cache() -> DataSiteWatcherCache: return DataSiteWatcherCache.from_config( - DataSiteWatcherCacheConfig(email=VICTIM, use_in_memory_cache=True) + DataSiteWatcherCacheConfig(email=VICTIM, use_in_memory_cache=True), + router_without_connections(VICTIM), ) @@ -127,11 +129,12 @@ def test_dropped_event_is_not_restored_after_restart(tmp_path): ) forged = _event(VICTIM, "syft.pub.yaml") legitimate = _event(PEER, "data/file.txt") - DataSiteWatcherCache.from_config(config).apply_event_message( + router = router_without_connections(VICTIM) + DataSiteWatcherCache.from_config(config, router).apply_event_message( FileChangeEventsMessage(events=[forged, legitimate]), PEER ) - restarted = DataSiteWatcherCache.from_config(config) + restarted = DataSiteWatcherCache.from_config(config, router) assert Path(f"{VICTIM}/syft.pub.yaml") not in restarted.file_hashes assert VICTIM not in restarted.last_event_timestamp_per_peer diff --git a/tests/unit/utils.py b/tests/unit/utils.py index 2d7c1f3da8d..01c01dedb27 100644 --- a/tests/unit/utils.py +++ b/tests/unit/utils.py @@ -9,6 +9,8 @@ from syft.sync.messages.proposed_filechange import ProposedFileChangesMessage from syft.sync.messages.proposed_filechange import ProposedFileChange from syft.sync.utils.syftbox_utils import get_event_hash_from_content +from syft.sync.connections.connection_router import ConnectionRouter +from syft.sync.peers.peer_store import PeerStore def get_mock_event(path: str = "email@email.com/test.job") -> FileChangeEvent: @@ -216,3 +218,10 @@ def grant_job_inbox_access(do_manager, ds_email: str) -> None: do_manager.datasite_owner_syncer.perm_context.open( f"app_data/job/inbox/{ds_email}/" ).grant_write_access(ds_email) + + +def router_without_connections(email: str) -> ConnectionRouter: + """A connection router with no backend, for a part a test builds on its own. + In a manager every part shares the router ``SyftboxManager.from_config`` + builds.""" + return ConnectionRouter(connections=[], peer_store=PeerStore(email=email))