chore: import upstream snapshot with attribution
CI / Run CI (push) Has been cancelled
CI / check-backend (push) Has been cancelled
CI / check-frontend (push) Has been cancelled
CI / tests (push) Has been cancelled
CI / e2e-tests (push) Has been cancelled
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
CI / Run CI (push) Has been cancelled
CI / check-backend (push) Has been cancelled
CI / check-frontend (push) Has been cancelled
CI / tests (push) Has been cancelled
CI / e2e-tests (push) Has been cancelled
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,449 @@
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import click
|
||||
import pytest
|
||||
|
||||
from chainlit.utils import (
|
||||
check_file,
|
||||
check_module_version,
|
||||
make_module_getattr,
|
||||
timestamp_utc,
|
||||
utc_now,
|
||||
wrap_user_function,
|
||||
)
|
||||
|
||||
|
||||
class TestUtcNow:
|
||||
"""Test suite for utc_now function."""
|
||||
|
||||
def test_utc_now_returns_string(self):
|
||||
"""Test that utc_now returns a string."""
|
||||
result = utc_now()
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_utc_now_ends_with_z(self):
|
||||
"""Test that utc_now returns ISO format with Z suffix."""
|
||||
result = utc_now()
|
||||
assert result.endswith("Z")
|
||||
|
||||
def test_utc_now_is_iso_format(self):
|
||||
"""Test that utc_now returns valid ISO format."""
|
||||
result = utc_now()
|
||||
# Remove the Z and parse
|
||||
dt_str = result[:-1]
|
||||
# Should be parseable as ISO format
|
||||
datetime.fromisoformat(dt_str)
|
||||
|
||||
def test_utc_now_is_current_time(self):
|
||||
"""Test that utc_now returns approximately current time."""
|
||||
before = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
result = utc_now()
|
||||
after = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
# Parse the result (naive datetime)
|
||||
result_dt = datetime.fromisoformat(result[:-1])
|
||||
|
||||
# Should be between before and after (with some tolerance for microseconds)
|
||||
assert (
|
||||
before.replace(microsecond=0) <= result_dt <= after.replace(microsecond=0)
|
||||
or before <= result_dt <= after
|
||||
)
|
||||
|
||||
def test_utc_now_multiple_calls(self):
|
||||
"""Test that multiple calls to utc_now return different values."""
|
||||
result1 = utc_now()
|
||||
result2 = utc_now()
|
||||
|
||||
# Results should be very close but might differ
|
||||
assert isinstance(result1, str)
|
||||
assert isinstance(result2, str)
|
||||
|
||||
|
||||
class TestTimestampUtc:
|
||||
"""Test suite for timestamp_utc function."""
|
||||
|
||||
def test_timestamp_utc_returns_string(self):
|
||||
"""Test that timestamp_utc returns a string."""
|
||||
result = timestamp_utc(1234567890.0)
|
||||
assert isinstance(result, str)
|
||||
|
||||
def test_timestamp_utc_ends_with_z(self):
|
||||
"""Test that timestamp_utc returns ISO format with Z suffix."""
|
||||
result = timestamp_utc(1234567890.0)
|
||||
assert result.endswith("Z")
|
||||
|
||||
def test_timestamp_utc_converts_correctly(self):
|
||||
"""Test that timestamp_utc converts timestamp correctly."""
|
||||
# Known timestamp: 2009-02-13 23:31:30 UTC
|
||||
timestamp = 1234567890.0
|
||||
result = timestamp_utc(timestamp)
|
||||
|
||||
# Parse and verify
|
||||
dt = datetime.fromisoformat(result[:-1])
|
||||
assert dt.year == 2009
|
||||
assert dt.month == 2
|
||||
assert dt.day == 13
|
||||
|
||||
def test_timestamp_utc_with_zero(self):
|
||||
"""Test timestamp_utc with epoch (0)."""
|
||||
result = timestamp_utc(0.0)
|
||||
dt = datetime.fromisoformat(result[:-1])
|
||||
assert dt.year == 1970
|
||||
assert dt.month == 1
|
||||
assert dt.day == 1
|
||||
|
||||
def test_timestamp_utc_with_fractional_seconds(self):
|
||||
"""Test timestamp_utc with fractional seconds."""
|
||||
timestamp = 1234567890.123456
|
||||
result = timestamp_utc(timestamp)
|
||||
|
||||
# Should be valid ISO format
|
||||
dt = datetime.fromisoformat(result[:-1])
|
||||
assert isinstance(dt, datetime)
|
||||
|
||||
def test_timestamp_utc_with_negative_timestamp(self):
|
||||
"""Test timestamp_utc with negative timestamp (before epoch)."""
|
||||
# 1969-12-31 23:00:00 UTC
|
||||
timestamp = -3600.0
|
||||
result = timestamp_utc(timestamp)
|
||||
|
||||
dt = datetime.fromisoformat(result[:-1])
|
||||
assert dt.year == 1969
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestWrapUserFunction:
|
||||
"""Test suite for wrap_user_function."""
|
||||
|
||||
async def test_wrap_user_function_with_sync_function(self, mock_chainlit_context):
|
||||
"""Test wrapping a synchronous function."""
|
||||
async with mock_chainlit_context:
|
||||
|
||||
def user_func(a, b):
|
||||
return a + b
|
||||
|
||||
wrapped = wrap_user_function(user_func)
|
||||
result = await wrapped(5, 3)
|
||||
|
||||
assert result == 8
|
||||
|
||||
async def test_wrap_user_function_with_async_function(self, mock_chainlit_context):
|
||||
"""Test wrapping an asynchronous function."""
|
||||
async with mock_chainlit_context:
|
||||
|
||||
async def user_func(x, y):
|
||||
return x * y
|
||||
|
||||
wrapped = wrap_user_function(user_func)
|
||||
result = await wrapped(4, 7)
|
||||
|
||||
assert result == 28
|
||||
|
||||
async def test_wrap_user_function_with_no_args(self, mock_chainlit_context):
|
||||
"""Test wrapping a function with no arguments."""
|
||||
async with mock_chainlit_context:
|
||||
|
||||
def user_func():
|
||||
return "hello"
|
||||
|
||||
wrapped = wrap_user_function(user_func)
|
||||
result = await wrapped()
|
||||
|
||||
assert result == "hello"
|
||||
|
||||
async def test_wrap_user_function_with_task(self, mock_chainlit_context):
|
||||
"""Test wrapping a function with task management."""
|
||||
async with mock_chainlit_context as ctx:
|
||||
ctx.emitter.task_start = AsyncMock()
|
||||
ctx.emitter.task_end = AsyncMock()
|
||||
|
||||
def user_func(value):
|
||||
return value * 2
|
||||
|
||||
wrapped = wrap_user_function(user_func, with_task=True)
|
||||
result = await wrapped(10)
|
||||
|
||||
assert result == 20
|
||||
ctx.emitter.task_start.assert_called_once()
|
||||
ctx.emitter.task_end.assert_called_once()
|
||||
|
||||
async def test_wrap_user_function_handles_exception(self, mock_chainlit_context):
|
||||
"""Test that wrapped function handles exceptions."""
|
||||
async with mock_chainlit_context:
|
||||
|
||||
def user_func():
|
||||
raise ValueError("Test error")
|
||||
|
||||
wrapped = wrap_user_function(user_func)
|
||||
result = await wrapped()
|
||||
|
||||
# Should return None when exception occurs
|
||||
assert result is None
|
||||
|
||||
async def test_wrap_user_function_with_task_handles_exception(
|
||||
self, mock_chainlit_context
|
||||
):
|
||||
"""Test that wrapped function with task handles exceptions."""
|
||||
async with mock_chainlit_context as ctx:
|
||||
ctx.emitter.task_start = AsyncMock()
|
||||
ctx.emitter.task_end = AsyncMock()
|
||||
|
||||
def user_func():
|
||||
raise ValueError("Test error")
|
||||
|
||||
with patch("chainlit.utils.logger") as mock_logger:
|
||||
wrapped = wrap_user_function(user_func, with_task=True)
|
||||
result = await wrapped()
|
||||
|
||||
assert result is None
|
||||
ctx.emitter.task_start.assert_called_once()
|
||||
ctx.emitter.task_end.assert_called_once()
|
||||
mock_logger.exception.assert_called_once()
|
||||
|
||||
async def test_wrap_user_function_preserves_function_metadata(
|
||||
self, mock_chainlit_context
|
||||
):
|
||||
"""Test that wrapping preserves function metadata."""
|
||||
async with mock_chainlit_context:
|
||||
|
||||
def user_func(a, b):
|
||||
"""Test function docstring."""
|
||||
return a + b
|
||||
|
||||
wrapped = wrap_user_function(user_func)
|
||||
|
||||
assert wrapped.__name__ == "user_func"
|
||||
assert wrapped.__doc__ == "Test function docstring."
|
||||
|
||||
async def test_wrap_user_function_with_kwargs(self, mock_chainlit_context):
|
||||
"""Test wrapping a function and calling with positional args."""
|
||||
async with mock_chainlit_context:
|
||||
|
||||
def user_func(x, y, z):
|
||||
return x + y + z
|
||||
|
||||
wrapped = wrap_user_function(user_func)
|
||||
result = await wrapped(1, 2, 3)
|
||||
|
||||
assert result == 6
|
||||
|
||||
|
||||
class TestMakeModuleGetattr:
|
||||
"""Test suite for make_module_getattr."""
|
||||
|
||||
def test_make_module_getattr_creates_function(self):
|
||||
"""Test that make_module_getattr creates a function."""
|
||||
registry = {"SomeClass": "some.module"}
|
||||
getattr_func = make_module_getattr(registry)
|
||||
|
||||
assert callable(getattr_func)
|
||||
|
||||
def test_make_module_getattr_imports_module(self):
|
||||
"""Test that the created function imports modules."""
|
||||
# Use a real module for testing
|
||||
registry = {"datetime": "datetime"}
|
||||
getattr_func = make_module_getattr(registry)
|
||||
|
||||
result = getattr_func("datetime")
|
||||
assert result is datetime
|
||||
|
||||
def test_make_module_getattr_with_nested_module(self):
|
||||
"""Test with nested module path."""
|
||||
registry = {"timezone": "datetime"}
|
||||
getattr_func = make_module_getattr(registry)
|
||||
|
||||
result = getattr_func("timezone")
|
||||
assert result is timezone
|
||||
|
||||
|
||||
class TestCheckModuleVersion:
|
||||
"""Test suite for check_module_version."""
|
||||
|
||||
def test_check_module_version_with_installed_module(self):
|
||||
"""Test checking version of an installed module."""
|
||||
# pytest should be installed
|
||||
result = check_module_version("pytest", "1.0.0")
|
||||
assert result is True
|
||||
|
||||
def test_check_module_version_with_higher_required_version(self):
|
||||
"""Test with a required version higher than installed."""
|
||||
# Require an impossibly high version
|
||||
result = check_module_version("pytest", "999.0.0")
|
||||
assert result is False
|
||||
|
||||
def test_check_module_version_with_nonexistent_module(self):
|
||||
"""Test with a module that doesn't exist."""
|
||||
result = check_module_version("nonexistent_module_xyz", "1.0.0")
|
||||
assert result is False
|
||||
|
||||
def test_check_module_version_exact_match(self):
|
||||
"""Test with exact version match."""
|
||||
# Get actual pytest version
|
||||
result = check_module_version("pytest", pytest.__version__)
|
||||
assert result is True
|
||||
|
||||
def test_check_module_version_with_builtin_module(self):
|
||||
"""Test with a builtin module that has no __version__."""
|
||||
# os module doesn't have __version__
|
||||
with pytest.raises(AttributeError):
|
||||
check_module_version("os", "1.0.0")
|
||||
|
||||
|
||||
class TestCheckFile:
|
||||
"""Test suite for check_file function."""
|
||||
|
||||
def test_check_file_with_valid_py_file(self):
|
||||
"""Test check_file with a valid .py file."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
# Should not raise any exception
|
||||
check_file(temp_file)
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
def test_check_file_with_valid_py3_file(self):
|
||||
"""Test check_file with a valid .py3 file."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".py3", delete=False) as f:
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
# Should not raise any exception
|
||||
check_file(temp_file)
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
def test_check_file_with_invalid_extension(self):
|
||||
"""Test check_file with invalid file extension."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
with pytest.raises(click.BadArgumentUsage) as exc_info:
|
||||
check_file(temp_file)
|
||||
assert ".txt" in str(exc_info.value)
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
def test_check_file_with_no_extension(self):
|
||||
"""Test check_file with file that has no extension."""
|
||||
with tempfile.NamedTemporaryFile(suffix="", delete=False) as f:
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
with pytest.raises(click.BadArgumentUsage) as exc_info:
|
||||
check_file(temp_file)
|
||||
assert "no extension" in str(exc_info.value)
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
def test_check_file_with_nonexistent_file(self):
|
||||
"""Test check_file with a file that doesn't exist."""
|
||||
nonexistent_file = "/path/to/nonexistent/file.py"
|
||||
|
||||
with pytest.raises(click.BadParameter) as exc_info:
|
||||
check_file(nonexistent_file)
|
||||
assert "does not exist" in str(exc_info.value)
|
||||
|
||||
def test_check_file_with_json_extension(self):
|
||||
"""Test check_file with .json extension."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
with pytest.raises(click.BadArgumentUsage) as exc_info:
|
||||
check_file(temp_file)
|
||||
assert ".json" in str(exc_info.value)
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
class TestUtilsEdgeCases:
|
||||
"""Test suite for utils edge cases."""
|
||||
|
||||
def test_utc_now_format_consistency(self):
|
||||
"""Test that utc_now format is consistent across calls."""
|
||||
results = [utc_now() for _ in range(5)]
|
||||
|
||||
for result in results:
|
||||
# All should have same format
|
||||
assert result.endswith("Z")
|
||||
assert "T" in result
|
||||
# Should be parseable
|
||||
datetime.fromisoformat(result[:-1])
|
||||
|
||||
def test_timestamp_utc_with_large_timestamp(self):
|
||||
"""Test timestamp_utc with very large timestamp (far future)."""
|
||||
# Year 2100
|
||||
timestamp = 4102444800.0
|
||||
result = timestamp_utc(timestamp)
|
||||
|
||||
dt = datetime.fromisoformat(result[:-1])
|
||||
assert dt.year == 2100
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wrap_user_function_with_multiple_exceptions(
|
||||
self, mock_chainlit_context
|
||||
):
|
||||
"""Test wrapped function handles different exception types."""
|
||||
async with mock_chainlit_context:
|
||||
exceptions = [ValueError("error1"), TypeError("error2"), KeyError("error3")]
|
||||
|
||||
for exc in exceptions:
|
||||
|
||||
def user_func():
|
||||
raise exc
|
||||
|
||||
with patch("chainlit.utils.logger"):
|
||||
wrapped = wrap_user_function(user_func)
|
||||
result = await wrapped()
|
||||
assert result is None
|
||||
|
||||
def test_check_file_with_relative_path(self):
|
||||
"""Test check_file with relative path."""
|
||||
# Create a temp file in current directory
|
||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False, dir=".") as f:
|
||||
temp_file = os.path.basename(f.name)
|
||||
|
||||
try:
|
||||
# Should work with relative path
|
||||
check_file(temp_file)
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
def test_check_file_with_absolute_path(self):
|
||||
"""Test check_file with absolute path."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
|
||||
temp_file = os.path.abspath(f.name)
|
||||
|
||||
try:
|
||||
# Should work with absolute path
|
||||
check_file(temp_file)
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
def test_make_module_getattr_with_empty_registry(self):
|
||||
"""Test make_module_getattr with empty registry."""
|
||||
registry = {}
|
||||
getattr_func = make_module_getattr(registry)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
getattr_func("nonexistent")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wrap_user_function_with_default_args(self, mock_chainlit_context):
|
||||
"""Test wrapping function with default arguments."""
|
||||
async with mock_chainlit_context:
|
||||
|
||||
def user_func(a, b=10):
|
||||
return a + b
|
||||
|
||||
wrapped = wrap_user_function(user_func)
|
||||
|
||||
# Call with only required arg
|
||||
result = await wrapped(5)
|
||||
assert result == 15
|
||||
Reference in New Issue
Block a user