diff --git a/.gitignore b/.gitignore index 3986f586..dbe4bea4 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,35 @@ results/ thirdparty/ncore/ playground/assets/ + +# Testing related +.pytest_cache/ +.coverage +htmlcov/ +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.tox/ +.nox/ + +# Claude settings +.claude/* + +# Poetry +dist/ +build/ +*.egg-info/ + +# Virtual environments +venv/ +ENV/ +env/ +.venv/ + +# IDE files +.idea/ +*.swp +*.swo +*~ +.DS_Store diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..101c25a4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,112 @@ +[tool.poetry] +name = "threedgrut" +version = "0.1.0" +description = "3D Gaussian Rendering and Tracing" +authors = ["Your Name "] +readme = "README.md" +packages = [ + { include = "threedgrut" }, + { include = "threedgrt_tracer" }, + { include = "threedgut_tracer" }, + { include = "threedgrut_playground" } +] + +[tool.poetry.dependencies] +python = "^3.11" +torchmetrics = "*" +tensorboard = "*" +slangtorch = "1.3.4" +plyfile = "*" +polyscope = ">=2.3.0" +libigl = "*" +pygltflib = "*" +scikit-learn = "*" +wandb = "*" +fire = "*" +omegaconf = "*" +hydra-core = "*" +kornia = "*" +opencv-python = "*" +einops = "*" +imageio = "*" +msgpack = "*" +dataclasses-json = "*" +addict = "*" +rich = "*" +tqdm = "*" +setuptools = "<72.1.0" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.0.0" +pytest-cov = "^4.1.0" +pytest-mock = "^3.12.0" + +[tool.poetry.scripts] +test = "pytest:main" +tests = "pytest:main" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +minversion = "8.0" +addopts = [ + "-ra", + "--strict-markers", + "--cov=threedgrut", + "--cov=threedgrt_tracer", + "--cov=threedgut_tracer", + "--cov=threedgrut_playground", + "--cov-report=term-missing", + "--cov-report=html", + "--cov-report=xml", + "--cov-fail-under=80", + "-vv" +] +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +markers = [ + "unit: marks tests as unit tests (deselect with '-m \"not unit\"')", + "integration: marks tests as integration tests (deselect with '-m \"not integration\"')", + "slow: marks tests as slow (deselect with '-m \"not slow\"')" +] +filterwarnings = [ + "ignore::DeprecationWarning", + "ignore::PendingDeprecationWarning" +] + +[tool.coverage.run] +source = ["threedgrut", "threedgrt_tracer", "threedgut_tracer", "threedgrut_playground"] +omit = [ + "*/tests/*", + "*/__pycache__/*", + "*/site-packages/*", + "*/setup*.py", + "*/conftest.py" +] + +[tool.coverage.report] +precision = 2 +show_missing = true +skip_covered = false +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod" +] + +[tool.coverage.html] +directory = "htmlcov" + +[tool.coverage.xml] +output = "coverage.xml" \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..e2dd9155 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,207 @@ +"""Shared pytest fixtures and configuration for the threedgrut test suite.""" +import os +import tempfile +from pathlib import Path +from typing import Generator, Any +import shutil + +import pytest +from omegaconf import DictConfig, OmegaConf + + +@pytest.fixture +def temp_dir() -> Generator[Path, None, None]: + """Create a temporary directory for test files.""" + with tempfile.TemporaryDirectory() as tmp_dir: + yield Path(tmp_dir) + + +@pytest.fixture +def temp_file(temp_dir: Path) -> Generator[Path, None, None]: + """Create a temporary file within the temp directory.""" + temp_path = temp_dir / "test_file.txt" + temp_path.write_text("test content") + yield temp_path + + +@pytest.fixture +def sample_config() -> DictConfig: + """Create a sample configuration for testing.""" + config = { + "model": { + "type": "gaussian", + "num_points": 1000, + "learning_rate": 0.001, + }, + "dataset": { + "type": "nerf", + "path": "/path/to/dataset", + "batch_size": 32, + }, + "training": { + "max_iterations": 10000, + "checkpoint_interval": 1000, + "validation_interval": 500, + }, + "render": { + "resolution": [800, 600], + "samples_per_pixel": 1, + } + } + return OmegaConf.create(config) + + +@pytest.fixture +def mock_dataset_path(temp_dir: Path) -> Path: + """Create a mock dataset directory structure.""" + dataset_dir = temp_dir / "mock_dataset" + dataset_dir.mkdir() + + # Create subdirectories + (dataset_dir / "images").mkdir() + (dataset_dir / "sparse").mkdir() + (dataset_dir / "dense").mkdir() + + # Create some mock files + (dataset_dir / "images" / "image_001.jpg").touch() + (dataset_dir / "images" / "image_002.jpg").touch() + (dataset_dir / "sparse" / "cameras.bin").touch() + (dataset_dir / "sparse" / "images.bin").touch() + (dataset_dir / "sparse" / "points3D.bin").touch() + + return dataset_dir + + +@pytest.fixture +def mock_checkpoint_path(temp_dir: Path) -> Path: + """Create a mock checkpoint file.""" + checkpoint_path = temp_dir / "checkpoint.ckpt" + checkpoint_path.write_text("mock checkpoint data") + return checkpoint_path + + +@pytest.fixture +def mock_ply_file(temp_dir: Path) -> Path: + """Create a mock PLY file for testing.""" + ply_path = temp_dir / "pointcloud.ply" + ply_content = """ply +format ascii 1.0 +element vertex 3 +property float x +property float y +property float z +end_header +0.0 0.0 0.0 +1.0 0.0 0.0 +0.0 1.0 0.0 +""" + ply_path.write_text(ply_content) + return ply_path + + +@pytest.fixture +def mock_camera_params() -> dict[str, Any]: + """Create mock camera parameters.""" + return { + "width": 800, + "height": 600, + "fx": 500.0, + "fy": 500.0, + "cx": 400.0, + "cy": 300.0, + "k1": 0.0, + "k2": 0.0, + "p1": 0.0, + "p2": 0.0, + } + + +@pytest.fixture +def mock_render_params() -> dict[str, Any]: + """Create mock render parameters.""" + return { + "resolution": [800, 600], + "samples_per_pixel": 1, + "background_color": [0.0, 0.0, 0.0], + "near_plane": 0.1, + "far_plane": 100.0, + } + + +@pytest.fixture +def clean_environment(monkeypatch): + """Clean environment variables for testing.""" + # Remove any existing CUDA-related environment variables + cuda_vars = ["CUDA_VISIBLE_DEVICES", "CUDA_DEVICE_ORDER"] + for var in cuda_vars: + monkeypatch.delenv(var, raising=False) + + # Set testing environment + monkeypatch.setenv("TESTING", "1") + monkeypatch.setenv("HYDRA_FULL_ERROR", "1") + + +@pytest.fixture +def mock_wandb(monkeypatch): + """Mock wandb to prevent actual logging during tests.""" + monkeypatch.setenv("WANDB_MODE", "disabled") + monkeypatch.setenv("WANDB_SILENT", "true") + + +@pytest.fixture(autouse=True) +def change_test_dir(request, monkeypatch, temp_dir): + """Automatically change to temp directory for each test.""" + monkeypatch.chdir(temp_dir) + yield + # No need to change back as monkeypatch handles cleanup + + +@pytest.fixture +def sample_point_cloud() -> dict[str, list[float]]: + """Create a sample point cloud data.""" + return { + "positions": [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ], + "colors": [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + [1.0, 1.0, 0.0], + ], + "scales": [ + [0.1, 0.1, 0.1], + [0.1, 0.1, 0.1], + [0.1, 0.1, 0.1], + [0.1, 0.1, 0.1], + ], + } + + +@pytest.fixture +def gpu_available() -> bool: + """Check if GPU is available for testing.""" + try: + import torch + return torch.cuda.is_available() + except ImportError: + return False + + +@pytest.fixture +def skip_if_no_gpu(gpu_available): + """Skip test if GPU is not available.""" + if not gpu_available: + pytest.skip("GPU not available") + + +@pytest.fixture +def mock_timer(mocker): + """Mock timer for performance testing.""" + timer = mocker.MagicMock() + timer.elapsed_time = 0.1 + timer.average_time = 0.1 + return timer \ No newline at end of file diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_infrastructure.py b/tests/test_infrastructure.py new file mode 100644 index 00000000..3a41a296 --- /dev/null +++ b/tests/test_infrastructure.py @@ -0,0 +1,159 @@ +"""Validation tests to ensure the testing infrastructure is set up correctly.""" +import sys +from pathlib import Path + +import pytest +from omegaconf import DictConfig + + +class TestInfrastructureSetup: + """Test class to validate the testing infrastructure.""" + + def test_python_version(self): + """Verify Python version meets requirements.""" + assert sys.version_info >= (3, 11), "Python 3.11+ is required" + + def test_project_imports(self): + """Test that project modules can be imported.""" + try: + import threedgrut + import threedgrt_tracer + import threedgut_tracer + import threedgrut_playground + except ImportError as e: + pytest.fail(f"Failed to import project modules: {e}") + + def test_temp_dir_fixture(self, temp_dir): + """Test the temp_dir fixture creates a valid directory.""" + assert temp_dir.exists() + assert temp_dir.is_dir() + + # Test we can write to it + test_file = temp_dir / "test.txt" + test_file.write_text("test") + assert test_file.exists() + assert test_file.read_text() == "test" + + def test_temp_file_fixture(self, temp_file): + """Test the temp_file fixture creates a valid file.""" + assert temp_file.exists() + assert temp_file.is_file() + assert temp_file.read_text() == "test content" + + def test_sample_config_fixture(self, sample_config): + """Test the sample_config fixture returns valid config.""" + assert isinstance(sample_config, DictConfig) + assert "model" in sample_config + assert "dataset" in sample_config + assert "training" in sample_config + assert "render" in sample_config + + # Test nested values + assert sample_config.model.type == "gaussian" + assert sample_config.dataset.batch_size == 32 + assert sample_config.training.max_iterations == 10000 + assert sample_config.render.resolution == [800, 600] + + def test_mock_dataset_fixture(self, mock_dataset_path): + """Test the mock_dataset_path fixture creates proper structure.""" + assert mock_dataset_path.exists() + assert (mock_dataset_path / "images").exists() + assert (mock_dataset_path / "sparse").exists() + assert (mock_dataset_path / "dense").exists() + + # Check files + assert (mock_dataset_path / "images" / "image_001.jpg").exists() + assert (mock_dataset_path / "sparse" / "cameras.bin").exists() + + def test_mock_checkpoint_fixture(self, mock_checkpoint_path): + """Test the mock_checkpoint_path fixture.""" + assert mock_checkpoint_path.exists() + assert mock_checkpoint_path.suffix == ".ckpt" + assert mock_checkpoint_path.read_text() == "mock checkpoint data" + + def test_mock_ply_fixture(self, mock_ply_file): + """Test the mock_ply_file fixture creates valid PLY.""" + assert mock_ply_file.exists() + assert mock_ply_file.suffix == ".ply" + content = mock_ply_file.read_text() + assert "ply" in content + assert "element vertex 3" in content + + def test_clean_environment_fixture(self, clean_environment): + """Test environment is properly cleaned.""" + import os + assert os.environ.get("TESTING") == "1" + assert os.environ.get("HYDRA_FULL_ERROR") == "1" + assert "CUDA_VISIBLE_DEVICES" not in os.environ + + def test_change_test_dir_fixture(self, temp_dir): + """Test that we're running in temp directory.""" + assert Path.cwd().parent == temp_dir.parent + + @pytest.mark.unit + def test_unit_marker(self): + """Test that unit test marker works.""" + assert True + + @pytest.mark.integration + def test_integration_marker(self): + """Test that integration test marker works.""" + assert True + + @pytest.mark.slow + def test_slow_marker(self): + """Test that slow test marker works.""" + import time + time.sleep(0.01) # Simulate slow test + assert True + + def test_coverage_tracking(self): + """Test that coverage is being tracked.""" + # This test ensures coverage is working + def dummy_function(x): + if x > 0: + return x * 2 + else: + return 0 + + assert dummy_function(5) == 10 + assert dummy_function(-1) == 0 + + def test_pytest_mock_available(self, mocker): + """Test that pytest-mock is available and working.""" + mock_func = mocker.Mock(return_value=42) + assert mock_func() == 42 + mock_func.assert_called_once() + + def test_mock_wandb_fixture(self, mock_wandb): + """Test wandb is properly mocked.""" + import os + assert os.environ.get("WANDB_MODE") == "disabled" + assert os.environ.get("WANDB_SILENT") == "true" + + +class TestParametrizedExamples: + """Examples of parametrized tests.""" + + @pytest.mark.parametrize("input_val,expected", [ + (1, 2), + (2, 4), + (3, 6), + (4, 8), + ]) + def test_parametrized_example(self, input_val, expected): + """Example of parametrized test.""" + assert input_val * 2 == expected + + @pytest.mark.parametrize("resolution", [ + [800, 600], + [1920, 1080], + [1024, 768], + ]) + def test_resolution_validation(self, resolution): + """Example of testing different resolutions.""" + width, height = resolution + assert width > 0 + assert height > 0 + assert isinstance(width, int) + assert isinstance(height, int) \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b