# -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2025 Brad Collette * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * # * as published by the Free Software Foundation; either version 2 of * # * the License, or (at your option) any later version. * # * for detail see the LICENCE text file. * # * * # *************************************************************************** import FreeCAD import tempfile import pathlib import CAMTests.PathTestUtils as PathTestUtils from Path.Machine.models.machine import ( Machine, Spindle, OutputOptions, GCodeBlocks, ProcessingOptions, MachineFactory, ) class TestMachineDataclass(PathTestUtils.PathTestBase): """Test the unified Machine dataclass""" def setUp(self): """Set up test fixtures""" self.default_machine = Machine() def test_default_initialization(self): """Test that Machine initializes with sensible defaults""" machine = Machine() # Basic identification self.assertEqual(machine.name, "Default Machine") self.assertEqual(machine.manufacturer, "") self.assertEqual(machine.description, "") # Machine type is derived from axes configuration self.assertEqual(machine.machine_type, "custom") # No axes configured yet # Add axes and verify machine type updates machine.add_linear_axis("X", FreeCAD.Vector(1, 0, 0)) machine.add_linear_axis("Y", FreeCAD.Vector(0, 1, 0)) self.assertEqual(machine.machine_type, "custom") # Still missing Z axis machine.add_linear_axis("Z", FreeCAD.Vector(0, 0, 1)) self.assertEqual(machine.machine_type, "xyz") # Now has XYZ axes # Add rotary axes and verify machine type updates machine.add_rotary_axis("A", FreeCAD.Vector(1, 0, 0), -120, 120) self.assertEqual(machine.machine_type, "xyza") machine.add_rotary_axis("C", FreeCAD.Vector(0, 0, 1), -360, 360) self.assertEqual(machine.machine_type, "xyzac") # Coordinate system defaults self.assertEqual(machine.reference_system["X"], FreeCAD.Vector(1, 0, 0)) self.assertEqual(machine.reference_system["Y"], FreeCAD.Vector(0, 1, 0)) self.assertEqual(machine.reference_system["Z"], FreeCAD.Vector(0, 0, 1)) self.assertEqual(machine.tool_axis, FreeCAD.Vector(0, 0, -1)) # Units and versioning self.assertEqual(machine.configuration_units, "metric") self.assertEqual(machine.version, 1) self.assertIsNotNone(machine.freecad_version) # Post-processor defaults self.assertIsInstance(machine.output, OutputOptions) self.assertIsInstance(machine.blocks, GCodeBlocks) self.assertIsInstance(machine.processing, ProcessingOptions) def test_custom_initialization(self): """Test Machine initialization with custom values and verify machine_type is derived""" # Create a 5-axis machine (XYZAC) machine = Machine( name="Test Mill", manufacturer="ACME Corp", description="5-axis mill", configuration_units="imperial", ) # Add axes to make it a 5-axis machine machine.add_linear_axis("X", FreeCAD.Vector(1, 0, 0)) machine.add_linear_axis("Y", FreeCAD.Vector(0, 1, 0)) machine.add_linear_axis("Z", FreeCAD.Vector(0, 0, 1)) machine.add_rotary_axis("A", FreeCAD.Vector(1, 0, 0), -120, 120) machine.add_rotary_axis("C", FreeCAD.Vector(0, 0, 1), -360, 360) self.assertEqual(machine.name, "Test Mill") self.assertEqual(machine.manufacturer, "ACME Corp") self.assertEqual(machine.description, "5-axis mill") self.assertEqual(machine.machine_type, "xyzac") self.assertEqual(machine.configuration_units, "imperial") def test_configuration_units_property(self): """Test configuration_units property returns correct values""" metric_machine = Machine(configuration_units="metric") self.assertEqual(metric_machine.configuration_units, "metric") imperial_machine = Machine(configuration_units="imperial") self.assertEqual(imperial_machine.configuration_units, "imperial") class TestOutputOptions(PathTestUtils.PathTestBase): """Test OutputOptions dataclass""" def test_default_initialization(self): """Test OutputOptions initialization with defaults""" opts = OutputOptions() # Default values self.assertTrue(opts.comments) self.assertTrue(opts.blank_lines) self.assertTrue(opts.header) self.assertFalse(opts.line_numbers) self.assertFalse(opts.bcnc_blocks) self.assertFalse(opts.path_labels) self.assertFalse(opts.machine_name) self.assertTrue(opts.tool_change) self.assertTrue(opts.doubles) self.assertFalse(opts.adaptive) def test_custom_initialization(self): """Test OutputOptions initialization with custom values""" opts = OutputOptions( comments=False, blank_lines=False, header=False, line_numbers=True, bcnc_blocks=True, path_labels=True, machine_name=True, tool_change=False, doubles=False, adaptive=True, ) # Verify custom values self.assertFalse(opts.comments) self.assertFalse(opts.blank_lines) self.assertFalse(opts.header) self.assertTrue(opts.line_numbers) self.assertTrue(opts.bcnc_blocks) self.assertTrue(opts.path_labels) self.assertTrue(opts.machine_name) self.assertFalse(opts.tool_change) self.assertFalse(opts.doubles) self.assertTrue(opts.adaptive) def test_equality(self): """Test OutputOptions equality comparison""" opts1 = OutputOptions() opts2 = OutputOptions() self.assertEqual(opts1, opts2) opts2.comments = False self.assertNotEqual(opts1, opts2) class TestSpindle(PathTestUtils.PathTestBase): """Test Spindle dataclass""" def test_spindle_initialization(self): """Test Spindle initialization with defaults""" spindle = Spindle( name="Main Spindle", max_power_kw=5.5, max_rpm=24000, min_rpm=1000, tool_change="automatic", ) self.assertEqual(spindle.name, "Main Spindle") self.assertEqual(spindle.max_power_kw, 5.5) self.assertEqual(spindle.max_rpm, 24000) self.assertEqual(spindle.min_rpm, 1000) self.assertEqual(spindle.tool_change, "automatic") # Default tool axis should be set self.assertEqual(spindle.tool_axis, FreeCAD.Vector(0, 0, -1)) def test_spindle_custom_tool_axis(self): """Test Spindle with custom tool axis""" spindle = Spindle( name="Side Spindle", tool_axis=FreeCAD.Vector(1, 0, 0), ) self.assertEqual(spindle.tool_axis, FreeCAD.Vector(1, 0, 0)) def test_spindle_serialization(self): """Test to_dict and from_dict""" spindle = Spindle( name="Test Spindle", id="spindle-001", max_power_kw=3.0, max_rpm=18000, min_rpm=500, tool_change="manual", tool_axis=FreeCAD.Vector(0, 1, 0), ) data = spindle.to_dict() self.assertEqual(data["name"], "Test Spindle") self.assertEqual(data["id"], "spindle-001") self.assertEqual(data["max_power_kw"], 3.0) self.assertEqual(data["tool_axis"], [0, 1, 0]) restored = Spindle.from_dict(data) self.assertEqual(restored.name, spindle.name) self.assertEqual(restored.id, spindle.id) self.assertEqual(restored.max_power_kw, spindle.max_power_kw) self.assertEqual(restored.tool_axis, spindle.tool_axis) class TestMachineFactory(PathTestUtils.PathTestBase): """Test MachineFactory class for loading/saving configurations""" def setUp(self): """Set up test fixtures with temporary directory""" self.temp_dir = tempfile.mkdtemp() self.temp_path = pathlib.Path(self.temp_dir) MachineFactory.set_config_directory(self.temp_dir) def tearDown(self): """Clean up temporary directory""" import shutil if self.temp_path.exists(): shutil.rmtree(self.temp_path) def test_set_and_get_config_directory(self): """Test setting and getting configuration directory""" test_dir = self.temp_path / "test_configs" MachineFactory.set_config_directory(test_dir) config_dir = MachineFactory.get_config_directory() self.assertEqual(config_dir, test_dir) self.assertTrue(config_dir.exists()) def test_save_and_load_configuration(self): """Test saving and loading a machine configuration""" # Create a test machine machine = Machine( name="Test Machine", manufacturer="Test Corp", description="Test description", configuration_units="metric", ) # Add axes to make it an XYZ machine machine.add_linear_axis("X", FreeCAD.Vector(1, 0, 0)) machine.add_linear_axis("Y", FreeCAD.Vector(0, 1, 0)) machine.add_linear_axis("Z", FreeCAD.Vector(0, 0, 1)) # Add a spindle spindle = Spindle( name="Main Spindle", max_power_kw=5.5, max_rpm=24000, min_rpm=1000, ) machine.spindles.append(spindle) # Save configuration filepath = MachineFactory.save_configuration(machine, "test_machine.fcm") self.assertTrue(filepath.exists()) # Load configuration loaded_machine = MachineFactory.load_configuration("test_machine.fcm") # Verify loaded data self.assertEqual(loaded_machine.name, "Test Machine") self.assertEqual(loaded_machine.manufacturer, "Test Corp") self.assertEqual(loaded_machine.description, "Test description") self.assertEqual(loaded_machine.machine_type, "xyz") self.assertEqual(loaded_machine.configuration_units, "metric") self.assertEqual(len(loaded_machine.spindles), 1) self.assertEqual(loaded_machine.spindles[0].name, "Main Spindle") def test_save_configuration_auto_filename(self): """Test saving with automatic filename generation""" machine = Machine(name="My Test Machine") filepath = MachineFactory.save_configuration(machine) # Should create file with sanitized name self.assertTrue(filepath.exists()) self.assertEqual(filepath.name, "My_Test_Machine.fcm") def test_load_nonexistent_file(self): """Test loading a file that doesn't exist""" with self.assertRaises(FileNotFoundError): MachineFactory.load_configuration("nonexistent.fcm") def test_create_default_machine_data(self): """Test creating default machine data dictionary""" data = MachineFactory.create_default_machine_data() self.assertIsInstance(data, dict) # The data structure has nested "machine" key self.assertIn("machine", data) self.assertEqual(data["machine"]["name"], "New Machine") self.assertIn("spindles", data["machine"]) def test_list_configuration_files(self): """Test listing available configuration files""" # Create some test configurations machine1 = Machine(name="Machine 1") machine2 = Machine(name="Machine 2") MachineFactory.save_configuration(machine1, "machine1.fcm") MachineFactory.save_configuration(machine2, "machine2.fcm") # List configurations configs = MachineFactory.list_configuration_files() # Should include plus our two machines self.assertGreaterEqual(len(configs), 3) self.assertEqual(configs[0][0], "") # Check that our machines are in the list (by display name, not filename) names = [name for name, path in configs] self.assertIn("Machine 1", names) self.assertIn("Machine 2", names) def test_list_configurations(self): """Test listing configuration names""" machine = Machine(name="Test Machine") MachineFactory.save_configuration(machine, "test.fcm") configs = MachineFactory.list_configurations() self.assertIsInstance(configs, list) self.assertIn("", configs) # Returns display name from JSON, not filename self.assertIn("Test Machine", configs) def test_delete_configuration(self): """Test deleting a configuration file""" machine = Machine(name="To Delete") filepath = MachineFactory.save_configuration(machine, "delete_me.fcm") self.assertTrue(filepath.exists()) # Delete the configuration result = MachineFactory.delete_configuration("delete_me.fcm") self.assertTrue(result) self.assertFalse(filepath.exists()) # Try deleting again (should return False) result = MachineFactory.delete_configuration("delete_me.fcm") self.assertFalse(result) def test_get_builtin_config(self): """Test getting built-in machine configurations""" # Test each built-in config type config_types = ["XYZ", "XYZAC", "XYZBC", "XYZA", "XYZB"] for config_type in config_types: machine = MachineFactory.get_builtin_config(config_type) self.assertIsInstance(machine, Machine) self.assertIsNotNone(machine.name) def test_get_builtin_config_invalid_type(self): """Test getting built-in config with invalid type""" with self.assertRaises(ValueError): MachineFactory.get_builtin_config("INVALID") def test_serialization_roundtrip(self): """Test full serialization roundtrip with complex machine""" # Create a complex machine with all components machine = Machine( name="Complex Machine", manufacturer="Test Mfg", description="Full featured machine", configuration_units="metric", ) # Add spindle machine.spindles.append( Spindle( name="Main", max_power_kw=7.5, max_rpm=30000, ) ) # Configure post-processor settings machine.output.output_comments = False machine.output.axis_precision = 4 machine.output.line_increment = 5 # line_increment is set to default 10 in OutputOptions # Save and load filepath = MachineFactory.save_configuration(machine, "complex.fcm") loaded = MachineFactory.load_configuration(filepath) # Verify all components self.assertEqual(loaded.name, machine.name) self.assertEqual(loaded.manufacturer, machine.manufacturer) self.assertEqual(len(loaded.spindles), 1) self.assertFalse(loaded.output.output_comments) self.assertEqual(loaded.output.axis_precision, 4) self.assertEqual(loaded.output.line_increment, 5)