|
1 | 1 | from __future__ import annotations
|
2 | 2 |
|
| 3 | +import asyncio |
3 | 4 | import json
|
4 | 5 | import os
|
5 | 6 | import re
|
| 7 | +import warnings |
| 8 | +from itertools import cycle |
6 | 9 | from typing import TYPE_CHECKING, Any
|
7 | 10 |
|
8 | 11 | import numpy as np
|
|
42 | 45 | ]
|
43 | 46 |
|
44 | 47 | fsspec = pytest.importorskip("fsspec")
|
| 48 | +AsyncFileSystem = pytest.importorskip("fsspec.asyn").AsyncFileSystem |
45 | 49 | s3fs = pytest.importorskip("s3fs")
|
46 | 50 | requests = pytest.importorskip("requests")
|
47 | 51 | moto_server = pytest.importorskip("moto.moto_server.threaded_moto_server")
|
@@ -440,3 +444,121 @@ async def test_with_read_only_auto_mkdir(tmp_path: Path) -> None:
|
440 | 444 |
|
441 | 445 | store_w = FsspecStore.from_url(f"file://{tmp_path}", storage_options={"auto_mkdir": False})
|
442 | 446 | _ = store_w.with_read_only()
|
| 447 | + |
| 448 | + |
| 449 | +class LockableFileSystem(AsyncFileSystem): |
| 450 | + """ |
| 451 | + A mock file system that simulates asynchronous and synchronous behaviors with artificial delays. |
| 452 | + """ |
| 453 | + |
| 454 | + def __init__( |
| 455 | + self, |
| 456 | + asynchronous: bool, |
| 457 | + lock: bool | None = None, |
| 458 | + delays: tuple[float, ...] | None = None, |
| 459 | + ) -> None: |
| 460 | + if delays is None: |
| 461 | + delays = ( |
| 462 | + 0.03, |
| 463 | + 0.01, |
| 464 | + ) |
| 465 | + lock = lock if lock is not None else not asynchronous |
| 466 | + |
| 467 | + # self.asynchronous = asynchronous |
| 468 | + self.lock = asyncio.Lock() if lock else None |
| 469 | + self.delays = cycle(delays) |
| 470 | + self.async_impl = True |
| 471 | + |
| 472 | + super().__init__(asynchronous=asynchronous) |
| 473 | + |
| 474 | + async def _check_active(self) -> None: |
| 475 | + if self.lock and self.lock.locked(): |
| 476 | + raise RuntimeError("Concurrent requests!") |
| 477 | + |
| 478 | + async def _cat_file(self, path, start=None, end=None) -> bytes: |
| 479 | + await self._simulate_io_operation(path) |
| 480 | + return self.get_data(path) |
| 481 | + |
| 482 | + async def _await_io(self) -> None: |
| 483 | + await asyncio.sleep(next(self.delays)) |
| 484 | + |
| 485 | + async def _simulate_io_operation(self, path) -> None: |
| 486 | + if self.lock: |
| 487 | + await self._check_active() |
| 488 | + async with self.lock: |
| 489 | + await self._await_io() |
| 490 | + else: |
| 491 | + await self._await_io() |
| 492 | + |
| 493 | + def get_store(self, path: str) -> FsspecStore: |
| 494 | + with warnings.catch_warnings(): |
| 495 | + warnings.simplefilter("ignore", category=UserWarning) |
| 496 | + return FsspecStore(fs=self, path=path) |
| 497 | + |
| 498 | + @staticmethod |
| 499 | + def get_data(key: str) -> bytes: |
| 500 | + return f"{key}_data".encode() |
| 501 | + |
| 502 | + |
| 503 | +@pytest.mark.asyncio |
| 504 | +class TestLockableFSSPECFileSystem: |
| 505 | + @pytest.fixture(autouse=True) |
| 506 | + async def setup(self): |
| 507 | + self.path = "root" |
| 508 | + self.store_async = LockableFileSystem(asynchronous=True).get_store(path=self.path) |
| 509 | + self.store_sync = LockableFileSystem(asynchronous=False).get_store(path=self.path) |
| 510 | + |
| 511 | + def get_requests_and_true_results(self, path_components=("a", "b")): |
| 512 | + true_results = [ |
| 513 | + (component, LockableFileSystem.get_data(os.path.join(self.path, component))) |
| 514 | + for component in path_components |
| 515 | + ] |
| 516 | + requests = [(component, default_buffer_prototype(), None) for component in path_components] |
| 517 | + return requests, true_results |
| 518 | + |
| 519 | + async def test_get_many_asynchronous_fs(self): |
| 520 | + requests, true_results = self.get_requests_and_true_results(("a", "b", "c")) |
| 521 | + |
| 522 | + results = [] |
| 523 | + async for k, v in self.store_async._get_many(requests): |
| 524 | + results.append((k, v.to_bytes() if v else None)) |
| 525 | + |
| 526 | + results_ordered = sorted(results, key=lambda x: x[0]) |
| 527 | + assert results_ordered == true_results |
| 528 | + |
| 529 | + async def test_get_many_synchronous_fs(self): |
| 530 | + requests, true_results = self.get_requests_and_true_results() |
| 531 | + |
| 532 | + results = [] |
| 533 | + async for k, v in self.store_sync._get_many(requests): |
| 534 | + results.append((k, v.to_bytes() if v else None)) |
| 535 | + # Results should already be in the same order as requests |
| 536 | + |
| 537 | + assert results == true_results |
| 538 | + |
| 539 | + async def test_get_many_ordered_synchronous_fs(self): |
| 540 | + requests, true_results = self.get_requests_and_true_results() |
| 541 | + |
| 542 | + results = await self.store_sync._get_many_ordered(requests) |
| 543 | + results = [value.to_bytes() if value else None for value in results] |
| 544 | + |
| 545 | + assert results == [value[1] for value in true_results] |
| 546 | + |
| 547 | + async def test_get_many_ordered_asynchronous_fs(self): |
| 548 | + requests, true_results = self.get_requests_and_true_results() |
| 549 | + |
| 550 | + results = await self.store_async._get_many_ordered(requests) |
| 551 | + results = [value.to_bytes() if value else None for value in results] |
| 552 | + |
| 553 | + assert results == [value[1] for value in true_results] |
| 554 | + |
| 555 | + async def test_asynchronous_locked_fs_raises(self): |
| 556 | + store = LockableFileSystem(asynchronous=True, lock=True).get_store(path="root") |
| 557 | + requests, _ = self.get_requests_and_true_results() |
| 558 | + |
| 559 | + with pytest.raises(RuntimeError, match="Concurrent requests!"): |
| 560 | + async for _, _ in store._get_many(requests): |
| 561 | + pass |
| 562 | + |
| 563 | + with pytest.raises(RuntimeError, match="Concurrent requests!"): |
| 564 | + await store._get_many_ordered(requests) |
0 commit comments