153 lines
6.5 KiB
Python
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()
|