Delete tests
Browse files- tests/.gitkeep +0 -0
- tests/README.md +0 -18
- tests/__init__.py +0 -3
- tests/conftest.py +0 -82
- tests/test_input_validation.py +0 -316
- tests/test_models.py +0 -614
- tests/test_policy_engine.py +0 -291
- tests/test_timeline_calculator.py +0 -169
- tests/test_timeline_formatter.py +0 -159
- tests/test_timeline_integration.py +0 -178
tests/.gitkeep
DELETED
|
File without changes
|
tests/README.md
DELETED
|
@@ -1,18 +0,0 @@
|
|
| 1 |
-
# Timeline Feature Test Suite
|
| 2 |
-
|
| 3 |
-
## Overview
|
| 4 |
-
|
| 5 |
-
Test suite for the Before/After Timeline feature in ARF.
|
| 6 |
-
|
| 7 |
-
## Test Files
|
| 8 |
-
|
| 9 |
-
- `test_timeline_calculator.py` - Unit tests for calculation logic
|
| 10 |
-
- `test_timeline_formatter.py` - Unit tests for formatting
|
| 11 |
-
- `test_timeline_integration.py` - Integration tests
|
| 12 |
-
- `conftest.py` - Shared fixtures
|
| 13 |
-
|
| 14 |
-
## Running Tests
|
| 15 |
-
|
| 16 |
-
### Run all tests
|
| 17 |
-
```bash
|
| 18 |
-
pytest tests/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/__init__.py
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Test package for Enterprise Agentic Reliability Framework
|
| 3 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
tests/conftest.py
DELETED
|
@@ -1,82 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Pytest configuration and shared fixtures for timeline tests
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import pytest
|
| 6 |
-
from unittest.mock import Mock
|
| 7 |
-
from datetime import datetime
|
| 8 |
-
|
| 9 |
-
# Add your shared fixtures here
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
@pytest.fixture
|
| 13 |
-
def sample_timeline_metrics():
|
| 14 |
-
"""Create sample TimelineMetrics for testing"""
|
| 15 |
-
# TODO: Return a standard TimelineMetrics instance
|
| 16 |
-
pass
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
@pytest.fixture
|
| 20 |
-
def timeline_calculator():
|
| 21 |
-
"""Create TimelineCalculator with test defaults"""
|
| 22 |
-
# TODO: Return calculator instance
|
| 23 |
-
pass
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
@pytest.fixture
|
| 27 |
-
def timeline_formatter():
|
| 28 |
-
"""Create TimelineFormatter instance"""
|
| 29 |
-
# TODO: Return formatter instance
|
| 30 |
-
pass
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
@pytest.fixture
|
| 34 |
-
def mock_business_metrics():
|
| 35 |
-
"""Mock BusinessMetricsTracker"""
|
| 36 |
-
# TODO: Return mock with predefined behavior
|
| 37 |
-
pass
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
@pytest.fixture
|
| 41 |
-
def mock_enhanced_engine():
|
| 42 |
-
"""Mock EnhancedReliabilityEngine"""
|
| 43 |
-
# TODO: Return mock engine
|
| 44 |
-
pass
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
@pytest.fixture
|
| 48 |
-
def sample_incident_data():
|
| 49 |
-
"""Create sample incident data for testing"""
|
| 50 |
-
return {
|
| 51 |
-
"component": "api-service",
|
| 52 |
-
"latency": 450.0,
|
| 53 |
-
"error_rate": 0.22,
|
| 54 |
-
"throughput": 8500,
|
| 55 |
-
"cpu_util": 0.95,
|
| 56 |
-
"memory_util": 0.88,
|
| 57 |
-
"severity": "CRITICAL"
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
@pytest.fixture
|
| 62 |
-
def sample_timeline_display():
|
| 63 |
-
"""Create sample timeline markdown display"""
|
| 64 |
-
# TODO: Return formatted markdown string
|
| 65 |
-
pass
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
# Markers for different test categories
|
| 69 |
-
def pytest_configure(config):
|
| 70 |
-
"""Configure custom pytest markers"""
|
| 71 |
-
config.addinivalue_line(
|
| 72 |
-
"markers", "integration: mark test as integration test"
|
| 73 |
-
)
|
| 74 |
-
config.addinivalue_line(
|
| 75 |
-
"markers", "unit: mark test as unit test"
|
| 76 |
-
)
|
| 77 |
-
config.addinivalue_line(
|
| 78 |
-
"markers", "benchmark: mark test as performance benchmark"
|
| 79 |
-
)
|
| 80 |
-
config.addinivalue_line(
|
| 81 |
-
"markers", "slow: mark test as slow running"
|
| 82 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_input_validation.py
DELETED
|
@@ -1,316 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Unit tests for input validation functions
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import pytest
|
| 6 |
-
from app import validate_inputs, validate_component_id
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
class TestComponentIDValidation:
|
| 10 |
-
"""Test component ID validation"""
|
| 11 |
-
|
| 12 |
-
def test_valid_component_ids(self):
|
| 13 |
-
"""Test that valid component IDs pass validation"""
|
| 14 |
-
valid_ids = [
|
| 15 |
-
"api-service",
|
| 16 |
-
"auth-service",
|
| 17 |
-
"payment-service-v2",
|
| 18 |
-
"db-01",
|
| 19 |
-
"cache",
|
| 20 |
-
"a", # Single character
|
| 21 |
-
"api-gateway-prod-001",
|
| 22 |
-
]
|
| 23 |
-
|
| 24 |
-
for component_id in valid_ids:
|
| 25 |
-
is_valid, msg = validate_component_id(component_id)
|
| 26 |
-
assert is_valid is True, f"'{component_id}' should be valid but got: {msg}"
|
| 27 |
-
assert msg == ""
|
| 28 |
-
|
| 29 |
-
def test_invalid_uppercase(self):
|
| 30 |
-
"""Test that uppercase letters are rejected"""
|
| 31 |
-
invalid_ids = ["API-SERVICE", "Auth-Service", "PaymentService"]
|
| 32 |
-
|
| 33 |
-
for component_id in invalid_ids:
|
| 34 |
-
is_valid, msg = validate_component_id(component_id)
|
| 35 |
-
assert is_valid is False
|
| 36 |
-
assert "lowercase" in msg.lower()
|
| 37 |
-
|
| 38 |
-
def test_invalid_underscore(self):
|
| 39 |
-
"""Test that underscores are rejected"""
|
| 40 |
-
is_valid, msg = validate_component_id("api_service")
|
| 41 |
-
assert is_valid is False
|
| 42 |
-
assert "lowercase" in msg.lower() or "hyphen" in msg.lower()
|
| 43 |
-
|
| 44 |
-
def test_invalid_special_characters(self):
|
| 45 |
-
"""Test that special characters are rejected"""
|
| 46 |
-
invalid_ids = [
|
| 47 |
-
"api@service",
|
| 48 |
-
"api.service",
|
| 49 |
-
"api service", # Space
|
| 50 |
-
"api/service",
|
| 51 |
-
"api&service",
|
| 52 |
-
]
|
| 53 |
-
|
| 54 |
-
for component_id in invalid_ids:
|
| 55 |
-
is_valid, msg = validate_component_id(component_id)
|
| 56 |
-
assert is_valid is False, f"'{component_id}' should be invalid"
|
| 57 |
-
|
| 58 |
-
def test_empty_string(self):
|
| 59 |
-
"""Test that empty string is rejected"""
|
| 60 |
-
is_valid, msg = validate_component_id("")
|
| 61 |
-
assert is_valid is False
|
| 62 |
-
assert "1-255" in msg or "character" in msg.lower()
|
| 63 |
-
|
| 64 |
-
def test_too_long(self):
|
| 65 |
-
"""Test that component IDs longer than 255 chars are rejected"""
|
| 66 |
-
long_id = "a" * 256
|
| 67 |
-
is_valid, msg = validate_component_id(long_id)
|
| 68 |
-
assert is_valid is False
|
| 69 |
-
assert "255" in msg
|
| 70 |
-
|
| 71 |
-
def test_non_string_type(self):
|
| 72 |
-
"""Test that non-string types are rejected"""
|
| 73 |
-
is_valid, msg = validate_component_id(123)
|
| 74 |
-
assert is_valid is False
|
| 75 |
-
assert "string" in msg.lower()
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
class TestNumericInputValidation:
|
| 79 |
-
"""Test numeric input validation"""
|
| 80 |
-
|
| 81 |
-
def test_valid_inputs(self):
|
| 82 |
-
"""Test that valid inputs pass validation"""
|
| 83 |
-
is_valid, msg = validate_inputs(
|
| 84 |
-
latency=150.0,
|
| 85 |
-
error_rate=0.05,
|
| 86 |
-
throughput=1000.0,
|
| 87 |
-
cpu_util=0.7,
|
| 88 |
-
memory_util=0.6
|
| 89 |
-
)
|
| 90 |
-
|
| 91 |
-
assert is_valid is True
|
| 92 |
-
assert msg == ""
|
| 93 |
-
|
| 94 |
-
def test_valid_inputs_with_none_optionals(self):
|
| 95 |
-
"""Test that None is valid for optional fields"""
|
| 96 |
-
is_valid, msg = validate_inputs(
|
| 97 |
-
latency=150.0,
|
| 98 |
-
error_rate=0.05,
|
| 99 |
-
throughput=1000.0,
|
| 100 |
-
cpu_util=None,
|
| 101 |
-
memory_util=None
|
| 102 |
-
)
|
| 103 |
-
|
| 104 |
-
assert is_valid is True
|
| 105 |
-
assert msg == ""
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
class TestLatencyValidation:
|
| 109 |
-
"""Test latency validation"""
|
| 110 |
-
|
| 111 |
-
def test_valid_latency(self):
|
| 112 |
-
"""Test valid latency values"""
|
| 113 |
-
valid_values = [0, 1, 100, 500, 1000, 9999]
|
| 114 |
-
|
| 115 |
-
for latency in valid_values:
|
| 116 |
-
is_valid, msg = validate_inputs(latency, 0.05, 1000, None, None)
|
| 117 |
-
assert is_valid is True, f"Latency {latency} should be valid"
|
| 118 |
-
|
| 119 |
-
def test_negative_latency(self):
|
| 120 |
-
"""Test that negative latency is rejected"""
|
| 121 |
-
is_valid, msg = validate_inputs(-10, 0.05, 1000, None, None)
|
| 122 |
-
assert is_valid is False
|
| 123 |
-
assert "latency" in msg.lower()
|
| 124 |
-
|
| 125 |
-
def test_excessive_latency(self):
|
| 126 |
-
"""Test that excessive latency is rejected"""
|
| 127 |
-
is_valid, msg = validate_inputs(20000, 0.05, 1000, None, None)
|
| 128 |
-
assert is_valid is False
|
| 129 |
-
assert "latency" in msg.lower()
|
| 130 |
-
|
| 131 |
-
def test_non_numeric_latency(self):
|
| 132 |
-
"""Test that non-numeric latency is rejected"""
|
| 133 |
-
is_valid, msg = validate_inputs("invalid", 0.05, 1000, None, None)
|
| 134 |
-
assert is_valid is False
|
| 135 |
-
assert "latency" in msg.lower()
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
class TestErrorRateValidation:
|
| 139 |
-
"""Test error rate validation"""
|
| 140 |
-
|
| 141 |
-
def test_valid_error_rates(self):
|
| 142 |
-
"""Test valid error rate values"""
|
| 143 |
-
valid_values = [0, 0.01, 0.05, 0.5, 0.99, 1.0]
|
| 144 |
-
|
| 145 |
-
for error_rate in valid_values:
|
| 146 |
-
is_valid, msg = validate_inputs(100, error_rate, 1000, None, None)
|
| 147 |
-
assert is_valid is True, f"Error rate {error_rate} should be valid"
|
| 148 |
-
|
| 149 |
-
def test_negative_error_rate(self):
|
| 150 |
-
"""Test that negative error rate is rejected"""
|
| 151 |
-
is_valid, msg = validate_inputs(100, -0.1, 1000, None, None)
|
| 152 |
-
assert is_valid is False
|
| 153 |
-
assert "error rate" in msg.lower()
|
| 154 |
-
|
| 155 |
-
def test_error_rate_exceeds_one(self):
|
| 156 |
-
"""Test that error rate > 1 is rejected"""
|
| 157 |
-
is_valid, msg = validate_inputs(100, 1.5, 1000, None, None)
|
| 158 |
-
assert is_valid is False
|
| 159 |
-
assert "error rate" in msg.lower()
|
| 160 |
-
|
| 161 |
-
def test_non_numeric_error_rate(self):
|
| 162 |
-
"""Test that non-numeric error rate is rejected"""
|
| 163 |
-
is_valid, msg = validate_inputs(100, "high", 1000, None, None)
|
| 164 |
-
assert is_valid is False
|
| 165 |
-
assert "error rate" in msg.lower()
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
class TestThroughputValidation:
|
| 169 |
-
"""Test throughput validation"""
|
| 170 |
-
|
| 171 |
-
def test_valid_throughput(self):
|
| 172 |
-
"""Test valid throughput values"""
|
| 173 |
-
valid_values = [0, 1, 100, 1000, 10000]
|
| 174 |
-
|
| 175 |
-
for throughput in valid_values:
|
| 176 |
-
is_valid, msg = validate_inputs(100, 0.05, throughput, None, None)
|
| 177 |
-
assert is_valid is True, f"Throughput {throughput} should be valid"
|
| 178 |
-
|
| 179 |
-
def test_negative_throughput(self):
|
| 180 |
-
"""Test that negative throughput is rejected"""
|
| 181 |
-
is_valid, msg = validate_inputs(100, 0.05, -500, None, None)
|
| 182 |
-
assert is_valid is False
|
| 183 |
-
assert "throughput" in msg.lower()
|
| 184 |
-
|
| 185 |
-
def test_non_numeric_throughput(self):
|
| 186 |
-
"""Test that non-numeric throughput is rejected"""
|
| 187 |
-
is_valid, msg = validate_inputs(100, 0.05, "many", None, None)
|
| 188 |
-
assert is_valid is False
|
| 189 |
-
assert "throughput" in msg.lower()
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
class TestCPUUtilizationValidation:
|
| 193 |
-
"""Test CPU utilization validation"""
|
| 194 |
-
|
| 195 |
-
def test_valid_cpu_util(self):
|
| 196 |
-
"""Test valid CPU utilization values"""
|
| 197 |
-
valid_values = [0, 0.1, 0.5, 0.85, 1.0]
|
| 198 |
-
|
| 199 |
-
for cpu_util in valid_values:
|
| 200 |
-
is_valid, msg = validate_inputs(100, 0.05, 1000, cpu_util, None)
|
| 201 |
-
assert is_valid is True, f"CPU util {cpu_util} should be valid"
|
| 202 |
-
|
| 203 |
-
def test_negative_cpu_util(self):
|
| 204 |
-
"""Test that negative CPU utilization is rejected"""
|
| 205 |
-
is_valid, msg = validate_inputs(100, 0.05, 1000, -0.1, None)
|
| 206 |
-
assert is_valid is False
|
| 207 |
-
assert "cpu" in msg.lower()
|
| 208 |
-
|
| 209 |
-
def test_cpu_util_exceeds_one(self):
|
| 210 |
-
"""Test that CPU utilization > 1 is rejected"""
|
| 211 |
-
is_valid, msg = validate_inputs(100, 0.05, 1000, 1.5, None)
|
| 212 |
-
assert is_valid is False
|
| 213 |
-
assert "cpu" in msg.lower()
|
| 214 |
-
|
| 215 |
-
def test_non_numeric_cpu_util(self):
|
| 216 |
-
"""Test that non-numeric CPU utilization is rejected"""
|
| 217 |
-
is_valid, msg = validate_inputs(100, 0.05, 1000, "high", None)
|
| 218 |
-
assert is_valid is False
|
| 219 |
-
assert "cpu" in msg.lower()
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
class TestMemoryUtilizationValidation:
|
| 223 |
-
"""Test memory utilization validation"""
|
| 224 |
-
|
| 225 |
-
def test_valid_memory_util(self):
|
| 226 |
-
"""Test valid memory utilization values"""
|
| 227 |
-
valid_values = [0, 0.1, 0.5, 0.85, 1.0]
|
| 228 |
-
|
| 229 |
-
for memory_util in valid_values:
|
| 230 |
-
is_valid, msg = validate_inputs(100, 0.05, 1000, None, memory_util)
|
| 231 |
-
assert is_valid is True, f"Memory util {memory_util} should be valid"
|
| 232 |
-
|
| 233 |
-
def test_negative_memory_util(self):
|
| 234 |
-
"""Test that negative memory utilization is rejected"""
|
| 235 |
-
is_valid, msg = validate_inputs(100, 0.05, 1000, None, -0.1)
|
| 236 |
-
assert is_valid is False
|
| 237 |
-
assert "memory" in msg.lower()
|
| 238 |
-
|
| 239 |
-
def test_memory_util_exceeds_one(self):
|
| 240 |
-
"""Test that memory utilization > 1 is rejected"""
|
| 241 |
-
is_valid, msg = validate_inputs(100, 0.05, 1000, None, 1.5)
|
| 242 |
-
assert is_valid is False
|
| 243 |
-
assert "memory" in msg.lower()
|
| 244 |
-
|
| 245 |
-
def test_non_numeric_memory_util(self):
|
| 246 |
-
"""Test that non-numeric memory utilization is rejected"""
|
| 247 |
-
is_valid, msg = validate_inputs(100, 0.05, 1000, None, "full")
|
| 248 |
-
assert is_valid is False
|
| 249 |
-
assert "memory" in msg.lower()
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
class TestEdgeCases:
|
| 253 |
-
"""Test edge cases and boundary conditions"""
|
| 254 |
-
|
| 255 |
-
def test_zero_values(self):
|
| 256 |
-
"""Test that zero is valid for all metrics"""
|
| 257 |
-
is_valid, msg = validate_inputs(0, 0, 0, 0, 0)
|
| 258 |
-
assert is_valid is True
|
| 259 |
-
|
| 260 |
-
def test_maximum_values(self):
|
| 261 |
-
"""Test maximum boundary values"""
|
| 262 |
-
is_valid, msg = validate_inputs(10000, 1.0, 999999, 1.0, 1.0)
|
| 263 |
-
assert is_valid is True
|
| 264 |
-
|
| 265 |
-
def test_float_precision(self):
|
| 266 |
-
"""Test that high-precision floats are handled"""
|
| 267 |
-
is_valid, msg = validate_inputs(
|
| 268 |
-
latency=123.456789,
|
| 269 |
-
error_rate=0.123456,
|
| 270 |
-
throughput=1234.56,
|
| 271 |
-
cpu_util=0.87654321,
|
| 272 |
-
memory_util=0.76543210
|
| 273 |
-
)
|
| 274 |
-
assert is_valid is True
|
| 275 |
-
|
| 276 |
-
def test_integer_inputs(self):
|
| 277 |
-
"""Test that integer inputs are accepted"""
|
| 278 |
-
is_valid, msg = validate_inputs(100, 0, 1000, 1, 1)
|
| 279 |
-
assert is_valid is True
|
| 280 |
-
|
| 281 |
-
def test_string_numbers(self):
|
| 282 |
-
"""Test that string numbers are converted"""
|
| 283 |
-
is_valid, msg = validate_inputs("100", "0.05", "1000", "0.7", "0.6")
|
| 284 |
-
assert is_valid is True
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
class TestErrorMessages:
|
| 288 |
-
"""Test that error messages are helpful"""
|
| 289 |
-
|
| 290 |
-
def test_error_message_contains_field_name(self):
|
| 291 |
-
"""Test that error messages identify the problematic field"""
|
| 292 |
-
# Latency error
|
| 293 |
-
is_valid, msg = validate_inputs(-10, 0.05, 1000, None, None)
|
| 294 |
-
assert "latency" in msg.lower()
|
| 295 |
-
|
| 296 |
-
# Error rate error
|
| 297 |
-
is_valid, msg = validate_inputs(100, 2.0, 1000, None, None)
|
| 298 |
-
assert "error rate" in msg.lower()
|
| 299 |
-
|
| 300 |
-
# Throughput error
|
| 301 |
-
is_valid, msg = validate_inputs(100, 0.05, -100, None, None)
|
| 302 |
-
assert "throughput" in msg.lower()
|
| 303 |
-
|
| 304 |
-
def test_error_message_has_emoji(self):
|
| 305 |
-
"""Test that error messages include emoji for visibility"""
|
| 306 |
-
is_valid, msg = validate_inputs(-10, 0.05, 1000, None, None)
|
| 307 |
-
assert "❌" in msg
|
| 308 |
-
|
| 309 |
-
def test_error_message_provides_guidance(self):
|
| 310 |
-
"""Test that error messages provide guidance"""
|
| 311 |
-
is_valid, msg = validate_inputs(-10, 0.05, 1000, None, None)
|
| 312 |
-
assert "between" in msg.lower() or "range" in msg.lower() or "0-10000" in msg
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
if __name__ == "__main__":
|
| 316 |
-
pytest.main([__file__, "-v", "--tb=short"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_models.py
DELETED
|
@@ -1,614 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Unit tests for Pydantic models with validation and security tests
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import pytest
|
| 6 |
-
from datetime import datetime, timezone
|
| 7 |
-
from pydantic import ValidationError
|
| 8 |
-
from models import (
|
| 9 |
-
ReliabilityEvent,
|
| 10 |
-
EventSeverity,
|
| 11 |
-
HealingPolicy,
|
| 12 |
-
HealingAction,
|
| 13 |
-
PolicyCondition,
|
| 14 |
-
AnomalyResult,
|
| 15 |
-
ForecastResult
|
| 16 |
-
)
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
class TestReliabilityEventValidation:
|
| 20 |
-
"""Test ReliabilityEvent validation"""
|
| 21 |
-
|
| 22 |
-
def test_valid_event_creation(self):
|
| 23 |
-
"""Test creating a valid event"""
|
| 24 |
-
event = ReliabilityEvent(
|
| 25 |
-
component="api-service",
|
| 26 |
-
latency_p99=150.0,
|
| 27 |
-
error_rate=0.05,
|
| 28 |
-
throughput=1000.0,
|
| 29 |
-
cpu_util=0.7,
|
| 30 |
-
memory_util=0.6
|
| 31 |
-
)
|
| 32 |
-
|
| 33 |
-
assert event.component == "api-service"
|
| 34 |
-
assert event.latency_p99 == 150.0
|
| 35 |
-
assert event.error_rate == 0.05
|
| 36 |
-
assert isinstance(event.timestamp, datetime)
|
| 37 |
-
assert event.severity == EventSeverity.LOW
|
| 38 |
-
|
| 39 |
-
def test_component_validation_valid(self):
|
| 40 |
-
"""Test valid component IDs"""
|
| 41 |
-
valid_ids = ["api-service", "auth-service", "payment-service-v2", "db-01"]
|
| 42 |
-
|
| 43 |
-
for component_id in valid_ids:
|
| 44 |
-
event = ReliabilityEvent(
|
| 45 |
-
component=component_id,
|
| 46 |
-
latency_p99=100.0,
|
| 47 |
-
error_rate=0.01,
|
| 48 |
-
throughput=1000.0
|
| 49 |
-
)
|
| 50 |
-
assert event.component == component_id
|
| 51 |
-
|
| 52 |
-
def test_component_validation_invalid(self):
|
| 53 |
-
"""Test invalid component IDs are rejected"""
|
| 54 |
-
invalid_ids = [
|
| 55 |
-
"API-SERVICE", # Uppercase
|
| 56 |
-
"api_service", # Underscore
|
| 57 |
-
"api service", # Space
|
| 58 |
-
"api@service", # Special char
|
| 59 |
-
"", # Empty
|
| 60 |
-
]
|
| 61 |
-
|
| 62 |
-
for component_id in invalid_ids:
|
| 63 |
-
with pytest.raises(ValidationError) as exc_info:
|
| 64 |
-
ReliabilityEvent(
|
| 65 |
-
component=component_id,
|
| 66 |
-
latency_p99=100.0,
|
| 67 |
-
error_rate=0.01,
|
| 68 |
-
throughput=1000.0
|
| 69 |
-
)
|
| 70 |
-
assert "component" in str(exc_info.value).lower()
|
| 71 |
-
|
| 72 |
-
def test_latency_bounds(self):
|
| 73 |
-
"""Test latency validation bounds"""
|
| 74 |
-
# Valid latency
|
| 75 |
-
event = ReliabilityEvent(
|
| 76 |
-
component="test-service",
|
| 77 |
-
latency_p99=100.0,
|
| 78 |
-
error_rate=0.01,
|
| 79 |
-
throughput=1000.0
|
| 80 |
-
)
|
| 81 |
-
assert event.latency_p99 == 100.0
|
| 82 |
-
|
| 83 |
-
# Negative latency should fail
|
| 84 |
-
with pytest.raises(ValidationError):
|
| 85 |
-
ReliabilityEvent(
|
| 86 |
-
component="test-service",
|
| 87 |
-
latency_p99=-10.0,
|
| 88 |
-
error_rate=0.01,
|
| 89 |
-
throughput=1000.0
|
| 90 |
-
)
|
| 91 |
-
|
| 92 |
-
# Extremely high latency should fail (> 5 minutes)
|
| 93 |
-
with pytest.raises(ValidationError):
|
| 94 |
-
ReliabilityEvent(
|
| 95 |
-
component="test-service",
|
| 96 |
-
latency_p99=400000.0, # > 300000ms limit
|
| 97 |
-
error_rate=0.01,
|
| 98 |
-
throughput=1000.0
|
| 99 |
-
)
|
| 100 |
-
|
| 101 |
-
def test_error_rate_bounds(self):
|
| 102 |
-
"""Test error rate validation"""
|
| 103 |
-
# Valid error rate
|
| 104 |
-
event = ReliabilityEvent(
|
| 105 |
-
component="test-service",
|
| 106 |
-
latency_p99=100.0,
|
| 107 |
-
error_rate=0.5,
|
| 108 |
-
throughput=1000.0
|
| 109 |
-
)
|
| 110 |
-
assert event.error_rate == 0.5
|
| 111 |
-
|
| 112 |
-
# Negative error rate should fail
|
| 113 |
-
with pytest.raises(ValidationError):
|
| 114 |
-
ReliabilityEvent(
|
| 115 |
-
component="test-service",
|
| 116 |
-
latency_p99=100.0,
|
| 117 |
-
error_rate=-0.1,
|
| 118 |
-
throughput=1000.0
|
| 119 |
-
)
|
| 120 |
-
|
| 121 |
-
# Error rate > 1 should fail
|
| 122 |
-
with pytest.raises(ValidationError):
|
| 123 |
-
ReliabilityEvent(
|
| 124 |
-
component="test-service",
|
| 125 |
-
latency_p99=100.0,
|
| 126 |
-
error_rate=1.5,
|
| 127 |
-
throughput=1000.0
|
| 128 |
-
)
|
| 129 |
-
|
| 130 |
-
def test_resource_utilization_bounds(self):
|
| 131 |
-
"""Test CPU and memory utilization bounds"""
|
| 132 |
-
# Valid utilization
|
| 133 |
-
event = ReliabilityEvent(
|
| 134 |
-
component="test-service",
|
| 135 |
-
latency_p99=100.0,
|
| 136 |
-
error_rate=0.01,
|
| 137 |
-
throughput=1000.0,
|
| 138 |
-
cpu_util=0.85,
|
| 139 |
-
memory_util=0.75
|
| 140 |
-
)
|
| 141 |
-
assert event.cpu_util == 0.85
|
| 142 |
-
assert event.memory_util == 0.75
|
| 143 |
-
|
| 144 |
-
# CPU > 1 should fail
|
| 145 |
-
with pytest.raises(ValidationError):
|
| 146 |
-
ReliabilityEvent(
|
| 147 |
-
component="test-service",
|
| 148 |
-
latency_p99=100.0,
|
| 149 |
-
error_rate=0.01,
|
| 150 |
-
throughput=1000.0,
|
| 151 |
-
cpu_util=1.5
|
| 152 |
-
)
|
| 153 |
-
|
| 154 |
-
# Memory < 0 should fail
|
| 155 |
-
with pytest.raises(ValidationError):
|
| 156 |
-
ReliabilityEvent(
|
| 157 |
-
component="test-service",
|
| 158 |
-
latency_p99=100.0,
|
| 159 |
-
error_rate=0.01,
|
| 160 |
-
throughput=1000.0,
|
| 161 |
-
memory_util=-0.1
|
| 162 |
-
)
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
class TestEventFingerprint:
|
| 166 |
-
"""Test event fingerprint generation (SHA-256)"""
|
| 167 |
-
|
| 168 |
-
def test_fingerprint_is_sha256(self):
|
| 169 |
-
"""Test that fingerprint uses SHA-256 (64 hex chars)"""
|
| 170 |
-
event = ReliabilityEvent(
|
| 171 |
-
component="test-service",
|
| 172 |
-
latency_p99=100.0,
|
| 173 |
-
error_rate=0.05,
|
| 174 |
-
throughput=1000.0
|
| 175 |
-
)
|
| 176 |
-
|
| 177 |
-
# SHA-256 produces 64 hex characters
|
| 178 |
-
assert len(event.fingerprint) == 64
|
| 179 |
-
assert all(c in '0123456789abcdef' for c in event.fingerprint)
|
| 180 |
-
|
| 181 |
-
def test_fingerprint_deterministic(self):
|
| 182 |
-
"""Test that same inputs produce same fingerprint"""
|
| 183 |
-
event1 = ReliabilityEvent(
|
| 184 |
-
component="test-service",
|
| 185 |
-
service_mesh="default",
|
| 186 |
-
latency_p99=100.0,
|
| 187 |
-
error_rate=0.05,
|
| 188 |
-
throughput=1000.0
|
| 189 |
-
)
|
| 190 |
-
|
| 191 |
-
event2 = ReliabilityEvent(
|
| 192 |
-
component="test-service",
|
| 193 |
-
service_mesh="default",
|
| 194 |
-
latency_p99=100.0,
|
| 195 |
-
error_rate=0.05,
|
| 196 |
-
throughput=1000.0
|
| 197 |
-
)
|
| 198 |
-
|
| 199 |
-
# Should produce same fingerprint (timestamp not included)
|
| 200 |
-
assert event1.fingerprint == event2.fingerprint
|
| 201 |
-
|
| 202 |
-
def test_fingerprint_different_for_different_events(self):
|
| 203 |
-
"""Test that different events produce different fingerprints"""
|
| 204 |
-
event1 = ReliabilityEvent(
|
| 205 |
-
component="service-1",
|
| 206 |
-
latency_p99=100.0,
|
| 207 |
-
error_rate=0.05,
|
| 208 |
-
throughput=1000.0
|
| 209 |
-
)
|
| 210 |
-
|
| 211 |
-
event2 = ReliabilityEvent(
|
| 212 |
-
component="service-2",
|
| 213 |
-
latency_p99=100.0,
|
| 214 |
-
error_rate=0.05,
|
| 215 |
-
throughput=1000.0
|
| 216 |
-
)
|
| 217 |
-
|
| 218 |
-
assert event1.fingerprint != event2.fingerprint
|
| 219 |
-
|
| 220 |
-
def test_fingerprint_not_md5(self):
|
| 221 |
-
"""Test that fingerprint is NOT MD5 (security fix verification)"""
|
| 222 |
-
event = ReliabilityEvent(
|
| 223 |
-
component="test-service",
|
| 224 |
-
latency_p99=100.0,
|
| 225 |
-
error_rate=0.05,
|
| 226 |
-
throughput=1000.0
|
| 227 |
-
)
|
| 228 |
-
|
| 229 |
-
# MD5 produces 32 hex chars, SHA-256 produces 64
|
| 230 |
-
assert len(event.fingerprint) != 32
|
| 231 |
-
assert len(event.fingerprint) == 64
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
class TestEventImmutability:
|
| 235 |
-
"""Test that events are immutable (frozen)"""
|
| 236 |
-
|
| 237 |
-
def test_event_is_frozen(self):
|
| 238 |
-
"""Test that ReliabilityEvent is frozen"""
|
| 239 |
-
event = ReliabilityEvent(
|
| 240 |
-
component="test-service",
|
| 241 |
-
latency_p99=100.0,
|
| 242 |
-
error_rate=0.05,
|
| 243 |
-
throughput=1000.0
|
| 244 |
-
)
|
| 245 |
-
|
| 246 |
-
# Attempting to modify should raise ValidationError
|
| 247 |
-
with pytest.raises(ValidationError):
|
| 248 |
-
event.latency_p99 = 200.0
|
| 249 |
-
|
| 250 |
-
def test_model_copy_with_update(self):
|
| 251 |
-
"""Test that model_copy creates new instance with updates"""
|
| 252 |
-
event1 = ReliabilityEvent(
|
| 253 |
-
component="test-service",
|
| 254 |
-
latency_p99=100.0,
|
| 255 |
-
error_rate=0.05,
|
| 256 |
-
throughput=1000.0,
|
| 257 |
-
severity=EventSeverity.LOW
|
| 258 |
-
)
|
| 259 |
-
|
| 260 |
-
# Create modified copy
|
| 261 |
-
event2 = event1.model_copy(update={'severity': EventSeverity.HIGH})
|
| 262 |
-
|
| 263 |
-
# Original unchanged
|
| 264 |
-
assert event1.severity == EventSeverity.LOW
|
| 265 |
-
# Copy updated
|
| 266 |
-
assert event2.severity == EventSeverity.HIGH
|
| 267 |
-
# Other fields same
|
| 268 |
-
assert event2.component == event1.component
|
| 269 |
-
assert event2.latency_p99 == event1.latency_p99
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
class TestDependencyValidation:
|
| 273 |
-
"""Test dependency cycle detection"""
|
| 274 |
-
|
| 275 |
-
def test_valid_dependencies(self):
|
| 276 |
-
"""Test valid dependency configuration"""
|
| 277 |
-
event = ReliabilityEvent(
|
| 278 |
-
component="api-service",
|
| 279 |
-
latency_p99=100.0,
|
| 280 |
-
error_rate=0.05,
|
| 281 |
-
throughput=1000.0,
|
| 282 |
-
upstream_deps=["auth-service", "database"],
|
| 283 |
-
downstream_deps=["frontend", "mobile-app"]
|
| 284 |
-
)
|
| 285 |
-
|
| 286 |
-
assert "auth-service" in event.upstream_deps
|
| 287 |
-
assert "frontend" in event.downstream_deps
|
| 288 |
-
|
| 289 |
-
def test_circular_dependency_detected(self):
|
| 290 |
-
"""Test that circular dependencies are detected"""
|
| 291 |
-
with pytest.raises(ValidationError) as exc_info:
|
| 292 |
-
ReliabilityEvent(
|
| 293 |
-
component="api-service",
|
| 294 |
-
latency_p99=100.0,
|
| 295 |
-
error_rate=0.05,
|
| 296 |
-
throughput=1000.0,
|
| 297 |
-
upstream_deps=["auth-service", "database"],
|
| 298 |
-
downstream_deps=["database", "frontend"] # 'database' in both
|
| 299 |
-
)
|
| 300 |
-
|
| 301 |
-
error_msg = str(exc_info.value).lower()
|
| 302 |
-
assert "circular" in error_msg or "database" in error_msg
|
| 303 |
-
|
| 304 |
-
def test_dependency_name_validation(self):
|
| 305 |
-
"""Test that dependency names follow same rules as component IDs"""
|
| 306 |
-
# Valid dependency names
|
| 307 |
-
event = ReliabilityEvent(
|
| 308 |
-
component="api-service",
|
| 309 |
-
latency_p99=100.0,
|
| 310 |
-
error_rate=0.05,
|
| 311 |
-
throughput=1000.0,
|
| 312 |
-
upstream_deps=["auth-service", "db-01", "cache-v2"]
|
| 313 |
-
)
|
| 314 |
-
assert len(event.upstream_deps) == 3
|
| 315 |
-
|
| 316 |
-
# Invalid dependency names
|
| 317 |
-
with pytest.raises(ValidationError):
|
| 318 |
-
ReliabilityEvent(
|
| 319 |
-
component="api-service",
|
| 320 |
-
latency_p99=100.0,
|
| 321 |
-
error_rate=0.05,
|
| 322 |
-
throughput=1000.0,
|
| 323 |
-
upstream_deps=["AUTH_SERVICE"] # Uppercase/underscore
|
| 324 |
-
)
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
class TestPolicyConditionModel:
|
| 328 |
-
"""Test PolicyCondition structured model"""
|
| 329 |
-
|
| 330 |
-
def test_valid_policy_condition(self):
|
| 331 |
-
"""Test creating valid policy conditions"""
|
| 332 |
-
condition = PolicyCondition(
|
| 333 |
-
metric="latency_p99",
|
| 334 |
-
operator="gt",
|
| 335 |
-
threshold=150.0
|
| 336 |
-
)
|
| 337 |
-
|
| 338 |
-
assert condition.metric == "latency_p99"
|
| 339 |
-
assert condition.operator == "gt"
|
| 340 |
-
assert condition.threshold == 150.0
|
| 341 |
-
|
| 342 |
-
def test_policy_condition_frozen(self):
|
| 343 |
-
"""Test that PolicyCondition is immutable"""
|
| 344 |
-
condition = PolicyCondition(
|
| 345 |
-
metric="error_rate",
|
| 346 |
-
operator="gt",
|
| 347 |
-
threshold=0.1
|
| 348 |
-
)
|
| 349 |
-
|
| 350 |
-
with pytest.raises(ValidationError):
|
| 351 |
-
condition.threshold = 0.2
|
| 352 |
-
|
| 353 |
-
def test_invalid_metric(self):
|
| 354 |
-
"""Test that invalid metrics are rejected"""
|
| 355 |
-
with pytest.raises(ValidationError):
|
| 356 |
-
PolicyCondition(
|
| 357 |
-
metric="invalid_metric",
|
| 358 |
-
operator="gt",
|
| 359 |
-
threshold=100.0
|
| 360 |
-
)
|
| 361 |
-
|
| 362 |
-
def test_invalid_operator(self):
|
| 363 |
-
"""Test that invalid operators are rejected"""
|
| 364 |
-
with pytest.raises(ValidationError):
|
| 365 |
-
PolicyCondition(
|
| 366 |
-
metric="latency_p99",
|
| 367 |
-
operator="invalid_op",
|
| 368 |
-
threshold=100.0
|
| 369 |
-
)
|
| 370 |
-
|
| 371 |
-
def test_negative_threshold(self):
|
| 372 |
-
"""Test that negative thresholds are rejected"""
|
| 373 |
-
with pytest.raises(ValidationError):
|
| 374 |
-
PolicyCondition(
|
| 375 |
-
metric="latency_p99",
|
| 376 |
-
operator="gt",
|
| 377 |
-
threshold=-100.0
|
| 378 |
-
)
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
class TestHealingPolicyModel:
|
| 382 |
-
"""Test HealingPolicy model"""
|
| 383 |
-
|
| 384 |
-
def test_valid_healing_policy(self):
|
| 385 |
-
"""Test creating valid healing policy"""
|
| 386 |
-
policy = HealingPolicy(
|
| 387 |
-
name="high_latency_restart",
|
| 388 |
-
conditions=[
|
| 389 |
-
PolicyCondition(metric="latency_p99", operator="gt", threshold=300.0)
|
| 390 |
-
],
|
| 391 |
-
actions=[HealingAction.RESTART_CONTAINER, HealingAction.ALERT_TEAM],
|
| 392 |
-
priority=1,
|
| 393 |
-
cool_down_seconds=300
|
| 394 |
-
)
|
| 395 |
-
|
| 396 |
-
assert policy.name == "high_latency_restart"
|
| 397 |
-
assert len(policy.conditions) == 1
|
| 398 |
-
assert len(policy.actions) == 2
|
| 399 |
-
assert policy.priority == 1
|
| 400 |
-
|
| 401 |
-
def test_policy_frozen(self):
|
| 402 |
-
"""Test that HealingPolicy is immutable"""
|
| 403 |
-
policy = HealingPolicy(
|
| 404 |
-
name="test_policy",
|
| 405 |
-
conditions=[
|
| 406 |
-
PolicyCondition(metric="error_rate", operator="gt", threshold=0.1)
|
| 407 |
-
],
|
| 408 |
-
actions=[HealingAction.ROLLBACK],
|
| 409 |
-
priority=2
|
| 410 |
-
)
|
| 411 |
-
|
| 412 |
-
with pytest.raises(ValidationError):
|
| 413 |
-
policy.priority = 5
|
| 414 |
-
|
| 415 |
-
def test_empty_conditions_rejected(self):
|
| 416 |
-
"""Test that policies must have at least one condition"""
|
| 417 |
-
with pytest.raises(ValidationError):
|
| 418 |
-
HealingPolicy(
|
| 419 |
-
name="empty_policy",
|
| 420 |
-
conditions=[], # Empty
|
| 421 |
-
actions=[HealingAction.ALERT_TEAM],
|
| 422 |
-
priority=3
|
| 423 |
-
)
|
| 424 |
-
|
| 425 |
-
def test_empty_actions_rejected(self):
|
| 426 |
-
"""Test that policies must have at least one action"""
|
| 427 |
-
with pytest.raises(ValidationError):
|
| 428 |
-
HealingPolicy(
|
| 429 |
-
name="empty_actions",
|
| 430 |
-
conditions=[
|
| 431 |
-
PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)
|
| 432 |
-
],
|
| 433 |
-
actions=[], # Empty
|
| 434 |
-
priority=3
|
| 435 |
-
)
|
| 436 |
-
|
| 437 |
-
def test_priority_bounds(self):
|
| 438 |
-
"""Test priority validation (1-5)"""
|
| 439 |
-
# Valid priority
|
| 440 |
-
policy = HealingPolicy(
|
| 441 |
-
name="test",
|
| 442 |
-
conditions=[PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)],
|
| 443 |
-
actions=[HealingAction.ALERT_TEAM],
|
| 444 |
-
priority=3
|
| 445 |
-
)
|
| 446 |
-
assert policy.priority == 3
|
| 447 |
-
|
| 448 |
-
# Priority < 1 should fail
|
| 449 |
-
with pytest.raises(ValidationError):
|
| 450 |
-
HealingPolicy(
|
| 451 |
-
name="test",
|
| 452 |
-
conditions=[PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)],
|
| 453 |
-
actions=[HealingAction.ALERT_TEAM],
|
| 454 |
-
priority=0
|
| 455 |
-
)
|
| 456 |
-
|
| 457 |
-
# Priority > 5 should fail
|
| 458 |
-
with pytest.raises(ValidationError):
|
| 459 |
-
HealingPolicy(
|
| 460 |
-
name="test",
|
| 461 |
-
conditions=[PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)],
|
| 462 |
-
actions=[HealingAction.ALERT_TEAM],
|
| 463 |
-
priority=10
|
| 464 |
-
)
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
class TestAnomalyResultModel:
|
| 468 |
-
"""Test AnomalyResult model"""
|
| 469 |
-
|
| 470 |
-
def test_valid_anomaly_result(self):
|
| 471 |
-
"""Test creating valid anomaly result"""
|
| 472 |
-
result = AnomalyResult(
|
| 473 |
-
is_anomaly=True,
|
| 474 |
-
confidence=0.85,
|
| 475 |
-
anomaly_score=0.75,
|
| 476 |
-
affected_metrics=["latency", "error_rate"]
|
| 477 |
-
)
|
| 478 |
-
|
| 479 |
-
assert result.is_anomaly is True
|
| 480 |
-
assert result.confidence == 0.85
|
| 481 |
-
assert isinstance(result.detection_timestamp, datetime)
|
| 482 |
-
|
| 483 |
-
def test_confidence_bounds(self):
|
| 484 |
-
"""Test confidence is bounded 0-1"""
|
| 485 |
-
# Valid
|
| 486 |
-
result = AnomalyResult(
|
| 487 |
-
is_anomaly=True,
|
| 488 |
-
confidence=0.5,
|
| 489 |
-
anomaly_score=0.6
|
| 490 |
-
)
|
| 491 |
-
assert result.confidence == 0.5
|
| 492 |
-
|
| 493 |
-
# Confidence > 1 should fail
|
| 494 |
-
with pytest.raises(ValidationError):
|
| 495 |
-
AnomalyResult(
|
| 496 |
-
is_anomaly=True,
|
| 497 |
-
confidence=1.5,
|
| 498 |
-
anomaly_score=0.5
|
| 499 |
-
)
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
class TestForecastResultModel:
|
| 503 |
-
"""Test ForecastResult model"""
|
| 504 |
-
|
| 505 |
-
def test_valid_forecast(self):
|
| 506 |
-
"""Test creating valid forecast"""
|
| 507 |
-
result = ForecastResult(
|
| 508 |
-
metric="latency",
|
| 509 |
-
predicted_value=250.0,
|
| 510 |
-
confidence=0.75,
|
| 511 |
-
trend="increasing",
|
| 512 |
-
time_to_threshold=15.5,
|
| 513 |
-
risk_level="high"
|
| 514 |
-
)
|
| 515 |
-
|
| 516 |
-
assert result.metric == "latency"
|
| 517 |
-
assert result.trend == "increasing"
|
| 518 |
-
assert result.risk_level == "high"
|
| 519 |
-
|
| 520 |
-
def test_trend_validation(self):
|
| 521 |
-
"""Test that only valid trends are accepted"""
|
| 522 |
-
valid_trends = ["increasing", "decreasing", "stable"]
|
| 523 |
-
|
| 524 |
-
for trend in valid_trends:
|
| 525 |
-
result = ForecastResult(
|
| 526 |
-
metric="latency",
|
| 527 |
-
predicted_value=200.0,
|
| 528 |
-
confidence=0.7,
|
| 529 |
-
trend=trend,
|
| 530 |
-
risk_level="medium"
|
| 531 |
-
)
|
| 532 |
-
assert result.trend == trend
|
| 533 |
-
|
| 534 |
-
# Invalid trend
|
| 535 |
-
with pytest.raises(ValidationError):
|
| 536 |
-
ForecastResult(
|
| 537 |
-
metric="latency",
|
| 538 |
-
predicted_value=200.0,
|
| 539 |
-
confidence=0.7,
|
| 540 |
-
trend="invalid_trend",
|
| 541 |
-
risk_level="medium"
|
| 542 |
-
)
|
| 543 |
-
|
| 544 |
-
def test_risk_level_validation(self):
|
| 545 |
-
"""Test that only valid risk levels are accepted"""
|
| 546 |
-
valid_levels = ["low", "medium", "high", "critical"]
|
| 547 |
-
|
| 548 |
-
for level in valid_levels:
|
| 549 |
-
result = ForecastResult(
|
| 550 |
-
metric="error_rate",
|
| 551 |
-
predicted_value=0.08,
|
| 552 |
-
confidence=0.8,
|
| 553 |
-
trend="stable",
|
| 554 |
-
risk_level=level
|
| 555 |
-
)
|
| 556 |
-
assert result.risk_level == level
|
| 557 |
-
|
| 558 |
-
# Invalid risk level
|
| 559 |
-
with pytest.raises(ValidationError):
|
| 560 |
-
ForecastResult(
|
| 561 |
-
metric="error_rate",
|
| 562 |
-
predicted_value=0.08,
|
| 563 |
-
confidence=0.8,
|
| 564 |
-
trend="stable",
|
| 565 |
-
risk_level="extreme"
|
| 566 |
-
)
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
class TestTimestampHandling:
|
| 570 |
-
"""Test datetime timestamp handling"""
|
| 571 |
-
|
| 572 |
-
def test_timestamp_is_datetime(self):
|
| 573 |
-
"""Test that timestamp is datetime, not string"""
|
| 574 |
-
event = ReliabilityEvent(
|
| 575 |
-
component="test-service",
|
| 576 |
-
latency_p99=100.0,
|
| 577 |
-
error_rate=0.05,
|
| 578 |
-
throughput=1000.0
|
| 579 |
-
)
|
| 580 |
-
|
| 581 |
-
# Should be datetime object
|
| 582 |
-
assert isinstance(event.timestamp, datetime)
|
| 583 |
-
|
| 584 |
-
# Should have timezone
|
| 585 |
-
assert event.timestamp.tzinfo is not None
|
| 586 |
-
|
| 587 |
-
def test_timestamp_is_utc(self):
|
| 588 |
-
"""Test that timestamp uses UTC"""
|
| 589 |
-
event = ReliabilityEvent(
|
| 590 |
-
component="test-service",
|
| 591 |
-
latency_p99=100.0,
|
| 592 |
-
error_rate=0.05,
|
| 593 |
-
throughput=1000.0
|
| 594 |
-
)
|
| 595 |
-
|
| 596 |
-
assert event.timestamp.tzinfo == timezone.utc
|
| 597 |
-
|
| 598 |
-
def test_timestamp_serialization(self):
|
| 599 |
-
"""Test that timestamp can be serialized"""
|
| 600 |
-
event = ReliabilityEvent(
|
| 601 |
-
component="test-service",
|
| 602 |
-
latency_p99=100.0,
|
| 603 |
-
error_rate=0.05,
|
| 604 |
-
throughput=1000.0
|
| 605 |
-
)
|
| 606 |
-
|
| 607 |
-
# Can convert to ISO format
|
| 608 |
-
iso_str = event.timestamp.isoformat()
|
| 609 |
-
assert isinstance(iso_str, str)
|
| 610 |
-
assert 'T' in iso_str # ISO format
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
if __name__ == "__main__":
|
| 614 |
-
pytest.main([__file__, "-v", "--tb=short"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_policy_engine.py
DELETED
|
@@ -1,291 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Unit tests for PolicyEngine with thread safety and concurrency tests
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import pytest
|
| 6 |
-
import threading
|
| 7 |
-
import time
|
| 8 |
-
from datetime import datetime, timezone
|
| 9 |
-
from models import ReliabilityEvent, EventSeverity, HealingPolicy, HealingAction, PolicyCondition
|
| 10 |
-
from healing_policies import PolicyEngine
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class TestPolicyEngineBasics:
|
| 14 |
-
"""Basic policy engine functionality tests"""
|
| 15 |
-
|
| 16 |
-
def test_initialization(self, policy_engine):
|
| 17 |
-
"""Test policy engine initializes correctly"""
|
| 18 |
-
assert policy_engine is not None
|
| 19 |
-
assert len(policy_engine.policies) > 0
|
| 20 |
-
assert policy_engine.max_cooldown_history == 100
|
| 21 |
-
|
| 22 |
-
def test_policy_evaluation_no_match(self, policy_engine, normal_event):
|
| 23 |
-
"""Test that normal events don't trigger policies"""
|
| 24 |
-
actions = policy_engine.evaluate_policies(normal_event)
|
| 25 |
-
assert actions == [HealingAction.NO_ACTION]
|
| 26 |
-
|
| 27 |
-
def test_policy_evaluation_match(self, policy_engine, critical_event):
|
| 28 |
-
"""Test that critical events trigger policies"""
|
| 29 |
-
actions = policy_engine.evaluate_policies(critical_event)
|
| 30 |
-
assert len(actions) > 0
|
| 31 |
-
assert HealingAction.NO_ACTION not in actions
|
| 32 |
-
|
| 33 |
-
def test_policy_disabled(self, sample_policy, sample_event):
|
| 34 |
-
"""Test that disabled policies don't execute"""
|
| 35 |
-
disabled_policy = sample_policy.model_copy(update={'enabled': False})
|
| 36 |
-
engine = PolicyEngine(policies=[disabled_policy])
|
| 37 |
-
|
| 38 |
-
actions = engine.evaluate_policies(sample_event)
|
| 39 |
-
assert actions == [HealingAction.NO_ACTION]
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
class TestPolicyCooldown:
|
| 43 |
-
"""Test cooldown mechanism"""
|
| 44 |
-
|
| 45 |
-
def test_cooldown_prevents_immediate_re_execution(self, sample_policy, sample_event):
|
| 46 |
-
"""Test that cooldown prevents immediate re-execution"""
|
| 47 |
-
policy = sample_policy.model_copy(update={'cool_down_seconds': 60})
|
| 48 |
-
engine = PolicyEngine(policies=[policy])
|
| 49 |
-
|
| 50 |
-
# First execution should work
|
| 51 |
-
actions1 = engine.evaluate_policies(sample_event)
|
| 52 |
-
assert HealingAction.RESTART_CONTAINER in actions1
|
| 53 |
-
|
| 54 |
-
# Second execution should be blocked by cooldown
|
| 55 |
-
actions2 = engine.evaluate_policies(sample_event)
|
| 56 |
-
assert actions2 == [HealingAction.NO_ACTION]
|
| 57 |
-
|
| 58 |
-
def test_cooldown_expires(self, sample_policy, sample_event):
|
| 59 |
-
"""Test that actions work again after cooldown expires"""
|
| 60 |
-
policy = sample_policy.model_copy(update={'cool_down_seconds': 1})
|
| 61 |
-
engine = PolicyEngine(policies=[policy])
|
| 62 |
-
|
| 63 |
-
# First execution
|
| 64 |
-
actions1 = engine.evaluate_policies(sample_event)
|
| 65 |
-
assert HealingAction.RESTART_CONTAINER in actions1
|
| 66 |
-
|
| 67 |
-
# Wait for cooldown to expire
|
| 68 |
-
time.sleep(1.1)
|
| 69 |
-
|
| 70 |
-
# Should work again
|
| 71 |
-
actions2 = engine.evaluate_policies(sample_event)
|
| 72 |
-
assert HealingAction.RESTART_CONTAINER in actions2
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
class TestRateLimiting:
|
| 76 |
-
"""Test rate limiting functionality"""
|
| 77 |
-
|
| 78 |
-
def test_rate_limit_enforcement(self, sample_policy, sample_event):
|
| 79 |
-
"""Test that rate limiting prevents excessive executions"""
|
| 80 |
-
policy = sample_policy.model_copy(update={
|
| 81 |
-
'cool_down_seconds': 0, # No cooldown
|
| 82 |
-
'max_executions_per_hour': 3
|
| 83 |
-
})
|
| 84 |
-
engine = PolicyEngine(policies=[policy])
|
| 85 |
-
|
| 86 |
-
# Execute 3 times (should all work)
|
| 87 |
-
for i in range(3):
|
| 88 |
-
actions = engine.evaluate_policies(sample_event)
|
| 89 |
-
assert HealingAction.RESTART_CONTAINER in actions
|
| 90 |
-
time.sleep(0.1) # Small delay to avoid race
|
| 91 |
-
|
| 92 |
-
# 4th execution should be rate limited
|
| 93 |
-
actions = engine.evaluate_policies(sample_event)
|
| 94 |
-
assert actions == [HealingAction.NO_ACTION]
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
class TestThreadSafety:
|
| 98 |
-
"""Test thread safety of policy engine"""
|
| 99 |
-
|
| 100 |
-
def test_concurrent_evaluations_no_race_condition(self, sample_policy, sample_event):
|
| 101 |
-
"""
|
| 102 |
-
CRITICAL TEST: Verify no race condition in cooldown check
|
| 103 |
-
|
| 104 |
-
This tests the fix for the race condition where multiple threads
|
| 105 |
-
could simultaneously pass the cooldown check
|
| 106 |
-
"""
|
| 107 |
-
policy = sample_policy.model_copy(update={'cool_down_seconds': 5})
|
| 108 |
-
engine = PolicyEngine(policies=[policy])
|
| 109 |
-
|
| 110 |
-
results = []
|
| 111 |
-
|
| 112 |
-
def evaluate():
|
| 113 |
-
actions = engine.evaluate_policies(sample_event)
|
| 114 |
-
results.append(actions)
|
| 115 |
-
|
| 116 |
-
# Launch 10 concurrent threads
|
| 117 |
-
threads = [threading.Thread(target=evaluate) for _ in range(10)]
|
| 118 |
-
for t in threads:
|
| 119 |
-
t.start()
|
| 120 |
-
for t in threads:
|
| 121 |
-
t.join()
|
| 122 |
-
|
| 123 |
-
# Count how many actually triggered the policy
|
| 124 |
-
trigger_count = sum(
|
| 125 |
-
1 for actions in results
|
| 126 |
-
if HealingAction.RESTART_CONTAINER in actions
|
| 127 |
-
)
|
| 128 |
-
|
| 129 |
-
# Only ONE should have triggered (atomic check + update)
|
| 130 |
-
assert trigger_count == 1, f"Expected 1 trigger, got {trigger_count}"
|
| 131 |
-
|
| 132 |
-
def test_concurrent_different_components(self, sample_policy):
|
| 133 |
-
"""Test that different components don't interfere with each other"""
|
| 134 |
-
engine = PolicyEngine(policies=[sample_policy])
|
| 135 |
-
|
| 136 |
-
results = {'service-1': [], 'service-2': []}
|
| 137 |
-
|
| 138 |
-
def evaluate_service(service_name):
|
| 139 |
-
event = ReliabilityEvent(
|
| 140 |
-
component=service_name,
|
| 141 |
-
latency_p99=400.0,
|
| 142 |
-
error_rate=0.1,
|
| 143 |
-
throughput=1000.0
|
| 144 |
-
)
|
| 145 |
-
actions = engine.evaluate_policies(event)
|
| 146 |
-
results[service_name].append(actions)
|
| 147 |
-
|
| 148 |
-
# Run both services concurrently multiple times
|
| 149 |
-
threads = []
|
| 150 |
-
for _ in range(5):
|
| 151 |
-
threads.append(threading.Thread(target=evaluate_service, args=('service-1',)))
|
| 152 |
-
threads.append(threading.Thread(target=evaluate_service, args=('service-2',)))
|
| 153 |
-
|
| 154 |
-
for t in threads:
|
| 155 |
-
t.start()
|
| 156 |
-
for t in threads:
|
| 157 |
-
t.join()
|
| 158 |
-
|
| 159 |
-
# Each service should have triggered at least once
|
| 160 |
-
assert any(HealingAction.RESTART_CONTAINER in actions
|
| 161 |
-
for actions in results['service-1'])
|
| 162 |
-
assert any(HealingAction.RESTART_CONTAINER in actions
|
| 163 |
-
for actions in results['service-2'])
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
class TestMemoryManagement:
|
| 167 |
-
"""Test memory leak prevention"""
|
| 168 |
-
|
| 169 |
-
def test_cooldown_history_bounded(self, sample_policy):
|
| 170 |
-
"""Test that cooldown history doesn't grow unbounded"""
|
| 171 |
-
engine = PolicyEngine(
|
| 172 |
-
policies=[sample_policy],
|
| 173 |
-
max_cooldown_history=100
|
| 174 |
-
)
|
| 175 |
-
|
| 176 |
-
# Trigger policy for many different components
|
| 177 |
-
for i in range(500):
|
| 178 |
-
event = ReliabilityEvent(
|
| 179 |
-
component=f"service-{i}",
|
| 180 |
-
latency_p99=400.0,
|
| 181 |
-
error_rate=0.1,
|
| 182 |
-
throughput=1000.0
|
| 183 |
-
)
|
| 184 |
-
engine.evaluate_policies(event)
|
| 185 |
-
|
| 186 |
-
# Cooldown history should be capped
|
| 187 |
-
assert len(engine.last_execution) <= engine.max_cooldown_history
|
| 188 |
-
|
| 189 |
-
def test_execution_history_bounded(self, sample_policy):
|
| 190 |
-
"""Test that execution history is bounded"""
|
| 191 |
-
engine = PolicyEngine(
|
| 192 |
-
policies=[sample_policy],
|
| 193 |
-
max_execution_history=50
|
| 194 |
-
)
|
| 195 |
-
|
| 196 |
-
# Trigger many times
|
| 197 |
-
for i in range(200):
|
| 198 |
-
event = ReliabilityEvent(
|
| 199 |
-
component="test-service",
|
| 200 |
-
latency_p99=400.0,
|
| 201 |
-
error_rate=0.1,
|
| 202 |
-
throughput=1000.0
|
| 203 |
-
)
|
| 204 |
-
engine.evaluate_policies(event)
|
| 205 |
-
time.sleep(0.01)
|
| 206 |
-
|
| 207 |
-
# Check execution history size
|
| 208 |
-
for timestamps in engine.execution_timestamps.values():
|
| 209 |
-
assert len(timestamps) <= engine.max_execution_history
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
class TestPriorityHandling:
|
| 213 |
-
"""Test priority-based policy evaluation"""
|
| 214 |
-
|
| 215 |
-
def test_policies_evaluated_by_priority(self):
|
| 216 |
-
"""Test that higher priority policies are evaluated first"""
|
| 217 |
-
high_priority = HealingPolicy(
|
| 218 |
-
name="high_priority",
|
| 219 |
-
conditions=[PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)],
|
| 220 |
-
actions=[HealingAction.ROLLBACK],
|
| 221 |
-
priority=1
|
| 222 |
-
)
|
| 223 |
-
|
| 224 |
-
low_priority = HealingPolicy(
|
| 225 |
-
name="low_priority",
|
| 226 |
-
conditions=[PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)],
|
| 227 |
-
actions=[HealingAction.ALERT_TEAM],
|
| 228 |
-
priority=5
|
| 229 |
-
)
|
| 230 |
-
|
| 231 |
-
# Add in reverse priority order
|
| 232 |
-
engine = PolicyEngine(policies=[low_priority, high_priority])
|
| 233 |
-
|
| 234 |
-
event = ReliabilityEvent(
|
| 235 |
-
component="test",
|
| 236 |
-
latency_p99=200.0,
|
| 237 |
-
error_rate=0.05,
|
| 238 |
-
throughput=1000.0
|
| 239 |
-
)
|
| 240 |
-
|
| 241 |
-
actions = engine.evaluate_policies(event)
|
| 242 |
-
|
| 243 |
-
# Both should execute, but high priority action should come first
|
| 244 |
-
assert HealingAction.ROLLBACK in actions
|
| 245 |
-
assert HealingAction.ALERT_TEAM in actions
|
| 246 |
-
assert actions.index(HealingAction.ROLLBACK) < actions.index(HealingAction.ALERT_TEAM)
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
class TestOperatorComparisons:
|
| 250 |
-
"""Test operator comparison logic"""
|
| 251 |
-
|
| 252 |
-
def test_greater_than_operator(self, policy_engine):
|
| 253 |
-
"""Test > operator"""
|
| 254 |
-
result = policy_engine._compare_values(100.0, "gt", 50.0)
|
| 255 |
-
assert result is True
|
| 256 |
-
|
| 257 |
-
result = policy_engine._compare_values(50.0, "gt", 100.0)
|
| 258 |
-
assert result is False
|
| 259 |
-
|
| 260 |
-
def test_less_than_operator(self, policy_engine):
|
| 261 |
-
"""Test < operator"""
|
| 262 |
-
result = policy_engine._compare_values(50.0, "lt", 100.0)
|
| 263 |
-
assert result is True
|
| 264 |
-
|
| 265 |
-
result = policy_engine._compare_values(100.0, "lt", 50.0)
|
| 266 |
-
assert result is False
|
| 267 |
-
|
| 268 |
-
def test_type_mismatch_handling(self, policy_engine):
|
| 269 |
-
"""Test that type mismatches are handled gracefully"""
|
| 270 |
-
result = policy_engine._compare_values("invalid", "gt", 50.0)
|
| 271 |
-
assert result is False
|
| 272 |
-
|
| 273 |
-
def test_none_value_handling(self, sample_policy):
|
| 274 |
-
"""Test that None values are handled correctly"""
|
| 275 |
-
engine = PolicyEngine(policies=[sample_policy])
|
| 276 |
-
|
| 277 |
-
event = ReliabilityEvent(
|
| 278 |
-
component="test",
|
| 279 |
-
latency_p99=100.0,
|
| 280 |
-
error_rate=0.05,
|
| 281 |
-
throughput=1000.0,
|
| 282 |
-
cpu_util=None # None value
|
| 283 |
-
)
|
| 284 |
-
|
| 285 |
-
# Should not crash
|
| 286 |
-
actions = engine.evaluate_policies(event)
|
| 287 |
-
assert actions is not None
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
if __name__ == "__main__":
|
| 291 |
-
pytest.main([__file__, "-v", "--tb=short"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_timeline_calculator.py
DELETED
|
@@ -1,169 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Test suite for TimelineCalculator
|
| 3 |
-
|
| 4 |
-
Tests the core calculation logic for incident response timeline comparisons.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import pytest
|
| 8 |
-
from datetime import datetime
|
| 9 |
-
from typing import Dict, Any
|
| 10 |
-
|
| 11 |
-
# Import your timeline calculator (adjust path as needed)
|
| 12 |
-
# from app import TimelineCalculator, TimelineMetrics
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
class TestTimelineCalculator:
|
| 16 |
-
"""Test suite for TimelineCalculator class"""
|
| 17 |
-
|
| 18 |
-
@pytest.fixture
|
| 19 |
-
def calculator(self):
|
| 20 |
-
"""Create a TimelineCalculator instance for testing"""
|
| 21 |
-
# TODO: Initialize with test parameters
|
| 22 |
-
pass
|
| 23 |
-
|
| 24 |
-
def test_calculator_initialization(self):
|
| 25 |
-
"""Test that calculator initializes with correct defaults"""
|
| 26 |
-
# TODO: Verify default values
|
| 27 |
-
# - industry_avg_response_min = 14.0
|
| 28 |
-
# - arf_avg_response_min = 2.0
|
| 29 |
-
# - cost_per_minute = 50000.0
|
| 30 |
-
pass
|
| 31 |
-
|
| 32 |
-
def test_calculator_custom_initialization(self):
|
| 33 |
-
"""Test calculator initialization with custom values"""
|
| 34 |
-
# TODO: Create calculator with custom values and verify
|
| 35 |
-
pass
|
| 36 |
-
|
| 37 |
-
def test_calculate_metrics_default(self):
|
| 38 |
-
"""Test metrics calculation with default parameters"""
|
| 39 |
-
# TODO: Calculate metrics and verify structure
|
| 40 |
-
# - Should return TimelineMetrics instance
|
| 41 |
-
# - All fields should be populated
|
| 42 |
-
pass
|
| 43 |
-
|
| 44 |
-
def test_calculate_metrics_critical_severity(self, calculator):
|
| 45 |
-
"""Test metrics calculation for CRITICAL severity incident"""
|
| 46 |
-
# TODO: Calculate for CRITICAL severity
|
| 47 |
-
# - Verify industry_total_min is correct
|
| 48 |
-
# - Verify arf_total_min is correct
|
| 49 |
-
# - Verify cost_savings calculation
|
| 50 |
-
pass
|
| 51 |
-
|
| 52 |
-
def test_calculate_metrics_high_severity(self, calculator):
|
| 53 |
-
"""Test metrics calculation for HIGH severity incident"""
|
| 54 |
-
# TODO: Calculate for HIGH severity
|
| 55 |
-
# - May have different response times
|
| 56 |
-
pass
|
| 57 |
-
|
| 58 |
-
def test_calculate_metrics_low_severity(self, calculator):
|
| 59 |
-
"""Test metrics calculation for LOW severity incident"""
|
| 60 |
-
# TODO: Calculate for LOW severity
|
| 61 |
-
pass
|
| 62 |
-
|
| 63 |
-
def test_time_savings_calculation(self, calculator):
|
| 64 |
-
"""Test that time savings are calculated correctly"""
|
| 65 |
-
# TODO: Verify time_saved_min = industry_total - arf_total
|
| 66 |
-
pass
|
| 67 |
-
|
| 68 |
-
def test_cost_savings_calculation(self, calculator):
|
| 69 |
-
"""Test that cost savings are calculated correctly"""
|
| 70 |
-
# TODO: Verify cost_savings = (industry_total - arf_total) * cost_per_minute
|
| 71 |
-
pass
|
| 72 |
-
|
| 73 |
-
def test_time_improvement_percentage(self, calculator):
|
| 74 |
-
"""Test that time improvement percentage is correct"""
|
| 75 |
-
# TODO: Verify time_improvement_pct = (time_saved / industry_total) * 100
|
| 76 |
-
pass
|
| 77 |
-
|
| 78 |
-
def test_zero_cost_per_minute(self):
|
| 79 |
-
"""Test behavior when cost_per_minute is 0"""
|
| 80 |
-
# TODO: Edge case - should not crash
|
| 81 |
-
pass
|
| 82 |
-
|
| 83 |
-
def test_negative_values_handling(self):
|
| 84 |
-
"""Test that negative values are handled appropriately"""
|
| 85 |
-
# TODO: Should raise error or handle gracefully
|
| 86 |
-
pass
|
| 87 |
-
|
| 88 |
-
def test_calculate_metrics_different_components(self, calculator):
|
| 89 |
-
"""Test that different components can have different timelines"""
|
| 90 |
-
# TODO: Test api-service vs database vs cache-service
|
| 91 |
-
# - May have different complexity
|
| 92 |
-
pass
|
| 93 |
-
|
| 94 |
-
def test_metrics_immutability(self, calculator):
|
| 95 |
-
"""Test that calculated metrics are immutable"""
|
| 96 |
-
# TODO: Verify TimelineMetrics is a frozen dataclass
|
| 97 |
-
pass
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
class TestTimelineMetrics:
|
| 101 |
-
"""Test suite for TimelineMetrics dataclass"""
|
| 102 |
-
|
| 103 |
-
def test_metrics_creation(self):
|
| 104 |
-
"""Test creating TimelineMetrics with all fields"""
|
| 105 |
-
# TODO: Create instance and verify all fields
|
| 106 |
-
pass
|
| 107 |
-
|
| 108 |
-
def test_metrics_default_values(self):
|
| 109 |
-
"""Test that metrics have sensible default values"""
|
| 110 |
-
# TODO: Verify defaults are set correctly
|
| 111 |
-
pass
|
| 112 |
-
|
| 113 |
-
def test_metrics_serialization(self):
|
| 114 |
-
"""Test that metrics can be serialized to dict/JSON"""
|
| 115 |
-
# TODO: Verify can convert to dict for API responses
|
| 116 |
-
pass
|
| 117 |
-
|
| 118 |
-
def test_metrics_field_types(self):
|
| 119 |
-
"""Test that all fields have correct types"""
|
| 120 |
-
# TODO: Verify float types for time/cost values
|
| 121 |
-
pass
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
class TestTimelineCalculatorEdgeCases:
|
| 125 |
-
"""Test edge cases and error conditions"""
|
| 126 |
-
|
| 127 |
-
def test_very_fast_arf_response(self):
|
| 128 |
-
"""Test when ARF response is < 1 minute"""
|
| 129 |
-
# TODO: Verify calculations still work
|
| 130 |
-
pass
|
| 131 |
-
|
| 132 |
-
def test_very_slow_industry_response(self):
|
| 133 |
-
"""Test when industry response is > 60 minutes"""
|
| 134 |
-
# TODO: Verify calculations scale correctly
|
| 135 |
-
pass
|
| 136 |
-
|
| 137 |
-
def test_equal_response_times(self):
|
| 138 |
-
"""Test when industry and ARF times are equal"""
|
| 139 |
-
# TODO: Should show 0% improvement
|
| 140 |
-
pass
|
| 141 |
-
|
| 142 |
-
def test_concurrent_calculations(self):
|
| 143 |
-
"""Test that calculator is thread-safe"""
|
| 144 |
-
# TODO: Run multiple calculations concurrently
|
| 145 |
-
pass
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
# Parametrized tests for different scenarios
|
| 149 |
-
@pytest.mark.parametrize("severity,expected_industry_min,expected_arf_min", [
|
| 150 |
-
("CRITICAL", 60.0, 5.0),
|
| 151 |
-
("HIGH", 45.0, 4.0),
|
| 152 |
-
("MEDIUM", 30.0, 3.0),
|
| 153 |
-
("LOW", 20.0, 2.0),
|
| 154 |
-
])
|
| 155 |
-
def test_calculate_metrics_by_severity(severity, expected_industry_min, expected_arf_min):
|
| 156 |
-
"""Test that different severities produce different timelines"""
|
| 157 |
-
# TODO: Implement parametrized test
|
| 158 |
-
pass
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
@pytest.mark.parametrize("cost_per_minute,expected_savings", [
|
| 162 |
-
(50000.0, 2750000.0),
|
| 163 |
-
(100000.0, 5500000.0),
|
| 164 |
-
(10000.0, 550000.0),
|
| 165 |
-
])
|
| 166 |
-
def test_cost_calculations_by_rate(cost_per_minute, expected_savings):
|
| 167 |
-
"""Test cost calculations with different rates"""
|
| 168 |
-
# TODO: Implement parametrized test
|
| 169 |
-
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_timeline_formatter.py
DELETED
|
@@ -1,159 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Test suite for TimelineFormatter
|
| 3 |
-
|
| 4 |
-
Tests the formatting and display generation for timeline visualizations.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import pytest
|
| 8 |
-
from typing import Dict, Any
|
| 9 |
-
|
| 10 |
-
# Import your formatter (adjust path as needed)
|
| 11 |
-
# from app import TimelineFormatter, TimelineMetrics
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
class TestTimelineFormatter:
|
| 15 |
-
"""Test suite for TimelineFormatter class"""
|
| 16 |
-
|
| 17 |
-
@pytest.fixture
|
| 18 |
-
def sample_metrics(self):
|
| 19 |
-
"""Create sample TimelineMetrics for testing"""
|
| 20 |
-
# TODO: Create TimelineMetrics instance with known values
|
| 21 |
-
pass
|
| 22 |
-
|
| 23 |
-
def test_format_markdown_comparison(self, sample_metrics):
|
| 24 |
-
"""Test markdown comparison formatting"""
|
| 25 |
-
# TODO: Generate markdown and verify structure
|
| 26 |
-
# - Should contain both timelines
|
| 27 |
-
# - Should show costs
|
| 28 |
-
# - Should include time savings
|
| 29 |
-
pass
|
| 30 |
-
|
| 31 |
-
def test_markdown_contains_industry_timeline(self, sample_metrics):
|
| 32 |
-
"""Test that markdown includes industry standard timeline"""
|
| 33 |
-
# TODO: Verify "WITHOUT ARF" section exists
|
| 34 |
-
# - T+0, T+14, T+28, T+60 markers
|
| 35 |
-
# - Cost display
|
| 36 |
-
pass
|
| 37 |
-
|
| 38 |
-
def test_markdown_contains_arf_timeline(self, sample_metrics):
|
| 39 |
-
"""Test that markdown includes ARF timeline"""
|
| 40 |
-
# TODO: Verify "WITH ARF" section exists
|
| 41 |
-
# - T+0, T+2, T+3, T+5 markers
|
| 42 |
-
# - Cost display
|
| 43 |
-
pass
|
| 44 |
-
|
| 45 |
-
def test_markdown_shows_difference_section(self, sample_metrics):
|
| 46 |
-
"""Test that markdown includes difference section"""
|
| 47 |
-
# TODO: Verify "THE DIFFERENCE" section
|
| 48 |
-
# - Time saved
|
| 49 |
-
# - Cost saved
|
| 50 |
-
# - Speed multiplier
|
| 51 |
-
pass
|
| 52 |
-
|
| 53 |
-
def test_format_summary_stats(self, sample_metrics):
|
| 54 |
-
"""Test summary statistics formatting"""
|
| 55 |
-
# TODO: Verify returns dict with correct keys
|
| 56 |
-
# - time_saved_minutes
|
| 57 |
-
# - cost_savings
|
| 58 |
-
# - speed_multiplier
|
| 59 |
-
# - time_improvement_pct
|
| 60 |
-
# - arf_total_time
|
| 61 |
-
# - industry_total_time
|
| 62 |
-
pass
|
| 63 |
-
|
| 64 |
-
def test_summary_stats_rounding(self, sample_metrics):
|
| 65 |
-
"""Test that summary stats are rounded appropriately"""
|
| 66 |
-
# TODO: Verify decimal precision
|
| 67 |
-
# - Cost should be integer
|
| 68 |
-
# - Time should be 1 decimal
|
| 69 |
-
# - Percentage should be 1 decimal
|
| 70 |
-
pass
|
| 71 |
-
|
| 72 |
-
def test_format_visual_bars(self, sample_metrics):
|
| 73 |
-
"""Test visual bar chart formatting"""
|
| 74 |
-
# TODO: Generate bars and verify
|
| 75 |
-
# - Industry bar length
|
| 76 |
-
# - ARF bar length (proportional)
|
| 77 |
-
# - Percentage display
|
| 78 |
-
pass
|
| 79 |
-
|
| 80 |
-
def test_visual_bars_proportional(self, sample_metrics):
|
| 81 |
-
"""Test that visual bars maintain correct proportions"""
|
| 82 |
-
# TODO: Verify bar lengths are proportional to time
|
| 83 |
-
pass
|
| 84 |
-
|
| 85 |
-
def test_visual_bars_max_length(self):
|
| 86 |
-
"""Test that visual bars don't exceed max length"""
|
| 87 |
-
# TODO: Even with extreme values, bars should fit
|
| 88 |
-
pass
|
| 89 |
-
|
| 90 |
-
def test_format_with_zero_values(self):
|
| 91 |
-
"""Test formatting with edge case values"""
|
| 92 |
-
# TODO: Handle zero time savings gracefully
|
| 93 |
-
pass
|
| 94 |
-
|
| 95 |
-
def test_format_with_large_numbers(self):
|
| 96 |
-
"""Test formatting with very large cost savings"""
|
| 97 |
-
# TODO: Verify comma formatting for readability
|
| 98 |
-
# - \$1,000,000 not \$1000000
|
| 99 |
-
pass
|
| 100 |
-
|
| 101 |
-
def test_format_special_characters_escaped(self, sample_metrics):
|
| 102 |
-
"""Test that special markdown characters are escaped"""
|
| 103 |
-
# TODO: Ensure no markdown injection possible
|
| 104 |
-
pass
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
class TestTimelineFormatterEdgeCases:
|
| 108 |
-
"""Test edge cases in formatting"""
|
| 109 |
-
|
| 110 |
-
def test_format_negative_time_savings(self):
|
| 111 |
-
"""Test formatting when ARF is slower (shouldn't happen)"""
|
| 112 |
-
# TODO: Handle gracefully, maybe show "N/A"
|
| 113 |
-
pass
|
| 114 |
-
|
| 115 |
-
def test_format_very_small_time_differences(self):
|
| 116 |
-
"""Test formatting when times are very close"""
|
| 117 |
-
# TODO: Should still display clearly
|
| 118 |
-
pass
|
| 119 |
-
|
| 120 |
-
def test_format_extremely_large_costs(self):
|
| 121 |
-
"""Test formatting multi-million dollar savings"""
|
| 122 |
-
# TODO: Verify readability with large numbers
|
| 123 |
-
pass
|
| 124 |
-
|
| 125 |
-
def test_unicode_characters_in_bars(self):
|
| 126 |
-
"""Test that unicode bar characters render correctly"""
|
| 127 |
-
# TODO: Verify █ character displays properly
|
| 128 |
-
pass
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
class TestTimelineFormatterIntegration:
|
| 132 |
-
"""Test formatter integration with calculator"""
|
| 133 |
-
|
| 134 |
-
def test_calculator_to_formatter_pipeline(self):
|
| 135 |
-
"""Test complete flow from calculation to formatting"""
|
| 136 |
-
# TODO: Calculate metrics → Format → Verify output
|
| 137 |
-
pass
|
| 138 |
-
|
| 139 |
-
def test_multiple_format_calls_consistent(self, sample_metrics):
|
| 140 |
-
"""Test that formatter is deterministic"""
|
| 141 |
-
# TODO: Same input should always produce same output
|
| 142 |
-
pass
|
| 143 |
-
|
| 144 |
-
def test_all_format_methods_use_same_metrics(self, sample_metrics):
|
| 145 |
-
"""Test that all format methods work with same metrics object"""
|
| 146 |
-
# TODO: Verify consistency across formats
|
| 147 |
-
pass
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
# Parametrized tests for different metric scenarios
|
| 151 |
-
@pytest.mark.parametrize("time_saved,expected_emoji", [
|
| 152 |
-
(55.0, "⏰"), # Good savings
|
| 153 |
-
(30.0, "⏰"), # Medium savings
|
| 154 |
-
(5.0, "⏰"), # Small savings
|
| 155 |
-
])
|
| 156 |
-
def test_format_includes_appropriate_emojis(time_saved, expected_emoji):
|
| 157 |
-
"""Test that formatting includes appropriate visual indicators"""
|
| 158 |
-
# TODO: Implement parametrized test
|
| 159 |
-
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_timeline_integration.py
DELETED
|
@@ -1,178 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Integration tests for Timeline feature
|
| 3 |
-
|
| 4 |
-
Tests the timeline feature integration with the rest of ARF.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import pytest
|
| 8 |
-
from unittest.mock import Mock, patch, AsyncMock
|
| 9 |
-
|
| 10 |
-
# Import your components (adjust paths as needed)
|
| 11 |
-
# from app import (
|
| 12 |
-
# TimelineCalculator,
|
| 13 |
-
# TimelineFormatter,
|
| 14 |
-
# BusinessMetricsTracker,
|
| 15 |
-
# EnhancedReliabilityEngine
|
| 16 |
-
# )
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
class TestTimelineGradioIntegration:
|
| 20 |
-
"""Test timeline integration with Gradio UI"""
|
| 21 |
-
|
| 22 |
-
@pytest.fixture
|
| 23 |
-
def mock_gradio_components(self):
|
| 24 |
-
"""Mock Gradio UI components"""
|
| 25 |
-
# TODO: Create mock components for testing
|
| 26 |
-
pass
|
| 27 |
-
|
| 28 |
-
def test_timeline_display_updates_on_submit(self):
|
| 29 |
-
"""Test that timeline display updates when event is submitted"""
|
| 30 |
-
# TODO: Submit event → Verify timeline updates
|
| 31 |
-
pass
|
| 32 |
-
|
| 33 |
-
def test_timeline_metrics_update_on_submit(self):
|
| 34 |
-
"""Test that timeline metrics boxes update"""
|
| 35 |
-
# TODO: Verify time_saved, cost_saved, speed displays update
|
| 36 |
-
pass
|
| 37 |
-
|
| 38 |
-
def test_timeline_accordion_expansion(self):
|
| 39 |
-
"""Test that timeline accordion can expand/collapse"""
|
| 40 |
-
# TODO: Verify accordion functionality
|
| 41 |
-
pass
|
| 42 |
-
|
| 43 |
-
def test_timeline_with_demo_scenarios(self):
|
| 44 |
-
"""Test timeline works with pre-configured demo scenarios"""
|
| 45 |
-
# TODO: Select demo scenario → Submit → Verify timeline
|
| 46 |
-
pass
|
| 47 |
-
|
| 48 |
-
def test_timeline_persists_across_submissions(self):
|
| 49 |
-
"""Test that timeline updates with each new submission"""
|
| 50 |
-
# TODO: Multiple submissions should show latest timeline
|
| 51 |
-
pass
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
class TestTimelineWithROIDashboard:
|
| 55 |
-
"""Test timeline feature interaction with ROI dashboard"""
|
| 56 |
-
|
| 57 |
-
def test_timeline_and_roi_both_update(self):
|
| 58 |
-
"""Test that both timeline and ROI update on submission"""
|
| 59 |
-
# TODO: Verify both features update correctly
|
| 60 |
-
pass
|
| 61 |
-
|
| 62 |
-
def test_timeline_cost_matches_roi_savings(self):
|
| 63 |
-
"""Test that timeline cost savings align with ROI metrics"""
|
| 64 |
-
# TODO: Numbers should be consistent
|
| 65 |
-
pass
|
| 66 |
-
|
| 67 |
-
def test_reset_metrics_affects_timeline(self):
|
| 68 |
-
"""Test that reset button affects timeline calculations"""
|
| 69 |
-
# TODO: Reset → Timeline should reset too
|
| 70 |
-
pass
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
class TestTimelineWithBusinessMetrics:
|
| 74 |
-
"""Test timeline integration with business metrics tracker"""
|
| 75 |
-
|
| 76 |
-
@pytest.fixture
|
| 77 |
-
def metrics_tracker(self):
|
| 78 |
-
"""Create BusinessMetricsTracker for testing"""
|
| 79 |
-
# TODO: Initialize tracker
|
| 80 |
-
pass
|
| 81 |
-
|
| 82 |
-
def test_timeline_uses_business_metrics(self, metrics_tracker):
|
| 83 |
-
"""Test that timeline calculations use business metrics"""
|
| 84 |
-
# TODO: Verify cost_per_minute from business context
|
| 85 |
-
pass
|
| 86 |
-
|
| 87 |
-
def test_timeline_records_to_metrics_tracker(self):
|
| 88 |
-
"""Test that timeline calculations are tracked"""
|
| 89 |
-
# TODO: Verify incidents recorded with timeline data
|
| 90 |
-
pass
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
class TestTimelineWithMultiAgentSystem:
|
| 94 |
-
"""Test timeline with multi-agent analysis"""
|
| 95 |
-
|
| 96 |
-
def test_timeline_reflects_agent_performance(self):
|
| 97 |
-
"""Test that timeline shows actual agent response times"""
|
| 98 |
-
# TODO: If agents are slow, timeline should reflect it
|
| 99 |
-
pass
|
| 100 |
-
|
| 101 |
-
def test_timeline_severity_matches_agents(self):
|
| 102 |
-
"""Test that timeline uses severity from agents"""
|
| 103 |
-
# TODO: Agent determines CRITICAL → Timeline uses CRITICAL times
|
| 104 |
-
pass
|
| 105 |
-
|
| 106 |
-
def test_timeline_with_failed_agent_analysis(self):
|
| 107 |
-
"""Test timeline behavior when agents fail"""
|
| 108 |
-
# TODO: Should still calculate with defaults
|
| 109 |
-
pass
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
class TestTimelinePerformance:
|
| 113 |
-
"""Test performance characteristics of timeline feature"""
|
| 114 |
-
|
| 115 |
-
def test_timeline_calculation_speed(self):
|
| 116 |
-
"""Test that timeline calculations are fast"""
|
| 117 |
-
# TODO: Should complete in < 100ms
|
| 118 |
-
pass
|
| 119 |
-
|
| 120 |
-
def test_timeline_formatting_speed(self):
|
| 121 |
-
"""Test that formatting is fast"""
|
| 122 |
-
# TODO: Should complete in < 50ms
|
| 123 |
-
pass
|
| 124 |
-
|
| 125 |
-
def test_timeline_memory_usage(self):
|
| 126 |
-
"""Test that timeline doesn't leak memory"""
|
| 127 |
-
# TODO: Multiple calculations shouldn't grow memory
|
| 128 |
-
pass
|
| 129 |
-
|
| 130 |
-
def test_timeline_with_many_incidents(self):
|
| 131 |
-
"""Test timeline performance with high volume"""
|
| 132 |
-
# TODO: 100+ incidents shouldn't slow down
|
| 133 |
-
pass
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
class TestTimelineErrorHandling:
|
| 137 |
-
"""Test error handling in timeline feature"""
|
| 138 |
-
|
| 139 |
-
def test_timeline_with_invalid_metrics(self):
|
| 140 |
-
"""Test timeline handles invalid input gracefully"""
|
| 141 |
-
# TODO: Bad data shouldn't crash app
|
| 142 |
-
pass
|
| 143 |
-
|
| 144 |
-
def test_timeline_with_missing_data(self):
|
| 145 |
-
"""Test timeline works with incomplete data"""
|
| 146 |
-
# TODO: Should use defaults for missing values
|
| 147 |
-
pass
|
| 148 |
-
|
| 149 |
-
def test_timeline_with_extreme_values(self):
|
| 150 |
-
"""Test timeline handles extreme values"""
|
| 151 |
-
# TODO: Very large/small numbers shouldn't break
|
| 152 |
-
pass
|
| 153 |
-
|
| 154 |
-
def test_timeline_logging_on_error(self):
|
| 155 |
-
"""Test that errors are logged appropriately"""
|
| 156 |
-
# TODO: Verify logger is called on errors
|
| 157 |
-
pass
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
# End-to-end test
|
| 161 |
-
@pytest.mark.asyncio
|
| 162 |
-
async def test_complete_timeline_flow():
|
| 163 |
-
"""Test complete flow from incident to timeline display"""
|
| 164 |
-
# TODO:
|
| 165 |
-
# 1. Create incident event
|
| 166 |
-
# 2. Submit to engine
|
| 167 |
-
# 3. Calculate timeline
|
| 168 |
-
# 4. Format display
|
| 169 |
-
# 5. Verify all components updated
|
| 170 |
-
pass
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
# Performance benchmark
|
| 174 |
-
@pytest.mark.benchmark
|
| 175 |
-
def test_timeline_benchmark(benchmark):
|
| 176 |
-
"""Benchmark timeline calculation performance"""
|
| 177 |
-
# TODO: Use pytest-benchmark to measure performance
|
| 178 |
-
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|