import contextlib import io import subprocess import sys import unittest from unittest import mock from tests.python.support.paths import add_repo_path, repo_path add_repo_path("skills/cad/scripts") from step import cli class StepCliTests(unittest.TestCase): def test_requires_explicit_target(self) -> None: with self.assertRaises(SystemExit) as cm: cli.main([]) self.assertEqual(2, cm.exception.code) def test_passes_targets_in_order_without_kind_for_generated_sources(self) -> None: with mock.patch.object(cli, "generate_step_targets", return_value=0) as generate: self.assertEqual(0, cli.main(["parts/second.py", "parts/first.py"])) generate.assert_called_once() self.assertEqual(["parts/second.py", "parts/first.py"], generate.call_args.args[0]) self.assertIsNone(generate.call_args.kwargs["direct_step_kind"]) self.assertFalse(generate.call_args.kwargs["step_options"].has_metadata) self.assertIsNone(generate.call_args.kwargs["output"]) self.assertFalse(generate.call_args.kwargs["skip_step_write"]) self.assertFalse(generate.call_args.kwargs["verbose"]) def test_passes_direct_step_kind(self) -> None: with mock.patch.object(cli, "generate_step_targets", return_value=0) as generate: self.assertEqual(0, cli.main(["--kind", "assembly", "assemblies/second.step", "assemblies/first.step"])) generate.assert_called_once() self.assertEqual(["assemblies/second.step", "assemblies/first.step"], generate.call_args.args[0]) self.assertEqual("assembly", generate.call_args.kwargs["direct_step_kind"]) self.assertFalse(generate.call_args.kwargs["step_options"].has_metadata) self.assertIsNone(generate.call_args.kwargs["output"]) self.assertFalse(generate.call_args.kwargs["verbose"]) def test_passes_verbose_flag(self) -> None: with mock.patch.object(cli, "generate_step_targets", return_value=0) as generate: self.assertEqual(0, cli.main(["parts/sample.py", "--verbose"])) self.assertTrue(generate.call_args.kwargs["verbose"]) def test_passes_skip_step_write_flag(self) -> None: with mock.patch.object(cli, "generate_step_targets", return_value=0) as generate: self.assertEqual(0, cli.main(["parts/sample.py", "--skip-step-write"])) self.assertTrue(generate.call_args.kwargs["skip_step_write"]) def test_passes_output_flag(self) -> None: with mock.patch.object(cli, "generate_step_targets", return_value=0) as generate: self.assertEqual(0, cli.main(["parts/sample.py", "-o", "STEP/sample.step"])) self.assertEqual("STEP/sample.step", generate.call_args.kwargs["output"]) def test_output_flag_rejects_multiple_targets(self) -> None: with self.assertRaises(SystemExit) as cm: cli.main(["parts/first.py", "parts/second.py", "-o", "STEP/first.step"]) self.assertEqual(2, cm.exception.code) def test_passes_import_metadata_flags(self) -> None: with mock.patch.object(cli, "generate_step_targets", return_value=0) as generate: self.assertEqual( 0, cli.main( [ "--kind", "part", "imports/sample_part.step", "--stl", "../meshes/sample_part.stl", "--3mf", "../meshes/sample_part.3mf", "--glb", "../meshes/sample_part.glb", "--mesh-tolerance", "0.2", "--mesh-angular-tolerance", "0.25", ] ), ) generate.assert_called_once() self.assertEqual(["imports/sample_part.step"], generate.call_args.args[0]) self.assertEqual("part", generate.call_args.kwargs["direct_step_kind"]) options = generate.call_args.kwargs["step_options"] self.assertEqual("../meshes/sample_part.stl", options.stl) self.assertEqual("../meshes/sample_part.3mf", options.three_mf) self.assertEqual("../meshes/sample_part.glb", options.glb) self.assertEqual(0.2, options.mesh_tolerance) self.assertEqual(0.25, options.mesh_angular_tolerance) def test_rejects_invalid_numeric_flag(self) -> None: with self.assertRaises(SystemExit) as cm: cli.main(["imports/sample_part.step", "--mesh-tolerance", "nan"]) self.assertEqual(2, cm.exception.code) def test_rejects_removed_inspect_flag(self) -> None: with self.assertRaises(SystemExit) as cm: cli.main(["parts/sample.step", "--inspect", "refs {cad_token} --facts"]) self.assertEqual(2, cm.exception.code) def test_help_has_current_step_flags_only(self) -> None: stream = io.StringIO() with self.assertRaises(SystemExit) as cm, contextlib.redirect_stdout(stream): cli.main(["--help"]) self.assertEqual(0, cm.exception.code) help_text = stream.getvalue() self.assertIn("--kind", help_text) self.assertIn("--stl", help_text) self.assertIn("--3mf", help_text) self.assertIn("--glb", help_text) self.assertIn("--skip-step-write", help_text) self.assertIn("--output", help_text) self.assertIn("--mesh-tolerance", help_text) self.assertIn("--verbose", help_text) def test_cli_does_not_reserve_common_module_name(self) -> None: skill_root = repo_path("skills/cad") code = ( "import sys; sys.path.insert(0, 'scripts'); import step.cli; " "print('common' in sys.modules); " "print('OCP.OCP' in sys.modules); " "print('cadpy.step_scene' in sys.modules)" ) result = subprocess.run( [sys.executable, "-c", code], cwd=skill_root, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) self.assertEqual("", result.stderr) self.assertEqual(0, result.returncode) self.assertEqual(["False", "False", "False"], result.stdout.strip().splitlines()) if __name__ == "__main__": unittest.main()