119 lines
4.1 KiB
Python
119 lines
4.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Pytest configuration for stress tests."""
|
|
|
|
import pytest
|
|
import os
|
|
import sys
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from typing import List, Dict, Any
|
|
|
|
# Add src to path
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'src'))
|
|
|
|
|
|
@dataclass
|
|
class StressTestResult:
|
|
"""Container for stress test results."""
|
|
test_name: str
|
|
total_requests: int = 0
|
|
successful_requests: int = 0
|
|
failed_requests: int = 0
|
|
total_duration: float = 0.0
|
|
min_response_time: float = float('inf')
|
|
max_response_time: float = 0.0
|
|
response_times: List[float] = field(default_factory=list)
|
|
errors: List[str] = field(default_factory=list)
|
|
|
|
@property
|
|
def avg_response_time(self) -> float:
|
|
if not self.response_times:
|
|
return 0.0
|
|
return sum(self.response_times) / len(self.response_times)
|
|
|
|
@property
|
|
def success_rate(self) -> float:
|
|
if self.total_requests == 0:
|
|
return 0.0
|
|
return (self.successful_requests / self.total_requests) * 100
|
|
|
|
@property
|
|
def requests_per_second(self) -> float:
|
|
if self.total_duration == 0:
|
|
return 0.0
|
|
return self.total_requests / self.total_duration
|
|
|
|
def add_success(self, response_time: float):
|
|
self.total_requests += 1
|
|
self.successful_requests += 1
|
|
self.response_times.append(response_time)
|
|
self.min_response_time = min(self.min_response_time, response_time)
|
|
self.max_response_time = max(self.max_response_time, response_time)
|
|
|
|
def add_failure(self, error: str, response_time: float = 0):
|
|
self.total_requests += 1
|
|
self.failed_requests += 1
|
|
self.errors.append(error)
|
|
if response_time > 0:
|
|
self.response_times.append(response_time)
|
|
|
|
def report(self) -> str:
|
|
"""Generate human-readable report."""
|
|
lines = [
|
|
f"\n{'='*60}",
|
|
f"Stress Test Report: {self.test_name}",
|
|
f"{'='*60}",
|
|
f"Total Requests: {self.total_requests}",
|
|
f"Successful: {self.successful_requests}",
|
|
f"Failed: {self.failed_requests}",
|
|
f"Success Rate: {self.success_rate:.2f}%",
|
|
f"{'─'*60}",
|
|
f"Total Duration: {self.total_duration:.2f}s",
|
|
f"Requests/Second: {self.requests_per_second:.2f}",
|
|
f"{'─'*60}",
|
|
f"Min Response Time: {self.min_response_time*1000:.2f}ms" if self.min_response_time != float('inf') else "Min Response Time: N/A",
|
|
f"Max Response Time: {self.max_response_time*1000:.2f}ms",
|
|
f"Avg Response Time: {self.avg_response_time*1000:.2f}ms",
|
|
f"{'='*60}",
|
|
]
|
|
if self.errors:
|
|
lines.append(f"Errors (first 5):")
|
|
for err in self.errors[:5]:
|
|
lines.append(f" - {err[:100]}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def base_url() -> str:
|
|
"""Get the base URL for stress testing."""
|
|
return os.environ.get('STRESS_TEST_URL', 'http://127.0.0.1:8080')
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def stress_config() -> Dict[str, Any]:
|
|
"""Get stress test configuration."""
|
|
return {
|
|
'concurrent_users': int(os.environ.get('STRESS_CONCURRENT_USERS', '10')),
|
|
'requests_per_user': int(os.environ.get('STRESS_REQUESTS_PER_USER', '20')),
|
|
'ramp_up_time': float(os.environ.get('STRESS_RAMP_UP_TIME', '2.0')),
|
|
'timeout': float(os.environ.get('STRESS_TIMEOUT', '30.0')),
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def stress_result():
|
|
"""Factory fixture to create stress test results."""
|
|
def _create_result(test_name: str) -> StressTestResult:
|
|
return StressTestResult(test_name=test_name)
|
|
return _create_result
|
|
|
|
|
|
def pytest_configure(config):
|
|
"""Add custom markers for stress tests."""
|
|
config.addinivalue_line(
|
|
"markers", "stress: mark test as stress test (may take longer)"
|
|
)
|
|
config.addinivalue_line(
|
|
"markers", "load: mark test as load test (concurrent requests)"
|
|
)
|