"""Интеграционные тесты feed_quality.evaluator.""" from __future__ import annotations import unittest from datetime import datetime from sqlalchemy import select from app import create_app, db from app.models import ( ComponentLoadingTime, LoadingReport, LoadingReportComponent, Recipe, UnloadingReport, UnloadingReportGroup, ) from app.models.feed_alert import FeedAlert from app.services.feed_quality.evaluator import evaluate_loading_report, evaluate_unloading_report from app.services.feed_quality.rules import ( EVENT_LEFT_IN_MIXER, EVENT_LOADING_FAST, EVENT_LOADING_TIME, EVENT_OVERLOAD, EVENT_UNDERLOAD, ) from app.services.feed_quality.settings_store import save_feed_quality_settings from app.services.setup_state import mark_setup_complete from tests.helpers.zootech_test_helpers import ZootechTestConfig class FeedQualityEvaluatorTests(unittest.TestCase): def setUp(self) -> None: self.app = create_app(ZootechTestConfig) self.ctx = self.app.app_context() self.ctx.push() db.create_all() mark_setup_complete(self.app) db.session.add( Recipe( id="fq-r1", name="Тестовый рейс", heads_per_trip=10, mixing_time=5, content_hash="", ) ) db.session.commit() def tearDown(self) -> None: db.session.remove() db.drop_all() self.ctx.pop() def _loading_with_overload(self) -> str: report = LoadingReport( id="fq-lr-1", recipe_id="fq-r1", recipe_name="Тестовый рейс", start_time=datetime.now(), target_mixing_time=300, actual_mixing_time=350, total_weight=100.0, created_by="system", updated_by="system", ) db.session.add(report) db.session.flush() db.session.add( LoadingReportComponent( report_id=report.id, component_name="Силос", target_weight=100.0, actual_weight=115.0, overload=15.0, loading_order=1, created_by="system", updated_by="system", ) ) db.session.commit() return report.id def test_evaluate_loading_creates_alerts(self) -> None: report_id = self._loading_with_overload() rows = evaluate_loading_report(report_id, send_notifications=False) self.assertGreaterEqual(len(rows), 2) types = {r.event_type for r in rows} self.assertIn(EVENT_OVERLOAD, types) def test_idempotent_re_evaluate(self) -> None: report_id = self._loading_with_overload() first = evaluate_loading_report(report_id, send_notifications=False) evaluate_loading_report(report_id, send_notifications=False) second_count = len( db.session.execute( select(FeedAlert).where(FeedAlert.loading_report_id == report_id) ) .scalars() .all() ) self.assertEqual(second_count, len(first)) def test_fast_loading_merged_with_underload(self) -> None: report = LoadingReport( id="fq-lr-fast", recipe_id="fq-r1", recipe_name="Тестовый рейс", start_time=datetime.now(), target_mixing_time=60, actual_mixing_time=60, total_weight=62.0, created_by="system", updated_by="system", ) db.session.add(report) db.session.flush() db.session.add( LoadingReportComponent( report_id=report.id, component_name="Сено", target_weight=62.0, actual_weight=0.0, overload=-62.0, loading_order=1, created_by="system", updated_by="system", ) ) now = datetime.now() db.session.add( ComponentLoadingTime( report_id=report.id, component_name="Сено", start_time=now, end_time=now, loading_duration=0.9, loading_order=1, created_by="system", updated_by="system", ) ) db.session.commit() rows = evaluate_loading_report(report.id, send_notifications=False) component_rows = [r for r in rows if r.component_name == "Сено"] self.assertEqual(len(component_rows), 1) self.assertEqual(component_rows[0].event_type, EVENT_UNDERLOAD) self.assertIn("0.9 с", component_rows[0].detail) def test_fast_loading_separate_when_weight_ok(self) -> None: report = LoadingReport( id="fq-lr-fast-ok", recipe_id="fq-r1", recipe_name="Тестовый рейс", start_time=datetime.now(), target_mixing_time=60, actual_mixing_time=60, total_weight=62.0, created_by="system", updated_by="system", ) db.session.add(report) db.session.flush() db.session.add( LoadingReportComponent( report_id=report.id, component_name="Сено", target_weight=62.0, actual_weight=62.0, overload=0.0, loading_order=1, created_by="system", updated_by="system", ) ) now = datetime.now() db.session.add( ComponentLoadingTime( report_id=report.id, component_name="Сено", start_time=now, end_time=now, loading_duration=0.9, loading_order=1, created_by="system", updated_by="system", ) ) db.session.commit() rows = evaluate_loading_report(report.id, send_notifications=False) fast = [r for r in rows if r.component_name == "Сено"] self.assertEqual(len(fast), 1) self.assertEqual(fast[0].event_type, EVENT_LOADING_FAST) def test_slow_loading_separate_when_weight_ok(self) -> None: save_feed_quality_settings({"loading": {"warning_max_sec": 30, "critical_max_sec": 120}}) report = LoadingReport( id="fq-lr-slow", recipe_id="fq-r1", recipe_name="Тестовый рейс", start_time=datetime.now(), target_mixing_time=60, actual_mixing_time=60, total_weight=100.0, created_by="system", updated_by="system", ) db.session.add(report) db.session.flush() db.session.add( LoadingReportComponent( report_id=report.id, component_name="Силос", target_weight=100.0, actual_weight=100.0, overload=0.0, loading_order=1, created_by="system", updated_by="system", ) ) now = datetime.now() db.session.add( ComponentLoadingTime( report_id=report.id, component_name="Силос", start_time=now, end_time=now, loading_duration=90.0, loading_order=1, created_by="system", updated_by="system", ) ) db.session.commit() rows = evaluate_loading_report(report.id, send_notifications=False) slow = [r for r in rows if r.component_name == "Силос"] self.assertEqual(len(slow), 1) self.assertEqual(slow[0].event_type, EVENT_LOADING_TIME) def test_left_in_mixer_on_unloading(self) -> None: report_id = self._loading_with_overload() unloading = UnloadingReport( id="fq-ur-1", recipe_id="fq-r1", recipe_name="Тестовый рейс", loading_report_id=report_id, start_time=datetime.now(), total_weight=100.0, total_unloaded_weight=90.0, remaining_weight=10.0, created_by="system", updated_by="system", ) db.session.add(unloading) db.session.flush() db.session.add( UnloadingReportGroup( report_id=unloading.id, name="G1", target_weight=100.0, unloaded_weight=90.0, remaining_weight=10.0, distribution_type="percent", distribution_value=100, order=1, created_by="system", updated_by="system", ) ) db.session.commit() rows = evaluate_unloading_report(unloading.id, send_notifications=False) types = {r.event_type for r in rows} self.assertIn(EVENT_LEFT_IN_MIXER, types) if __name__ == "__main__": unittest.main()