@@ -0,0 +1,279 @@
|
||||
"""Интеграционные тесты 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()
|
||||
Reference in New Issue
Block a user