|
1 | 1 | """Tests for DOCSIS channel health analyzer.""" |
2 | 2 |
|
3 | 3 | import pytest |
| 4 | +from unittest.mock import patch |
4 | 5 | from app import analyzer |
5 | 6 | from app.analyzer import analyze, _parse_float, _parse_qam_order, _resolve_modulation, _channel_bitrate_mbps |
6 | 7 |
|
@@ -771,3 +772,101 @@ def test_default_spike_expiry_hours(self): |
771 | 772 | from app.analyzer import _get_spike_expiry_hours |
772 | 773 | hours = _get_spike_expiry_hours() |
773 | 774 | assert hours == 48 |
| 775 | + |
| 776 | + |
| 777 | +class TestSpikeSuppression: |
| 778 | + """Tests for apply_spike_suppression().""" |
| 779 | + |
| 780 | + def _make_analysis_with_uncorr(self, uncorr_pct=86.6, health="poor", |
| 781 | + extra_issues=None): |
| 782 | + """Build a minimal analysis dict with uncorrectable error issues.""" |
| 783 | + issues = ["uncorr_errors_critical"] |
| 784 | + if extra_issues: |
| 785 | + issues.extend(extra_issues) |
| 786 | + return { |
| 787 | + "summary": { |
| 788 | + "health": health, |
| 789 | + "health_issues": issues, |
| 790 | + "ds_uncorr_pct": uncorr_pct, |
| 791 | + "ds_correctable_errors": 155000, |
| 792 | + "ds_uncorrectable_errors": 1000000, |
| 793 | + "ds_total": 33, |
| 794 | + "us_total": 4, |
| 795 | + }, |
| 796 | + "ds_channels": [], |
| 797 | + "us_channels": [], |
| 798 | + } |
| 799 | + |
| 800 | + def test_no_spike_no_change(self): |
| 801 | + """No spike timestamp — analysis stays unchanged.""" |
| 802 | + from app.analyzer import apply_spike_suppression |
| 803 | + analysis = self._make_analysis_with_uncorr() |
| 804 | + apply_spike_suppression(analysis, None) |
| 805 | + assert analysis["summary"]["ds_uncorr_pct"] == 86.6 |
| 806 | + assert "uncorr_errors_critical" in analysis["summary"]["health_issues"] |
| 807 | + assert analysis["summary"]["health"] == "poor" |
| 808 | + assert "spike_suppression" not in analysis["summary"] |
| 809 | + |
| 810 | + @patch("app.analyzer.utc_now") |
| 811 | + def test_recent_spike_no_suppression(self, mock_now): |
| 812 | + """Spike < 48h ago — still in observation period, no suppression.""" |
| 813 | + from app.analyzer import apply_spike_suppression |
| 814 | + mock_now.return_value = "2026-02-28T12:00:00Z" |
| 815 | + analysis = self._make_analysis_with_uncorr() |
| 816 | + apply_spike_suppression(analysis, "2026-02-27T14:00:00Z") |
| 817 | + assert analysis["summary"]["ds_uncorr_pct"] == 86.6 |
| 818 | + assert "uncorr_errors_critical" in analysis["summary"]["health_issues"] |
| 819 | + assert analysis["summary"]["health"] == "poor" |
| 820 | + assert "spike_suppression" not in analysis["summary"] |
| 821 | + |
| 822 | + @patch("app.analyzer.utc_now") |
| 823 | + def test_expired_spike_suppresses(self, mock_now): |
| 824 | + """Spike >= 48h ago — suppression active.""" |
| 825 | + from app.analyzer import apply_spike_suppression |
| 826 | + mock_now.return_value = "2026-03-01T15:00:00Z" # 72.5h after spike |
| 827 | + analysis = self._make_analysis_with_uncorr() |
| 828 | + apply_spike_suppression(analysis, "2026-02-27T14:30:00Z") |
| 829 | + assert analysis["summary"]["ds_uncorr_pct"] == 0.0 |
| 830 | + assert "uncorr_errors_critical" not in analysis["summary"]["health_issues"] |
| 831 | + assert "uncorr_errors_high" not in analysis["summary"]["health_issues"] |
| 832 | + assert analysis["summary"]["health"] == "good" |
| 833 | + sup = analysis["summary"]["spike_suppression"] |
| 834 | + assert sup["active"] is True |
| 835 | + assert sup["last_spike"] == "2026-02-27T14:30:00Z" |
| 836 | + assert sup["expiry_hours"] == 48 |
| 837 | + |
| 838 | + @patch("app.analyzer.utc_now") |
| 839 | + def test_expired_spike_other_issues_remain(self, mock_now): |
| 840 | + """Spike expired but other critical issues exist — health stays poor.""" |
| 841 | + from app.analyzer import apply_spike_suppression |
| 842 | + mock_now.return_value = "2026-03-01T15:00:00Z" |
| 843 | + analysis = self._make_analysis_with_uncorr( |
| 844 | + extra_issues=["snr_critical"] |
| 845 | + ) |
| 846 | + apply_spike_suppression(analysis, "2026-02-27T14:00:00Z") |
| 847 | + assert analysis["summary"]["ds_uncorr_pct"] == 0.0 |
| 848 | + assert "uncorr_errors_critical" not in analysis["summary"]["health_issues"] |
| 849 | + assert "snr_critical" in analysis["summary"]["health_issues"] |
| 850 | + assert analysis["summary"]["health"] == "poor" |
| 851 | + assert analysis["summary"]["spike_suppression"]["active"] is True |
| 852 | + |
| 853 | + @patch("app.analyzer.utc_now") |
| 854 | + def test_expired_spike_warning_issues_marginal(self, mock_now): |
| 855 | + """Spike expired, only warning issues remain — health becomes marginal.""" |
| 856 | + from app.analyzer import apply_spike_suppression |
| 857 | + mock_now.return_value = "2026-03-01T15:00:00Z" |
| 858 | + analysis = self._make_analysis_with_uncorr( |
| 859 | + extra_issues=["snr_warn"] |
| 860 | + ) |
| 861 | + apply_spike_suppression(analysis, "2026-02-27T14:00:00Z") |
| 862 | + assert analysis["summary"]["health"] == "marginal" |
| 863 | + |
| 864 | + @patch("app.analyzer.utc_now") |
| 865 | + def test_spike_at_exact_boundary(self, mock_now): |
| 866 | + """Spike exactly 48h ago — suppressed (>= boundary).""" |
| 867 | + from app.analyzer import apply_spike_suppression |
| 868 | + mock_now.return_value = "2026-03-01T14:00:00Z" |
| 869 | + analysis = self._make_analysis_with_uncorr() |
| 870 | + apply_spike_suppression(analysis, "2026-02-27T14:00:00Z") |
| 871 | + assert analysis["summary"]["ds_uncorr_pct"] == 0.0 |
| 872 | + assert analysis["summary"]["spike_suppression"]["active"] is True |
0 commit comments