Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ __pycache__/
.venv
venv/
logs/
uv.lock
main.exp
main.lib
main.obj
Expand All @@ -12,3 +11,26 @@ GEMINI.md
.gemini/
AGENTS.md
.vscode/settings.json

# Testing
.pytest_cache/
.coverage
htmlcov/
coverage.xml
.tox/
.nox/

# Build artifacts
build/
dist/
*.egg-info/

# IDE files
.idea/
*.swp
*.swo
*~

# OS files
.DS_Store
Thumbs.db
75 changes: 75 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ conflicts = [
],
]

[tool.uv.scripts]
test = "pytest"
tests = "pytest"

[dependency-groups]
dev = [
"ascii-magic==2.3.0",
Expand All @@ -52,6 +56,11 @@ dev = [
"prompt-toolkit==3.0.51",
"ruff>=0.12.10",
]
test = [
"pytest>=8.0.0",
"pytest-cov>=4.0.0",
"pytest-mock>=3.12.0",
]

[build-system]
requires = ["hatchling"]
Expand Down Expand Up @@ -172,3 +181,69 @@ docstring-code-line-length = "dynamic"
"src/musubi_tuner/wan/utils/fm_solvers.py" = ["ALL"]
"src/musubi_tuner/wan/utils/fm_solvers_unipc.py" = ["ALL"]
"src/musubi_tuner/wan/utils/utils.py" = ["ALL"]

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--strict-markers",
"--strict-config",
"--cov=src/musubi_tuner",
"--cov-report=term-missing",
"--cov-report=html:htmlcov",
"--cov-report=xml:coverage.xml",
"--cov-fail-under=80",
"-v",
]
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow running tests",
]
filterwarnings = [
"ignore::DeprecationWarning",
"ignore::PendingDeprecationWarning",
]

[tool.coverage.run]
source = ["src/musubi_tuner"]
omit = [
"tests/*",
"*/test_*.py",
"*/*_test.py",
"src/musubi_tuner/flux/flux_models.py",
"src/musubi_tuner/frame_pack/hunyuan.py",
"src/musubi_tuner/frame_pack/hunyuan_video_packed.py",
"src/musubi_tuner/frame_pack/k_diffusion_hunyuan.py",
"src/musubi_tuner/hunyuan_model/autoencoder_kl_causal_3d.py",
"src/musubi_tuner/hunyuan_model/pipeline_hunyuan_video.py",
"src/musubi_tuner/modules/scheduling_flow_match_discrete.py",
"src/musubi_tuner/modules/unet_causal_3d_blocks.py",
"src/musubi_tuner/qwen_image/qwen_image_autoencoder_kl.py",
"src/musubi_tuner/qwen_image/qwen_image_model.py",
"src/musubi_tuner/wan/configs/*",
"src/musubi_tuner/wan/modules/*",
"src/musubi_tuner/wan/utils/*",
]

[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__.:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]
fail_under = 80
show_missing = true
skip_covered = false

[tool.coverage.html]
directory = "htmlcov"
Empty file added tests/__init__.py
Empty file.
162 changes: 162 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import os
import tempfile
from pathlib import Path
from unittest.mock import MagicMock
import pytest


@pytest.fixture
def temp_dir():
"""Provide a temporary directory for tests."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)


@pytest.fixture
def temp_file(temp_dir):
"""Provide a temporary file for tests."""
temp_file_path = temp_dir / "test_file.txt"
temp_file_path.write_text("test content")
yield temp_file_path


@pytest.fixture
def mock_config():
"""Provide a mock configuration object."""
config = MagicMock()
config.learning_rate = 1e-4
config.batch_size = 4
config.num_epochs = 10
config.model_name = "test_model"
config.output_dir = "/tmp/test_output"
return config


@pytest.fixture
def sample_image_path(temp_dir):
"""Provide a path for a sample test image."""
return temp_dir / "sample_image.jpg"


@pytest.fixture
def sample_video_path(temp_dir):
"""Provide a path for a sample test video."""
return temp_dir / "sample_video.mp4"


@pytest.fixture
def mock_torch_device():
"""Mock torch device for testing."""
return MagicMock()


@pytest.fixture
def mock_model():
"""Provide a mock model for testing."""
model = MagicMock()
model.eval.return_value = model
model.train.return_value = model
model.parameters.return_value = []
return model


@pytest.fixture
def mock_dataset():
"""Provide a mock dataset for testing."""
dataset = MagicMock()
dataset.__len__.return_value = 100
dataset.__getitem__.return_value = {
"image": MagicMock(),
"text": "test caption",
"label": 0
}
return dataset


@pytest.fixture
def mock_dataloader(mock_dataset):
"""Provide a mock dataloader for testing."""
dataloader = MagicMock()
dataloader.__iter__.return_value = iter([{
"image": MagicMock(),
"text": ["test caption"] * 4,
"label": [0] * 4
}])
dataloader.__len__.return_value = 25
return dataloader


@pytest.fixture
def sample_lora_config():
"""Provide a sample LoRA configuration."""
return {
"rank": 16,
"alpha": 32,
"dropout": 0.1,
"target_modules": ["to_k", "to_q", "to_v", "to_out.0"]
}


@pytest.fixture
def mock_text_encoder():
"""Provide a mock text encoder."""
encoder = MagicMock()
encoder.encode.return_value = MagicMock()
return encoder


@pytest.fixture
def mock_tokenizer():
"""Provide a mock tokenizer."""
tokenizer = MagicMock()
tokenizer.encode.return_value = [1, 2, 3, 4, 5]
tokenizer.decode.return_value = "test text"
return tokenizer


@pytest.fixture(autouse=True)
def setup_test_env():
"""Set up test environment variables."""
original_env = os.environ.copy()

# Set test-specific environment variables
os.environ["TESTING"] = "1"
os.environ["CUDA_VISIBLE_DEVICES"] = ""

yield

# Restore original environment
os.environ.clear()
os.environ.update(original_env)


@pytest.fixture
def capture_logs(caplog):
"""Fixture to capture logs during tests."""
yield caplog


class MockHuggingFaceModel:
"""Mock HuggingFace model for testing."""

def __init__(self):
self.config = MagicMock()
self.state_dict = lambda: {}

def eval(self):
return self

def train(self):
return self

def to(self, device):
return self

def __call__(self, *args, **kwargs):
return MagicMock()


@pytest.fixture
def mock_huggingface_model():
"""Provide a mock HuggingFace model."""
return MockHuggingFaceModel()
Empty file added tests/integration/__init__.py
Empty file.
Loading