zapretgui/tests/test_transfer_integration.py
loop-uh 416a38bf15 BlockCheck: паузы и скорость ответа сайта видны в отчёте, на карточке и в экскурсии
Проверка сайта теперь оценивает качество передачи по замерам пробы объёма (новых запросов нет): у каждого адреса в отчёте есть поле transfer (пауза, медленный ответ, медленное тело, число замеров и уверенность), в тексте отчёта строка «Скорость ответа», у карточки сайта дорога «Передача», метка «паузы в ответах» и строка в подробностях. Подтверждённая пауза делает сайт «открывающимся через раз» и попадает в итог; единичная пауза при малом числе замеров остаётся только строкой отчёта. Экскурсия и демо-карточка описывают новую дорогу.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 08:19:17 -04:00

141 lines
6.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Оценка передачи в проверке сайтов: движок, текст отчёта, дорога, метка и строка карточки."""
from __future__ import annotations
import json
import unittest
from pathlib import Path
from unittest.mock import patch
from blockcheck.ui.result_cards_model import build_cards
from blockcheck_words import report_with_words
from diagnostics import engine, report_text
from diagnostics import transfer_probe as tp
from diagnostics import volume_probe as vp
from diagnostics.tls_probe import KIND_OK, ProbeResult
from diagnostics.verdict import ReachState
S = tp.RequestSample
FIXTURE = Path(__file__).parent / "fixtures" / "blockcheck_report_sample.json"
def _samples(*millis: float) -> tuple[tp.RequestSample, ...]:
return (S(ttfb_ms=300, fresh=True), *(S(received=5_000, ttfb_ms=ms) for ms in millis))
def _facts(samples) -> vp.VolumeFacts:
return vp.VolumeFacts(first=vp.VolumeRun(vp.RUN_PASSED, 49_500, len(samples), tuple(samples)))
class EngineTransferTests(unittest.TestCase):
def _check(self, samples) -> engine._Probe:
probe = engine._Probe(target=engine.Target("x.com", "сайт", main=True), service="x", host="x.com")
probe.reach = ProbeResult(ip="203.0.113.5", kind=KIND_OK, status=200, body_size=500)
probe.reach_state = ReachState.OK
run = engine._Run(None, workers=2, deadline=60)
self.addCleanup(run.close)
with patch.object(vp, "collect", return_value=_facts(samples)):
engine._check_volume(run, probe)
return probe
def test_confirmed_pause_makes_the_site_open_through_once_in_a_while(self) -> None:
probe = self._check(_samples(9, 42, 7, 12, 9, 9, 2_861, 2_900))
self.assertEqual(probe.transfer.code, tp.TRANSFER_PAUSE)
self.assertIn("рывками", probe.unstable)
self.assertEqual(probe.reach_state, ReachState.OK)
def test_single_unconfirmed_pause_is_only_a_report_line(self) -> None:
probe = self._check(_samples(9, 9, 9, 3_000))
self.assertEqual((probe.transfer.code, probe.transfer.confidence), (tp.TRANSFER_PAUSE, tp.CONFIDENCE_LOW))
self.assertEqual(probe.unstable, "")
def test_fast_answers_leave_no_complaint(self) -> None:
probe = self._check(_samples(20, 25, 30, 22, 24, 21))
self.assertEqual((probe.transfer.code, probe.unstable), (tp.TRANSFER_OK, ""))
def test_pause_does_not_replace_a_stronger_instability_reason(self) -> None:
probe = engine._Probe(target=engine.Target("x.com", "сайт", main=True), service="x", host="x.com")
probe.reach = ProbeResult(ip="203.0.113.5", kind=KIND_OK, status=200, body_size=500)
probe.reach_state = ReachState.OK
probe.unstable = "из трёх попыток подряд прошла одна"
run = engine._Run(None, workers=2, deadline=60)
self.addCleanup(run.close)
with patch.object(vp, "collect", return_value=_facts(_samples(9, 9, 9, 9, 9, 3_000, 3_100))):
engine._check_volume(run, probe)
self.assertEqual(probe.unstable, "из трёх попыток подряд прошла одна")
class ReportTransferTests(unittest.TestCase):
def _probe(self, verdict) -> engine._Probe:
probe = engine._Probe(target=engine.Target("x.com", "сайт", main=True), service="x", host="x.com")
probe.reach = ProbeResult(ip="203.0.113.5", kind=KIND_OK, status=200, body_size=500)
probe.reach_state = ReachState.OK
probe.transfer = verdict
return probe
def test_target_report_carries_the_numbers(self) -> None:
verdict = tp.judge(_samples(9, 42, 7, 12, 9, 9, 2_861))
report = report_text.target_report(self._probe(verdict))
self.assertEqual(report["transfer"]["code"], "pause")
self.assertEqual(report["transfer"]["ttfb_max_ms"], 2_861)
self.assertIsNone(report_text.target_report(self._probe(None))["transfer"])
def test_text_report_has_a_line_unless_unknown(self) -> None:
pause = tp.judge(_samples(9, 42, 7, 12, 9, 9, 2_861))
lines = report_text.probe_lines(self._probe(pause), full=True)
self.assertTrue(any("Скорость ответа" in line and "⚠️" in line for line in lines))
unknown = tp.judge(())
self.assertFalse(any("Скорость ответа" in line for line in report_text.probe_lines(self._probe(unknown), full=True)))
self.assertFalse(any("Скорость ответа" in line for line in report_text.probe_lines(self._probe(None), full=True)))
def test_road_and_tag_come_from_the_verdict(self) -> None:
pause = tp.judge(_samples(9, 42, 7, 12, 9, 9, 2_861))
road = report_text._transfer_road(pause)
self.assertEqual((road["key"], road["state"], road["word"]), ("transfer", "warn", "с паузами"))
fast = report_text._transfer_road(tp.judge(_samples(20, 25, 30)))
self.assertEqual((fast["state"], fast["word"]), ("ok", "быстро"))
class _Service:
control = False
tags = report_text.service_tags(_Service(), [self._probe(pause)])
self.assertIn(("pause", "warn"), [(tag["key"], tag["state"]) for tag in tags])
self.assertFalse([tag for tag in report_text.service_tags(_Service(), [self._probe(tp.judge(_samples(20, 25, 30)))]) if tag["key"] == "pause"])
class CardTransferTests(unittest.TestCase):
def test_card_shows_the_row_with_the_verdict_level(self) -> None:
report = json.loads(FIXTURE.read_text(encoding="utf-8"))
verdict = tp.judge(_samples(9, 42, 7, 12, 9, 9, 2_861)).as_dict()
report["services"][0]["targets"][0]["transfer"] = verdict
cards = build_cards(report_with_words(report))
rows = [
line
for card in cards
for section in getattr(card, "sections", ())
for line in section.lines
if line.name == "Скорость ответа"
]
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0].state, "warn")
self.assertIn("2.9 с", rows[0].text)
def test_old_report_without_the_field_has_no_row(self) -> None:
report = json.loads(FIXTURE.read_text(encoding="utf-8"))
cards = build_cards(report_with_words(report))
self.assertFalse(
[line for card in cards for section in getattr(card, "sections", ()) for line in section.lines if line.name == "Скорость ответа"]
)
if __name__ == "__main__":
unittest.main()