Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.
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
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.DS_Store
.idea/

tmp/
outputs/
Expand Down Expand Up @@ -127,7 +128,8 @@ ipython_config.py
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# Don't ignore poetry.lock - it should be committed
# poetry.lock

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
Expand Down Expand Up @@ -187,4 +189,7 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# Claude settings
.claude/*


3,670 changes: 3,670 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

134 changes: 134 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
[tool.poetry]
name = "stable-diffusion-webui-forge"
version = "0.1.0"
description = "Stable Diffusion WebUI Forge"
authors = ["Your Name <[email protected]>"]
readme = "readme.md"
packages = [{include = "api"}, {include = "lib"}, {include = "modules"}]

[tool.poetry.dependencies]
python = "^3.11"
gradio = "^5.31.0"
lycoris-lora = "^1.9.0"
pytorch-lightning = "^2.2.0"
diffusers = "^0.25.0"
invisible-watermark = "^0.2.0"
accelerate = "^0.27.2"
transformers = "^4.36.2"
sentencepiece = "^0.1.99"
pydantic = "^2.5.3"
omegaconf = "^2.3.0"
safetensors = "^0.5.3"
pillow = "^10.4.0"
toml = "^0.10.2"
colored = "^1.4.4"
psutil = "^7.0.0"

[tool.poetry.group.dev.dependencies]
isort = "*"
black = "*"
pre-commit = "*"
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]
minversion = "7.0"
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--strict-markers",
"--verbose",
"--tb=short",
"--cov=api",
"--cov=lib",
"--cov=modules",
"--cov-report=term-missing",
"--cov-report=html",
"--cov-report=xml",
"--cov-fail-under=0", # Set to 0 for initial setup, should be increased as tests are added
]
markers = [
"unit: marks tests as unit tests",
"integration: marks tests as integration tests",
"slow: marks tests as slow running",
]
filterwarnings = [
"error",
"ignore::UserWarning",
"ignore::DeprecationWarning",
]

[tool.coverage.run]
source = ["api", "lib", "modules"]
omit = [
"*/tests/*",
"*/test_*",
"*/__pycache__/*",
"*/venv/*",
"*/.venv/*",
"*/site-packages/*",
]

[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false
fail_under = 0 # Set to 0 for initial setup, should be increased to 80 as tests are added
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if __name__ == .__main__.:",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if False:",
"pass",
]

[tool.coverage.html]
directory = "htmlcov"

[tool.coverage.xml]
output = "coverage.xml"

[tool.isort]
profile = "black"
line_length = 120
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true

[tool.black]
line-length = 120
target-version = ['py38']
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| migrations
)/
'''

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Empty file added tests/__init__.py
Empty file.
220 changes: 220 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
"""Shared pytest fixtures and configuration for all tests."""

import os
import shutil
import tempfile
from pathlib import Path
from typing import Dict, Generator, Any
from unittest.mock import MagicMock

import pytest


@pytest.fixture
def temp_dir() -> Generator[Path, None, None]:
"""Create a temporary directory for test files.

Yields:
Path: Path to the temporary directory
"""
temp_path = Path(tempfile.mkdtemp())
yield temp_path
# Cleanup after test
if temp_path.exists():
shutil.rmtree(temp_path)


@pytest.fixture
def mock_config() -> Dict[str, Any]:
"""Provide a mock configuration dictionary for testing.

Returns:
Dict[str, Any]: Mock configuration with common settings
"""
return {
"model_path": "/tmp/test_models",
"output_path": "/tmp/test_outputs",
"device": "cpu",
"batch_size": 1,
"num_inference_steps": 10,
"guidance_scale": 7.5,
"width": 512,
"height": 512,
"seed": 42,
"enable_safety_checker": False,
}


@pytest.fixture
def mock_model():
"""Provide a mock diffusion model for testing.

Returns:
MagicMock: Mock model object
"""
model = MagicMock()
model.device = "cpu"
model.generate = MagicMock(return_value=MagicMock())
model.load_weights = MagicMock()
return model


@pytest.fixture
def sample_image_path(temp_dir: Path) -> Path:
"""Create a sample image file for testing.

Args:
temp_dir: Temporary directory fixture

Returns:
Path: Path to the created sample image
"""
from PIL import Image

# Create a simple test image
img = Image.new('RGB', (64, 64), color='red')
img_path = temp_dir / "test_image.png"
img.save(img_path)
return img_path


@pytest.fixture
def mock_gradio_interface():
"""Provide a mock Gradio interface for testing.

Returns:
MagicMock: Mock Gradio interface
"""
interface = MagicMock()
interface.launch = MagicMock()
interface.close = MagicMock()
return interface


@pytest.fixture
def env_vars() -> Generator[Dict[str, str], None, None]:
"""Temporarily set environment variables for testing.

Yields:
Dict[str, str]: Dictionary to set environment variables
"""
original_env = os.environ.copy()
test_env = {}

def set_env(**kwargs):
test_env.update(kwargs)
os.environ.update(kwargs)

test_env.set = set_env
yield test_env

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


@pytest.fixture
def mock_torch():
"""Provide a mock torch module for testing without GPU.

Returns:
MagicMock: Mock torch module
"""
torch_mock = MagicMock()
torch_mock.cuda.is_available = MagicMock(return_value=False)
torch_mock.device = MagicMock(return_value="cpu")
torch_mock.tensor = MagicMock()
torch_mock.no_grad = MagicMock()
return torch_mock


@pytest.fixture
def sample_prompt() -> str:
"""Provide a sample prompt for testing.

Returns:
str: Sample text prompt
"""
return "A beautiful landscape with mountains and a lake"


@pytest.fixture
def sample_negative_prompt() -> str:
"""Provide a sample negative prompt for testing.

Returns:
str: Sample negative text prompt
"""
return "low quality, blurry, distorted"


@pytest.fixture(autouse=True)
def cleanup_test_outputs():
"""Automatically cleanup any test outputs after each test."""
yield
# Cleanup code runs after test
test_dirs = ["/tmp/test_models", "/tmp/test_outputs"]
for test_dir in test_dirs:
if os.path.exists(test_dir):
shutil.rmtree(test_dir)


@pytest.fixture
def mock_pipeline():
"""Provide a mock diffusion pipeline for testing.

Returns:
MagicMock: Mock pipeline object
"""
pipeline = MagicMock()
pipeline.to = MagicMock(return_value=pipeline)
pipeline.__call__ = MagicMock()
pipeline.enable_xformers_memory_efficient_attention = MagicMock()
pipeline.disable_xformers_memory_efficient_attention = MagicMock()
return pipeline


# Markers for test organization
def pytest_configure(config):
"""Configure pytest with custom markers."""
config.addinivalue_line(
"markers", "unit: mark test as a unit test"
)
config.addinivalue_line(
"markers", "integration: mark test as an integration test"
)
config.addinivalue_line(
"markers", "slow: mark test as slow running"
)


# Test session fixtures
@pytest.fixture(scope="session")
def test_data_dir() -> Path:
"""Provide a session-scoped test data directory.

Returns:
Path: Path to test data directory
"""
return Path(__file__).parent / "data"


@pytest.fixture
def capture_logs():
"""Capture log messages during tests.

Yields:
list: List of captured log records
"""
import logging

logs = []
handler = logging.Handler()
handler.emit = lambda record: logs.append(record)

root_logger = logging.getLogger()
root_logger.addHandler(handler)

yield logs

root_logger.removeHandler(handler)
Empty file added tests/integration/__init__.py
Empty file.
Loading