EU-Utility/tests/pytest_compat.py

134 lines
3.4 KiB
Python

"""
Pytest compatibility layer for unittest.
Provides pytest-style decorators and fixtures using unittest.
"""
import unittest
import functools
import threading
import time
from pathlib import Path
from unittest.mock import MagicMock, patch, mock_open
from datetime import datetime
from typing import Generator
# Global fixture registry
_fixtures = {}
def fixture(scope="function"):
"""Decorator to mark a function as a fixture."""
def decorator(func):
func._is_fixture = True
func._scope = scope
_fixtures[func.__name__] = func
return func
return decorator
def pytestmark(*marks):
"""Decorator to apply pytest markers (no-op for unittest)."""
def decorator(cls):
return cls
return decorator
# Marker decorators
class Markers:
@staticmethod
def unit(func):
"""Mark as unit test."""
func._marker = 'unit'
return func
@staticmethod
def integration(func):
"""Mark as integration test."""
func._marker = 'integration'
return func
@staticmethod
def ui(func):
"""Mark as UI test."""
func._marker = 'ui'
return func
@staticmethod
def performance(func):
"""Mark as performance test."""
func._marker = 'performance'
return func
@staticmethod
def slow(func):
"""Mark as slow test."""
func._marker = 'slow'
return func
@staticmethod
def requires_qt(func):
"""Mark as requiring Qt."""
func._marker = 'requires_qt'
return func
@staticmethod
def requires_network(func):
"""Mark as requiring network."""
func._marker = 'requires_network'
return func
# Create pytest.mark-style interface
class PytestMark:
unit = Markers.unit
integration = Markers.integration
ui = Markers.ui
performance = Markers.performance
slow = Markers.slow
requires_qt = Markers.requires_qt
requires_network = Markers.requires_network
# Test result storage for compatibility
class TestGlobals:
last_result = None
def raises(expected_exception, match=None):
"""Context manager for expecting exceptions (pytest.raises style)."""
class RaisesContext:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
raise AssertionError(f"Expected {expected_exception.__name__} but no exception was raised")
if not issubclass(exc_type, expected_exception):
return False # Re-raise
if match and not match in str(exc_val):
raise AssertionError(f"Exception message did not match '{match}': {exc_val}")
return True # Suppress exception
return RaisesContext()
# Approximate matcher for floating point
class Approx:
def __init__(self, value, rel=None, abs=None):
self.value = value
self.rel = rel or 1e-6
self.abs = abs or 1e-12
def __eq__(self, other):
if not isinstance(other, (int, float)):
return False
tolerance = max(self.abs, self.rel * abs(self.value))
return abs(self.value - other) <= tolerance
def __repr__(self):
return f"Approx({self.value})"
def approx(value, rel=None, abs=None):
"""Create an approximate matcher for floating point comparison."""
return Approx(value, rel, abs)