diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..209c67e --- /dev/null +++ b/.gitignore @@ -0,0 +1,159 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# poetry +poetry.lock + +# pdm +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# IDE settings +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Claude settings +.claude/* + +# OS files +.DS_Store +Thumbs.db + +# Project specific +output/ +checkpoints/ +data/ +wandb/ +runs/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..35613b6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,23 @@ +# CLAUDE.md - Project Context + +This file contains important context and instructions for Claude Code. + +## Project Overview +Python project requiring testing infrastructure setup. + +## Commands to Run After Code Changes +- `poetry run test` - Run all tests +- `poetry run lint` - Run linting (if configured) +- `poetry run typecheck` - Run type checking (if configured) + +## Testing Infrastructure +- Package manager: Poetry (or UV if detected) +- Test framework: pytest +- Coverage: pytest-cov +- Test directories: tests/unit/, tests/integration/ + +## Important Notes +- Always run tests after making changes +- Coverage threshold is currently set to 0% for infrastructure setup +- **TODO**: Change `--cov-fail-under=0` to `--cov-fail-under=80` in pyproject.toml after writing tests +- Use conventional commit messages \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0f477a9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,85 @@ +[tool.poetry] +name = "gaussian-splatting" +version = "0.1.0" +description = "3D Gaussian Splatting for Real-Time Radiance Field Rendering" +authors = ["Your Name "] +readme = "README.md" +packages = [{include = "scene"}, {include = "utils"}, {include = "arguments"}, {include = "gaussian_renderer"}, {include = "lpipsPyTorch"}, {include = "scripts"}] + +[tool.poetry.dependencies] +python = "^3.8" +plyfile = "*" +opencv-python = "*" +lpips = "*" +trimesh = "*" +numpy = "*" +torch = "*" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.0" +pytest-cov = "^4.1.0" +pytest-mock = "^3.11.1" + +[tool.poetry.scripts] +test = "pytest:main" +tests = "pytest:main" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "-v", + "--strict-markers", + "--tb=short", + "--cov=scene", + "--cov=utils", + "--cov=arguments", + "--cov=gaussian_renderer", + "--cov=lpipsPyTorch", + "--cov-report=html", + "--cov-report=xml", + "--cov-report=term-missing", + "--cov-fail-under=0" +] +markers = [ + "unit: Unit tests", + "integration: Integration tests", + "slow: Slow running tests" +] + +[tool.coverage.run] +source = ["scene", "utils", "arguments", "gaussian_renderer", "lpipsPyTorch"] +omit = [ + "*/tests/*", + "*/test_*.py", + "*/__pycache__/*", + "*/site-packages/*", + "setup.py", + "*/migrations/*" +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:" +] +show_missing = true +precision = 2 + +[tool.coverage.html] +directory = "htmlcov" + +[tool.coverage.xml] +output = "coverage.xml" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..687b583 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,175 @@ +"""Shared pytest fixtures and configuration for all tests.""" + +import os +import tempfile +import shutil +from pathlib import Path +from typing import Generator, Dict, Any +import pytest +import numpy as np + + +@pytest.fixture +def temp_dir() -> Generator[Path, None, None]: + """Create a temporary directory for test files.""" + temp_path = tempfile.mkdtemp() + yield Path(temp_path) + shutil.rmtree(temp_path) + + +@pytest.fixture +def mock_config() -> Dict[str, Any]: + """Provide a mock configuration dictionary for testing.""" + return { + "model_path": "/tmp/test_model", + "source_path": "/tmp/test_data", + "iterations": 100, + "test_iterations": [50, 100], + "save_iterations": [50, 100], + "checkpoint_iterations": [], + "resolution": 1, + "white_background": False, + "data_device": "cpu", + "eval": False, + "sh_degree": 3, + "feature_lr": 0.0025, + "opacity_lr": 0.05, + "scaling_lr": 0.005, + "rotation_lr": 0.001, + "position_lr_init": 0.00016, + "position_lr_final": 0.0000016, + "position_lr_delay_mult": 0.01, + "position_lr_max_steps": 30000, + "percent_dense": 0.01, + "lambda_dssim": 0.2, + "densification_interval": 100, + "opacity_reset_interval": 3000, + "densify_from_iter": 500, + "densify_until_iter": 15000, + "densify_grad_threshold": 0.0002, + } + + +@pytest.fixture +def sample_point_cloud() -> np.ndarray: + """Generate a sample 3D point cloud for testing.""" + num_points = 100 + points = np.random.randn(num_points, 3).astype(np.float32) + return points + + +@pytest.fixture +def sample_image_data() -> np.ndarray: + """Generate sample image data for testing.""" + height, width = 64, 64 + image = np.random.rand(height, width, 3).astype(np.float32) + return image + + +@pytest.fixture +def mock_camera_params() -> Dict[str, Any]: + """Provide mock camera parameters for testing.""" + return { + "width": 800, + "height": 600, + "fovx": 1.0472, # 60 degrees in radians + "fovy": 0.7854, # 45 degrees in radians + "znear": 0.01, + "zfar": 100.0, + "world_view_transform": np.eye(4, dtype=np.float32), + "full_proj_transform": np.eye(4, dtype=np.float32), + "camera_center": np.array([0, 0, 0], dtype=np.float32), + } + + +@pytest.fixture +def temp_ply_file(temp_dir: Path, sample_point_cloud: np.ndarray) -> Path: + """Create a temporary PLY file with sample point cloud data.""" + ply_path = temp_dir / "test_points.ply" + + # Simple PLY file writing (minimal implementation for testing) + with open(ply_path, 'w') as f: + f.write("ply\n") + f.write("format ascii 1.0\n") + f.write(f"element vertex {len(sample_point_cloud)}\n") + f.write("property float x\n") + f.write("property float y\n") + f.write("property float z\n") + f.write("end_header\n") + + for point in sample_point_cloud: + f.write(f"{point[0]} {point[1]} {point[2]}\n") + + return ply_path + + +@pytest.fixture +def mock_training_args(): + """Mock training arguments for testing.""" + class Args: + def __init__(self): + self.source_path = "/tmp/test_data" + self.model_path = "/tmp/test_model" + self.iterations = 1000 + self.test_iterations = [500, 1000] + self.save_iterations = [1000] + self.checkpoint_iterations = [] + self.quiet = True + self.resolution = 1 + self.white_background = False + self.data_device = "cpu" + self.eval = False + + return Args() + + +@pytest.fixture +def capture_stdout(monkeypatch): + """Capture stdout for testing print statements.""" + import io + import sys + + captured_output = io.StringIO() + + def _capture(): + monkeypatch.setattr(sys, 'stdout', captured_output) + return captured_output + + return _capture + + +@pytest.fixture(autouse=True) +def reset_random_seed(): + """Reset random seeds for reproducible tests.""" + np.random.seed(42) + + # If using PyTorch + try: + import torch + torch.manual_seed(42) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(42) + except ImportError: + pass + + +@pytest.fixture +def mock_gaussian_model_data() -> Dict[str, np.ndarray]: + """Provide mock data for Gaussian model testing.""" + num_gaussians = 50 + return { + "means": np.random.randn(num_gaussians, 3).astype(np.float32), + "scales": np.random.rand(num_gaussians, 3).astype(np.float32) * 0.1, + "rotations": np.random.randn(num_gaussians, 4).astype(np.float32), + "opacities": np.random.rand(num_gaussians, 1).astype(np.float32), + "features": np.random.rand(num_gaussians, 48).astype(np.float32), + } + + +@pytest.fixture +def environment_setup(monkeypatch): + """Set up test environment variables.""" + monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "") + monkeypatch.setenv("TEST_MODE", "1") + yield + # Cleanup happens automatically with monkeypatch \ No newline at end of file diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_setup_validation.py b/tests/test_setup_validation.py new file mode 100644 index 0000000..d5eaef2 --- /dev/null +++ b/tests/test_setup_validation.py @@ -0,0 +1,104 @@ +"""Validation tests to ensure the testing infrastructure is properly set up.""" + +import pytest +from pathlib import Path +import numpy as np + + +def test_pytest_is_working(): + """Basic test to verify pytest is working.""" + assert True + + +def test_fixtures_are_available(temp_dir, mock_config): + """Test that our fixtures are properly configured.""" + assert isinstance(temp_dir, Path) + assert temp_dir.exists() + + assert isinstance(mock_config, dict) + assert "model_path" in mock_config + assert mock_config["iterations"] == 100 + + +def test_numpy_is_available(): + """Test that numpy is available for tests.""" + arr = np.array([1, 2, 3]) + assert len(arr) == 3 + assert arr.sum() == 6 + + +@pytest.mark.unit +def test_unit_marker(): + """Test that unit test marker works.""" + assert 1 + 1 == 2 + + +@pytest.mark.integration +def test_integration_marker(): + """Test that integration test marker works.""" + assert "integration" in "integration test" + + +@pytest.mark.slow +def test_slow_marker(): + """Test that slow test marker works.""" + # This would be a slow test in real scenarios + assert True + + +def test_temp_dir_cleanup(temp_dir): + """Test that temporary directory is created and will be cleaned up.""" + test_file = temp_dir / "test.txt" + test_file.write_text("test content") + + assert test_file.exists() + assert test_file.read_text() == "test content" + + +def test_mock_camera_params(mock_camera_params): + """Test that camera parameters fixture provides expected data.""" + assert mock_camera_params["width"] == 800 + assert mock_camera_params["height"] == 600 + assert "fovx" in mock_camera_params + assert isinstance(mock_camera_params["world_view_transform"], np.ndarray) + + +def test_sample_point_cloud(sample_point_cloud): + """Test that sample point cloud fixture generates valid data.""" + assert isinstance(sample_point_cloud, np.ndarray) + assert sample_point_cloud.shape == (100, 3) + assert sample_point_cloud.dtype == np.float32 + + +def test_temp_ply_file(temp_ply_file): + """Test that temporary PLY file is created correctly.""" + assert temp_ply_file.exists() + assert temp_ply_file.suffix == ".ply" + + content = temp_ply_file.read_text() + assert "ply" in content + assert "element vertex" in content + + +def test_coverage_is_tracked(): + """Test to ensure coverage tracking is working.""" + def sample_function(x, y): + if x > y: + return x + else: + return y + + assert sample_function(5, 3) == 5 + assert sample_function(2, 7) == 7 + + +class TestClassBasedTests: + """Test that class-based tests work correctly.""" + + def test_class_method(self): + """Test method in a test class.""" + assert isinstance(self, TestClassBasedTests) + + def test_another_method(self, mock_config): + """Test that fixtures work in test classes.""" + assert mock_config["sh_degree"] == 3 \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29