"""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()