import tempfile import unittest from pathlib import Path from machine_constraints import ( MACHINE_CONFIG_FILE, MachineConfigurationError, MachineRepository, check_production_feasibility, get_machine, ) def request(**changes): payload = { "machine": "bento_1", "roll_weight_kg": 2500.0, "average_diameter_mm": 800.0, "maximum_diameter_mm": 850.0, "core_diameter_mm": 168.0, "product_width_m": 4.8, } payload.update(changes) return payload class MachineRepositoryTests(unittest.TestCase): def setUp(self): self.repository = MachineRepository.load() def test_yaml_load_represents_all_source_machines_and_bento_transformation(self): self.assertTrue(MACHINE_CONFIG_FILE.exists()) self.assertEqual(len(self.repository.machines), 15) self.assertEqual( {machine.name for machine in self.repository.machines}, { "Bento 1", "Bento 2", "K1", "K3/4", "K5", "K6", "K7", "E1", "E4", "Gitter 1", "Gitter 2", "Gitter 3", "Gitter 4", "Secudrain 1/2", "Secudrain 3", }, ) for identifier in ("bento_1", "bento_2"): machine = self.repository.resolve(identifier)["resolved_machine"] self.assertEqual(machine.constraints.roll_weight_kg.maximum, 2500.0) def test_blank_source_line_speed_is_explicitly_not_configured(self): machine = self.repository.resolve("Gitter 1")["machine"] self.assertIsNone(machine["constraints"]["line_speed_m_per_min"]) def test_lookup_uses_ids_names_aliases_and_harmless_normalization(self): for value, matched_by in ( ("bento_1", "id"), (" BENTO 1 ", "name"), (" b1 ", "alias"), ): with self.subTest(value=value): result = self.repository.resolve(value) self.assertEqual(result["status"], "resolved") self.assertEqual(result["matched_by"], matched_by) self.assertEqual(result["machine"]["id"], "bento_1") self.assertEqual(self.repository.resolve("bento") ["status"], "machine_not_found") def test_get_machine_returns_public_configured_data(self): result = get_machine("K 3/4", repository=self.repository) self.assertEqual(result["status"], "resolved") self.assertNotIn("resolved_machine", result) self.assertEqual(result["machine"]["id"], "k_3_4") def config_error(self, body): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "machines.yaml" path.write_text(body, encoding="utf-8") with self.assertRaises(MachineConfigurationError): MachineRepository.load(path) def test_configuration_validation_rejects_malformed_and_unsupported_versions(self): self.config_error("machines: [") self.config_error("schema_version: 2\nmachines: []\n") self.config_error("schema_version: 1\nunknown: true\nmachines: []\n") def test_configuration_validation_rejects_invalid_machine_data(self): valid = """ schema_version: 1 machines: - id: alpha name: Alpha aliases: [A] location: Here constraints: roll_weight_kg: {maximum: 3} roll_diameter_mm: {minimum: 1, maximum: 2} allowed_core_diameters_mm: [150] product_width_m: {minimum: 1, maximum: 2} line_speed_m_per_min: null """ self.config_error(valid.replace("minimum: 1, maximum: 2", "minimum: 2, maximum: 1")) self.config_error(valid.replace("[150]", "[0]")) self.config_error(valid.replace("aliases: [A]", "aliases: bad")) self.config_error(valid.replace("line_speed_m_per_min: null", "unsupported: true")) self.config_error(valid.replace("[150]", "[not-a-number]")) duplicate_row = valid.split(" - id: alpha", 1)[1].replace( "name: Alpha", "name: Beta", 1 ) duplicate = valid + "\n - id: beta" + duplicate_row self.config_error(duplicate) class MachineFeasibilityTests(unittest.TestCase): def check(self, **changes): return check_production_feasibility(request(**changes)) def test_inclusive_weight_and_range_boundaries(self): self.assertEqual(self.check()["feasibility"], "feasible") self.assertEqual(self.check(roll_weight_kg=2500.0001)["feasibility"], "not_feasible") self.assertEqual(self.check(average_diameter_mm=300, maximum_diameter_mm=300)["feasibility"], "feasible") self.assertEqual(self.check(average_diameter_mm=876, maximum_diameter_mm=876)["feasibility"], "feasible") self.assertEqual(self.check(average_diameter_mm=299, maximum_diameter_mm=299)["feasibility"], "not_feasible") def test_exact_width_machines_do_not_have_an_implicit_tolerance(self): exact = self.check(machine="e1", product_width_m=7.5, average_diameter_mm=900, maximum_diameter_mm=900) low = self.check(machine="e1", product_width_m=7.4999, average_diameter_mm=900, maximum_diameter_mm=900) high = self.check(machine="e1", product_width_m=7.5001, average_diameter_mm=900, maximum_diameter_mm=900) self.assertEqual(exact["feasibility"], "feasible") self.assertIn("product_width_m", low["failed_constraints"]) self.assertIn("product_width_m", high["failed_constraints"]) def test_configured_core_equivalents_are_exact_membership_only(self): for core in (130, 133, 150, 168, 170): with self.subTest(core=core): self.assertEqual(self.check(machine="k1", roll_weight_kg=800, core_diameter_mm=core)["feasibility"], "feasible") failed = self.check(machine="k1", roll_weight_kg=800, core_diameter_mm=169.999) self.assertIn("allowed_core_diameters_mm", failed["failed_constraints"]) def test_diameter_distinguishes_pass_warning_and_failure(self): passed = self.check(average_diameter_mm=800, maximum_diameter_mm=850) warning = self.check(average_diameter_mm=876, maximum_diameter_mm=876.1) failed = self.check(average_diameter_mm=876.1, maximum_diameter_mm=900) self.assertEqual(passed["feasibility"], "feasible") self.assertEqual(warning["feasibility"], "feasible_with_warnings") self.assertEqual(warning["warning_constraints"], ["roll_diameter_mm"]) self.assertEqual(failed["feasibility"], "not_feasible") self.assertIn("roll_diameter_mm", failed["failed_constraints"]) def test_failures_and_warnings_are_all_retained(self): result = self.check(roll_weight_kg=3000, average_diameter_mm=800, maximum_diameter_mm=900, core_diameter_mm=151, product_width_m=4.0) self.assertEqual(result["feasibility"], "not_feasible") self.assertEqual(result["warning_constraints"], ["roll_diameter_mm"]) self.assertEqual(set(result["failed_constraints"]), {"roll_weight_kg", "allowed_core_diameters_mm", "product_width_m"}) def test_missing_values_keep_evaluable_checks_but_never_claim_feasible(self): result = self.check(roll_weight_kg=None, maximum_diameter_mm=None, core_diameter_mm=None) self.assertEqual(result["status"], "needs_clarification") self.assertIsNone(result["feasible"]) self.assertEqual(set(result["missing_required_inputs"]), {"roll_weight_kg", "maximum_diameter_mm", "core_diameter_mm"}) self.assertIn("product_width_m", result["passed_constraints"]) def test_line_speed_is_not_required_or_defaulted_in_v1(self): result = self.check(machine="gitter_1", roll_weight_kg=400, average_diameter_mm=600, maximum_diameter_mm=600, core_diameter_mm=140, product_width_m=4.0) line_speed = result["checks"][-1] self.assertEqual(result["feasibility"], "feasible") self.assertEqual(line_speed["state"], "not_evaluated") self.assertIsNone(line_speed["limits"]) def test_invalid_and_unknown_requests_are_structured(self): self.assertEqual(check_production_feasibility({"machine": "B1", "unknown": 1})["status"], "invalid_parameter") self.assertEqual(check_production_feasibility({"machine": "B1", "core_weight_kg": 10})["status"], "invalid_parameter") self.assertEqual(self.check(machine="not a machine")["status"], "machine_not_found")