Skip to content

Commit 8443a62

Browse files
Test BGEN options stable across repeated opens
Each open of the streaming `.bgen` file spawns a fresh server-side thread that constructs its own `BgenEncoder` via `spec.encoder_factory(reader, opts)`. Pin that the options dataclass (notably `--no-header-samples`) flows through every per-connection encoder consistently — three sequential open/read/close cycles must all return the same bytes, both at the `EncoderClient.open_stream` layer and through a real FUSE mount. Also exercise `--no-sample-file` / `--no-bgi` across three independent `EncoderClient.start` cycles to confirm the handshake-time static-file set is stable.
1 parent 59d9f89 commit 8443a62

2 files changed

Lines changed: 114 additions & 3 deletions

File tree

tests/test_bgen_apps.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"""
99

1010
import contextlib
11+
import dataclasses
1112
import errno
1213
import os
1314
import pathlib
@@ -88,14 +89,19 @@ async def _arun(cmd) -> None:
8889

8990

9091
@contextlib.asynccontextmanager
91-
async def _mount_bgen(tmp_path, vcz):
92-
"""Mount ``vcz`` as a BGEN fileset; yield ``(mnt, basename)``."""
92+
async def _mount_bgen(tmp_path, vcz, opts=None):
93+
"""Mount ``vcz`` as a BGEN fileset; yield ``(mnt, basename)``.
94+
95+
``opts`` is the ``vcztools.ViewBgenOptions`` dataclass the
96+
encoder-server runs under; defaults to a fresh ``ViewBgenOptions()``
97+
(every field at its dataclass default).
98+
"""
9399
mnt = tmp_path / "mnt"
94100
mnt.mkdir()
95101
basename = vcz.path.stem
96102
sock_path = tmp_path / "bgen.sock"
97103
async with await encoder_client.EncoderClient.start(
98-
str(vcz.path), sock_path, formats.BGEN_SPEC
104+
str(vcz.path), sock_path, formats.BGEN_SPEC, opts=opts
99105
) as client:
100106
ops = encoder_ops.EncoderOps(client, basename, formats.BGEN_SPEC)
101107
async with fuse_adapter.mount(ops, str(mnt)):
@@ -144,6 +150,39 @@ async def test_random_pread(self, fx_mounted_bgen):
144150
assert got == expected[off : off + size]
145151

146152

153+
class TestBgenOptionsAcrossOpens:
154+
"""Open the mounted ``.bgen`` repeatedly and verify the bytes are
155+
invariant under the mount's options dataclass.
156+
157+
Each ``open()`` of the streaming file triggers a fresh FUSE ``open``
158+
handler → fresh socket → fresh server-side thread that constructs
159+
its own :class:`vcztools.BgenEncoder` via
160+
``spec.encoder_factory(reader, opts)``. The test pins that the
161+
options dataclass flows through every per-connection encoder
162+
consistently — e.g. ``--no-header-samples`` keeps producing the
163+
same header-stripped bytes on the 2nd and 3rd read after open/close.
164+
"""
165+
166+
@pytest.mark.parametrize("no_header_samples", [True, False])
167+
async def test_no_header_samples_stable_across_opens(
168+
self, tmp_path, fx_small_vcz, no_header_samples
169+
):
170+
opts = dataclasses.replace(
171+
vcztools.ViewBgenOptions(), no_header_samples=no_header_samples
172+
)
173+
ref_reader = opts.make_reader(str(fx_small_vcz.path))
174+
with vcztools.BgenEncoder(
175+
ref_reader, embed_header_samples=not no_header_samples
176+
) as ref:
177+
expected = ref.read(0, ref.total_size)
178+
179+
async with _mount_bgen(tmp_path, fx_small_vcz, opts=opts) as (mnt, basename):
180+
bgen_path = mnt / f"{basename}.bgen"
181+
for cycle in range(3):
182+
data = await trio.to_thread.run_sync(bgen_path.read_bytes)
183+
assert data == expected, f"cycle {cycle} differed from reference"
184+
185+
147186
def _pread_sync(path: pathlib.Path, off: int, size: int) -> bytes:
148187
with path.open("rb") as f:
149188
f.seek(off)

tests/test_encoder_client.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,78 @@ async def test_max_alleles_filter_through_start(
424424
assert not client._proc.is_alive()
425425
assert client._proc.exitcode == 0
426426

427+
@pytest.mark.parametrize("no_header_samples", [True, False])
428+
async def test_bgen_no_header_samples_stable_across_opens(
429+
self, fx_small_vcz, tmp_path, no_header_samples
430+
):
431+
"""``--no-header-samples`` flows into every per-connection
432+
``BgenEncoder`` consistently.
433+
434+
Each ``open_stream`` spawns a fresh server-side thread that
435+
constructs a new encoder via ``spec.encoder_factory(reader, opts)``.
436+
Three sequential open/read/close cycles must all return the same
437+
bytes — and those bytes must match an in-process reference
438+
``BgenEncoder`` constructed with the matching
439+
``embed_header_samples`` flag.
440+
"""
441+
opts = dataclasses.replace(
442+
vcztools.ViewBgenOptions(), no_header_samples=no_header_samples
443+
)
444+
ref_reader = opts.make_reader(str(fx_small_vcz.path))
445+
with vcztools.BgenEncoder(
446+
ref_reader, embed_header_samples=not no_header_samples
447+
) as ref:
448+
expected_size = int(ref.total_size)
449+
expected = ref.read(0, expected_size)
450+
451+
socket_path = tmp_path / "encoder.sock"
452+
async with await encoder_client.EncoderClient.start(
453+
str(fx_small_vcz.path), socket_path, formats.BGEN_SPEC, opts=opts
454+
) as client:
455+
assert client.stream_size == expected_size
456+
for cycle in range(3):
457+
conn = await client.open_stream()
458+
try:
459+
data = await conn.read(0, client.stream_size)
460+
finally:
461+
await conn.aclose()
462+
assert data == expected, f"cycle {cycle} differed from reference"
463+
464+
@pytest.mark.parametrize(
465+
("no_sample_file", "no_bgi"),
466+
[(True, False), (False, True), (True, True), (False, False)],
467+
)
468+
async def test_bgen_sidecar_toggles_stable_across_starts(
469+
self, fx_small_vcz, tmp_path, no_sample_file, no_bgi
470+
):
471+
"""The handshake's ``static_files`` honours ``--no-sample-file``
472+
/ ``--no-bgi`` and yields the same suffix set on each
473+
``EncoderClient.start`` against the same options."""
474+
opts = dataclasses.replace(
475+
vcztools.ViewBgenOptions(),
476+
no_sample_file=no_sample_file,
477+
no_bgi=no_bgi,
478+
)
479+
expected_suffixes = formats.BGEN_SPEC.static_suffixes(opts)
480+
first: dict[str, bytes] | None = None
481+
for cycle in range(3):
482+
socket_path = tmp_path / f"encoder-{cycle}.sock"
483+
async with await encoder_client.EncoderClient.start(
484+
str(fx_small_vcz.path), socket_path, formats.BGEN_SPEC, opts=opts
485+
) as client:
486+
assert tuple(client.static_files) == expected_suffixes
487+
# ``.bgen.bgi`` SQLite payloads have non-deterministic
488+
# header bytes across independent writes; compare sizes
489+
# only for that entry and the full body for ``.sample``.
490+
if first is None:
491+
first = dict(client.static_files)
492+
continue
493+
for suffix, body in client.static_files.items():
494+
if suffix == ".bgen.bgi":
495+
assert len(body) == len(first[suffix])
496+
else:
497+
assert body == first[suffix]
498+
427499
@pytest.mark.parametrize(
428500
"spec", [formats.PLINK_SPEC, formats.BGEN_SPEC], ids=["plink", "bgen"]
429501
)

0 commit comments

Comments
 (0)