Files
site/WESP_REL/tests/test_recipe_calculate_pipeline.py
T
2026-07-17 12:57:18 +03:00

214 lines
8.4 KiB
Python
Raw 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.
"""Согласованность POST /api/recipes/calculate с app.services.recipe_calculator."""
from __future__ import annotations
import os
import tempfile
import unittest
from app import create_app, db
from app.models import Component
from app.services.recipe_calculator import calculate_recipe
from config import TestingConfig
class RecipeCalculatePipelineConfig(TestingConfig):
_TMP_DIR = tempfile.mkdtemp(prefix="wesp-calc-pipeline-")
SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(_TMP_DIR, 'calc_pipeline.db')}"
SQLALCHEMY_BINDS = {"reports": f"sqlite:///{os.path.join(_TMP_DIR, 'reports_pipeline.db')}"}
AUTH_LOGIN = "calc-pipeline-admin"
AUTH_PASSWORD = "calc-pipeline-secret"
def _format_like_api(result: dict) -> dict:
"""Те же правила округления, что в calculate_recipe_endpoint."""
out = {
"ingredients": [dict(i) for i in result.get("ingredients", [])],
"totals": dict(result.get("totals", {})),
"unloadingGroups": [dict(g) for g in result.get("unloadingGroups", [])],
"unloadingTotals": dict(result.get("unloadingTotals", {})),
}
def to_float2(x):
try:
return round(float(x), 2)
except (TypeError, ValueError):
return 0.0
def truncate2(x):
try:
v = float(x)
return float(int(v * 100)) / 100.0
except (TypeError, ValueError):
return 0.0
for ing in out["ingredients"]:
for key in ("weightPerHead", "tripWeight", "totalWeight", "dryMatterPerHead"):
if key in ing and ing[key] is not None:
raw_val = ing[key]
v = truncate2(raw_val) if key == "weightPerHead" else to_float2(raw_val)
ing[key] = f"{v:.2f}" if key == "weightPerHead" else v
for key in (
"totalWeight",
"totalTripWeight",
"totalDryMatterPerHead",
"totalWeightPerHead",
):
if key in out["totals"] and out["totals"][key] is not None:
out["totals"][key] = to_float2(out["totals"][key])
return out
class RecipeCalculatePipelineTests(unittest.TestCase):
def setUp(self) -> None:
self.app = create_app(RecipeCalculatePipelineConfig)
self.client = self.app.test_client()
self.ctx = self.app.app_context()
self.ctx.push()
db.create_all()
self.client.post(
"/api/auth/login",
json={"login": "calc-pipeline-admin", "password": "calc-pipeline-secret"},
)
self.comp_grain = "pipe-comp-grain"
self.comp_protein = "pipe-comp-protein"
db.session.add_all(
[
Component(
id=self.comp_grain,
name="Зерно",
type="grain",
dry_matter=80.0,
protein=0.0,
energy=0.0,
price=0.0,
),
Component(
id=self.comp_protein,
name="Белок",
type="protein",
dry_matter=90.0,
protein=0.0,
energy=0.0,
price=0.0,
),
]
)
db.session.commit()
def tearDown(self) -> None:
db.session.remove()
db.drop_all()
self.ctx.pop()
def _post_calculate(self, payload: dict) -> dict:
resp = self.client.post("/api/recipes/calculate", json=payload)
self.assertEqual(resp.status_code, 200, resp.get_data(as_text=True))
return resp.get_json()
def _expected_from_service(self, payload: dict) -> dict:
heads_count = int(
payload.get("headsCount")
or payload.get("heads_count")
or payload.get("headsPerTrip")
or 0
)
trip_percent = float(payload.get("tripPercent") or payload.get("trip_percent") or 100)
ingredients = payload.get("ingredients", []) or []
unloading_groups = payload.get("unloadingGroups") or payload.get("unloading_groups") or []
calculate_from_dry_matter = bool(
payload.get("calculateFromDryMatter")
if payload.get("calculateFromDryMatter") is not None
else payload.get("calculate_from_dry_matter", False)
)
normalized_ingredients = []
for ing in ingredients:
ing = dict(ing)
if "component_id" not in ing and "componentId" in ing:
ing["component_id"] = ing.get("componentId")
if "dryMatterPerHead" not in ing and "dry_matter_per_head" in ing:
ing["dryMatterPerHead"] = ing.get("dry_matter_per_head")
if "dryMatter" not in ing and "dry_matter" in ing:
ing["dryMatter"] = ing.get("dry_matter")
if "weightPerHead" not in ing and "weight_per_head" in ing:
ing["weightPerHead"] = ing.get("weight_per_head")
normalized_ingredients.append(ing)
normalized_groups = []
for g in unloading_groups:
g = dict(g)
if "distributionType" not in g and "distribution_type" in g:
g["distributionType"] = g.get("distribution_type")
normalized_groups.append(g)
component_ids = [i.get("component_id") for i in normalized_ingredients if i.get("component_id")]
component_dry_matter_map = {}
if component_ids:
comps = db.session.query(Component).filter(Component.id.in_(component_ids)).all()
component_dry_matter_map = {c.id: c.dry_matter for c in comps}
raw = calculate_recipe(
ingredients=normalized_ingredients,
heads_count=heads_count,
trip_percent=trip_percent,
unloading_groups=normalized_groups,
component_dry_matter_map=component_dry_matter_map or None,
calculate_from_dry_matter=calculate_from_dry_matter,
)
return _format_like_api(raw)
def test_api_matches_service_weight_mode_with_groups(self) -> None:
payload = {
"headsCount": 137,
"tripPercent": 75.0,
"ingredients": [
{"weightPerHead": 2.5, "dryMatter": 50.0, "componentId": self.comp_grain},
{"weightPerHead": 1.0, "dryMatter": 0, "componentId": self.comp_protein},
],
"unloadingGroups": [
{"distributionType": "heads", "value": 67},
{"distributionType": "heads", "value": 68},
],
}
api = self._post_calculate(payload)
expected = self._expected_from_service(payload)
self.assertEqual(api["totals"], expected["totals"])
self.assertEqual(api["unloadingGroups"], expected["unloadingGroups"])
self.assertEqual(api["unloadingTotals"], expected["unloadingTotals"])
self.assertEqual(len(api["ingredients"]), 2)
self.assertEqual(api["ingredients"][0]["weightPerHead"], expected["ingredients"][0]["weightPerHead"])
self.assertEqual(api["ingredients"][1]["dryMatterPerHead"], expected["ingredients"][1]["dryMatterPerHead"])
def test_api_matches_service_dry_matter_mode(self) -> None:
payload = {
"heads_count": 50,
"trip_percent": 100,
"calculate_from_dry_matter": True,
"ingredients": [
{"dry_matter_per_head": 0.8, "dry_matter": 0, "component_id": self.comp_grain},
{"dry_matter_per_head": 0.45, "dryMatter": 90.0, "component_id": self.comp_protein},
],
"unloading_groups": [{"distribution_type": "percent", "value": 100}],
}
api = self._post_calculate(payload)
expected = self._expected_from_service(payload)
self.assertEqual(api["totals"], expected["totals"])
self.assertEqual(api["ingredients"][0]["weightPerHead"], expected["ingredients"][0]["weightPerHead"])
self.assertEqual(api["ingredients"][1]["weightPerHead"], expected["ingredients"][1]["weightPerHead"])
self.assertEqual(api["unloadingGroups"][0]["calculatedWeight"], expected["unloadingGroups"][0]["calculatedWeight"])
def test_api_rejects_invalid_numeric_params(self) -> None:
resp = self.client.post(
"/api/recipes/calculate",
json={"headsCount": "abc", "ingredients": []},
)
self.assertEqual(resp.status_code, 400)
self.assertIn("error", resp.get_json())
if __name__ == "__main__":
unittest.main()