From 4e7a4f3e0e2984b883a0cd04fecff91fa54eb498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlson=20B=C3=BCth?= Date: Thu, 30 Jul 2026 19:15:00 +0200 Subject: [PATCH 1/2] ci: add Codecov coverage reporting Add Codecov integration for branch-coverage tracking: - Run pytest with --cov=delaynet --cov-branch on latest Python matrix entry - Upload via codecov/codecov-action@v5 using CODECOV_TOKEN secret - Enforce 100% patch coverage on PRs (codecov.yml) - Add Codecov badge to README --- .github/workflows/test.yml | 11 ++++++++++- README.md | 1 + codecov.yml | 13 +++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 codecov.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b0b6a9a..e843841 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,5 +31,14 @@ jobs: else uv python install uv sync --extra test - uv run pytest -n auto --tb=short + uv run pytest -n auto --tb=short \ + --cov=delaynet --cov-branch \ + --cov-report=xml --cov-report=term-missing fi + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + files: ./coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + if: matrix.python-version == 'latest' diff --git a/README.md b/README.md index 0604646..953d14f 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ [![CI](https://github.com/cbueth/delaynet/actions/workflows/test.yml/badge.svg)](https://github.com/cbueth/delaynet/actions/workflows/test.yml) [![Lint](https://github.com/cbueth/delaynet/actions/workflows/lint.yml/badge.svg)](https://github.com/cbueth/delaynet/actions/workflows/lint.yml) [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://app.codspeed.io//badge.json)](https://app.codspeed.io//cbueth/delaynet?utm_source=badge) +[![codecov](https://codecov.io/gh/cbueth/delaynet/branch/main/graph/badge.svg)](https://codecov.io/gh/cbueth/delaynet) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..47d75c2 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,13 @@ +coverage: + status: + project: + default: + target: auto + threshold: 2% + patch: + default: + target: 100% + +comment: + layout: header, diff + behavior: default From 34361ea3a726c2ef5c50a223cc735e51afe94b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlson=20B=C3=BCth?= Date: Thu, 30 Jul 2026 19:27:48 +0200 Subject: [PATCH 2/2] test: reach 100% branch coverage across all modules - test_granger: mock np.linalg.solve to raise LinAlgError on second call, covering the lstsq fallback at granger.py:92-94 - metrics: remove redundant if max_betweenness > 0 guard (always True when reached due to outer n_nodes > 2 check), eliminating dead branch - test_network_reconstruction: direct tests for parallel worker functions _compute_pair_connectivity_shared and _compute_with_progress using real shared memory setup --- delaynet/network_analysis/metrics.py | 3 +- tests/connectivities/test_granger.py | 24 ++++++++++++ tests/test_network_reconstruction.py | 55 ++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/delaynet/network_analysis/metrics.py b/delaynet/network_analysis/metrics.py index 60153a7..1e9a2ec 100644 --- a/delaynet/network_analysis/metrics.py +++ b/delaynet/network_analysis/metrics.py @@ -116,8 +116,7 @@ def betweenness_centrality( # For undirected graphs: max betweenness = (n-1)*(n-2)/2 max_betweenness = (n_nodes - 1) * (n_nodes - 2) / 2.0 - if max_betweenness > 0: - centrality = centrality / max_betweenness + centrality = centrality / max_betweenness return centrality diff --git a/tests/connectivities/test_granger.py b/tests/connectivities/test_granger.py index 56c6136..ae3d16c 100644 --- a/tests/connectivities/test_granger.py +++ b/tests/connectivities/test_granger.py @@ -152,3 +152,27 @@ def test_stronger_causal_signal_lower_pvalue(rng): assert p_strong < p_weak, ( f"Stronger signal should have lower p-value (strong: {p_strong}, weak: {p_weak})" ) + + +def test_gt_single_lag_lstsq_fallback(): + """Test lstsq fallback when solve raises LinAlgError at line 92.""" + from unittest.mock import patch + + ts1 = np.random.normal(0, 1, size=100) + ts2 = np.random.normal(0, 1, size=100) + + original_solve = np.linalg.solve + solve_calls = [] + + def mock_solve(a, b): + solve_calls.append((a, b)) + if len(solve_calls) == 2: + raise np.linalg.LinAlgError( + "Simulating singular R_vcov_R for test coverage" + ) + return original_solve(a, b) + + with patch("numpy.linalg.solve", mock_solve): + p_value = gt_single_lag(ts1, ts2, lag_step=2) + assert isinstance(p_value, float) + assert 0 <= p_value <= 1 diff --git a/tests/test_network_reconstruction.py b/tests/test_network_reconstruction.py index be56179..13b7a83 100644 --- a/tests/test_network_reconstruction.py +++ b/tests/test_network_reconstruction.py @@ -1,5 +1,6 @@ """Tests for the network reconstruction module.""" +import numpy as np import pytest from time import time from numpy import ( @@ -26,6 +27,8 @@ format_time, print_progress, update_progress, + _compute_pair_connectivity_shared, + _compute_with_progress, ) @@ -663,3 +666,55 @@ def __init__(self, value): # Verify print_progress was called with explicit parameter assert called[0] is True assert sphinx_mode_value[0] is True + + +def test_compute_pair_connectivity_shared(): + """Test the shared-memory worker function directly.""" + from multiprocessing import shared_memory + + data = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]]) + shm = shared_memory.SharedMemory(create=True, size=data.nbytes) + shared_array = ndarray(data.shape, dtype=data.dtype, buffer=shm.buf) + shared_array[:] = data + try: + args = (0, 1, shm.name, data.shape, data.dtype, "linear_correlation", 1, {}) + i, j, p, lag = _compute_pair_connectivity_shared(args) + assert i == 0 + assert j == 1 + assert isinstance(p, float) + assert isinstance(lag, int) + finally: + shm.close() + shm.unlink() + + +def test_compute_with_progress(monkeypatch): + """Test the _compute_with_progress worker wrapper.""" + from multiprocessing import shared_memory, Manager + + monkeypatch.setattr("sys.stdout.write", lambda x: None) + monkeypatch.setattr("sys.stdout.flush", lambda: None) + + data = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]]) + shm = shared_memory.SharedMemory(create=True, size=data.nbytes) + shared_array = ndarray(data.shape, dtype=data.dtype, buffer=shm.buf) + shared_array[:] = data + try: + args = (0, 1, shm.name, data.shape, data.dtype, "linear_correlation", 1, {}) + with Manager() as manager: + counter = manager.Value("i", 0) + lock = manager.Lock() + result = _compute_with_progress( + args, + counter, + lock, + total_pairs=1, + start_time=time(), + sphinx_mode=True, + ) + assert result is not None + assert result[0] == 0 + assert result[1] == 1 + finally: + shm.close() + shm.unlink()