280 lines
9.1 KiB
Python
280 lines
9.1 KiB
Python
"""Интеграционные тесты 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()
|