Skip to content

Commit bc2b214

Browse files
authored
Merge pull request #55 from quadbio/fix/codecov
Update test workflow
2 parents ab5915f + 09e4a85 commit bc2b214

File tree

7 files changed

+94
-34
lines changed

7 files changed

+94
-34
lines changed

.github/workflows/test.yaml

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,48 @@ defaults:
1818
shell: bash -euo pipefail {0}
1919

2020
jobs:
21+
# Get the test environment from hatch as defined in pyproject.toml.
22+
# This ensures that the pyproject.toml is the single point of truth for test definitions and the same tests are
23+
# run locally and on continuous integration.
24+
# Check [[tool.hatch.envs.hatch-test.matrix]] in pyproject.toml and https://hatch.pypa.io/latest/environment/ for
25+
# more details.
26+
get-environments:
27+
runs-on: ubuntu-latest
28+
outputs:
29+
envs: ${{ steps.get-envs.outputs.envs }}
30+
steps:
31+
- uses: actions/checkout@v4
32+
with:
33+
filter: blob:none
34+
fetch-depth: 0
35+
- name: Install uv
36+
uses: astral-sh/setup-uv@v5
37+
- name: Get test environments
38+
id: get-envs
39+
run: |
40+
ENVS_JSON=$(uvx hatch env show --json | jq -c 'to_entries
41+
| map(
42+
select(.key | startswith("hatch-test"))
43+
| {
44+
name: .key,
45+
label: (if (.key | contains("pre")) then .key + " (PRE-RELEASE DEPENDENCIES)" else .key end),
46+
python: .value.python
47+
}
48+
)')
49+
echo "envs=${ENVS_JSON}" | tee $GITHUB_OUTPUT
50+
51+
# Run tests through hatch. Spawns a separate runner for each environment defined in the hatch matrix obtained above.
2152
test:
22-
runs-on: ${{ matrix.os }}
53+
needs: get-environments
2354

2455
strategy:
2556
fail-fast: false
2657
matrix:
27-
include:
28-
- os: ubuntu-latest
29-
python: "3.10"
30-
- os: ubuntu-latest
31-
python: "3.12"
32-
- os: ubuntu-latest
33-
python: "3.12"
34-
pip-flags: "--pre"
35-
name: PRE-RELEASE DEPENDENCIES
58+
os: [ubuntu-latest]
59+
env: ${{ fromJSON(needs.get-environments.outputs.envs) }}
3660

37-
name: ${{ matrix.name }} Python ${{ matrix.python }}
61+
name: ${{ matrix.env.label }}
62+
runs-on: ${{ matrix.os }}
3863

3964
env:
4065
OS: ${{ matrix.os }}
@@ -51,25 +76,33 @@ jobs:
5176
- name: Install uv
5277
uses: astral-sh/setup-uv@v5
5378
with:
79+
python-version: ${{ matrix.env.python }}
5480
cache-dependency-glob: pyproject.toml
55-
- name: Install coverage
56-
run: pip install coverage
81+
- name: create hatch environment
82+
run: uvx hatch env create ${{ matrix.env.name }}
5783
- name: run tests using hatch
5884
env:
5985
MPLBACKEND: agg
6086
PLATFORM: ${{ matrix.os }}
6187
DISPLAY: :42
62-
run: |
63-
if [ "${{ matrix.python }}" == "3.12" ] && [ -z "${{ matrix.pip-flags }}" ]; then
64-
uvx hatch test --cover --python ${{ matrix.python }}
65-
else
66-
uvx hatch test --cover --python ${{ matrix.python }} -m "not openai"
67-
fi
68-
- name: Report coverage
69-
run: |
70-
coverage report
71-
- name: Upload coverage reports to Codecov
72-
uses: codecov/codecov-action@v5
88+
run: uvx hatch run ${{ matrix.env.name }}:run-cov
89+
- name: generate coverage report
90+
run: uvx hatch run ${{ matrix.env.name }}:coverage xml
91+
- name: Upload coverage
92+
uses: codecov/codecov-action@v4
7393
with:
7494
token: ${{ secrets.CODECOV_TOKEN }}
75-
slug: quadbio/cell-annotator
95+
96+
# Check that all tests defined above pass. This makes it easy to set a single "required" test in branch
97+
# protection instead of having to update it frequently. See https://github.com/re-actors/alls-green#why.
98+
check:
99+
name: Tests pass in all hatch environments
100+
if: always()
101+
needs:
102+
- get-environments
103+
- test
104+
runs-on: ubuntu-latest
105+
steps:
106+
- uses: re-actors/alls-green@release/v1
107+
with:
108+
jobs: ${{ toJSON(needs) }}

pyproject.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,26 @@ scripts.build = "sphinx-build -M html docs docs/_build {args}"
108108
scripts.open = "python -m webbrowser -t docs/_build/html/index.html"
109109
scripts.clean = "git clean -fdX -- {args:docs}"
110110

111+
# Test the lowest and highest supported Python versions with normal deps
112+
[[tool.hatch.envs.hatch-test.matrix]]
113+
deps = [ "stable" ]
114+
python = [ "3.10", "3.13" ]
115+
116+
# Test the newest supported Python version also with pre-release deps
117+
[[tool.hatch.envs.hatch-test.matrix]]
118+
deps = [ "pre" ]
119+
python = [ "3.13" ]
120+
111121
[tool.hatch.envs.hatch-test]
112122
features = [ "test" ]
113123

124+
[tool.hatch.envs.hatch-test.overrides]
125+
# If the matrix variable `deps` is set to "pre",
126+
# set the environment variable `UV_PRERELEASE` to "allow".
127+
matrix.deps.env-vars = [
128+
{ key = "UV_PRERELEASE", value = "allow", if = [ "pre" ] },
129+
]
130+
114131
[tool.ruff]
115132
line-length = 120
116133
src = [ "src" ]

tests/model/test_base_annotator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from unittest.mock import patch
22

33
import pytest
4+
from flaky import flaky
45

56
from cell_annotator._response_formats import OutputForTesting
67
from cell_annotator.model.base_annotator import BaseAnnotator
@@ -24,6 +25,7 @@ def test_query_llm_mock(self, mock_query_llm, base_annotator):
2425
response_format=OutputForTesting,
2526
)
2627

28+
@flaky
2729
@pytest.mark.real_llm_query()
2830
def test_query_llm_real(self, base_annotator):
2931
"""Test actual query_llm call across all available providers."""

tests/model/test_cell_annotator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55

66
class TestCellAnnotator:
7+
@flaky
78
@pytest.mark.real_llm_query()
89
def test_get_expected_cell_type_markers(self, cell_annotator_single):
910
"""Test getting expected cell type markers with single sample data."""
@@ -36,6 +37,7 @@ def test_get_expected_cell_type_markers(self, cell_annotator_single):
3637
assert neuron_markers_found
3738
assert fibroblast_markers_found
3839

40+
@flaky
3941
@pytest.mark.real_llm_query()
4042
def test_annotate_clusters_single(self, cell_annotator_single):
4143
"""Test annotating clusters with single sample data."""

tests/model/test_obs_beautifier.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import numpy as np
22
import pandas as pd
33
import pytest
4+
from flaky import flaky
45

56
from cell_annotator.model.obs_beautifier import ObsBeautifier
67

78

89
class TestObsBeautifier:
10+
@flaky
911
@pytest.mark.real_llm_query()
1012
def test_reorder_and_color_clusters(self, cell_annotator_single):
1113
"""Test reordering and coloring clusters."""
@@ -30,6 +32,7 @@ def test_reorder_and_color_clusters(self, cell_annotator_single):
3032
assert f"{key}_colors" in cell_annotator.adata.uns
3133
assert len(cell_annotator.adata.uns[f"{key}_colors"]) == cell_annotator.adata.obs[key].nunique()
3234

35+
@flaky
3336
@pytest.mark.real_llm_query()
3437
def test_reorder_only_preserves_colors(self, cell_annotator_single):
3538
"""Test that reordering clusters without assigning new colors preserves the original colors."""

tests/model/test_providers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44

55
import pytest
6+
from flaky import flaky
67

78
from cell_annotator._response_formats import BaseOutput
89
from cell_annotator.model._providers import AnthropicProvider, GeminiProvider, OpenAIProvider
@@ -65,6 +66,7 @@ def test_provider_repr(self):
6566
class TestOpenAIProvider:
6667
"""Isolated tests for OpenAI provider."""
6768

69+
@flaky
6870
@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="OPENAI_API_KEY not available")
6971
@pytest.mark.real_llm_query()
7072
def test_openai_list_models_real(self):
@@ -78,6 +80,7 @@ def test_openai_list_models_real(self):
7880
model_names = [model.lower() for model in models]
7981
assert any("gpt" in model for model in model_names)
8082

83+
@flaky
8184
@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="OPENAI_API_KEY not available")
8285
@pytest.mark.real_llm_query()
8386
def test_openai_query_real(self):
@@ -118,6 +121,7 @@ def test_openai_query_error_handling(self):
118121
class TestGeminiProvider:
119122
"""Isolated tests for Gemini provider."""
120123

124+
@flaky
121125
@pytest.mark.skipif(not os.getenv("GEMINI_API_KEY"), reason="GEMINI_API_KEY not available")
122126
@pytest.mark.real_llm_query()
123127
def test_gemini_list_models_real(self):
@@ -131,6 +135,7 @@ def test_gemini_list_models_real(self):
131135
model_names = [model.lower() for model in models]
132136
assert any("gemini" in model for model in model_names)
133137

138+
@flaky
134139
@pytest.mark.skipif(not os.getenv("GEMINI_API_KEY"), reason="GEMINI_API_KEY not available")
135140
@pytest.mark.real_llm_query()
136141
def test_gemini_query_real(self):
@@ -163,6 +168,7 @@ def test_gemini_initialization(self):
163168
class TestAnthropicProvider:
164169
"""Isolated tests for Anthropic provider."""
165170

171+
@flaky
166172
@pytest.mark.skipif(not os.getenv("ANTHROPIC_API_KEY"), reason="ANTHROPIC_API_KEY not available")
167173
@pytest.mark.real_llm_query()
168174
def test_anthropic_list_models_real(self):
@@ -176,6 +182,7 @@ def test_anthropic_list_models_real(self):
176182
model_names = [model.lower() for model in models]
177183
assert any("claude" in model for model in model_names)
178184

185+
@flaky
179186
@pytest.mark.skipif(not os.getenv("ANTHROPIC_API_KEY"), reason="ANTHROPIC_API_KEY not available")
180187
@pytest.mark.real_llm_query()
181188
def test_anthropic_query_real(self):
@@ -221,6 +228,7 @@ def test_provider_factory_pattern(self):
221228
assert provider is not None
222229
assert provider.__class__.__name__.lower().startswith(name)
223230

231+
@flaky
224232
@pytest.mark.skipif(
225233
not any(os.getenv(key) for key in ["OPENAI_API_KEY", "GEMINI_API_KEY", "ANTHROPIC_API_KEY"]),
226234
reason="No API keys available for testing",

tests/model/test_sample_annotator.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,10 @@
22

33
import pandas as pd
44
import pytest
5-
from tests.utils import (
6-
expected_marker_genes,
7-
fibroblast_cell_types,
8-
neuronal_cell_types,
9-
)
5+
from flaky import flaky
6+
from tests.utils import expected_marker_genes, fibroblast_cell_types, neuronal_cell_types
107

11-
from cell_annotator._response_formats import (
12-
CellTypeMappingOutput,
13-
PredictedCellTypeOutput,
14-
)
8+
from cell_annotator._response_formats import CellTypeMappingOutput, PredictedCellTypeOutput
159

1610

1711
class TestSampleAnnotator:
@@ -59,6 +53,7 @@ def test_get_cluster_markers(self, sample_annotator):
5953
for _cluster, genes in sample_annotator.marker_genes.items():
6054
assert len(genes) > 0
6155

56+
@flaky
6257
@pytest.mark.real_llm_query()
6358
def test_annotate_clusters_actual(self, sample_annotator):
6459
sample_annotator.get_cluster_markers(min_auc=0.6)

0 commit comments

Comments
 (0)