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()