|
| 1 | +####################################################################### |
| 2 | +# Copyright (c) 2019-present, Blosc Development Team <blosc@blosc.org> |
| 3 | +# All rights reserved. |
| 4 | +# |
| 5 | +# This source code is licensed under a BSD-style license (found in the |
| 6 | +# LICENSE file in the root directory of this source tree) |
| 7 | +####################################################################### |
| 8 | + |
| 9 | +"""fsspec reads against a real S3 endpoint, served locally by moto. |
| 10 | +
|
| 11 | +Everything else about the fsspec support is tested over ``memory://``, which is |
| 12 | +protocol-generic and needs no dependencies. Two things it structurally cannot |
| 13 | +cover, both of which have already hidden a bug: |
| 14 | +
|
| 15 | +- it is not an *async* backend, so ``aget_chunk`` always took its blocking |
| 16 | + fallback there, while against s3fs it raised "got Future attached to a |
| 17 | + different loop" on every chunk; |
| 18 | +- it is poorer in metadata than any real store (no mtime), which let a |
| 19 | + size-only cache stamp serve a stale chunk cache. |
| 20 | +
|
| 21 | +These run offline -- moto is a local server, no credentials, no network -- so |
| 22 | +they are not marked ``network``. |
| 23 | +""" |
| 24 | + |
| 25 | +import asyncio |
| 26 | + |
| 27 | +import numpy as np |
| 28 | +import pytest |
| 29 | + |
| 30 | +import blosc2 |
| 31 | + |
| 32 | +pytest.importorskip("s3fs") |
| 33 | +pytest.importorskip("moto") |
| 34 | +fsspec = pytest.importorskip("fsspec") |
| 35 | + |
| 36 | +BUCKET = "blosc2-test" |
| 37 | + |
| 38 | + |
| 39 | +@pytest.fixture(scope="module") |
| 40 | +def s3_endpoint(): |
| 41 | + """A local S3 server, and fsspec configured to reach it.""" |
| 42 | + import fsspec.config |
| 43 | + from moto.server import ThreadedMotoServer |
| 44 | + |
| 45 | + server = ThreadedMotoServer(ip_address="127.0.0.1", port=0, verbose=False) |
| 46 | + server.start() |
| 47 | + host, port = server.get_host_and_port() |
| 48 | + endpoint = f"http://{host}:{port}" |
| 49 | + |
| 50 | + # blosc2.open() has no storage_options passthrough, so the endpoint and the |
| 51 | + # dummy credentials go through fsspec's own per-protocol defaults |
| 52 | + previous = fsspec.config.conf.get("s3") |
| 53 | + fsspec.config.conf["s3"] = { |
| 54 | + "endpoint_url": endpoint, |
| 55 | + "key": "testing", |
| 56 | + "secret": "testing", |
| 57 | + # Not us-east-1: creating a bucket there must carry no location |
| 58 | + # constraint, and s3fs sends one whenever it knows the region |
| 59 | + "client_kwargs": {"region_name": "eu-west-1"}, |
| 60 | + } |
| 61 | + fsspec.filesystem("s3", **fsspec.config.conf["s3"]).mkdir(BUCKET) |
| 62 | + yield endpoint |
| 63 | + |
| 64 | + fsspec.config.conf.pop("s3", None) |
| 65 | + if previous is not None: |
| 66 | + fsspec.config.conf["s3"] = previous |
| 67 | + server.stop() |
| 68 | + |
| 69 | + |
| 70 | +@pytest.fixture(scope="module") |
| 71 | +def stored(s3_endpoint): |
| 72 | + """A 10-chunk array in the bucket, plus the array it was made from.""" |
| 73 | + a = blosc2.arange(0, 1000, dtype=np.int32, chunks=(100,)) |
| 74 | + urlpath = f"s3://{BUCKET}/ds.b2nd" |
| 75 | + a.save(urlpath) |
| 76 | + return urlpath, a |
| 77 | + |
| 78 | + |
| 79 | +def test_save_and_open_whole(stored): |
| 80 | + urlpath, a = stored |
| 81 | + assert np.array_equal(blosc2.open(urlpath)[:], a[:]) |
| 82 | + |
| 83 | + |
| 84 | +def test_cache_storage(stored, tmp_path): |
| 85 | + urlpath, a = stored |
| 86 | + b = blosc2.open(urlpath, cache_storage=tmp_path, mmap_mode="r") |
| 87 | + assert np.array_equal(b[:], a[:]) |
| 88 | + |
| 89 | + |
| 90 | +def test_lazy_range_reads(stored): |
| 91 | + urlpath, a = stored |
| 92 | + p = blosc2.open(urlpath, lazy=True) |
| 93 | + assert np.array_equal(p[150:250], a[150:250]) |
| 94 | + assert np.array_equal(p[:], a[:]) |
| 95 | + |
| 96 | + |
| 97 | +@pytest.mark.parametrize("max_concurrency", [1, 8]) |
| 98 | +def test_lazy_concurrency(stored, max_concurrency): |
| 99 | + urlpath, a = stored |
| 100 | + p = blosc2.open(urlpath, lazy=True, max_concurrency=max_concurrency) |
| 101 | + assert np.array_equal(p[:], a[:]) |
| 102 | + |
| 103 | + |
| 104 | +@pytest.mark.parametrize("max_concurrency", [1, 8]) |
| 105 | +def test_afetch_on_an_async_backend(stored, max_concurrency): |
| 106 | + # The regression this file exists for: s3fs runs its coroutines on a private |
| 107 | + # event loop, so awaiting one from the caller's loop fails outright |
| 108 | + urlpath, a = stored |
| 109 | + p = blosc2.open(urlpath, lazy=True) |
| 110 | + cache = asyncio.run(p.afetch(slice(150, 250), max_concurrency=max_concurrency)) |
| 111 | + assert np.array_equal(cache[150:250], a[150:250]) |
| 112 | + |
| 113 | + |
| 114 | +def test_lazy_expression(stored): |
| 115 | + urlpath, a = stored |
| 116 | + p = blosc2.open(urlpath, lazy=True) |
| 117 | + assert np.array_equal((p * 2)[150:250], a[150:250] * 2) |
0 commit comments