Spaces:
Sleeping
Sleeping
Commit ·
ef30000
1
Parent(s): 17e56f5
test: add CadQuery executor and CNC validator integration tests
Browse filesCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- tests/test_executor.py +104 -0
- 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)
|