Skip to content

test_threading

Thread-safety tests for shared stream objects.

frameCPP streams are stateful, and the bindings release the GIL around I/O, so stream access must be serialized in the bindings themselves. These tests drive shared readers/writers from many threads at once; on unserialized builds they corrupt the stream ("stream is no longer in good state") or crash outright.

TestSharedReader

test_concurrent_reads_independent_readers

test_concurrent_reads_independent_readers(test_gwf_file)

Concurrent reads of the same file via separate handles.

Source code in gwframe/tests/test_threading.py
def test_concurrent_reads_independent_readers(self, test_gwf_file):
    """Concurrent reads of the same file via separate handles."""
    reference = gwframe.read(test_gwf_file, STRAIN_CHANNEL).array

    def worker(_tid):
        for _ in range(4):
            ts = gwframe.read(test_gwf_file, STRAIN_CHANNEL)
            np.testing.assert_array_equal(ts.array, reference)

    assert run_threads(worker) == []

test_concurrent_reads_shared_reader

test_concurrent_reads_shared_reader(test_gwf_file)

Many threads reading through one FrameReader must not corrupt it.

Source code in gwframe/tests/test_threading.py
def test_concurrent_reads_shared_reader(self, test_gwf_file):
    """Many threads reading through one FrameReader must not corrupt it."""
    reader = gwframe.FrameReader(test_gwf_file)
    reference = reader.read(STRAIN_CHANNEL).array

    def worker(_tid):
        for _ in range(N_ITER):
            ts = reader.read(STRAIN_CHANNEL)
            np.testing.assert_array_equal(ts.array, reference)

    assert run_threads(worker) == []

TestSharedWriter

test_concurrent_writes_independent_files

test_concurrent_writes_independent_files(tmp_path)

Concurrent writes to distinct files via separate writers.

Source code in gwframe/tests/test_threading.py
def test_concurrent_writes_independent_files(self, tmp_path):
    """Concurrent writes to distinct files via separate writers."""
    sample_rate = 1024

    def worker(tid):
        for i in range(4):
            path = tmp_path / f"out_{tid}_{i}.gwf"
            data = np.arange(sample_rate, dtype=np.float64) * (tid + 1)
            gwframe.write(path, {f"X1:TEST_{tid}": data}, 1000000000.0, sample_rate)
            back = gwframe.read(path, f"X1:TEST_{tid}")
            np.testing.assert_array_equal(back.array, data)

    assert run_threads(worker) == []

test_concurrent_writes_shared_writer

test_concurrent_writes_shared_writer(tmp_path)

Many threads writing through one FrameWriter must not corrupt it.

All threads write the same channel name: frames whose channel sets differ produce a broken TOC even single-threaded (separate, preexisting bug), which would mask the threading result here.

Source code in gwframe/tests/test_threading.py
def test_concurrent_writes_shared_writer(self, tmp_path):
    """Many threads writing through one FrameWriter must not corrupt it.

    All threads write the same channel name: frames whose channel sets
    differ produce a broken TOC even single-threaded (separate,
    preexisting bug), which would mask the threading result here.
    """
    path = tmp_path / "threaded.gwf"
    sample_rate = 1024
    data = {
        tid: np.full(sample_rate, float(tid), dtype=np.float64)
        for tid in range(N_THREADS)
    }
    with gwframe.FrameWriter(path) as writer:

        def worker(tid):
            for i in range(N_ITER):
                # unique start time per frame, no cross-thread coordination
                frame_index = tid * N_ITER + i
                writer.write(
                    data[tid],
                    start=1000000000.0 + frame_index,
                    sample_rate=sample_rate,
                    name="X1:TEST",
                )

        failures = run_threads(worker)
    assert failures == []

    # every frame written must read back intact, with the data of the
    # thread its start time encodes
    frames = list(gwframe.read_frames(path))
    assert len(frames) == N_THREADS * N_ITER
    for frame in frames:
        ts = frame["X1:TEST"]
        tid = int(ts.start - 1000000000.0) // N_ITER
        np.testing.assert_array_equal(ts.array, data[tid])

run_threads

run_threads(worker)

Run worker(tid) in N_THREADS threads, starting simultaneously.

Returns the list of exceptions raised by workers.

Source code in gwframe/tests/test_threading.py
def run_threads(worker):
    """Run worker(tid) in N_THREADS threads, starting simultaneously.

    Returns the list of exceptions raised by workers.
    """
    barrier = threading.Barrier(N_THREADS)
    failures = []

    def wrap(tid):
        try:
            barrier.wait()
            worker(tid)
        except Exception as exc:  # noqa: BLE001
            failures.append(exc)

    threads = [threading.Thread(target=wrap, args=(tid,)) for tid in range(N_THREADS)]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()
    return failures