Skip to content

Commit a599cf1

Browse files
Copilotshaypal5Borda
authored
Add smoke test suite as fast-fail CI gate (#345)
* Add smoke test suite and fast-fail CI gates Co-authored-by: shaypal5 <917954+shaypal5@users.noreply.github.com> * Add matrix strategy with OS variations to CI workflow and define default Python version in environment --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: shaypal5 <917954+shaypal5@users.noreply.github.com> Co-authored-by: jirka <6035284+Borda@users.noreply.github.com>
1 parent d99f22f commit a599cf1

File tree

3 files changed

+222
-1
lines changed

3 files changed

+222
-1
lines changed

.github/workflows/ci-test.yml

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,59 @@ concurrency:
1414
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }}
1515
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
1616

17+
env:
18+
DEFAULT_PYTHON_VERSION: "3.12"
19+
1720
jobs:
21+
smoke-test:
22+
runs-on: ${{ matrix.os }}
23+
permissions:
24+
contents: read
25+
if: github.event_name == 'push' || github.event_name == 'pull_request'
26+
strategy:
27+
fail-fast: false
28+
matrix:
29+
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
30+
steps:
31+
- uses: actions/checkout@v6
32+
33+
- name: Set up Python
34+
uses: actions/setup-python@v6
35+
with:
36+
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
37+
cache: "pip"
38+
39+
- name: Install package & dependencies
40+
run: python -m pip install -e . -r tests/requirements.txt
41+
42+
- name: Run smoke tests
43+
run: pytest -m smoke tests/test_smoke.py
44+
45+
pre-commit:
46+
runs-on: ubuntu-latest
47+
permissions:
48+
contents: read
49+
if: github.event_name == 'push' || github.event_name == 'pull_request'
50+
steps:
51+
- uses: actions/checkout@v6
52+
53+
- name: Set up Python
54+
uses: actions/setup-python@v6
55+
with:
56+
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
57+
cache: "pip"
58+
59+
- name: Install pre-commit
60+
run: pip install pre-commit
61+
62+
- name: Run pre-commit
63+
run: pre-commit run --all-files
64+
1865
pytester:
1966
# run on both push & normal PR
67+
needs: [smoke-test, pre-commit]
68+
permissions:
69+
contents: read
2070
if: github.event_name == 'push' || github.event_name == 'pull_request'
2171
runs-on: ${{ matrix.os }}
2272
environment: test
@@ -150,7 +200,7 @@ jobs:
150200

151201
testing-guardian:
152202
runs-on: ubuntu-latest
153-
needs: pytester
203+
needs: [smoke-test, pre-commit, pytester]
154204
if: always()
155205
steps:
156206
- run: echo "${{ needs.pytester.result }}"

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ markers = [
198198
"s3: test the S3 core",
199199
"maxage: test the max_age functionality",
200200
"asyncio: marks tests as async",
201+
"smoke: fast smoke tests with no external service dependencies",
201202
]
202203

203204
[tool.coverage.report]

tests/test_smoke.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
"""Smoke tests for cachier - fast, no external service dependencies."""
2+
3+
import datetime
4+
5+
import pytest
6+
7+
import cachier
8+
from cachier import cachier as cachier_decorator
9+
from cachier.config import get_global_params, set_global_params
10+
11+
12+
@pytest.mark.smoke
13+
def test_import():
14+
"""Test that cachier can be imported and has a version."""
15+
assert cachier.__version__
16+
assert isinstance(cachier.__version__, str)
17+
18+
19+
@pytest.mark.smoke
20+
def test_get_global_params():
21+
"""Test that global params can be retrieved."""
22+
params = get_global_params()
23+
assert params is not None
24+
assert hasattr(params, "backend")
25+
assert hasattr(params, "stale_after")
26+
27+
28+
@pytest.mark.smoke
29+
def test_invalid_backend():
30+
"""Test that an invalid backend raises a ValueError."""
31+
with pytest.raises(ValueError, match="specified an invalid core"):
32+
33+
@cachier_decorator(backend="invalid_backend")
34+
def dummy():
35+
pass
36+
37+
38+
@pytest.mark.smoke
39+
def test_pickle_backend_basic(tmp_path):
40+
"""Test basic caching with the pickle backend."""
41+
call_count = 0
42+
43+
@cachier_decorator(cache_dir=tmp_path, backend="pickle")
44+
def add(a, b):
45+
nonlocal call_count
46+
call_count += 1
47+
return a + b
48+
49+
add.clear_cache()
50+
assert call_count == 0
51+
assert add(1, 2) == 3
52+
assert call_count == 1
53+
assert add(1, 2) == 3
54+
assert call_count == 1 # cache hit
55+
add.clear_cache()
56+
57+
58+
@pytest.mark.smoke
59+
def test_memory_backend_basic():
60+
"""Test basic caching with the memory backend."""
61+
call_count = 0
62+
63+
@cachier_decorator(backend="memory")
64+
def multiply(a, b):
65+
nonlocal call_count
66+
call_count += 1
67+
return a * b
68+
69+
multiply.clear_cache()
70+
assert call_count == 0
71+
assert multiply(3, 4) == 12
72+
assert call_count == 1
73+
assert multiply(3, 4) == 12
74+
assert call_count == 1 # cache hit
75+
multiply.clear_cache()
76+
77+
78+
@pytest.mark.smoke
79+
def test_clear_cache(tmp_path):
80+
"""Test that clear_cache resets the cache."""
81+
call_count = 0
82+
83+
@cachier_decorator(cache_dir=tmp_path, backend="pickle")
84+
def func():
85+
nonlocal call_count
86+
call_count += 1
87+
return 42
88+
89+
func.clear_cache()
90+
func()
91+
func()
92+
assert call_count == 1
93+
func.clear_cache()
94+
func()
95+
assert call_count == 2
96+
func.clear_cache()
97+
98+
99+
@pytest.mark.smoke
100+
def test_pickle_backend_stale_after(tmp_path):
101+
"""Test that stale_after=timedelta(0) always recalculates."""
102+
call_count = 0
103+
104+
@cachier_decorator(
105+
cache_dir=tmp_path,
106+
backend="pickle",
107+
stale_after=datetime.timedelta(seconds=0),
108+
next_time=False,
109+
)
110+
def func(x):
111+
nonlocal call_count
112+
call_count += 1
113+
return x * 2
114+
115+
func.clear_cache()
116+
assert func(5) == 10
117+
assert func(5) == 10
118+
assert call_count == 2 # always recalculates when immediately stale
119+
func.clear_cache()
120+
121+
122+
@pytest.mark.smoke
123+
def test_allow_none(tmp_path):
124+
"""Test that allow_none=True caches None return values."""
125+
call_count = 0
126+
127+
@cachier_decorator(cache_dir=tmp_path, backend="pickle", allow_none=True)
128+
def returns_none():
129+
nonlocal call_count
130+
call_count += 1
131+
return None
132+
133+
returns_none.clear_cache()
134+
assert returns_none() is None
135+
assert returns_none() is None
136+
assert call_count == 1 # second call uses cache
137+
returns_none.clear_cache()
138+
139+
140+
@pytest.mark.smoke
141+
def test_set_global_params_backend():
142+
"""Test that set_global_params changes the active backend."""
143+
original = get_global_params().backend
144+
try:
145+
set_global_params(backend="memory")
146+
assert get_global_params().backend == "memory"
147+
finally:
148+
set_global_params(backend=original)
149+
150+
151+
@pytest.mark.smoke
152+
def test_cache_dpath_pickle(tmp_path):
153+
"""Test that cache_dpath returns a path for the pickle backend."""
154+
155+
@cachier_decorator(cache_dir=tmp_path, backend="pickle")
156+
def func():
157+
return 1
158+
159+
assert func.cache_dpath() is not None
160+
161+
162+
@pytest.mark.smoke
163+
def test_cache_dpath_memory():
164+
"""Test that cache_dpath returns None for the memory backend."""
165+
166+
@cachier_decorator(backend="memory")
167+
def func():
168+
return 1
169+
170+
assert func.cache_dpath() is None

0 commit comments

Comments
 (0)