Skip to content

Commit 74145ac

Browse files
committed
feat: set up complete Python testing infrastructure
- Set up Poetry as package manager with pyproject.toml configuration - Migrated existing dependencies from requirements.txt to Poetry format - Added pytest, pytest-cov, and pytest-mock as testing dependencies - Created comprehensive testing directory structure (tests/unit/, tests/integration/) - Configured pytest with coverage reporting (80% threshold, HTML/XML output) - Added custom markers for unit, integration, and slow tests - Created conftest.py with shared fixtures for ML/AI testing (mocks for wandb, tensorflow, etc.) - Set up Poetry scripts for `poetry run test` and `poetry run tests` commands - Updated .gitignore with testing-related patterns and build artifacts - Added validation tests to verify infrastructure setup The testing infrastructure is now ready for developers to start writing tests.
1 parent 49107b8 commit 74145ac

File tree

8 files changed

+4177
-0
lines changed

8 files changed

+4177
-0
lines changed

.gitignore

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Testing
2+
.pytest_cache/
3+
.coverage
4+
htmlcov/
5+
coverage.xml
6+
.tox/
7+
.nox/
8+
9+
# Claude Code settings
10+
.claude/*
11+
12+
# Python
13+
__pycache__/
14+
*.py[cod]
15+
*$py.class
16+
*.so
17+
.Python
18+
build/
19+
develop-eggs/
20+
dist/
21+
downloads/
22+
eggs/
23+
.eggs/
24+
lib/
25+
lib64/
26+
parts/
27+
sdist/
28+
var/
29+
wheels/
30+
*.egg-info/
31+
.installed.cfg
32+
*.egg
33+
MANIFEST
34+
35+
# Virtual environments
36+
.env
37+
.venv
38+
env/
39+
venv/
40+
ENV/
41+
env.bak/
42+
venv.bak/
43+
44+
# IDE
45+
.vscode/
46+
.idea/
47+
*.swp
48+
*.swo
49+
*~
50+
51+
# OS
52+
.DS_Store
53+
.DS_Store?
54+
._*
55+
.Spotlight-V100
56+
.Trashes
57+
ehthumbs.db
58+
Thumbs.db
59+
60+
# Jupyter Notebook
61+
.ipynb_checkpoints
62+
63+
# Model files and data (often large)
64+
*.h5
65+
*.pkl
66+
*.joblib
67+
models/
68+
checkpoints/
69+
70+
# Logs
71+
logs/
72+
*.log
73+
74+
# DVC
75+
.dvc/tmp
76+
.dvcignore
77+
78+
# Weights & Biases
79+
wandb/

poetry.lock

Lines changed: 3679 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
[tool.poetry]
2+
name = "ml-project"
3+
version = "0.1.0"
4+
description = "Machine Learning Project with Testing Infrastructure"
5+
authors = ["Your Name <[email protected]>"]
6+
readme = "README.md"
7+
packages = [{include = "pipeline"}, {include = "clouds"}]
8+
9+
[tool.poetry.dependencies]
10+
python = ">=3.8,<3.12"
11+
dvc = {extras = ["gdrive"], version = "2.10.2"}
12+
wandb = "0.12.19"
13+
tensorflow = "2.8"
14+
typer = "0.4.1"
15+
docopt = "0.6.2"
16+
huggingface-hub = "^0.15.0"
17+
18+
[tool.poetry.group.test.dependencies]
19+
pytest = "^7.4.0"
20+
pytest-cov = "^4.1.0"
21+
pytest-mock = "^3.11.0"
22+
23+
[tool.poetry.scripts]
24+
test = "pytest:main"
25+
tests = "pytest:main"
26+
27+
[build-system]
28+
requires = ["poetry-core"]
29+
build-backend = "poetry.core.masonry.api"
30+
31+
[tool.pytest.ini_options]
32+
testpaths = ["tests"]
33+
python_files = ["test_*.py", "*_test.py"]
34+
python_classes = ["Test*"]
35+
python_functions = ["test_*"]
36+
addopts = [
37+
"--strict-markers",
38+
"--strict-config",
39+
"--verbose",
40+
"--cov=pipeline",
41+
"--cov=clouds",
42+
"--cov-report=term-missing",
43+
"--cov-report=html:htmlcov",
44+
"--cov-report=xml:coverage.xml",
45+
"--cov-fail-under=80",
46+
"--cov-config=pyproject.toml"
47+
]
48+
markers = [
49+
"unit: Unit tests",
50+
"integration: Integration tests",
51+
"slow: Slow running tests"
52+
]
53+
54+
[tool.coverage.run]
55+
source = ["pipeline", "clouds"]
56+
omit = [
57+
"tests/*",
58+
"*/test_*",
59+
"*/__pycache__/*",
60+
"*/migrations/*",
61+
"*/venv/*",
62+
"*/.venv/*"
63+
]
64+
65+
[tool.coverage.report]
66+
exclude_lines = [
67+
"pragma: no cover",
68+
"def __repr__",
69+
"raise AssertionError",
70+
"raise NotImplementedError",
71+
"if __name__ == .__main__.:",
72+
"@abstract"
73+
]
74+
show_missing = true
75+
skip_covered = false
76+
77+
[tool.coverage.html]
78+
directory = "htmlcov"

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
"""
2+
Shared pytest fixtures for the testing suite.
3+
4+
This module contains common fixtures that can be used across all test modules.
5+
"""
6+
7+
import pytest
8+
import tempfile
9+
import shutil
10+
from pathlib import Path
11+
from unittest.mock import Mock, patch
12+
import os
13+
14+
15+
@pytest.fixture
16+
def temp_dir():
17+
"""Create a temporary directory that gets cleaned up after the test."""
18+
temp_path = tempfile.mkdtemp()
19+
yield Path(temp_path)
20+
shutil.rmtree(temp_path)
21+
22+
23+
@pytest.fixture
24+
def temp_file():
25+
"""Create a temporary file that gets cleaned up after the test."""
26+
temp_fd, temp_path = tempfile.mkstemp()
27+
os.close(temp_fd)
28+
yield Path(temp_path)
29+
if Path(temp_path).exists():
30+
Path(temp_path).unlink()
31+
32+
33+
@pytest.fixture
34+
def mock_wandb():
35+
"""Mock wandb for tests that don't need actual logging."""
36+
try:
37+
with patch('wandb.init') as mock_init, \
38+
patch('wandb.log') as mock_log, \
39+
patch('wandb.finish') as mock_finish:
40+
mock_init.return_value = Mock()
41+
yield {
42+
'init': mock_init,
43+
'log': mock_log,
44+
'finish': mock_finish
45+
}
46+
except ImportError:
47+
# wandb not available, provide mock objects
48+
yield {
49+
'init': Mock(),
50+
'log': Mock(),
51+
'finish': Mock()
52+
}
53+
54+
55+
@pytest.fixture
56+
def mock_tensorflow():
57+
"""Mock tensorflow imports for tests that don't need actual TF."""
58+
with patch.dict('sys.modules', {
59+
'tensorflow': Mock(),
60+
'tensorflow.keras': Mock(),
61+
'tensorflow.keras.models': Mock(),
62+
'tensorflow.keras.layers': Mock()
63+
}):
64+
yield
65+
66+
67+
@pytest.fixture
68+
def sample_params():
69+
"""Sample parameters dictionary for testing."""
70+
return {
71+
'model': {
72+
'name': 'test_model',
73+
'layers': [128, 64, 32],
74+
'activation': 'relu'
75+
},
76+
'training': {
77+
'epochs': 10,
78+
'batch_size': 32,
79+
'learning_rate': 0.001
80+
},
81+
'data': {
82+
'path': '/path/to/data',
83+
'train_split': 0.8,
84+
'val_split': 0.2
85+
}
86+
}
87+
88+
89+
@pytest.fixture
90+
def sample_config_file(temp_dir):
91+
"""Create a sample configuration file for testing."""
92+
config_content = """
93+
model:
94+
name: test_model
95+
layers: [128, 64, 32]
96+
activation: relu
97+
98+
training:
99+
epochs: 10
100+
batch_size: 32
101+
learning_rate: 0.001
102+
103+
data:
104+
path: /path/to/data
105+
train_split: 0.8
106+
val_split: 0.2
107+
"""
108+
config_file = temp_dir / "config.yaml"
109+
config_file.write_text(config_content)
110+
return config_file
111+
112+
113+
@pytest.fixture
114+
def mock_huggingface_hub():
115+
"""Mock Hugging Face Hub for tests."""
116+
with patch('huggingface_hub.login') as mock_login, \
117+
patch('huggingface_hub.upload_file') as mock_upload, \
118+
patch('huggingface_hub.download_file') as mock_download:
119+
yield {
120+
'login': mock_login,
121+
'upload': mock_upload,
122+
'download': mock_download
123+
}
124+
125+
126+
@pytest.fixture
127+
def mock_dvc():
128+
"""Mock DVC operations for tests."""
129+
with patch('dvc.repo.Repo') as mock_repo:
130+
mock_instance = Mock()
131+
mock_repo.return_value = mock_instance
132+
yield mock_instance
133+
134+
135+
@pytest.fixture
136+
def sample_data():
137+
"""Sample data arrays for testing ML models."""
138+
import numpy as np
139+
140+
# Generate sample training data
141+
X_train = np.random.random((100, 10))
142+
y_train = np.random.randint(0, 2, (100,))
143+
144+
# Generate sample test data
145+
X_test = np.random.random((20, 10))
146+
y_test = np.random.randint(0, 2, (20,))
147+
148+
return {
149+
'X_train': X_train,
150+
'y_train': y_train,
151+
'X_test': X_test,
152+
'y_test': y_test
153+
}
154+
155+
156+
@pytest.fixture(scope="session")
157+
def test_env_vars():
158+
"""Set up test environment variables for the session."""
159+
test_vars = {
160+
'WANDB_MODE': 'offline',
161+
'TF_CPP_MIN_LOG_LEVEL': '3',
162+
'PYTHONPATH': str(Path.cwd())
163+
}
164+
165+
original_vars = {}
166+
for key, value in test_vars.items():
167+
original_vars[key] = os.environ.get(key)
168+
os.environ[key] = value
169+
170+
yield test_vars
171+
172+
# Restore original environment variables
173+
for key, original_value in original_vars.items():
174+
if original_value is None:
175+
os.environ.pop(key, None)
176+
else:
177+
os.environ[key] = original_value
178+
179+
180+
@pytest.fixture
181+
def mock_typer_app():
182+
"""Mock Typer application for CLI testing."""
183+
from unittest.mock import Mock
184+
return Mock()
185+
186+
187+
@pytest.fixture(autouse=True)
188+
def setup_test_environment(test_env_vars):
189+
"""Automatically set up the test environment for all tests."""
190+
pass

tests/integration/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)