CallMeDaniel Claude Opus 4.6 (1M context) commited on
Commit
ef30000
·
1 Parent(s): 17e56f5

test: add CadQuery executor and CNC validator integration tests

Browse files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. tests/test_executor.py +104 -0
  2. tests/test_validator.py +76 -0
tests/test_executor.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for core/executor.py — CadQuery code execution and export.
2
+
3
+ These tests require CadQuery to be installed.
4
+ """
5
+
6
+ import pytest
7
+ from pathlib import Path
8
+ from core.executor import sanitize_code, execute_cadquery, export_step, export_stl, export_all
9
+
10
+ pytestmark = pytest.mark.requires_cadquery
11
+
12
+
13
+ class TestSanitizeCode:
14
+ def test_strips_markdown_fences(self):
15
+ code = "```python\nresult = 1\n```"
16
+ assert "```" not in sanitize_code(code)
17
+
18
+ def test_strips_plain_fences(self):
19
+ code = "```\nresult = 1\n```"
20
+ assert "```" not in sanitize_code(code)
21
+
22
+ def test_removes_cadquery_imports(self):
23
+ code = "import cadquery as cq\nresult = cq.Workplane('XY').box(10,10,10)"
24
+ cleaned = sanitize_code(code)
25
+ assert "import cadquery" not in cleaned
26
+ assert "result" in cleaned
27
+
28
+ def test_removes_math_import(self):
29
+ code = "import math\nresult = cq.Workplane('XY').box(10,10,10)"
30
+ cleaned = sanitize_code(code)
31
+ assert "import math" not in cleaned
32
+
33
+ def test_preserves_valid_code(self):
34
+ code = "result = cq.Workplane('XY').box(10, 20, 30)"
35
+ assert sanitize_code(code) == code
36
+
37
+
38
+ class TestExecuteCadquery:
39
+ def test_simple_box(self):
40
+ result = execute_cadquery("result = cq.Workplane('XY').box(10, 20, 30)")
41
+ assert result.success is True
42
+ assert result.volume > 0
43
+ assert result.face_count == 6
44
+ assert result.edge_count == 12
45
+ assert len(result.bounding_box) == 3
46
+
47
+ def test_cylinder(self):
48
+ result = execute_cadquery("result = cq.Workplane('XY').cylinder(20, 10)")
49
+ assert result.success is True
50
+ assert result.volume > 0
51
+
52
+ def test_missing_result_variable(self):
53
+ result = execute_cadquery("x = cq.Workplane('XY').box(10,10,10)")
54
+ assert result.success is False
55
+ assert "result" in result.error
56
+
57
+ def test_syntax_error(self):
58
+ result = execute_cadquery("result = cq.Workplane('XY').box(10, 10,")
59
+ assert result.success is False
60
+ assert result.error is not None
61
+
62
+ def test_wrong_type(self):
63
+ result = execute_cadquery("result = 42")
64
+ assert result.success is False
65
+ assert "Workplane" in result.error
66
+
67
+ def test_code_with_markdown_fences(self):
68
+ code = "```python\nimport cadquery as cq\nresult = cq.Workplane('XY').box(5,5,5)\n```"
69
+ result = execute_cadquery(code)
70
+ assert result.success is True
71
+
72
+ def test_summary_on_success(self):
73
+ result = execute_cadquery("result = cq.Workplane('XY').box(10, 20, 30)")
74
+ summary = result.summary()
75
+ assert "OK" in summary
76
+ assert "Volume" in summary
77
+
78
+ def test_summary_on_failure(self):
79
+ result = execute_cadquery("result = bad_code")
80
+ summary = result.summary()
81
+ assert "FAILED" in summary
82
+
83
+
84
+ class TestExport:
85
+ def test_export_step(self, tmp_path):
86
+ exec_result = execute_cadquery("result = cq.Workplane('XY').box(10,10,10)")
87
+ assert exec_result.success
88
+ path = export_step(exec_result.result, tmp_path / "test.step")
89
+ assert path.exists()
90
+ assert path.suffix == ".step"
91
+
92
+ def test_export_stl(self, tmp_path):
93
+ exec_result = execute_cadquery("result = cq.Workplane('XY').box(10,10,10)")
94
+ assert exec_result.success
95
+ path = export_stl(exec_result.result, tmp_path / "test.stl")
96
+ assert path.exists()
97
+ assert path.suffix == ".stl"
98
+
99
+ def test_export_all(self, tmp_path):
100
+ exec_result = execute_cadquery("result = cq.Workplane('XY').box(10,10,10)")
101
+ assert exec_result.success
102
+ files = export_all(exec_result.result, tmp_path / "part")
103
+ assert files["step"].exists()
104
+ assert files["stl"].exists()
tests/test_validator.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for core/validator.py — CNC manufacturability validation.
2
+
3
+ These tests require CadQuery to be installed.
4
+ """
5
+
6
+ import pytest
7
+ from core.executor import execute_cadquery
8
+ from core.validator import validate_for_cnc, CNCValidationResult, CNCIssue
9
+
10
+ pytestmark = pytest.mark.requires_cadquery
11
+
12
+
13
+ def _make_solid(code: str):
14
+ """Helper to create a CadQuery Workplane from code."""
15
+ result = execute_cadquery(code)
16
+ assert result.success, f"Code failed: {result.error}"
17
+ return result.result
18
+
19
+
20
+ class TestValidateForCnc:
21
+ def test_simple_box_is_machinable(self):
22
+ solid = _make_solid("result = cq.Workplane('XY').box(50, 30, 10)")
23
+ val = validate_for_cnc(solid, "test_box")
24
+ assert val.machinable is True
25
+ assert val.error_count == 0
26
+
27
+ def test_result_has_part_name(self):
28
+ solid = _make_solid("result = cq.Workplane('XY').box(50, 30, 10)")
29
+ val = validate_for_cnc(solid, "my_part")
30
+ assert val.part_name == "my_part"
31
+
32
+ def test_axis_recommendation_default_3axis(self):
33
+ solid = _make_solid("result = cq.Workplane('XY').box(50, 30, 10)")
34
+ val = validate_for_cnc(solid)
35
+ assert "3-axis" in val.axis_recommendation or "3" in val.axis_recommendation
36
+
37
+ def test_complex_part_gets_higher_axis(self):
38
+ code = '''
39
+ result = cq.Workplane('XY').box(50, 50, 50)
40
+ for i in range(5):
41
+ result = result.faces('>Z').workplane().pushPoints([(i*8-16, 0)]).hole(3)
42
+ for i in range(5):
43
+ result = result.faces('>X').workplane().pushPoints([(i*8-16, 0)]).hole(3)
44
+ '''
45
+ solid = _make_solid(code)
46
+ val = validate_for_cnc(solid)
47
+ assert val.part_name is not None
48
+
49
+ def test_oversized_part_flagged(self):
50
+ solid = _make_solid("result = cq.Workplane('XY').box(600, 600, 600)")
51
+ val = validate_for_cnc(solid, config={"max_part_size_mm": 500.0})
52
+ assert any(i.category == "Size" for i in val.issues)
53
+
54
+ def test_tiny_part_flagged(self):
55
+ solid = _make_solid("result = cq.Workplane('XY').box(0.5, 0.5, 0.5)")
56
+ val = validate_for_cnc(solid, config={"min_part_size_mm": 1.0})
57
+ assert any(i.category == "Size" for i in val.issues)
58
+
59
+ def test_summary_format(self):
60
+ solid = _make_solid("result = cq.Workplane('XY').box(50, 30, 10)")
61
+ val = validate_for_cnc(solid, "test")
62
+ summary = val.summary()
63
+ assert isinstance(summary, str)
64
+ assert "test" in summary
65
+
66
+ def test_custom_config(self):
67
+ solid = _make_solid("result = cq.Workplane('XY').box(50, 30, 10)")
68
+ val = validate_for_cnc(solid, config={"min_wall_thickness_mm": 0.5})
69
+ assert isinstance(val, CNCValidationResult)
70
+
71
+ def test_error_and_warning_counts(self):
72
+ solid = _make_solid("result = cq.Workplane('XY').box(50, 30, 10)")
73
+ val = validate_for_cnc(solid)
74
+ assert val.error_count >= 0
75
+ assert val.warning_count >= 0
76
+ assert val.error_count + val.warning_count <= len(val.issues)