Files
RollCalcPython/tests/test_transport_calculation.py
T

153 lines
6.5 KiB
Python

import math
import unittest
from transport_calculation import analyze_transport, transport_presets
def request(**changes):
payload = {
"length_m": 10.0,
"width_m": 5.0,
"height_m": 5.0,
"max_weight_kg": 10_000.0,
"margin_side_m": 0.0,
"margin_ceiling_m": 0.0,
"roll_diameter_mm": 1000.0,
"core_diameter_mm": 150.0,
"roll_width_m": 1.0,
"roll_weight_kg": 1000.0,
"product_length_m": 10.0,
}
payload.update(changes)
return payload
class TransportCalculationTests(unittest.TestCase):
def analysis(self, **changes):
result = analyze_transport(request(**changes))
self.assertEqual(result["status"], "success")
return result["analysis"]
def test_active_presets_match_golden_browser_results(self):
expected = {
"container_20ft": (8, "Geometry", [21, 19, 8], (2, 2, 2)),
"container_40ft": (24, "Geometry", [30, 40, 24], (6, 2, 2)),
"container_40ft_hc": (24, "Geometry", [30, 46, 24], (6, 2, 2)),
"lkw_sattelzug": (24, "Weight", [24, 54, 24], (6, 2, 2)),
"lkw_tandem": (24, "Geometry", [25, 54, 24], (6, 2, 2)),
}
for preset in transport_presets():
with self.subTest(preset=preset.key):
result = analyze_transport({
"transport_preset": preset.key,
"margin_side_m": 0.0,
"margin_ceiling_m": 0.2,
"roll_diameter_mm": 1000.0,
"core_diameter_mm": 150.0,
"roll_width_m": 2.0,
"roll_weight_kg": 1000.0,
"product_length_m": 50.0,
})
analysis = result["analysis"]
rolls, limiting, scenarios, layout = expected[preset.key]
self.assertEqual(analysis["final_rolls"], rolls)
self.assertEqual(analysis["limiting"]["name"], limiting)
self.assertEqual(
[scenario["rolls"] for scenario in analysis["scenarios"]],
scenarios,
)
self.assertEqual(
(
analysis["layout"]["rolls_along_length"],
analysis["layout"]["rolls_along_width"],
analysis["layout"]["max_stack_height"],
),
layout,
)
def test_custom_weight_governing_case_matches_browser_semantics(self):
analysis = self.analysis()
self.assertEqual(analysis["limiting"]["name"], "Weight")
self.assertEqual(analysis["final_rolls"], 10)
self.assertEqual(analysis["remaining_payload_kg"], 0.0)
self.assertEqual(analysis["final_m2"], 100.0)
def test_geometry_governing_and_impossible_geometry_are_preserved(self):
geometry = self.analysis(max_weight_kg=1_000_000.0)
self.assertEqual(geometry["limiting"]["name"], "Geometry")
self.assertEqual(geometry["final_rolls"], 250)
impossible = self.analysis(
length_m=2.0,
width_m=2.0,
height_m=2.0,
roll_diameter_mm=3000.0,
max_weight_kg=100_000.0,
)
self.assertEqual(impossible["limiting"]["name"], "Geometry")
self.assertEqual(impossible["final_rolls"], 0)
self.assertEqual(impossible["layout"]["rolls_along_width"], 0)
def test_volume_governing_case_preserves_unvalidated_browser_sign_behavior(self):
# Browser getRollData() rejects zero but not a negative diameter. This
# non-physical case characterizes its otherwise reachable volume limit.
analysis = self.analysis(
roll_diameter_mm=-2400.0,
max_weight_kg=100_000.0,
)
self.assertEqual(analysis["limiting"]["name"], "Volume")
self.assertEqual([s["rolls"] for s in analysis["scenarios"]], [100, 55, 90])
def test_weight_volume_geometry_tie_order_is_weight_volume_geometry(self):
analysis = self.analysis(max_weight_kg=250_000.0)
self.assertEqual([s["rolls"] for s in analysis["scenarios"]], [250, 325, 250])
self.assertEqual(analysis["limiting"]["name"], "Weight")
def test_floor_and_clearance_behavior_matches_browser_math_floor(self):
below_boundary = self.analysis(max_weight_kg=999.9)
at_boundary = self.analysis(max_weight_kg=1000.0)
self.assertEqual(below_boundary["final_rolls"], 0)
self.assertEqual(at_boundary["final_rolls"], 1)
clearance = self.analysis(margin_side_m=0.5, margin_ceiling_m=1.0)
self.assertEqual(clearance["layout"]["effective_width_m"], 4.5)
self.assertEqual(clearance["layout"]["effective_height_m"], 4.0)
self.assertEqual(clearance["layout"]["rolls_along_width"], 4)
self.assertEqual(clearance["layout"]["max_stack_height"], 4)
def test_displayed_one_decimal_roll_weight_is_consumed_without_recalculation(self):
# The direct UI writes d-wo using toFixed(1), then transport reads it.
displayed = self.analysis(roll_weight_kg=1000.0)
unrounded = self.analysis(roll_weight_kg=1000.04)
self.assertEqual(displayed["final_rolls"], 10)
self.assertEqual(unrounded["final_rolls"], 9)
def test_existing_ui_validation_boundary_is_structured_for_the_api(self):
for field, value in (("length_m", 0), ("width_m", -1), ("max_weight_kg", 0)):
with self.subTest(field=field):
result = analyze_transport(request(**{field: value}))
self.assertEqual(result["status"], "invalid_parameter")
self.assertEqual(result["invalid"][0]["field"], field)
result = analyze_transport(request(roll_weight_kg=0))
self.assertEqual(result["status"], "invalid_parameter")
self.assertEqual(result["invalid"][0]["field"], "roll_weight_kg")
def test_missing_product_length_preserves_browser_zero_square_meter_behavior(self):
payload = request()
del payload["product_length_m"]
result = analyze_transport(payload)
self.assertEqual(result["status"], "success")
self.assertEqual(result["analysis"]["final_m2"], 0.0)
def test_volume_and_utilization_values_are_golden(self):
analysis = self.analysis()
self.assertAlmostEqual(analysis["volume_per_roll_m3"], 0.7677267047210057)
self.assertAlmostEqual(analysis["utilization_weight"], 100.0)
self.assertAlmostEqual(analysis["utilization_volume"], 3.0709068188840227)
if __name__ == "__main__":
unittest.main()