tests: Enhance component testing and error handling with dynamic retrieval (#4526)
* Add module parameter to build_component_instance_for_tests function for dynamic component retrieval * Enhance component test base with detailed version mapping and error handling - Introduced `VersionComponentMapping` TypedDict for structured version mapping. - Updated `FILE_NAMES_MAPPING` to use a list of `VersionComponentMapping`. - Added comprehensive error messages for missing or invalid mappings in `test_all_versions_have_a_file_name_defined`. - Improved `test_component_versions` with detailed exception handling and error reporting. - Ensured `component_class` is defined before running tests. * Refactor FILE_NAMES_MAPPING to use a list of dictionaries for better structure and readability in test_prompt_component.py * refactor: Enhance ComponentTestBase with fixture validation and improved version handling * Refactor test setup in `test_prompt_component.py` to use fixtures for improved modularity and readability * fix: Add PlaceholderGraph NamedTuple and handle 'graph' attribute in Component class * Add attribute checks for 'graph' and 'vertex' to prevent errors * Handle missing 'graph' attribute in 'store_message' method to prevent errors. * Handle missing 'graph' attribute in Message creation to prevent errors * Handle missing 'graph' attribute in chat message flow ID assignment * Add component code to test instance creation and error logging * Update SUPPORTED_VERSIONS to remove older versions * test: add unit tests for ChatInput and TextInputComponent Implement comprehensive tests for both ChatInput and TextInputComponent to ensure proper functionality, including message responses and handling of various input scenarios. This enhances reliability and aids in future development. * test: add unit tests for ChatOutput and TextOutputComponent Implement comprehensive tests for ChatOutput and TextOutputComponent, validating message responses, source properties, and behavior with various input types to ensure reliability and consistency across output components. * Update JSON files to improve code readability and add missing info fields - Added missing `info` fields to various input components to provide better context and descriptions. - Improved code readability by ensuring consistent formatting and structure across JSON files. - Updated `message_response` method to handle cases where `graph` attribute might not be present. - Enhanced `build_vectorize_options` method to set `authentication` and `parameters` to `None` if no values are provided. - Refined `AgentComponent` to include `info` for `agent_llm` and other fields, improving clarity on their purpose. * Refactor: update attribute access to use private `_vertex` attribute * test: enhance TextInputComponent tests and update properties assertions * Remove redundant unit tests for output components in test_output_components.py * feat: add PlaceholderGraph for backwards compatibility and enhance Component attributes * fix: improve run_id assignment and ensure user_id is a string in PlaceholderGraph * Add check for non-empty incoming_edges in get_properties_from_source_component
This commit is contained in:
parent
ae966ab588
commit
0dc6cce8dc
24 changed files with 407 additions and 56 deletions
|
|
@ -1,39 +1,146 @@
|
|||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from tests.constants import SUPPORTED_VERSIONS
|
||||
from tests.integration.utils import build_component_instance_for_tests
|
||||
|
||||
|
||||
class VersionComponentMapping(TypedDict):
|
||||
version: str
|
||||
module: str
|
||||
file_name: str
|
||||
|
||||
|
||||
# Sentinel value to mark undefined test cases
|
||||
DID_NOT_EXIST = object()
|
||||
|
||||
|
||||
class ComponentTestBase:
|
||||
component_class = None
|
||||
DEFAULT_KWARGS: dict[str, Any] = {}
|
||||
FILE_NAMES_MAPPING: dict[str, object | str] = {}
|
||||
@pytest.fixture(autouse=True)
|
||||
def _validate_required_fixtures(
|
||||
self,
|
||||
component_class: type[Any],
|
||||
default_kwargs: dict[str, Any],
|
||||
file_names_mapping: list[VersionComponentMapping],
|
||||
) -> None:
|
||||
"""Validate that all required fixtures are implemented."""
|
||||
# If we get here, all fixtures exist
|
||||
|
||||
@pytest.fixture
|
||||
def component_class(self) -> type[Any]:
|
||||
"""Return the component class to test."""
|
||||
msg = f"{self.__class__.__name__} must implement the component_class fixture"
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
@pytest.fixture
|
||||
def default_kwargs(self) -> dict[str, Any]:
|
||||
"""Return the default kwargs for the component."""
|
||||
return {}
|
||||
|
||||
@pytest.fixture
|
||||
def file_names_mapping(self) -> list[VersionComponentMapping]:
|
||||
"""Return the file names mapping for different versions."""
|
||||
msg = f"{self.__class__.__name__} must implement the file_names_mapping fixture"
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
def test_latest_version(self, component_class: type[Any], default_kwargs: dict[str, Any]) -> None:
|
||||
"""Test that the component works with the latest version."""
|
||||
result = component_class(**default_kwargs)()
|
||||
assert result is not None, "Component returned None for the latest version."
|
||||
|
||||
def test_all_versions_have_a_file_name_defined(self, file_names_mapping: list[VersionComponentMapping]) -> None:
|
||||
"""Ensure all supported versions have a file name defined."""
|
||||
if not file_names_mapping:
|
||||
msg = (
|
||||
f"file_names_mapping is empty for {self.__class__.__name__}. "
|
||||
"Please define the version mappings for your component."
|
||||
)
|
||||
raise AssertionError(msg)
|
||||
|
||||
version_mappings = {mapping["version"]: mapping for mapping in file_names_mapping}
|
||||
|
||||
def test_all_versions_have_a_file_name_defined(self):
|
||||
for version in SUPPORTED_VERSIONS:
|
||||
assert version in self.FILE_NAMES_MAPPING
|
||||
assert self.FILE_NAMES_MAPPING[version] is not None
|
||||
if version not in version_mappings:
|
||||
supported_versions = ", ".join(sorted(m["version"] for m in file_names_mapping))
|
||||
msg = (
|
||||
f"Version {version} not found in file_names_mapping for {self.__class__.__name__}.\n"
|
||||
f"Currently defined versions: {supported_versions}\n"
|
||||
"Please add this version to your component's file_names_mapping."
|
||||
)
|
||||
raise AssertionError(msg)
|
||||
|
||||
def test_component_versions(self):
|
||||
mapping = version_mappings[version]
|
||||
if mapping["file_name"] is None:
|
||||
msg = (
|
||||
f"file_name is None for version {version} in {self.__class__.__name__}.\n"
|
||||
"Please provide a valid file_name in file_names_mapping or set it to DID_NOT_EXIST."
|
||||
)
|
||||
raise AssertionError(msg)
|
||||
|
||||
if mapping["module"] is None:
|
||||
msg = (
|
||||
f"module is None for version {version} in {self.__class__.__name__}.\n"
|
||||
"Please provide a valid module name in file_names_mapping or set it to DID_NOT_EXIST."
|
||||
)
|
||||
raise AssertionError(msg)
|
||||
|
||||
@pytest.mark.parametrize("version", SUPPORTED_VERSIONS)
|
||||
def test_component_versions(
|
||||
self,
|
||||
version: str,
|
||||
default_kwargs: dict[str, Any],
|
||||
file_names_mapping: list[VersionComponentMapping],
|
||||
) -> None:
|
||||
"""Test if the component works across different versions."""
|
||||
for version, file_name in self.FILE_NAMES_MAPPING.items():
|
||||
if file_name is DID_NOT_EXIST:
|
||||
continue
|
||||
version_mappings = {mapping["version"]: mapping for mapping in file_names_mapping}
|
||||
|
||||
instance = build_component_instance_for_tests(version, file_name=file_name, **self.DEFAULT_KWARGS)
|
||||
mapping = version_mappings[version]
|
||||
if mapping["file_name"] is DID_NOT_EXIST:
|
||||
pytest.skip(f"Skipping version {version} as it does not have a file name defined.")
|
||||
|
||||
try:
|
||||
instance, component_code = build_component_instance_for_tests(
|
||||
version, file_name=mapping["file_name"], module=mapping["module"], **default_kwargs
|
||||
)
|
||||
except Exception as e:
|
||||
msg = (
|
||||
f"Failed to build component instance for {self.__class__.__name__} "
|
||||
f"version {version}:\n"
|
||||
f"Module: {mapping['module']}\n"
|
||||
f"File: {mapping['file_name']}\n"
|
||||
f"Error: {e!s}"
|
||||
)
|
||||
raise AssertionError(msg) from e
|
||||
|
||||
try:
|
||||
result = instance()
|
||||
assert result is not None, f"{self.component_class.__name__} failed to execute in version {version}"
|
||||
except Exception as e:
|
||||
msg = (
|
||||
f"Failed to execute component {self.__class__.__name__} "
|
||||
f"for version {version}:\n"
|
||||
f"Module: {mapping['module']}\n"
|
||||
f"File: {mapping['file_name']}\n"
|
||||
f"Error: {e!s}\n"
|
||||
f"Component Code: {component_code}"
|
||||
)
|
||||
raise AssertionError(msg) from e
|
||||
|
||||
if result is None:
|
||||
msg = (
|
||||
f"Component {self.__class__.__name__} returned None "
|
||||
f"for version {version}.\n"
|
||||
f"Module: {mapping['module']}\n"
|
||||
f"File: {mapping['file_name']}"
|
||||
)
|
||||
raise AssertionError(msg)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("client")
|
||||
class ComponentTestBaseWithClient:
|
||||
class ComponentTestBaseWithClient(ComponentTestBase):
|
||||
pass
|
||||
|
||||
|
||||
class ComponentTestBaseWithoutClient:
|
||||
class ComponentTestBaseWithoutClient(ComponentTestBase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
SUPPORTED_VERSIONS = ["1.0.15", "1.0.16", "1.0.17", "1.0.18", "1.0.19"]
|
||||
SUPPORTED_VERSIONS = ["1.0.17", "1.0.18", "1.0.19"]
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ async def run_single_component(
|
|||
return graph.get_vertex(component_id).built_object
|
||||
|
||||
|
||||
def build_component_instance_for_tests(version: str, file_name: str, **kwargs):
|
||||
component = download_component_from_github("prompts", file_name, version)
|
||||
def build_component_instance_for_tests(version: str, module: str, file_name: str, **kwargs):
|
||||
component = download_component_from_github(module, file_name, version)
|
||||
cc_class = eval_custom_component_code(component._code)
|
||||
return cc_class(**kwargs)
|
||||
return cc_class(**kwargs), component._code
|
||||
|
|
|
|||
0
src/backend/tests/unit/components/inputs/__init__.py
Normal file
0
src/backend/tests/unit/components/inputs/__init__.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import pytest
|
||||
from langflow.components.inputs import ChatInput, TextInputComponent
|
||||
from langflow.schema.message import Message
|
||||
from langflow.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_USER, MESSAGE_SENDER_USER
|
||||
|
||||
from tests.base import ComponentTestBaseWithClient, ComponentTestBaseWithoutClient
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("client")
|
||||
class TestChatInput(ComponentTestBaseWithClient):
|
||||
@pytest.fixture
|
||||
def component_class(self):
|
||||
return ChatInput
|
||||
|
||||
@pytest.fixture
|
||||
def default_kwargs(self):
|
||||
return {
|
||||
"input_value": "Hello, how are you?",
|
||||
"should_store_message": True,
|
||||
"sender": MESSAGE_SENDER_USER,
|
||||
"sender_name": MESSAGE_SENDER_NAME_USER,
|
||||
"session_id": "test_session_123",
|
||||
"files": [],
|
||||
"background_color": "#f0f0f0",
|
||||
"chat_icon": "👤",
|
||||
"text_color": "#000000",
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def file_names_mapping(self):
|
||||
return [
|
||||
{"version": "1.0.15", "module": "inputs", "file_name": "ChatInput"},
|
||||
{"version": "1.0.16", "module": "inputs", "file_name": "ChatInput"},
|
||||
{"version": "1.0.17", "module": "inputs", "file_name": "ChatInput"},
|
||||
{"version": "1.0.18", "module": "inputs", "file_name": "ChatInput"},
|
||||
{"version": "1.0.19", "module": "inputs", "file_name": "ChatInput"},
|
||||
]
|
||||
|
||||
def test_message_response(self, component_class, default_kwargs):
|
||||
"""Test that the message_response method returns a valid Message object."""
|
||||
component = component_class(**default_kwargs)
|
||||
message = component.message_response()
|
||||
|
||||
assert isinstance(message, Message)
|
||||
assert message.text == default_kwargs["input_value"]
|
||||
assert message.sender == default_kwargs["sender"]
|
||||
assert message.sender_name == default_kwargs["sender_name"]
|
||||
assert message.session_id == default_kwargs["session_id"]
|
||||
assert message.files == default_kwargs["files"]
|
||||
assert message.properties.model_dump() == {
|
||||
"background_color": default_kwargs["background_color"],
|
||||
"text_color": default_kwargs["text_color"],
|
||||
"icon": default_kwargs["chat_icon"],
|
||||
"edited": False,
|
||||
"source": {"id": None, "display_name": None, "source": None},
|
||||
"allow_markdown": False,
|
||||
"state": "complete",
|
||||
"targets": [],
|
||||
}
|
||||
|
||||
def test_message_response_ai_sender(self, component_class):
|
||||
"""Test message response with AI sender type."""
|
||||
kwargs = {
|
||||
"input_value": "I am an AI assistant",
|
||||
"sender": MESSAGE_SENDER_AI,
|
||||
"sender_name": "AI Assistant",
|
||||
"session_id": "test_session_123",
|
||||
}
|
||||
component = component_class(**kwargs)
|
||||
message = component.message_response()
|
||||
|
||||
assert isinstance(message, Message)
|
||||
assert message.sender == MESSAGE_SENDER_AI
|
||||
assert message.sender_name == "AI Assistant"
|
||||
|
||||
def test_message_response_without_session(self, component_class):
|
||||
"""Test message response without session ID."""
|
||||
kwargs = {
|
||||
"input_value": "Test message",
|
||||
"sender": MESSAGE_SENDER_USER,
|
||||
"sender_name": MESSAGE_SENDER_NAME_USER,
|
||||
"session_id": "", # Empty session ID
|
||||
}
|
||||
component = component_class(**kwargs)
|
||||
message = component.message_response()
|
||||
|
||||
assert isinstance(message, Message)
|
||||
assert message.session_id == ""
|
||||
|
||||
def test_message_response_with_files(self, component_class, tmp_path):
|
||||
"""Test message response with file attachments."""
|
||||
# Create a temporary test file
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("Test content")
|
||||
|
||||
kwargs = {
|
||||
"input_value": "Message with file",
|
||||
"sender": MESSAGE_SENDER_USER,
|
||||
"sender_name": MESSAGE_SENDER_NAME_USER,
|
||||
"session_id": "test_session_123",
|
||||
"files": [str(test_file)],
|
||||
}
|
||||
component = component_class(**kwargs)
|
||||
message = component.message_response()
|
||||
|
||||
assert isinstance(message, Message)
|
||||
assert len(message.files) == 1
|
||||
assert message.files[0] == str(test_file)
|
||||
|
||||
def test_message_storage_disabled(self, component_class):
|
||||
"""Test message response when storage is disabled."""
|
||||
kwargs = {
|
||||
"input_value": "Test message",
|
||||
"should_store_message": False,
|
||||
"sender": MESSAGE_SENDER_USER,
|
||||
"sender_name": MESSAGE_SENDER_NAME_USER,
|
||||
"session_id": "test_session_123",
|
||||
}
|
||||
component = component_class(**kwargs)
|
||||
message = component.message_response()
|
||||
|
||||
assert isinstance(message, Message)
|
||||
# The message should still be created but not stored
|
||||
assert message.text == "Test message"
|
||||
|
||||
|
||||
class TestTextInputComponent(ComponentTestBaseWithoutClient):
|
||||
@pytest.fixture
|
||||
def component_class(self):
|
||||
return TextInputComponent
|
||||
|
||||
@pytest.fixture
|
||||
def default_kwargs(self):
|
||||
return {
|
||||
"input_value": "Hello, world!",
|
||||
"data_template": "{text}",
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def file_names_mapping(self):
|
||||
return [
|
||||
{"version": "1.0.15", "module": "inputs", "file_name": "TextInput"},
|
||||
{"version": "1.0.16", "module": "inputs", "file_name": "TextInput"},
|
||||
{"version": "1.0.17", "module": "inputs", "file_name": "TextInput"},
|
||||
{"version": "1.0.18", "module": "inputs", "file_name": "TextInput"},
|
||||
{"version": "1.0.19", "module": "inputs", "file_name": "TextInput"},
|
||||
]
|
||||
0
src/backend/tests/unit/components/outputs/__init__.py
Normal file
0
src/backend/tests/unit/components/outputs/__init__.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import pytest
|
||||
from langflow.components.outputs import ChatOutput, TextOutputComponent
|
||||
from langflow.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_AI
|
||||
|
||||
from tests.base import ComponentTestBaseWithClient, ComponentTestBaseWithoutClient
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("client")
|
||||
class TestChatOutput(ComponentTestBaseWithClient):
|
||||
@pytest.fixture
|
||||
def component_class(self):
|
||||
return ChatOutput
|
||||
|
||||
@pytest.fixture
|
||||
def default_kwargs(self):
|
||||
return {
|
||||
"input_value": "Hello, how are you?",
|
||||
"should_store_message": True,
|
||||
"sender": MESSAGE_SENDER_AI,
|
||||
"sender_name": MESSAGE_SENDER_NAME_AI,
|
||||
"session_id": "test_session_123",
|
||||
"data_template": "{text}",
|
||||
"background_color": "#f0f0f0",
|
||||
"chat_icon": "🤖",
|
||||
"text_color": "#000000",
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def file_names_mapping(self):
|
||||
return [
|
||||
{"version": "1.0.15", "module": "outputs", "file_name": "ChatOutput"},
|
||||
{"version": "1.0.16", "module": "outputs", "file_name": "ChatOutput"},
|
||||
{"version": "1.0.17", "module": "outputs", "file_name": "ChatOutput"},
|
||||
{"version": "1.0.18", "module": "outputs", "file_name": "ChatOutput"},
|
||||
{"version": "1.0.19", "module": "outputs", "file_name": "ChatOutput"},
|
||||
]
|
||||
|
||||
|
||||
class TestTextOutputComponent(ComponentTestBaseWithoutClient):
|
||||
@pytest.fixture
|
||||
def component_class(self):
|
||||
return TextOutputComponent
|
||||
|
||||
@pytest.fixture
|
||||
def default_kwargs(self):
|
||||
return {
|
||||
"input_value": "Hello, world!",
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def file_names_mapping(self):
|
||||
return [
|
||||
{"version": "1.0.17", "module": "outputs", "file_name": "TextOutput"},
|
||||
{"version": "1.0.18", "module": "outputs", "file_name": "TextOutput"},
|
||||
{"version": "1.0.19", "module": "outputs", "file_name": "TextOutput"},
|
||||
]
|
||||
|
|
@ -6,18 +6,26 @@ from tests.base import ComponentTestBaseWithClient
|
|||
|
||||
@pytest.mark.usefixtures("client")
|
||||
class TestPromptComponent(ComponentTestBaseWithClient):
|
||||
component_class = PromptComponent
|
||||
DEFAULT_KWARGS = {"template": "Hello {name}!", "name": "John", "_session_id": "123"}
|
||||
FILE_NAMES_MAPPING = {
|
||||
"1.0.15": "Prompt",
|
||||
"1.0.16": "Prompt",
|
||||
"1.0.17": "Prompt",
|
||||
"1.0.18": "Prompt",
|
||||
"1.0.19": "Prompt",
|
||||
}
|
||||
@pytest.fixture
|
||||
def component_class(self):
|
||||
return PromptComponent
|
||||
|
||||
def test_post_code_processing(self):
|
||||
component = self.component_class(**self.DEFAULT_KWARGS)
|
||||
@pytest.fixture
|
||||
def default_kwargs(self):
|
||||
return {"template": "Hello {name}!", "name": "John", "_session_id": "123"}
|
||||
|
||||
@pytest.fixture
|
||||
def file_names_mapping(self):
|
||||
return [
|
||||
{"version": "1.0.15", "module": "prompts", "file_name": "Prompt"},
|
||||
{"version": "1.0.16", "module": "prompts", "file_name": "Prompt"},
|
||||
{"version": "1.0.17", "module": "prompts", "file_name": "Prompt"},
|
||||
{"version": "1.0.18", "module": "prompts", "file_name": "Prompt"},
|
||||
{"version": "1.0.19", "module": "prompts", "file_name": "Prompt"},
|
||||
]
|
||||
|
||||
def test_post_code_processing(self, component_class, default_kwargs):
|
||||
component = component_class(**default_kwargs)
|
||||
frontend_node = component.to_frontend_node()
|
||||
node_data = frontend_node["data"]["node"]
|
||||
assert node_data["template"]["template"]["value"] == "Hello {name}!"
|
||||
|
|
@ -25,6 +33,6 @@ class TestPromptComponent(ComponentTestBaseWithClient):
|
|||
assert "name" in node_data["template"]
|
||||
assert node_data["template"]["name"]["value"] == "John"
|
||||
|
||||
def test_prompt_component_latest(self):
|
||||
result = PromptComponent(**self.DEFAULT_KWARGS)()
|
||||
def test_prompt_component_latest(self, component_class, default_kwargs):
|
||||
result = component_class(**default_kwargs)()
|
||||
assert result is not None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue