from unittest.mock import MagicMock, patch import pytest from aiops.entity.guardian.diagnostic_package import CrossSectionalOutlierDetection from aiops.service.guardian.anomaly_detector.cross_sectional import ( CrossSectionalOutlierDetector, ) @pytest.fixture def sample_metrics_data() -> list[dict[str, any]]: # 3 series: target and 2 peers, all with same device, different instance ts_base = 100000 return [ { "metric": {"instance": "A", "device": "sda"}, "values": [[ts_base + i * 60, 10 + i] for i in range(6)], }, { "metric": {"instance": "B", "device": "sda"}, "values": [[ts_base + i * 60, 20 + i] for i in range(6)], }, { "metric": {"instance": "C", "device": "sda"}, "values": [[ts_base + i * 60, 30 + i] for i in range(6)], }, ] @pytest.fixture def target_labels() -> dict[str, str]: return {"instance": "A", "device": "sda"} @pytest.fixture def group_by_labels() -> list[str]: return ["instance", "device"] def test_anomaly_detected( sample_metrics_data: list[dict[str, any]], target_labels: dict[str, str], group_by_labels: list[str], ) -> None: detector = CrossSectionalOutlierDetector() # Make target value much higher than peers to trigger anomaly sample_metrics_data[0]["values"] = [[100000 + i * 60, 100] for i in range(6)] result = detector.run( sample_metrics_data, target_labels, group_by_labels, anomaly_threshold=3.5 ) assert isinstance(result, CrossSectionalOutlierDetection) assert result.z_score > 3.5 assert result.peer_count == 2 assert result.observed_value == 100 def test_no_anomaly( sample_metrics_data: list[dict[str, any]], target_labels: dict[str, str], group_by_labels: list[str], ) -> None: detector = CrossSectionalOutlierDetector() # Target value similar to peers sample_metrics_data[0]["values"] = [[100000 + i * 60, 25] for i in range(6)] sample_metrics_data[1]["values"] = [[100000 + i * 60, 20] for i in range(6)] sample_metrics_data[2]["values"] = [[100000 + i * 60, 30] for i in range(6)] result = detector.run( sample_metrics_data, target_labels, group_by_labels, anomaly_threshold=3.5 ) assert result is None def test_missing_group_by_labels( sample_metrics_data: list[dict[str, any]], target_labels: dict[str, str] ) -> None: detector = CrossSectionalOutlierDetector() with pytest.raises(ValueError): detector.run(sample_metrics_data, target_labels, []) def test_missing_target_series(group_by_labels: list[str]) -> None: metrics_data = [ {"metric": {"instance": "B", "device": "sda"}, "values": [[100000, 1]]}, {"metric": {"instance": "C", "device": "sda"}, "values": [[100000, 2]]}, ] target_labels = {"instance": "A", "device": "sda"} detector = CrossSectionalOutlierDetector() with pytest.raises(ValueError): detector.run(metrics_data, target_labels, group_by_labels) def test_missing_peers() -> None: metrics_data = [ {"metric": {"instance": "A", "device": "sda"}, "values": [[100000, 1]]}, ] target_labels = {"instance": "A", "device": "sda"} group_by_labels = ["instance", "device"] detector = CrossSectionalOutlierDetector() with pytest.raises(ValueError): detector.run(metrics_data, target_labels, group_by_labels) def test_not_enough_peers() -> None: metrics_data = [ {"metric": {"instance": "A", "device": "sda"}, "values": [[100000, 1]]}, {"metric": {"instance": "B", "device": "sda"}, "values": [[100000, 2]]}, ] target_labels = {"instance": "A", "device": "sda"} group_by_labels = ["instance", "device"] detector = CrossSectionalOutlierDetector() with pytest.raises(ValueError): detector.run(metrics_data, target_labels, group_by_labels) def test_calculate_representative_value() -> None: detector = CrossSectionalOutlierDetector() ts_base = 100000 values = [ (ts_base - 400, 1), (ts_base - 200, 2), (ts_base - 100, 3), (ts_base, 4), ] result = detector._CrossSectionalOutlierDetector__calculate_representative_value( values ) assert result == 3 # median of [2, 3, 4] def test_nan_and_none_values() -> None: detector = CrossSectionalOutlierDetector() ts_base = 100000 values = [ (ts_base - 400, None), (ts_base - 200, "2"), (ts_base - 100, 3), (ts_base, "4"), ] result = detector._CrossSectionalOutlierDetector__calculate_representative_value( values ) assert result == 3 # median of [2.0, 3.0, 4.0]