mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-10-12 02:16:43 +00:00
205 lines
9.7 KiB
Python
205 lines
9.7 KiB
Python
# conftest.py is a special pytest file. Fixtures defined in this file will be accessible to all tests in this directory
|
|
# without needing to explicitly import them. (https://docs.pytest.org/en/6.2.x/fixture.html)
|
|
|
|
|
|
# We import the model_installer and torch_device fixtures here so that they can be used by all tests. Flake8 does not
|
|
# play well with fixtures (F401 and F811), so this is cleaner than importing in all files that use these fixtures.
|
|
import logging
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from invokeai.app.services.board_image_records.board_image_records_default import BoardImageRecordStorage
|
|
from invokeai.app.services.board_records.board_records_default import BoardRecordStorage
|
|
from invokeai.app.services.board_video_records.board_video_records_default import BoardVideoRecordStorage
|
|
from invokeai.app.services.boards.boards_default import BoardService
|
|
from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService
|
|
from invokeai.app.services.client_state_persistence.client_state_persistence_default import ClientStatePersistence
|
|
from invokeai.app.services.config.config_default import InvokeAIAppConfig
|
|
from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService
|
|
from invokeai.app.services.gallery.gallery_default import GalleryService
|
|
from invokeai.app.services.image_index.image_index_default import ImageIndexService
|
|
from invokeai.app.services.image_index.image_index_records_default import ImageIndexRecords
|
|
from invokeai.app.services.image_records.image_records_default import ImageRecordStorage
|
|
from invokeai.app.services.images.images_default import ImageService
|
|
from invokeai.app.services.intermediates.intermediates_default import IntermediatesService
|
|
from invokeai.app.services.intermediates.intermediates_records_default import IntermediatesRecords
|
|
from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache
|
|
from invokeai.app.services.invocation_services import InvocationServices
|
|
from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService
|
|
from invokeai.app.services.invoker import Invoker
|
|
from invokeai.app.services.project_records.project_records_default import ProjectRecordsStorage
|
|
from invokeai.app.services.shared.database.database import Database
|
|
from invokeai.app.services.system_prompt_records.system_prompt_records_default import (
|
|
SystemPromptRecordsStorage,
|
|
)
|
|
from invokeai.app.services.users.users_default import UserService
|
|
from invokeai.app.services.video_records.video_records_default import VideoRecordStorage
|
|
from invokeai.app.services.wildcard_records.wildcard_records_default import WildcardRecordsStorage
|
|
from invokeai.app.services.workflow_records.workflow_records_default import WorkflowRecordsStorage
|
|
from invokeai.backend.util.logging import InvokeAILogger
|
|
from tests.backend.model_manager.model_manager_fixtures import * # noqa: F403
|
|
from tests.fixtures.database import ( # noqa: F401
|
|
_external_application_schema,
|
|
_external_test_schema,
|
|
_migrated_sqlite,
|
|
database,
|
|
empty_database,
|
|
external_test_db_url,
|
|
)
|
|
from tests.fixtures.races import lost_races # noqa: F401
|
|
from tests.fixtures.sqlite_database import create_mock_sqlite_database # noqa: F401
|
|
from tests.test_nodes import TestEventService
|
|
|
|
# Fixtures that put a test on the backend under test, which `-m uses_database` selects.
|
|
_DATABASE_FIXTURES = {"empty_database", "database"}
|
|
|
|
|
|
@pytest.hookimpl(tryfirst=True)
|
|
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
|
|
# tryfirst: the marks must exist before `-m` deselects by them.
|
|
external = external_test_db_url() is not None
|
|
for item in items:
|
|
if _DATABASE_FIXTURES & set(getattr(item, "fixturenames", ())):
|
|
item.add_marker(pytest.mark.uses_database)
|
|
if external and item.get_closest_marker("sqlite_only") is not None:
|
|
item.add_marker(pytest.mark.skip(reason="needs SQLite, but INVOKEAI_TEST_DB_URL names another backend"))
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_deferred_empty_cache():
|
|
"""`TorchDevice._empty_cache_deferred` is process-global: a test that exercises a peer-aware
|
|
skip must not make a later test perform a real (GPU-initializing) empty_cache."""
|
|
from invokeai.backend.util.devices import TorchDevice
|
|
|
|
TorchDevice._empty_cache_deferred.clear()
|
|
yield
|
|
TorchDevice._empty_cache_deferred.clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_sqlite_database() -> Database:
|
|
"""The in-memory database behind `mock_services`, for tests that need SQL of their own or a service that
|
|
`mock_services` leaves out (the session queue)."""
|
|
return create_mock_sqlite_database(InvokeAIAppConfig(use_memory_db=True), InvokeAILogger.get_logger())
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_services(mock_sqlite_database: Database) -> InvocationServices:
|
|
# Image indexing is on by default, but `model_manager` below is None: starting
|
|
# the indexer against these stub services would fail while resolving the
|
|
# embedding model. Tests that exercise the index enable it themselves.
|
|
configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0, image_index_enabled=False)
|
|
logger = InvokeAILogger.get_logger()
|
|
db = mock_sqlite_database
|
|
|
|
# NOTE: none of these are actually called by the test invocations
|
|
return InvocationServices(
|
|
board_image_records=BoardImageRecordStorage(db),
|
|
board_images=None, # type: ignore
|
|
board_records=BoardRecordStorage(db),
|
|
boards=BoardService(),
|
|
bulk_download=BulkDownloadService(),
|
|
configuration=configuration,
|
|
database=db,
|
|
events=TestEventService(),
|
|
image_files=None, # type: ignore
|
|
image_records=ImageRecordStorage(db),
|
|
images=ImageService(),
|
|
invocation_cache=MemoryInvocationCache(max_cache_size=0),
|
|
logger=logging, # type: ignore
|
|
model_images=None, # type: ignore
|
|
model_manager=None, # type: ignore
|
|
download_queue=None, # type: ignore
|
|
external_generation=ExternalGenerationService({}, logger),
|
|
names=None, # type: ignore
|
|
performance_statistics=InvocationStatsService(),
|
|
session_processor=None, # type: ignore
|
|
session_queue=None, # type: ignore
|
|
urls=None, # type: ignore
|
|
workflow_records=WorkflowRecordsStorage(db),
|
|
tensors=None, # type: ignore
|
|
conditioning=None, # type: ignore
|
|
style_preset_records=None, # type: ignore
|
|
style_preset_image_files=None, # type: ignore
|
|
system_prompt_records=SystemPromptRecordsStorage(db),
|
|
workflow_thumbnails=None, # type: ignore
|
|
model_relationship_records=None, # type: ignore
|
|
model_relationships=None, # type: ignore
|
|
client_state_persistence=ClientStatePersistence(db),
|
|
project_records=ProjectRecordsStorage(db),
|
|
users=UserService(db),
|
|
wildcard_records=WildcardRecordsStorage(db),
|
|
videos=None, # type: ignore
|
|
video_files=None, # type: ignore
|
|
video_records=VideoRecordStorage(db),
|
|
board_video_records=BoardVideoRecordStorage(db),
|
|
# Real SQLite-backed gallery service: the virtual-boards router reads dates and
|
|
# per-date item names through it, and MagicMock cannot exercise the filter SQL.
|
|
gallery=GalleryService(db),
|
|
image_index_records=ImageIndexRecords(db),
|
|
image_index=ImageIndexService(),
|
|
intermediates=IntermediatesService(records=IntermediatesRecords(db), logger=logger),
|
|
)
|
|
|
|
|
|
@pytest.fixture()
|
|
def mock_invoker(mock_services: InvocationServices) -> Invoker:
|
|
return Invoker(services=mock_services)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def invokeai_root_dir(tmp_path_factory) -> Path:
|
|
root_template = Path(__file__).parent.resolve() / "backend/model_manager/data/invokeai_root"
|
|
temp_dir: Path = tmp_path_factory.mktemp("data") / "invokeai_root"
|
|
shutil.copytree(root_template, temp_dir)
|
|
return temp_dir
|
|
|
|
|
|
# --- Peak memory reporting -------------------------------------------------------------------
|
|
#
|
|
# Running the suite across xdist workers multiplies its memory footprint, and the failure mode is
|
|
# silent: the runner is killed mid-run, so there is no summary, no failing test and no clue which
|
|
# file was responsible. Reporting each worker's peak turns a future blow-up into a number that
|
|
# moves in the CI log before it takes a runner down. `--max-worker-restart=0` in the workflow
|
|
# makes the death itself fail the run immediately rather than after sixteen restarts.
|
|
|
|
_worker_peak_rss: dict[str, int] = {}
|
|
|
|
|
|
def _peak_rss_bytes() -> int:
|
|
if sys.platform == "win32":
|
|
import psutil
|
|
|
|
return psutil.Process().memory_info().peak_wset
|
|
import resource
|
|
|
|
peak = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
|
# ru_maxrss is bytes on macOS and kilobytes on Linux.
|
|
return peak if sys.platform == "darwin" else peak * 1024
|
|
|
|
|
|
def pytest_sessionfinish(session: pytest.Session) -> None:
|
|
"""Hands this worker's peak to the controller; `workeroutput` exists only in a worker."""
|
|
output = getattr(session.config, "workeroutput", None)
|
|
if output is not None:
|
|
output["peak_rss_bytes"] = _peak_rss_bytes()
|
|
|
|
|
|
def pytest_testnodedown(node, error) -> None: # noqa: ANN001 # xdist types are not exported
|
|
peak = getattr(node, "workeroutput", {}).get("peak_rss_bytes")
|
|
if peak is not None:
|
|
_worker_peak_rss[node.gateway.id] = peak
|
|
|
|
|
|
def pytest_terminal_summary(terminalreporter) -> None: # noqa: ANN001
|
|
if not _worker_peak_rss:
|
|
return
|
|
peaks = _worker_peak_rss.values()
|
|
terminalreporter.write_line(
|
|
f"peak RSS: {max(peaks) / 2**30:.2f}GB worst worker, "
|
|
f"{sum(peaks) / 2**30:.2f}GB summed over {len(_worker_peak_rss)} workers"
|
|
)
|