Skip to content

Commit 8be54d9

Browse files
committed
test(data): add unit tests the mesh generator + add slow config
1 parent da1f55c commit 8be54d9

9 files changed

Lines changed: 145 additions & 59 deletions

File tree

.github/workflows/test.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ jobs:
2222
steps:
2323
- name: Checkout
2424
uses: actions/checkout@v6
25+
with:
26+
submodules: true
2527

2628
- name: Set up Python ${{ matrix.python-version }}
2729
uses: astral-sh/setup-uv@v7
@@ -32,4 +34,4 @@ jobs:
3234
run: uv sync --all-extras
3335

3436
- name: Run Tests
35-
run: uv run pytest
37+
run: uv run pytest --slow

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"python.testing.pytestArgs": ["edge_mender"],
2+
"python.testing.pytestArgs": ["edge_mender", "--slow"],
33
"python.testing.unittestEnabled": false,
44
"python.testing.pytestEnabled": true
55
}

conftest.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Configuration for pytest."""
2+
3+
import pytest
4+
5+
6+
def pytest_addoption(parser: pytest.Parser) -> None:
7+
"""Add command line options for pytest."""
8+
parser.addoption(
9+
"--slow",
10+
action="store_true",
11+
default=False,
12+
help="Run long-running tests",
13+
)
14+
15+
16+
def pytest_configure(config: pytest.Config) -> None:
17+
"""Configure pytest with custom markers."""
18+
config.addinivalue_line("markers", "slow: mark test as slow to run")
19+
20+
21+
def pytest_collection_modifyitems(
22+
config: pytest.Config,
23+
items: list[pytest.Item],
24+
) -> None:
25+
"""Modify collected test items to skip slow tests unless --slow is given."""
26+
if config.getoption("--slow"):
27+
# --slow given in cli: do not skip slow tests
28+
return
29+
skip_slow = pytest.mark.skip(reason="need --slow option to run")
30+
for item in items:
31+
if "slow" in item.keywords:
32+
item.add_marker(skip_slow)

edge_mender/mesh_generator.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Module for generating meshes from 3D numpy arrays using different algorithms."""
2+
13
import sys
24
from pathlib import Path
35

@@ -14,18 +16,20 @@
1416

1517

1618
class MeshGenerator:
19+
"""Class for generating meshes from 3D numpy arrays using different algorithms."""
20+
1721
@staticmethod
1822
def to_mesh_cuberille(data: NDArray) -> trimesh.Trimesh:
1923
"""Convert a Numpy array to a mesh using Cuberille from ITK."""
24+
# Implicitly load ITKCommon module
2025
if "itk.CuberillePython" not in sys.modules:
21-
print("Loading ITK Cuberille module. This will take a while...")
26+
print("Loading ITK Cuberille module. This will take a while...") # noqa: T201
2227

23-
itk.Image # implicitly load ITKCommon module
24-
from itk.CuberillePython import cuberille_image_to_mesh_filter
28+
itk.Image # pyright: ignore[reportAttributeAccessIssue] # noqa: B018
2529

26-
print("ITK Cuberille module loaded.")
30+
print("ITK Cuberille module loaded.") # noqa: T201
2731

28-
from itk.CuberillePython import cuberille_image_to_mesh_filter
32+
from itk.CuberillePython import cuberille_image_to_mesh_filter # noqa: PLC0415
2933

3034
# Generate the mesh using ITK's Cuberille implementation
3135
itk_mesh: itk.itkMeshBasePython.itkMeshD3 = cuberille_image_to_mesh_filter(
@@ -60,10 +64,11 @@ def to_mesh_cuberille(data: NDArray) -> trimesh.Trimesh:
6064
@staticmethod
6165
def to_mesh_surface_nets(data: NDArray) -> trimesh.Trimesh:
6266
"""Convert a Numpy array to a mesh using Surface Nets from PyVista/VTK."""
63-
pv_data: pv.ImageData = pv.wrap(data)
67+
pv_data: pv.ImageData = pv.wrap(data) # pyright: ignore[reportAssignmentType]
6468
mesh = pv_data.contour_labels(output_mesh_type="triangles", smoothing=False)
6569
faces = mesh.faces.reshape((mesh.n_cells, 4))[:, 1:]
6670
mesh = trimesh.Trimesh(mesh.points, faces)
71+
# TODO: This shouldn't be needed after https://gitlab.kitware.com/vtk/vtk/-/issues/19156
6772
mesh.fix_normals()
6873
if mesh.volume < 0:
6974
mesh.invert()
@@ -73,8 +78,8 @@ def to_mesh_surface_nets(data: NDArray) -> trimesh.Trimesh:
7378
def to_mesh_dual_contouring(data: NDArray) -> trimesh.Trimesh:
7479
"""Convert a Numpy array to a mesh using Dual Contouring from Daniel Wilmes."""
7580
# Add the submodule to the path so we can import it
76-
project_root = Path(__file__).parent / "edge_mender" / "Dual_Contouring_Voxel"
77-
if not project_root.exists():
81+
project_root = Path(__file__).parent / "Dual_Contouring_Voxel"
82+
if not project_root.exists(): # pragma: no cover
7883
missing_module_error = (
7984
"Could not find Dual Contouring module. "
8085
"Perhaps you forgot to run `git submodule update --init`?"
@@ -83,20 +88,15 @@ def to_mesh_dual_contouring(data: NDArray) -> trimesh.Trimesh:
8388
sys.path.append(str(project_root))
8489

8590
# Remove the app code from the Dual Contouring module
86-
p = (
87-
Path(__file__).parent
88-
/ "edge_mender"
89-
/ "Dual_Contouring_Voxel"
90-
/ "Dual_Contouring.py"
91-
)
92-
lines = p.read_text().splitlines()
91+
dual_contouring_py = project_root / "Dual_Contouring.py"
92+
lines = dual_contouring_py.read_text().splitlines()
9393
try:
9494
idx = next(i for i, line in enumerate(lines) if line == "app = myapp()")
9595
except StopIteration:
9696
idx = None
97-
if idx is not None:
97+
if idx is not None: # pragma: no cover
9898
new_content = "\n".join(lines[:idx])
99-
p.write_text(new_content + ("\n" if new_content else ""))
99+
dual_contouring_py.write_text(new_content + ("\n" if new_content else ""))
100100

101101
from edge_mender.Dual_Contouring_Voxel.Dual_Contouring import ( # noqa: PLC0415
102102
dual_contouring,

edge_mender/tests/test_data_factory.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Test the DataFactory class."""
2+
13
import numpy as np
24
import pytest
35
from numpy.typing import NDArray
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Test the DataFactory class."""
2+
3+
import pytest
4+
from numpy.typing import NDArray
5+
6+
from edge_mender.data_factory import DataFactory
7+
from edge_mender.mesh_generator import MeshGenerator
8+
9+
10+
@pytest.mark.slow
11+
@pytest.mark.parametrize(
12+
"data",
13+
[
14+
DataFactory.simple_extrusion(),
15+
DataFactory.double_extrusion(),
16+
DataFactory.triple_extrusion(),
17+
DataFactory.stairs(),
18+
DataFactory.ceiling(),
19+
# These two cases fail due to the Cuberille implementation in ITK
20+
# DataFactory.double_tower_ceiling(.),
21+
DataFactory.hanging_points(),
22+
# DataFactory.checkerboard(.),
23+
DataFactory.hole(),
24+
DataFactory.kill_you(),
25+
DataFactory.random(size=8, seed=0),
26+
],
27+
)
28+
def test_to_mesh_cuberille(data: NDArray) -> None:
29+
"""Test MeshGenerator.to_mesh_cuberille."""
30+
mesh = MeshGenerator.to_mesh_cuberille(data)
31+
assert len(mesh.vertices) > 0
32+
assert len(mesh.faces) > 0
33+
assert mesh.volume > 0
34+
35+
36+
@pytest.mark.parametrize(
37+
"data",
38+
[
39+
DataFactory.simple_extrusion(),
40+
DataFactory.double_extrusion(),
41+
DataFactory.triple_extrusion(),
42+
DataFactory.stairs(),
43+
DataFactory.ceiling(),
44+
DataFactory.double_tower_ceiling(),
45+
DataFactory.hanging_points(),
46+
DataFactory.checkerboard(),
47+
DataFactory.hole(),
48+
DataFactory.kill_you(),
49+
DataFactory.random(size=8, seed=0),
50+
# SurfaceNets makes this have negative volume until inverted
51+
DataFactory.random(size=3, seed=55),
52+
],
53+
)
54+
def test_to_mesh_surface_nets(data: NDArray) -> None:
55+
"""Test MeshGenerator.to_mesh_surface_nets."""
56+
mesh = MeshGenerator.to_mesh_surface_nets(data)
57+
assert len(mesh.vertices) > 0
58+
assert len(mesh.faces) > 0
59+
assert mesh.volume > 0
60+
61+
62+
@pytest.mark.parametrize(
63+
"data",
64+
[
65+
DataFactory.simple_extrusion(),
66+
DataFactory.double_extrusion(),
67+
DataFactory.triple_extrusion(),
68+
DataFactory.stairs(),
69+
DataFactory.ceiling(),
70+
DataFactory.double_tower_ceiling(),
71+
DataFactory.hanging_points(),
72+
DataFactory.checkerboard(),
73+
DataFactory.hole(),
74+
DataFactory.kill_you(),
75+
DataFactory.random(size=8, seed=0),
76+
],
77+
)
78+
def test_to_mesh_dual_contouring(data: NDArray) -> None:
79+
"""Test MeshGenerator.to_mesh_dual_contouring."""
80+
mesh = MeshGenerator.to_mesh_dual_contouring(data)
81+
assert len(mesh.vertices) > 0
82+
assert len(mesh.faces) > 0
83+
assert mesh.volume > 0

edge_mender/tmp.py

Lines changed: 0 additions & 40 deletions
This file was deleted.

pseudocode.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# type: ignore
2+
3+
14
def repair(mesh):
25
# Iterate over the mesh edges
36
for edge in mesh:

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,8 @@ unfixable = []
5959
convention = "numpy"
6060

6161
[tool.ruff.lint.per-file-ignores]
62+
"pseudocode.py" = ["ALL"]
6263
"*/tests/*" = ["S101"]
64+
65+
[tool.coverage.run]
66+
omit = ["conftest.py", "pseudocode.py", "edge_mender/Dual_Contouring_Voxel/*"]

0 commit comments

Comments
 (0)