Skip to content

Commit b22f886

Browse files
committed
Mark socket-based tests as network and run them only once
1 parent d38bafa commit b22f886

12 files changed

Lines changed: 94 additions & 13 deletions

‎pytest.ini‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ testpaths =
3030

3131
markers =
3232
heavy: tests that take long time to complete.
33-
network: tests that require network access.
33+
network: tests that require real network sockets, including localhost HTTP/S3 servers.
3434
tui: b2view Textual UI tests; each one boots a headless app session.
3535
xdist_group: pin a module to one `-n` worker; declared here so it is not an
3636
unknown mark (i.e. an error, given filterwarnings) when xdist is absent.

‎tests/conftest.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,13 @@ def cat2_context():
115115
yield c2params
116116

117117

118+
@pytest.hookimpl(wrapper=True)
118119
def pytest_runtest_call(item):
119120
# Skip network-marked tests when their endpoint or optional credentials are unavailable.
120121
if item.get_closest_marker("network") is None:
121-
return
122+
return (yield)
122123
try:
123-
item.runtest()
124+
return (yield)
124125
except _NETWORK_ERRORS as exc:
125126
pytest.skip(f"Skipping unavailable network test: {exc}")
126127

‎tests/ctable/test_remote_parquet.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,7 @@ def _raise_not_found_for_status(self, response, path):
814814
assert marker == {"size": "49961641", "x-bz-file-id": "file-version-1"}
815815

816816

817+
@pytest.mark.network
817818
def test_http_range_requests_are_narrow(tmp_path):
818819
path = tmp_path / "served.parquet"
819820
pq.write_table(
@@ -905,6 +906,7 @@ def test_s3_parquet_smoke():
905906
remote[remote.col_names[0]][0]
906907

907908

909+
@pytest.mark.network
908910
def test_http_without_range_support_fails_clearly(tmp_path):
909911
path = tmp_path / "large.parquet"
910912
pq.write_table(pa.table({"x": list(range(20_000))}), path, row_group_size=1_000)

‎tests/ndarray/test_c2array_blocks.py‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,13 @@
3535
# with it rather than raising. Windows serves the same stand-in badly -- a
3636
# client abort (WinError 10053) can leave an in-process server wedging the
3737
# xdist worker -- and serving HTTP from Windows is not what this suite covers.
38-
pytestmark = pytest.mark.skipif(
39-
blosc2.IS_WASM or sys.platform == "win32",
40-
reason="in-process HTTP servers not supported on wasm32 or Windows",
41-
)
38+
pytestmark = [
39+
pytest.mark.network,
40+
pytest.mark.skipif(
41+
blosc2.IS_WASM or sys.platform == "win32",
42+
reason="in-process HTTP servers not supported on wasm32 or Windows",
43+
),
44+
]
4245

4346

4447
class _Cat2Server:

‎tests/ndarray/test_c2array_writes.py‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,13 @@
3434
# The stand-in binds a real socket, which wasm32 has no listen(2) for, and
3535
# Windows serves badly enough to wedge an xdist worker (see
3636
# test_c2array_blocks.py); neither platform is tested as a server here.
37-
pytestmark = pytest.mark.skipif(
38-
blosc2.IS_WASM or sys.platform == "win32",
39-
reason="in-process HTTP servers not supported on wasm32 or Windows",
40-
)
37+
pytestmark = [
38+
pytest.mark.network,
39+
pytest.mark.skipif(
40+
blosc2.IS_WASM or sys.platform == "win32",
41+
reason="in-process HTTP servers not supported on wasm32 or Windows",
42+
),
43+
]
4144

4245
CHUNKS = (1000,)
4346
BLOCKS = (250,)

‎tests/test_b2z_source.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ def counted(self, path, start=None, end=None, **kwargs):
299299
@pytest.mark.parametrize("small", [False, True])
300300
@pytest.mark.parametrize("ctable", [False, True])
301301
@pytest.mark.usefixtures("b2z_range_reads")
302+
@pytest.mark.network
302303
def test_http_tail_bootstrap(suffix, small, ctable, tmp_path):
303304
import http.server
304305
import threading

‎tests/test_fsspec.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,7 @@ def test_unknown_protocol():
645645

646646
@_http_server_skip
647647
@pytest.mark.skipif(blosc2.IS_WASM, reason="no listening sockets on wasm32")
648+
@pytest.mark.network
648649
def test_http_url_is_read_through_fsspec(tmp_path):
649650
# A frame behind a plain web server -- no Caterva2 there to ask anything of --
650651
# is a frame like any other: fsspec reads it in ranges wherever the server
@@ -673,6 +674,7 @@ def test_http_url_is_read_through_fsspec(tmp_path):
673674

674675
@_http_server_skip
675676
@pytest.mark.skipif(blosc2.IS_WASM, reason="no listening sockets on wasm32")
677+
@pytest.mark.network
676678
def test_http_shared_b2nd_cache_across_processes(tmp_path):
677679
pytest.importorskip("aiohttp")
678680
blosc2.asarray(
@@ -704,6 +706,7 @@ def test_http_shared_b2nd_cache_across_processes(tmp_path):
704706

705707

706708
@_http_server_skip
709+
@pytest.mark.network
707710
def test_http_lazy_cache_rebuilt_when_remote_changes(tmp_path):
708711
pytest.importorskip("aiohttp")
709712
path = tmp_path / "www"
@@ -726,6 +729,7 @@ def test_http_lazy_cache_rebuilt_when_remote_changes(tmp_path):
726729

727730

728731
@_http_server_skip
732+
@pytest.mark.network
729733
def test_http_remote_array_checks_identity_without_refetching_cached_data(tmp_path):
730734
pytest.importorskip("aiohttp")
731735
path = tmp_path / "www"
@@ -814,6 +818,7 @@ def do_HEAD(self):
814818
server.server_close()
815819

816820

821+
@pytest.mark.network
817822
def test_http_hdf5_scan_and_warm_slice(tmp_path):
818823
h5py = pytest.importorskip("h5py")
819824
data = np.arange(10_000, dtype="int32")
@@ -830,6 +835,7 @@ def test_http_hdf5_scan_and_warm_slice(tmp_path):
830835

831836

832837
@pytest.mark.parametrize("format", ["hdf5", "b2z"])
838+
@pytest.mark.network
833839
def test_http_source_cache_across_processes(tmp_path, format):
834840
data = np.zeros(100, dtype=[("id", "i4"), ("value", "f8")])
835841
data["id"] = np.arange(len(data))
@@ -881,6 +887,7 @@ def test_http_source_cache_across_processes(tmp_path, format):
881887
assert head_requests == []
882888

883889

890+
@pytest.mark.network
884891
def test_http_large_hdf5_keeps_range_reads(tmp_path):
885892
h5py = pytest.importorskip("h5py")
886893
path = tmp_path / "large-seekable.h5"
@@ -893,6 +900,7 @@ def test_http_large_hdf5_keeps_range_reads(tmp_path):
893900
assert all(request is not None for request in requests)
894901

895902

903+
@pytest.mark.network
896904
def test_http_hdf5_source_close_closes_session(tmp_path):
897905
h5py = pytest.importorskip("h5py")
898906
data = np.arange(10_000, dtype="int32")
@@ -923,6 +931,7 @@ def test_http_hdf5_source_close_closes_session(tmp_path):
923931
sparse.get_chunk(0)
924932

925933

934+
@pytest.mark.network
926935
def test_http_hdf5_scan_closes_owned_session(tmp_path, monkeypatch):
927936
h5py = pytest.importorskip("h5py")
928937
import blosc2.hdf5_source as hdf5_source
@@ -947,6 +956,7 @@ def tracking(urlpath, storage_options=None, filesystem=None):
947956
assert created[0]._session.closed
948957

949958

959+
@pytest.mark.network
950960
def test_http_hdf5_failed_init_closes_owned_session(tmp_path, monkeypatch):
951961
h5py = pytest.importorskip("h5py")
952962
import blosc2.hdf5_source as hdf5_source
@@ -978,6 +988,7 @@ def tracking(urlpath, storage_options=None, filesystem=None):
978988
assert created[1]._session.closed
979989

980990

991+
@pytest.mark.network
981992
def test_http_hdf5_source_finalizer_closes_session(tmp_path):
982993
h5py = pytest.importorskip("h5py")
983994
data = np.arange(10_000, dtype="int32")
@@ -993,6 +1004,7 @@ def test_http_hdf5_source_finalizer_closes_session(tmp_path):
9931004
assert session.closed
9941005

9951006

1007+
@pytest.mark.network
9961008
def test_http_store_disk_reopen_and_transport_close(tmp_path):
9971009
h5py = pytest.importorskip("h5py")
9981010
data = np.arange(10_000, dtype="int32")

‎tests/test_fsspec_s3.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
- it is poorer in metadata than any real store (no mtime), which let a
1919
size-only cache stamp serve a stale chunk cache.
2020
21-
These run offline -- moto is a local server, no credentials, no network -- so
22-
they are not marked ``network``.
21+
These need no external service or credentials, but moto binds a real localhost
22+
socket, so they are marked ``network`` and excluded from release-time tests.
2323
"""
2424

2525
import asyncio
@@ -29,6 +29,8 @@
2929

3030
import blosc2
3131

32+
pytestmark = pytest.mark.network
33+
3234
pytest.importorskip("s3fs")
3335
pytest.importorskip("moto")
3436
fsspec = pytest.importorskip("fsspec")

‎tests/test_hdf5_source.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,6 +1268,7 @@ def s3_server():
12681268
server.stop()
12691269

12701270

1271+
@pytest.mark.network
12711272
def test_moto_s3_hdf5_read(s3_server):
12721273
s3_opts = {
12731274
"endpoint_url": s3_server,
@@ -1292,6 +1293,7 @@ def test_moto_s3_hdf5_read(s3_server):
12921293
np.testing.assert_array_equal(proxy[:], data)
12931294

12941295

1296+
@pytest.mark.network
12951297
def test_moto_s3_hdf5_caching(s3_server, tmp_path):
12961298
s3_opts = {
12971299
"endpoint_url": s3_server,

‎tests/test_network_hook.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Regression tests for optional-network test execution."""
2+
3+
from types import SimpleNamespace
4+
5+
import httpx
6+
import pytest
7+
8+
from conftest import pytest_runtest_call
9+
10+
11+
@pytest.mark.parametrize("network", [False, True])
12+
def test_network_hook_leaves_execution_to_pytest(network):
13+
def unexpected_run():
14+
raise AssertionError("the hook must not execute the test a second time")
15+
16+
item = SimpleNamespace(
17+
get_closest_marker=lambda name: pytest.mark.network if network else None,
18+
runtest=unexpected_run,
19+
)
20+
hook = pytest_runtest_call(item)
21+
assert next(hook) is None
22+
with pytest.raises(StopIteration) as finished:
23+
hook.send("test result")
24+
assert finished.value.value == "test result"
25+
26+
27+
@pytest.mark.parametrize("network", [False, True])
28+
def test_network_hook_only_skips_marked_transport_errors(network):
29+
item = SimpleNamespace(get_closest_marker=lambda name: pytest.mark.network if network else None)
30+
hook = pytest_runtest_call(item)
31+
next(hook)
32+
expected = pytest.skip.Exception if network else httpx.ReadTimeout
33+
with pytest.raises(expected):
34+
hook.throw(httpx.ReadTimeout("endpoint unavailable"))
35+
36+
37+
def test_network_hook_does_not_hide_assertion_failures():
38+
item = SimpleNamespace(get_closest_marker=lambda name: pytest.mark.network)
39+
hook = pytest_runtest_call(item)
40+
next(hook)
41+
with pytest.raises(AssertionError, match="regression"):
42+
hook.throw(AssertionError("regression"))

0 commit comments

Comments
 (0)