import tempfile from pathlib import Path import numpy as np import pytest from server.preprocessor import ( normalize_shape, sample_surface_points, voxelize, preprocess_from_code, _occ_to_trimesh, ) class TestNormalizeShape: def test_centered_at_origin(self, flat_plate_shape): normalized, info = normalize_shape(flat_plate_shape) bb = normalized.BoundingBox() cx = (bb.xmin + bb.xmax) / 2 cy = (bb.ymin + bb.ymax) / 2 cz = (bb.zmin + bb.zmax) / 2 assert abs(cx) < 0.02 assert abs(cy) < 0.02 assert abs(cz) < 0.02 def test_longest_axis_is_x(self, flat_plate_shape): normalized, info = normalize_shape(flat_plate_shape) bb = normalized.BoundingBox() assert bb.xlen >= bb.ylen assert bb.xlen >= bb.zlen def test_preserves_volume(self, flat_plate_shape): original_vol = flat_plate_shape.Volume() normalized, _ = normalize_shape(flat_plate_shape) new_vol = normalized.Volume() assert abs(original_vol - new_vol) / original_vol < 0.01 def test_transform_info(self, flat_plate_shape): _, info = normalize_shape(flat_plate_shape) assert "units" in info assert info["units"] == "mm" assert "translation_to_origin" in info assert "longest_axis" in info assert info["longest_axis"] == "X" def test_box_with_hole_normalized(self, box_with_hole_shape): normalized, info = normalize_shape(box_with_hole_shape) bb = normalized.BoundingBox() assert bb.xlen >= bb.ylen >= bb.zlen class TestOccToTrimesh: def test_returns_trimesh(self, flat_plate_shape): normalized, _ = normalize_shape(flat_plate_shape) mesh = _occ_to_trimesh(normalized) import trimesh assert isinstance(mesh, trimesh.Trimesh) assert len(mesh.vertices) > 0 assert len(mesh.faces) > 0 def test_mesh_is_valid(self, flat_plate_shape): normalized, _ = normalize_shape(flat_plate_shape) mesh = _occ_to_trimesh(normalized) assert not mesh.is_empty class TestSampleSurfacePoints: def test_returns_correct_shape(self, flat_plate_shape): normalized, _ = normalize_shape(flat_plate_shape) points = sample_surface_points(normalized, n_points=1024) assert points.shape == (1024, 3) assert points.dtype == np.float32 def test_points_near_surface(self, flat_plate_shape): normalized, _ = normalize_shape(flat_plate_shape) bb = normalized.BoundingBox() points = sample_surface_points(normalized, n_points=2048) assert np.all(points[:, 0] >= bb.xmin - 0.1) assert np.all(points[:, 0] <= bb.xmax + 0.1) assert np.all(points[:, 1] >= bb.ymin - 0.1) assert np.all(points[:, 1] <= bb.ymax + 0.1) assert np.all(points[:, 2] >= bb.zmin - 0.1) assert np.all(points[:, 2] <= bb.zmax + 0.1) def test_different_n_points(self, flat_plate_shape): normalized, _ = normalize_shape(flat_plate_shape) for n in [256, 512, 2048]: pts = sample_surface_points(normalized, n_points=n) assert pts.shape[0] == n class TestVoxelize: def test_returns_correct_shape(self, flat_plate_shape): normalized, _ = normalize_shape(flat_plate_shape) grid = voxelize(normalized, resolution=32) assert grid.shape == (32, 32, 32) assert grid.dtype == bool def test_has_filled_voxels(self, flat_plate_shape): normalized, _ = normalize_shape(flat_plate_shape) grid = voxelize(normalized, resolution=32) assert grid.sum() > 0 def test_not_all_filled(self, flat_plate_shape): normalized, _ = normalize_shape(flat_plate_shape) grid = voxelize(normalized, resolution=32) assert grid.sum() < 32 * 32 * 32 def test_hollow_shape_has_fewer_voxels(self, flat_plate_shape, box_with_hole_shape): norm_plate, _ = normalize_shape(flat_plate_shape) norm_hole, _ = normalize_shape(box_with_hole_shape) grid_plate = voxelize(norm_plate, resolution=32) grid_hole = voxelize(norm_hole, resolution=32) plate_fill = grid_plate.sum() / (32**3) hole_fill = grid_hole.sum() / (32**3) assert plate_fill > 0 assert hole_fill > 0 def test_resolution_64(self, flat_plate_shape): normalized, _ = normalize_shape(flat_plate_shape) grid = voxelize(normalized, resolution=64) assert grid.shape == (64, 64, 64) assert grid.sum() > 0 class TestPreprocessFromCode: def test_generates_all_files(self, flat_plate_code): with tempfile.TemporaryDirectory() as tmpdir: gt = preprocess_from_code(flat_plate_code, tmpdir, task_id="test_flat_plate") output_path = Path(tmpdir) assert (output_path / "ground_truth.json").exists() assert (output_path / "ground_truth.step").exists() assert (output_path / "ground_truth_normalized.step").exists() assert (output_path / "surface_points.npy").exists() assert (output_path / "voxels_64.npy").exists() def test_ground_truth_json_contents(self, flat_plate_code): with tempfile.TemporaryDirectory() as tmpdir: gt = preprocess_from_code(flat_plate_code, tmpdir, task_id="test_flat_plate") assert "volume_mm3" in gt assert "bbox_mm" in gt assert "face_count" in gt assert "dominant_face_type" in gt assert "surface_points_file" in gt assert "voxels_file" in gt assert gt["volume_mm3"] > 0 assert gt["face_count"] == 6 def test_surface_points_file(self, flat_plate_code): with tempfile.TemporaryDirectory() as tmpdir: preprocess_from_code(flat_plate_code, tmpdir) pts = np.load(str(Path(tmpdir) / "surface_points.npy")) assert pts.shape == (2048, 3) def test_voxels_file(self, flat_plate_code): with tempfile.TemporaryDirectory() as tmpdir: preprocess_from_code(flat_plate_code, tmpdir) vox = np.load(str(Path(tmpdir) / "voxels_64.npy")) assert vox.shape == (64, 64, 64) assert vox.sum() > 0 def test_invalid_code_raises(self, invalid_code): with tempfile.TemporaryDirectory() as tmpdir: with pytest.raises(Exception): preprocess_from_code(invalid_code, tmpdir) def test_no_result_raises(self, no_result_code): with tempfile.TemporaryDirectory() as tmpdir: with pytest.raises(ValueError, match="result"): preprocess_from_code(no_result_code, tmpdir)