generated from Templates/Docker_Image
400 lines
15 KiB
Python
400 lines
15 KiB
Python
"""Test logging utility for validating agent responses and system outputs."""
|
||
|
||
import re
|
||
from typing import Optional, Dict, Any, List
|
||
from datetime import datetime
|
||
|
||
# Color codes for terminal output
|
||
class Colors:
|
||
GREEN = '\033[92m'
|
||
RED = '\033[91m'
|
||
YELLOW = '\033[93m'
|
||
BLUE = '\033[94m'
|
||
CYAN = '\033[96m'
|
||
RESET = '\033[0m'
|
||
|
||
|
||
class TestLogger:
|
||
"""Utility class for logging test results and assertions."""
|
||
|
||
def __init__(self):
|
||
self.assertions: List[Dict[str, Any]] = []
|
||
self.errors: List[Dict[str, Any]] = []
|
||
self.logs: List[str] = []
|
||
|
||
def log(self, message: str, level: str = 'INFO') -> None:
|
||
"""Log an informational message."""
|
||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
formatted = f"[{timestamp}] [{level}] {message}"
|
||
self.logs.append(formatted)
|
||
print(formatted)
|
||
|
||
def success(self, message: str) -> None:
|
||
"""Log a success message with green color."""
|
||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
formatted = f"{Colors.GREEN}[{timestamp}] [✓ PASS] {message}{Colors.RESET}"
|
||
self.logs.append(formatted)
|
||
print(formatted)
|
||
|
||
def error(self, message: str) -> None:
|
||
"""Log an error message with red color."""
|
||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
formatted = f"{Colors.RED}[{timestamp}] [✗ ERROR] {message}{Colors.RESET}"
|
||
self.logs.append(formatted)
|
||
print(formatted)
|
||
|
||
def warning(self, message: str) -> None:
|
||
"""Log a warning message with yellow color."""
|
||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
formatted = f"{Colors.YELLOW}[{timestamp}] [!] WARN {message}{Colors.RESET}"
|
||
self.logs.append(formatted)
|
||
print(formatted)
|
||
|
||
def info(self, message: str) -> None:
|
||
"""Log an info message with blue color."""
|
||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
formatted = f"{Colors.BLUE}[{timestamp}] [ℹ INFO] {message}{Colors.RESET}"
|
||
self.logs.append(formatted)
|
||
print(formatted)
|
||
|
||
def assert_contains(self, text: str, expected: str, message: str = '') -> bool:
|
||
"""Assert that text contains expected substring."""
|
||
try:
|
||
contains = expected in text
|
||
if contains:
|
||
self.success(f"✓ '{expected}' found in text")
|
||
self.assertions.append({
|
||
'type': 'assert_contains',
|
||
'result': 'pass',
|
||
'expected': expected,
|
||
'message': message or f"'{expected}' in text"
|
||
})
|
||
return True
|
||
else:
|
||
self.error(f"✗ Expected '{expected}' not found in text")
|
||
self.assertions.append({
|
||
'type': 'assert_contains',
|
||
'result': 'fail',
|
||
'expected': expected,
|
||
'message': message or f"'{expected}' in text"
|
||
})
|
||
return False
|
||
except Exception as e:
|
||
self.error(f"Assertion failed with exception: {e}")
|
||
self.assertions.append({
|
||
'type': 'assert_contains',
|
||
'result': 'error',
|
||
'expected': expected,
|
||
'message': message or f"Assertion failed: {e}"
|
||
})
|
||
return False
|
||
|
||
def assert_not_contains(self, text: str, unexpected: str, message: str = '') -> bool:
|
||
"""Assert that text does not contain expected substring."""
|
||
try:
|
||
contains = unexpected in text
|
||
if not contains:
|
||
self.success(f"✓ '{unexpected}' not found in text")
|
||
self.assertions.append({
|
||
'type': 'assert_not_contains',
|
||
'result': 'pass',
|
||
'unexpected': unexpected,
|
||
'message': message or f"'{unexpected}' not in text"
|
||
})
|
||
return True
|
||
else:
|
||
self.error(f"✗ Unexpected '{unexpected}' found in text")
|
||
self.assertions.append({
|
||
'type': 'assert_not_contains',
|
||
'result': 'fail',
|
||
'unexpected': unexpected,
|
||
'message': message or f"'{unexpected}' not in text"
|
||
})
|
||
return False
|
||
except Exception as e:
|
||
self.error(f"Assertion failed with exception: {e}")
|
||
self.assertions.append({
|
||
'type': 'assert_not_contains',
|
||
'result': 'error',
|
||
'unexpected': unexpected,
|
||
'message': message or f"Assertion failed: {e}"
|
||
})
|
||
return False
|
||
|
||
def assert_equal(self, actual: str, expected: str, message: str = '') -> bool:
|
||
"""Assert that two strings are equal."""
|
||
try:
|
||
if actual == expected:
|
||
self.success(f"✓ Strings equal")
|
||
self.assertions.append({
|
||
'type': 'assert_equal',
|
||
'result': 'pass',
|
||
'expected': expected,
|
||
'message': message or f"actual == expected"
|
||
})
|
||
return True
|
||
else:
|
||
self.error(f"✗ Strings not equal. Expected: '{expected}', Got: '{actual}'")
|
||
self.assertions.append({
|
||
'type': 'assert_equal',
|
||
'result': 'fail',
|
||
'expected': expected,
|
||
'actual': actual,
|
||
'message': message or "actual == expected"
|
||
})
|
||
return False
|
||
except Exception as e:
|
||
self.error(f"Assertion failed with exception: {e}")
|
||
self.assertions.append({
|
||
'type': 'assert_equal',
|
||
'result': 'error',
|
||
'expected': expected,
|
||
'actual': actual,
|
||
'message': message or f"Assertion failed: {e}"
|
||
})
|
||
return False
|
||
|
||
def assert_starts_with(self, text: str, prefix: str, message: str = '') -> bool:
|
||
"""Assert that text starts with expected prefix."""
|
||
try:
|
||
starts_with = text.startswith(prefix)
|
||
if starts_with:
|
||
self.success(f"✓ Text starts with '{prefix}'")
|
||
self.assertions.append({
|
||
'type': 'assert_starts_with',
|
||
'result': 'pass',
|
||
'prefix': prefix,
|
||
'message': message or f"text starts with '{prefix}'"
|
||
})
|
||
return True
|
||
else:
|
||
self.error(f"✗ Text does not start with '{prefix}'")
|
||
self.assertions.append({
|
||
'type': 'assert_starts_with',
|
||
'result': 'fail',
|
||
'prefix': prefix,
|
||
'message': message or f"text starts with '{prefix}'"
|
||
})
|
||
return False
|
||
except Exception as e:
|
||
self.error(f"Assertion failed with exception: {e}")
|
||
self.assertions.append({
|
||
'type': 'assert_starts_with',
|
||
'result': 'error',
|
||
'prefix': prefix,
|
||
'message': message or f"Assertion failed: {e}"
|
||
})
|
||
return False
|
||
|
||
def assert_ends_with(self, text: str, suffix: str, message: str = '') -> bool:
|
||
"""Assert that text ends with expected suffix."""
|
||
try:
|
||
ends_with = text.endswith(suffix)
|
||
if ends_with:
|
||
self.success(f"✓ Text ends with '{suffix}'")
|
||
self.assertions.append({
|
||
'type': 'assert_ends_with',
|
||
'result': 'pass',
|
||
'suffix': suffix,
|
||
'message': message or f"text ends with '{suffix}'"
|
||
})
|
||
return True
|
||
else:
|
||
self.error(f"✗ Text does not end with '{suffix}'")
|
||
self.assertions.append({
|
||
'type': 'assert_ends_with',
|
||
'result': 'fail',
|
||
'suffix': suffix,
|
||
'message': message or f"text ends with '{suffix}'"
|
||
})
|
||
return False
|
||
except Exception as e:
|
||
self.error(f"Assertion failed with exception: {e}")
|
||
self.assertions.append({
|
||
'type': 'assert_ends_with',
|
||
'result': 'error',
|
||
'suffix': suffix,
|
||
'message': message or f"Assertion failed: {e}"
|
||
})
|
||
return False
|
||
|
||
def assert_regex(self, text: str, pattern: str, message: str = '') -> bool:
|
||
"""Assert that text matches a regex pattern."""
|
||
try:
|
||
if re.search(pattern, text):
|
||
self.success(f"✓ Regex pattern matched")
|
||
self.assertions.append({
|
||
'type': 'assert_regex',
|
||
'result': 'pass',
|
||
'pattern': pattern,
|
||
'message': message or f"text matches regex '{pattern}'"
|
||
})
|
||
return True
|
||
else:
|
||
self.error(f"✗ Regex pattern did not match")
|
||
self.assertions.append({
|
||
'type': 'assert_regex',
|
||
'result': 'fail',
|
||
'pattern': pattern,
|
||
'message': message or f"text matches regex '{pattern}'"
|
||
})
|
||
return False
|
||
except re.error as e:
|
||
self.error(f"✗ Invalid regex pattern: {e}")
|
||
self.assertions.append({
|
||
'type': 'assert_regex',
|
||
'result': 'error',
|
||
'pattern': pattern,
|
||
'message': message or f"Invalid regex: {e}"
|
||
})
|
||
return False
|
||
except Exception as ex:
|
||
self.error(f"Assertion failed with exception: {ex}")
|
||
self.assertions.append({
|
||
'type': 'assert_regex',
|
||
'result': 'error',
|
||
'pattern': pattern,
|
||
'message': message or f"Assertion failed: {ex}"
|
||
})
|
||
return False
|
||
|
||
def assert_length(self, text: str, expected_length: int, message: str = '') -> bool:
|
||
"""Assert that text has expected length."""
|
||
try:
|
||
length = len(text)
|
||
if length == expected_length:
|
||
self.success(f"✓ Length is {expected_length}")
|
||
self.assertions.append({
|
||
'type': 'assert_length',
|
||
'result': 'pass',
|
||
'expected_length': expected_length,
|
||
'message': message or f"len(text) == {expected_length}"
|
||
})
|
||
return True
|
||
else:
|
||
self.error(f"✗ Length is {length}, expected {expected_length}")
|
||
self.assertions.append({
|
||
'type': 'assert_length',
|
||
'result': 'fail',
|
||
'expected_length': expected_length,
|
||
'actual_length': length,
|
||
'message': message or f"len(text) == {expected_length}"
|
||
})
|
||
return False
|
||
except Exception as e:
|
||
self.error(f"Assertion failed with exception: {e}")
|
||
self.assertions.append({
|
||
'type': 'assert_length',
|
||
'result': 'error',
|
||
'expected_length': expected_length,
|
||
'message': message or f"Assertion failed: {e}"
|
||
})
|
||
return False
|
||
|
||
def assert_key_exists(self, text: str, key: str, message: str = '') -> bool:
|
||
"""Assert that a key exists in a JSON-like text."""
|
||
try:
|
||
if f'"{key}":' in text or f"'{key}':" in text:
|
||
self.success(f"✓ Key '{key}' exists")
|
||
self.assertions.append({
|
||
'type': 'assert_key_exists',
|
||
'result': 'pass',
|
||
'key': key,
|
||
'message': message or f"key '{key}' exists"
|
||
})
|
||
return True
|
||
else:
|
||
self.error(f"✗ Key '{key}' not found")
|
||
self.assertions.append({
|
||
'type': 'assert_key_exists',
|
||
'result': 'fail',
|
||
'key': key,
|
||
'message': message or f"key '{key}' exists"
|
||
})
|
||
return False
|
||
except Exception as e:
|
||
self.error(f"Assertion failed with exception: {e}")
|
||
self.assertions.append({
|
||
'type': 'assert_key_exists',
|
||
'result': 'error',
|
||
'key': key,
|
||
'message': message or f"Assertion failed: {e}"
|
||
})
|
||
return False
|
||
|
||
def assert_substring_count(self, text: str, substring: str, count: int, message: str = '') -> bool:
|
||
"""Assert that substring appears count times in text."""
|
||
try:
|
||
actual_count = text.count(substring)
|
||
if actual_count == count:
|
||
self.success(f"✓ Substring appears {count} time(s)")
|
||
self.assertions.append({
|
||
'type': 'assert_substring_count',
|
||
'result': 'pass',
|
||
'substring': substring,
|
||
'expected_count': count,
|
||
'actual_count': actual_count,
|
||
'message': message or f"'{substring}' appears {count} times"
|
||
})
|
||
return True
|
||
else:
|
||
self.error(f"✗ Substring appears {actual_count} time(s), expected {count}")
|
||
self.assertions.append({
|
||
'type': 'assert_substring_count',
|
||
'result': 'fail',
|
||
'substring': substring,
|
||
'expected_count': count,
|
||
'actual_count': actual_count,
|
||
'message': message or f"'{substring}' appears {count} times"
|
||
})
|
||
return False
|
||
except Exception as e:
|
||
self.error(f"Assertion failed with exception: {e}")
|
||
self.assertions.append({
|
||
'type': 'assert_substring_count',
|
||
'result': 'error',
|
||
'substring': substring,
|
||
'expected_count': count,
|
||
'message': message or f"Assertion failed: {e}"
|
||
})
|
||
return False
|
||
|
||
def get_assertion_count(self) -> int:
|
||
"""Get total number of assertions made."""
|
||
return len(self.assertions)
|
||
|
||
def get_failure_count(self) -> int:
|
||
"""Get number of failed assertions."""
|
||
return sum(1 for assertion in self.assertions if assertion.get('result') == 'fail')
|
||
|
||
def get_success_count(self) -> int:
|
||
"""Get number of passed assertions."""
|
||
return sum(1 for assertion in self.assertions if assertion.get('result') == 'pass')
|
||
|
||
def get_logs(self) -> List[str]:
|
||
"""Get all log messages."""
|
||
return self.logs.copy()
|
||
|
||
def get_errors(self) -> List[Dict[str, Any]]:
|
||
"""Get all error records."""
|
||
return self.errors.copy()
|
||
|
||
def clear(self) -> None:
|
||
"""Clear all logs and assertions."""
|
||
self.assertions.clear()
|
||
self.errors.clear()
|
||
self.logs.clear()
|
||
|
||
def __enter__(self):
|
||
"""Context manager entry."""
|
||
return self
|
||
|
||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||
"""Context manager exit."""
|
||
return False
|
||
|
||
|
||
# Convenience function for context manager usage
|
||
def test_logger():
|
||
"""Create and return a TestLogger instance."""
|
||
return TestLogger() |