This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
"""
|
||||
A test script to test the gRPC service and dynamic loader
|
||||
"""
|
||||
import unittest
|
||||
import subprocess
|
||||
import time
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# Import dynamic loader for testing (these don't need gRPC)
|
||||
import diffusers_dynamic_loader as loader
|
||||
from diffusers import DiffusionPipeline, StableDiffusionPipeline
|
||||
|
||||
# Try to import gRPC modules - may not be available during unit testing
|
||||
try:
|
||||
import grpc
|
||||
import backend_pb2
|
||||
import backend_pb2_grpc
|
||||
GRPC_AVAILABLE = True
|
||||
except ImportError:
|
||||
GRPC_AVAILABLE = False
|
||||
|
||||
|
||||
@unittest.skipUnless(GRPC_AVAILABLE, "gRPC modules not available")
|
||||
class TestBackendServicer(unittest.TestCase):
|
||||
"""
|
||||
TestBackendServicer is the class that tests the gRPC service
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
This method sets up the gRPC service by starting the server
|
||||
"""
|
||||
self.service = subprocess.Popen(["python3", "backend.py", "--addr", "localhost:50051"])
|
||||
|
||||
def tearDown(self) -> None:
|
||||
"""
|
||||
This method tears down the gRPC service by terminating the server
|
||||
"""
|
||||
self.service.kill()
|
||||
self.service.wait()
|
||||
|
||||
def test_server_startup(self):
|
||||
"""
|
||||
This method tests if the server starts up successfully
|
||||
"""
|
||||
time.sleep(20)
|
||||
try:
|
||||
self.setUp()
|
||||
with grpc.insecure_channel("localhost:50051") as channel:
|
||||
stub = backend_pb2_grpc.BackendStub(channel)
|
||||
response = stub.Health(backend_pb2.HealthMessage())
|
||||
self.assertEqual(response.message, b'OK')
|
||||
except Exception as err:
|
||||
print(err)
|
||||
self.fail("Server failed to start")
|
||||
finally:
|
||||
self.tearDown()
|
||||
|
||||
def test_load_model(self):
|
||||
"""
|
||||
This method tests if the model is loaded successfully
|
||||
"""
|
||||
time.sleep(20)
|
||||
try:
|
||||
self.setUp()
|
||||
with grpc.insecure_channel("localhost:50051") as channel:
|
||||
stub = backend_pb2_grpc.BackendStub(channel)
|
||||
response = stub.LoadModel(backend_pb2.ModelOptions(Model="Lykon/dreamshaper-8"))
|
||||
self.assertTrue(response.success)
|
||||
self.assertEqual(response.message, "Model loaded successfully")
|
||||
except Exception as err:
|
||||
print(err)
|
||||
self.fail("LoadModel service failed")
|
||||
finally:
|
||||
self.tearDown()
|
||||
|
||||
def test(self):
|
||||
"""
|
||||
This method tests if the backend can generate images
|
||||
"""
|
||||
time.sleep(20)
|
||||
try:
|
||||
self.setUp()
|
||||
with grpc.insecure_channel("localhost:50051") as channel:
|
||||
stub = backend_pb2_grpc.BackendStub(channel)
|
||||
response = stub.LoadModel(backend_pb2.ModelOptions(Model="Lykon/dreamshaper-8"))
|
||||
print(response.message)
|
||||
self.assertTrue(response.success)
|
||||
image_req = backend_pb2.GenerateImageRequest(positive_prompt="cat", width=16,height=16, dst="test.jpg")
|
||||
re = stub.GenerateImage(image_req)
|
||||
self.assertTrue(re.success)
|
||||
except Exception as err:
|
||||
print(err)
|
||||
self.fail("Image gen service failed")
|
||||
finally:
|
||||
self.tearDown()
|
||||
|
||||
|
||||
class TestDiffusersDynamicLoader(unittest.TestCase):
|
||||
"""Test cases for the diffusers dynamic loader functionality."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Set up test fixtures - clear caches to ensure fresh discovery."""
|
||||
# Reset the caches to ensure fresh discovery
|
||||
loader._pipeline_registry = None
|
||||
loader._task_aliases = None
|
||||
|
||||
def test_camel_to_kebab_conversion(self):
|
||||
"""Test CamelCase to kebab-case conversion."""
|
||||
test_cases = [
|
||||
("StableDiffusionPipeline", "stable-diffusion-pipeline"),
|
||||
("StableDiffusionXLPipeline", "stable-diffusion-xl-pipeline"),
|
||||
("FluxPipeline", "flux-pipeline"),
|
||||
("DiffusionPipeline", "diffusion-pipeline"),
|
||||
]
|
||||
for input_val, expected in test_cases:
|
||||
with self.subTest(input=input_val):
|
||||
result = loader._camel_to_kebab(input_val)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_extract_task_keywords(self):
|
||||
"""Test task keyword extraction from class names."""
|
||||
# Test text-to-image detection
|
||||
aliases = loader._extract_task_keywords("StableDiffusionPipeline")
|
||||
self.assertIn("stable-diffusion", aliases)
|
||||
|
||||
# Test img2img detection
|
||||
aliases = loader._extract_task_keywords("StableDiffusionImg2ImgPipeline")
|
||||
self.assertIn("image-to-image", aliases)
|
||||
self.assertIn("img2img", aliases)
|
||||
|
||||
# Test inpainting detection
|
||||
aliases = loader._extract_task_keywords("StableDiffusionInpaintPipeline")
|
||||
self.assertIn("inpainting", aliases)
|
||||
self.assertIn("inpaint", aliases)
|
||||
|
||||
# Test depth2img detection
|
||||
aliases = loader._extract_task_keywords("StableDiffusionDepth2ImgPipeline")
|
||||
self.assertIn("depth-to-image", aliases)
|
||||
|
||||
def test_discover_pipelines_finds_known_classes(self):
|
||||
"""Test that pipeline discovery finds at least one known pipeline class."""
|
||||
registry = loader.get_pipeline_registry()
|
||||
|
||||
# Check that the registry is not empty
|
||||
self.assertGreater(len(registry), 0, "Pipeline registry should not be empty")
|
||||
|
||||
# Check for known pipeline classes
|
||||
known_pipelines = [
|
||||
"StableDiffusionPipeline",
|
||||
"DiffusionPipeline",
|
||||
]
|
||||
|
||||
for pipeline_name in known_pipelines:
|
||||
with self.subTest(pipeline=pipeline_name):
|
||||
self.assertIn(
|
||||
pipeline_name,
|
||||
registry,
|
||||
f"Expected to find {pipeline_name} in registry"
|
||||
)
|
||||
|
||||
def test_discover_pipelines_caches_results(self):
|
||||
"""Test that pipeline discovery results are cached."""
|
||||
# Get registry twice
|
||||
registry1 = loader.get_pipeline_registry()
|
||||
registry2 = loader.get_pipeline_registry()
|
||||
|
||||
# Should be the same object (cached)
|
||||
self.assertIs(registry1, registry2, "Registry should be cached")
|
||||
|
||||
def test_get_available_pipelines(self):
|
||||
"""Test getting list of available pipelines."""
|
||||
available = loader.get_available_pipelines()
|
||||
|
||||
# Should return a list
|
||||
self.assertIsInstance(available, list)
|
||||
|
||||
# Should contain known pipelines
|
||||
self.assertIn("StableDiffusionPipeline", available)
|
||||
self.assertIn("DiffusionPipeline", available)
|
||||
|
||||
# Should be sorted
|
||||
self.assertEqual(available, sorted(available))
|
||||
|
||||
def test_get_available_tasks(self):
|
||||
"""Test getting list of available task aliases."""
|
||||
tasks = loader.get_available_tasks()
|
||||
|
||||
# Should return a list
|
||||
self.assertIsInstance(tasks, list)
|
||||
|
||||
# Should be sorted
|
||||
self.assertEqual(tasks, sorted(tasks))
|
||||
|
||||
def test_resolve_pipeline_class_by_name(self):
|
||||
"""Test resolving pipeline class by exact name."""
|
||||
cls = loader.resolve_pipeline_class(class_name="StableDiffusionPipeline")
|
||||
self.assertEqual(cls, StableDiffusionPipeline)
|
||||
|
||||
def test_resolve_pipeline_class_by_name_case_insensitive(self):
|
||||
"""Test that class name resolution is case-insensitive."""
|
||||
cls1 = loader.resolve_pipeline_class(class_name="StableDiffusionPipeline")
|
||||
cls2 = loader.resolve_pipeline_class(class_name="stablediffusionpipeline")
|
||||
self.assertEqual(cls1, cls2)
|
||||
|
||||
def test_resolve_pipeline_class_by_task(self):
|
||||
"""Test resolving pipeline class by task alias."""
|
||||
# Get the registry to find available tasks
|
||||
aliases = loader.get_task_aliases()
|
||||
|
||||
# Test with a common task that should be available
|
||||
if "stable-diffusion" in aliases:
|
||||
cls = loader.resolve_pipeline_class(task="stable-diffusion")
|
||||
self.assertIsNotNone(cls)
|
||||
|
||||
def test_resolve_pipeline_class_unknown_name_raises(self):
|
||||
"""Test that resolving unknown class name raises ValueError with helpful message."""
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
loader.resolve_pipeline_class(class_name="NonExistentPipeline")
|
||||
|
||||
# Check that error message includes available pipelines
|
||||
error_msg = str(ctx.exception)
|
||||
self.assertIn("Unknown pipeline class", error_msg)
|
||||
self.assertIn("Available pipelines", error_msg)
|
||||
|
||||
def test_resolve_pipeline_class_unknown_task_raises(self):
|
||||
"""Test that resolving unknown task raises ValueError with helpful message."""
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
loader.resolve_pipeline_class(task="nonexistent-task-xyz")
|
||||
|
||||
# Check that error message includes available tasks
|
||||
error_msg = str(ctx.exception)
|
||||
self.assertIn("Unknown task", error_msg)
|
||||
self.assertIn("Available tasks", error_msg)
|
||||
|
||||
def test_resolve_pipeline_class_no_params_raises(self):
|
||||
"""Test that calling with no parameters raises helpful ValueError."""
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
loader.resolve_pipeline_class()
|
||||
|
||||
error_msg = str(ctx.exception)
|
||||
self.assertIn("Must provide at least one of", error_msg)
|
||||
|
||||
def test_get_pipeline_info(self):
|
||||
"""Test getting pipeline information."""
|
||||
info = loader.get_pipeline_info("StableDiffusionPipeline")
|
||||
|
||||
self.assertEqual(info['name'], "StableDiffusionPipeline")
|
||||
self.assertIsInstance(info['aliases'], list)
|
||||
self.assertIsInstance(info['supports_single_file'], bool)
|
||||
|
||||
def test_get_pipeline_info_unknown_raises(self):
|
||||
"""Test that getting info for unknown pipeline raises ValueError."""
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
loader.get_pipeline_info("NonExistentPipeline")
|
||||
|
||||
self.assertIn("Unknown pipeline", str(ctx.exception))
|
||||
|
||||
def test_discover_diffusers_classes_pipelines(self):
|
||||
"""Test generic class discovery for DiffusionPipeline."""
|
||||
classes = loader.discover_diffusers_classes("DiffusionPipeline")
|
||||
|
||||
# Should return a dict
|
||||
self.assertIsInstance(classes, dict)
|
||||
|
||||
# Should contain known pipeline classes
|
||||
self.assertIn("DiffusionPipeline", classes)
|
||||
self.assertIn("StableDiffusionPipeline", classes)
|
||||
|
||||
def test_discover_diffusers_classes_caches_results(self):
|
||||
"""Test that class discovery results are cached."""
|
||||
classes1 = loader.discover_diffusers_classes("DiffusionPipeline")
|
||||
classes2 = loader.discover_diffusers_classes("DiffusionPipeline")
|
||||
|
||||
# Should be the same object (cached)
|
||||
self.assertIs(classes1, classes2)
|
||||
|
||||
def test_discover_diffusers_classes_exclude_base(self):
|
||||
"""Test discovering classes without base class."""
|
||||
classes = loader.discover_diffusers_classes("DiffusionPipeline", include_base=False)
|
||||
|
||||
# Should still contain subclasses
|
||||
self.assertIn("StableDiffusionPipeline", classes)
|
||||
|
||||
def test_get_available_classes(self):
|
||||
"""Test getting list of available classes for a base class."""
|
||||
classes = loader.get_available_classes("DiffusionPipeline")
|
||||
|
||||
# Should return a sorted list
|
||||
self.assertIsInstance(classes, list)
|
||||
self.assertEqual(classes, sorted(classes))
|
||||
|
||||
# Should contain known classes
|
||||
self.assertIn("StableDiffusionPipeline", classes)
|
||||
|
||||
|
||||
class TestDiffusersDynamicLoaderWithMocks(unittest.TestCase):
|
||||
"""Test cases using mocks to test edge cases."""
|
||||
|
||||
def test_load_pipeline_requires_model_id(self):
|
||||
"""Test that load_diffusers_pipeline requires model_id."""
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
loader.load_diffusers_pipeline(class_name="StableDiffusionPipeline")
|
||||
|
||||
self.assertIn("model_id is required", str(ctx.exception))
|
||||
|
||||
def test_resolve_with_model_id_uses_diffusion_pipeline_fallback(self):
|
||||
"""Test that resolving with only model_id falls back to DiffusionPipeline."""
|
||||
# When model_id is provided, if hub lookup is not successful,
|
||||
# should fall back to DiffusionPipeline.
|
||||
# This tests the fallback behavior - the actual hub lookup may succeed
|
||||
# or fail depending on network, but the fallback path should work.
|
||||
cls = loader.resolve_pipeline_class(model_id="some/nonexistent/model")
|
||||
self.assertEqual(cls, DiffusionPipeline)
|
||||
|
||||
|
||||
@unittest.skipUnless(GRPC_AVAILABLE, "gRPC modules not available")
|
||||
class TestGenerateImageOptionsKwargsMerge(unittest.TestCase):
|
||||
"""Test that GenerateImage merges the options dict into pipeline kwargs.
|
||||
|
||||
The options dict holds image (PIL), negative_prompt, and
|
||||
num_inference_steps. Without the merge, img2img pipelines never
|
||||
receive the source image and fail with 'Input is in incorrect format'.
|
||||
"""
|
||||
|
||||
def test_options_merged_into_pipeline_kwargs(self):
|
||||
from backend import BackendServicer
|
||||
from PIL import Image
|
||||
import tempfile, os
|
||||
|
||||
svc = BackendServicer.__new__(BackendServicer)
|
||||
# Minimal attributes the method reads
|
||||
svc.pipe = MagicMock()
|
||||
svc.pipe.return_value.images = [Image.new("RGB", (4, 4))]
|
||||
svc.cfg_scale = 7.5
|
||||
svc.controlnet = None
|
||||
svc.img2vid = False
|
||||
svc.txt2vid = False
|
||||
svc.clip_skip = 0
|
||||
svc.PipelineType = "StableDiffusionImg2ImgPipeline"
|
||||
svc.options = {}
|
||||
|
||||
# Create a tiny source image for the request's src field
|
||||
src_file = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
|
||||
Image.new("RGB", (4, 4), color="red").save(src_file, format="PNG")
|
||||
src_file.close()
|
||||
|
||||
dst_file = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
|
||||
dst_file.close()
|
||||
|
||||
try:
|
||||
request = MagicMock()
|
||||
request.positive_prompt = "a test prompt"
|
||||
request.negative_prompt = "bad quality"
|
||||
request.step = 10
|
||||
request.seed = 0
|
||||
request.width = 0
|
||||
request.height = 0
|
||||
request.src = src_file.name
|
||||
request.ref_images = []
|
||||
request.dst = dst_file.name
|
||||
|
||||
svc.GenerateImage(request, context=None)
|
||||
|
||||
# The pipeline must have been called with the image kwarg
|
||||
svc.pipe.assert_called_once()
|
||||
_, call_kwargs = svc.pipe.call_args
|
||||
self.assertIn("image", call_kwargs,
|
||||
"source image must be passed to pipeline via kwargs")
|
||||
self.assertIn("negative_prompt", call_kwargs,
|
||||
"negative_prompt must be passed to pipeline via kwargs")
|
||||
self.assertEqual(call_kwargs["num_inference_steps"], 10)
|
||||
finally:
|
||||
os.unlink(src_file.name)
|
||||
os.unlink(dst_file.name)
|
||||
Reference in New Issue
Block a user