"""
Tests for the ModuleWrapper with Qdrant Integration.
This module tests the functionality of the module_wrapper.py module,
which provides semantic search capabilities for Python modules using Qdrant.
"""
import logging
import os
import sys
from datetime import datetime
import pytest
import requests
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Load environment variables from .env file if available
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
logger.warning("dotenv not available, skipping .env file loading")
# Test email address from environment variable
TEST_EMAIL = os.getenv("TEST_EMAIL_ADDRESS", "test_user@example.com")
# Test webhook URLs for Google Chat
TEST_CHAT_WEBHOOK = os.getenv("TEST_CHAT_WEBHOOK", "")
TEST_WEBHOOK_URL = os.getenv(
"TEST_CHAT_WEBHOOK_URL",
TEST_CHAT_WEBHOOK
or "https://chat.googleapis.com/v1/spaces/AAAAAAAAAAA/messages?key=test&token=test",
)
# Add the parent directory to sys.path to import the module_wrapper
sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../adapters"))
)
# Import the ModuleWrapper
from module_wrapper import ModuleWrapper, _get_qdrant_imports
# Test modules - we'll use standard library modules that are always available
TEST_MODULES = ["json", "datetime", "collections"]
# Test if Qdrant is available
def is_qdrant_available():
"""Check if Qdrant server is running and available."""
try:
QdrantClient, _ = _get_qdrant_imports()
client = QdrantClient(host="localhost", port=6333)
# Try a simple operation
client.get_collections()
return True
except Exception as e:
logger.warning(f"Qdrant not available: {e}")
return False
# Skip all tests if Qdrant is not available
pytestmark = pytest.mark.skipif(
not is_qdrant_available(), reason="Qdrant server is not running or not available"
)
class TestModuleWrapper:
"""Test the ModuleWrapper functionality."""
@pytest.fixture(params=TEST_MODULES)
def module_name(self, request):
"""Fixture to provide test module names."""
return request.param
@pytest.fixture
def wrapper(self, module_name):
"""Fixture to create a ModuleWrapper instance."""
try:
# Create a unique collection name for each test run to avoid conflicts
collection_name = f"test_{module_name}_{os.getpid()}"
# Create the wrapper
wrapper = ModuleWrapper(
module_or_name=module_name,
collection_name=collection_name,
index_nested=True,
index_private=False,
)
yield wrapper
# Cleanup: delete the collection after the test
try:
wrapper.client.delete_collection(collection_name=collection_name)
logger.info(f"Deleted test collection: {collection_name}")
except Exception as e:
logger.warning(f"Failed to delete collection {collection_name}: {e}")
except Exception as e:
pytest.skip(f"Failed to create ModuleWrapper: {e}")
def test_initialization(self, wrapper, module_name):
"""Test that the ModuleWrapper initializes correctly."""
assert wrapper is not None
assert wrapper.module_name == module_name
assert wrapper._initialized is True
assert wrapper.client is not None
assert wrapper.embedder is not None
assert len(wrapper.components) > 0
logger.info(
f"ModuleWrapper initialized with {len(wrapper.components)} components"
)
def test_list_components(self, wrapper):
"""Test listing components."""
# List all components
all_components = wrapper.list_components()
assert len(all_components) > 0
# List components by type
functions = wrapper.list_components("function")
classes = wrapper.list_components("class")
logger.info(f"Found {len(functions)} functions and {len(classes)} classes")
# At least one of these should have components
assert len(functions) > 0 or len(classes) > 0
def test_get_component_info(self, wrapper):
"""Test getting component info."""
# Get a component path
components = wrapper.list_components()
assert len(components) > 0
# Get info for the first component
component_path = components[0]
info = wrapper.get_component_info(component_path)
assert info is not None
assert "name" in info
assert "type" in info
assert "module_path" in info
assert "full_path" in info
logger.info(f"Got info for component: {info['name']} ({info['type']})")
def test_get_component_by_path(self, wrapper):
"""Test getting a component by path."""
# Get a component path
components = wrapper.list_components()
assert len(components) > 0
# Get the component
component_path = components[0]
component = wrapper.get_component_by_path(component_path)
assert component is not None
logger.info(f"Retrieved component by path: {component_path}")
def test_search(self, wrapper, module_name):
"""Test searching for components."""
# Define search queries based on the module
queries = {
"json": "parse json string",
"datetime": "get current date and time",
"collections": "ordered dictionary",
}
# Use the appropriate query for the module
query = queries.get(module_name, "common functionality")
# Search for components with a lower score threshold
results = wrapper.search(query, limit=5, score_threshold=0.5)
assert len(results) > 0
assert "score" in results[0]
assert "name" in results[0]
assert "path" in results[0]
assert "component" in results[0]
logger.info(f"Search for '{query}' returned {len(results)} results")
logger.info(
f"Top result: {results[0]['name']} (score: {results[0]['score']:.4f})"
)
@pytest.mark.asyncio
async def test_search_async(self, wrapper, module_name):
"""Test asynchronous searching for components."""
# Define search queries based on the module
queries = {
"json": "parse json string",
"datetime": "get current date and time",
"collections": "ordered dictionary",
}
# Use the appropriate query for the module
query = queries.get(module_name, "common functionality")
# Search for components asynchronously with a lower score threshold
results = await wrapper.search_async(query, limit=5, score_threshold=0.5)
assert len(results) > 0
assert "score" in results[0]
assert "name" in results[0]
assert "path" in results[0]
assert "component" in results[0]
logger.info(f"Async search for '{query}' returned {len(results)} results")
logger.info(
f"Top result: {results[0]['name']} (score: {results[0]['score']:.4f})"
)
class TestCardFrameworkWrapper:
"""Test the ModuleWrapper specifically with the card_framework module."""
@pytest.fixture
def card_framework_wrapper(self):
"""Fixture to create a ModuleWrapper for card_framework."""
try:
# Try to import card_framework
try:
import card_framework
card_framework_available = True
except ImportError:
card_framework_available = False
pytest.skip("card_framework module not available")
# Create a unique collection name
collection_name = f"test_card_framework_{os.getpid()}"
# Create the wrapper
wrapper = ModuleWrapper(
module_or_name=card_framework,
collection_name=collection_name,
index_nested=True,
index_private=False,
)
yield wrapper
# Cleanup: delete the collection after the test
try:
wrapper.client.delete_collection(collection_name=collection_name)
logger.info(f"Deleted test collection: {collection_name}")
except Exception as e:
logger.warning(f"Failed to delete collection {collection_name}: {e}")
except Exception as e:
pytest.skip(f"Failed to create ModuleWrapper for card_framework: {e}")
def test_card_framework_initialization(self, card_framework_wrapper):
"""Test that the card_framework ModuleWrapper initializes correctly."""
assert card_framework_wrapper is not None
assert card_framework_wrapper.module_name == "card_framework"
assert card_framework_wrapper._initialized is True
assert len(card_framework_wrapper.components) > 0
logger.info(
f"card_framework ModuleWrapper initialized with {len(card_framework_wrapper.components)} components"
)
# List components by type
classes = card_framework_wrapper.list_components("class")
logger.info(f"Found {len(classes)} classes in card_framework")
# Log the first few classes to help with debugging
for i, class_path in enumerate(classes[:5]):
info = card_framework_wrapper.get_component_info(class_path)
logger.info(f"Class {i+1}: {info['name']} ({info['full_path']})")
def test_card_framework_search(self, card_framework_wrapper):
"""Test searching for card components."""
# Define search queries for different card types
queries = [
"simple card",
"interactive card with buttons",
"card with image",
"form card with input fields",
]
# Search for each query
for query in queries:
results = card_framework_wrapper.search(query, limit=3)
logger.info(f"Search for '{query}' returned {len(results)} results")
if len(results) > 0:
logger.info(
f"Top result: {results[0]['name']} (score: {results[0]['score']:.4f})"
)
logger.info(f"Path: {results[0]['path']}")
if results[0]["docstring"]:
logger.info(f"Docstring: {results[0]['docstring'][:100]}...")
else:
logger.warning(f"No results found for '{query}'")
def test_card_component_availability(self, card_framework_wrapper):
"""Test the availability of key card components."""
# Key components that should be available
key_components = ["Card", "Section", "Button", "TextInput", "Image"]
# Check each component
for component_name in key_components:
# Search for the component
results = card_framework_wrapper.search(component_name, limit=5)
# Check if any result matches the component name
found = False
for result in results:
if component_name.lower() in result["name"].lower():
found = True
logger.info(
f"Found component {component_name}: {result['path']} (score: {result['score']:.4f})"
)
break
if not found:
logger.warning(
f"Component {component_name} not found in search results"
)
# We don't assert here because we want to check all components,
# but we log warnings for missing ones
def test_create_and_send_card(self, card_framework_wrapper):
"""Test creating a card using ModuleWrapper and sending it to a webhook."""
# Skip if no webhook URL is available
if not TEST_WEBHOOK_URL:
pytest.skip("No webhook URL available for testing card sending")
try:
# Create a simple card directly using Google Chat API format
# This doesn't rely on card_framework or ModuleWrapper
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Create a simple card in Google Chat API format
card = {
"cardId": f"test_card_{int(datetime.now().timestamp())}",
"card": {
"header": {
"title": "Test Card from ModuleWrapper Test",
"subtitle": f"Created at {timestamp}",
"imageUrl": "https://picsum.photos/200/100",
"imageType": "CIRCLE",
},
"sections": [
{
"header": "Test Section",
"widgets": [
{
"textParagraph": {
"text": f"This card was created directly using the Google Chat API format at {timestamp}"
}
},
{
"buttonList": {
"buttons": [
{
"text": "Visit Google",
"onClick": {
"openLink": {
"url": "https://www.google.com"
}
},
}
]
}
},
],
}
],
},
}
# Create message payload
message_body = {
"text": "Test message from ModuleWrapper test",
"cardsV2": [card],
}
# Send via webhook
logger.info(f"Sending card to webhook URL: {TEST_WEBHOOK_URL}")
response = requests.post(
TEST_WEBHOOK_URL,
json=message_body,
headers={"Content-Type": "application/json"},
)
# Check response
assert (
response.status_code == 200
), f"Failed to send card: {response.status_code} - {response.text}"
logger.info(f"Card sent successfully! Status: {response.status_code}")
# Now try to find card components in the card_framework module
logger.info("Checking for card components in card_framework module...")
components = card_framework_wrapper.list_components()
logger.info(f"Found {len(components)} components in card_framework module")
# Log the first few components to help with debugging
for i, component_path in enumerate(components[:10]):
info = card_framework_wrapper.get_component_info(component_path)
logger.info(f"Component {i+1}: {info['name']} ({info['full_path']})")
except Exception as e:
logger.error(f"Error creating or sending card: {e}", exc_info=True)
pytest.skip(f"Error creating or sending card: {str(e)}")
def _create_card_from_component(self, component, params):
"""Create a card using a component."""
import inspect
if not component:
return None
try:
# Check if component is callable
if callable(component):
# Get signature
sig = inspect.signature(component)
# Filter params to match signature
valid_params = {}
for param_name, param in sig.parameters.items():
if param_name in params:
valid_params[param_name] = params[param_name]
# Call component with filtered params
return component(**valid_params)
# If component is a class, try to instantiate it
elif inspect.isclass(component):
# Get signature
sig = inspect.signature(component.__init__)
# Filter params to match signature
valid_params = {}
for param_name, param in sig.parameters.items():
if param_name in params and param_name != "self":
valid_params[param_name] = params[param_name]
# Instantiate class with filtered params
return component(**valid_params)
return None
except Exception as e:
logger.error(f"Failed to create card from component: {e}", exc_info=True)
return None
def _convert_card_to_google_format(self, card):
"""Convert Card Framework card to Google Chat format."""
try:
# Handle different card types
if hasattr(card, "to_dict"):
# Card Framework v2 object
card_dict = card.to_dict()
elif hasattr(card, "__dict__"):
# Object with __dict__
card_dict = card.__dict__
elif isinstance(card, dict):
# Already a dictionary
card_dict = card
else:
# Unknown type
logger.warning(f"Unknown card type: {type(card)}")
return {"error": f"Unknown card type: {type(card)}"}
# Ensure proper structure for Google Chat API
card_id = getattr(card, "card_id", None) or f"card_{hash(str(card_dict))}"
result = {"cardId": card_id, "card": card_dict}
return result
except Exception as e:
logger.error(f"Failed to convert card to Google format: {e}", exc_info=True)
return {"error": f"Failed to convert card: {str(e)}"}
if __name__ == "__main__":
# This allows running the tests directly with python
pytest.main(["-xvs", __file__])