Skip to content

Commit 5535521

Browse files
author
peng.li24
committed
[feat] comprehensive test suite with float32/float64 support
- Add 107 tests (all passing): geometry + ops with pixel-level Python alignment - Parametrize same-type tests for both float64 and float32 via 'make' fixture - Fix exterior_coords() to return closed ring matching Python shapely - Fix distance_to_multigeom to accept Point + list<Polygon> - Fix LineString buffer(0) test assertion to match Python behavior - CMake-based build with pybind11 + GEOS - Dual push remote: github + gitlab
1 parent c659941 commit 5535521

14 files changed

Lines changed: 1214 additions & 12 deletions

CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ add_custom_target(deb
8383
COMMENT "Packaging shapelycpp-dev-${CPACK_PACKAGE_VERSION}-Linux.deb"
8484
)
8585

86+
# ---- Tests (optional, requires pybind11 + GEOS) ------------------------------
87+
option(BUILD_TESTS "Build pybind11 test module" OFF)
88+
if(BUILD_TESTS)
89+
add_subdirectory(tests)
90+
endif()
91+
8692
message(STATUS "shapelycpp v${PROJECT_VERSION} (header-only C++ library)")
8793
message(STATUS " C++ Standard: ${CMAKE_CXX_STANDARD}")
8894
message(STATUS " DEB: make deb → shapelycpp-dev-${CPACK_PACKAGE_VERSION}-Linux.deb")

geometry/linestring_impl.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
#pragma once
66

7-
#include "shapely/geometry/point.h"
8-
#include "shapely/geometry/polygon.h"
7+
#include "geometry/point.h"
8+
#include "geometry/polygon.h"
99

1010
#include <geos/geom/GeometryFactory.h>
1111
#include <geos/geom/LineString.h>

geometry/point_impl.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
#pragma once
66

7-
#include "shapely/geometry/linestring.h"
8-
#include "shapely/geometry/polygon.h"
7+
#include "geometry/linestring.h"
8+
#include "geometry/polygon.h"
99

1010
#include <geos/geom/GeometryFactory.h>
1111
#include <geos/geom/Point.h>

geometry/polygon_impl.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
#pragma once
66

7-
#include "shapely/geometry/linestring.h"
7+
#include "geometry/linestring.h"
88

99
#include <geos/geom/GeometryFactory.h>
1010
#include <geos/geom/Polygon.h>
@@ -141,9 +141,9 @@ py::array_t<T> Polygon<T>::exterior_coords() const
141141
return empty;
142142
}
143143
size_t n = cs->getSize();
144-
py::array_t<T> result(std::vector<py::ssize_t>{static_cast<py::ssize_t>(n - 1), static_cast<py::ssize_t>(2)});
144+
py::array_t<T> result(std::vector<py::ssize_t>{static_cast<py::ssize_t>(n), static_cast<py::ssize_t>(2)});
145145
T *p = static_cast<T *>(result.request().ptr);
146-
for (size_t i = 0; i < n - 1; ++i) {
146+
for (size_t i = 0; i < n; ++i) {
147147
p[i * 2] = static_cast<T>(cs->getAt(i).x);
148148
p[i * 2 + 1] = static_cast<T>(cs->getAt(i).y);
149149
}

ops/distance_to_multigeom_impl.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
#pragma once
66

7-
#include "shapely/geometry/polygon.h"
8-
#include "shapely/geometry/linestring.h"
7+
#include "geometry/polygon.h"
8+
#include "geometry/linestring.h"
99

1010
#include <limits>
1111

ops/nearest_points_impl.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
#pragma once
66

7-
#include "shapely/geometry/polygon.h"
8-
#include "shapely/geometry/linestring.h"
9-
#include "shapely/geometry/point.h"
7+
#include "geometry/polygon.h"
8+
#include "geometry/linestring.h"
9+
#include "geometry/point.h"
1010

1111
#include <geos/geom/GeometryFactory.h>
1212
#include <geos/geom/Polygon.h>

tests/CMakeLists.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# ---- Dependencies -----------------------------------------------------------
2+
find_package(pybind11 REQUIRED)
3+
find_package(GEOS REQUIRED)
4+
5+
# ---- Build test module (named shapelycpp_test to avoid clash with header-only target) ----
6+
pybind11_add_module(shapelycpp_test module.cpp)
7+
target_include_directories(shapelycpp_test PRIVATE ${CMAKE_SOURCE_DIR})
8+
target_link_libraries(shapelycpp_test PRIVATE GEOS::geos)
9+
10+
# Rename the output .so to "shapelycpp" so Python "import shapelycpp" works
11+
set_target_properties(shapelycpp_test PROPERTIES OUTPUT_NAME shapelycpp)
12+
13+
# ---- Test target: run from repo root so "tests/" is a package ---------------
14+
add_test(
15+
NAME shapelycpp_all
16+
COMMAND python3 -m pytest tests/ -v
17+
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
18+
)
19+
20+
set_tests_properties(shapelycpp_all
21+
PROPERTIES ENVIRONMENT
22+
"PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}:${CMAKE_SOURCE_DIR}:$ENV{PYTHONPATH}"
23+
)

tests/Makefile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Build shapelycpp Python module for testing.
2+
# Requires: pybind11, GEOS, Python3 development headers.
3+
4+
CXX ?= g++
5+
CXXFLAGS ?= -std=c++17 -O2 -fPIC
6+
INCLUDES = -I.. $(shell python3 -m pybind11 --includes) $(shell /usr/local/bin/geos-config --cflags 2>/dev/null || echo -I/usr/local/include)
7+
LDFLAGS = -shared -L/usr/local/lib -lgeos
8+
EXT_SUFFIX := $(shell python3-config --extension-suffix 2>/dev/null || echo .so)
9+
10+
MODULE = shapelycpp$(EXT_SUFFIX)
11+
SRC = module.cpp
12+
13+
.PHONY: all clean test
14+
15+
all: $(MODULE)
16+
17+
$(MODULE): $(SRC)
18+
$(CXX) $(CXXFLAGS) $(INCLUDES) $< -o $@ $(LDFLAGS)
19+
20+
test: $(MODULE)
21+
cd .. && PYTHONPATH=tests:$$PYTHONPATH python3 -m pytest tests/ -v
22+
23+
clean:
24+
rm -f $(MODULE) shapelycpp*.so

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""
2+
Pytest fixtures and hooks for shapelycpp precision alignment tests.
3+
4+
Usage:
5+
pytest tests/ # run all tests
6+
pytest tests/ --cpp-module=my_module # custom C++ module name
7+
pytest tests/ --rtol=1e-10 --atol=1e-10 # custom tolerances
8+
"""
9+
10+
import numpy as np
11+
import pytest
12+
13+
14+
def pytest_addoption(parser):
15+
parser.addoption(
16+
"--cpp-module", action="store", default=None,
17+
help="Python module name for the compiled C++ shapelycpp library (default: shapelycpp)",
18+
)
19+
parser.addoption(
20+
"--rtol", action="store", type=float, default=1e-10,
21+
help="Relative tolerance for numerical comparisons (default: 1e-10)",
22+
)
23+
parser.addoption(
24+
"--atol", action="store", type=float, default=1e-10,
25+
help="Absolute tolerance for numerical comparisons (default: 1e-10)",
26+
)
27+
28+
29+
# -- Lazy C++ module import ---------------------------------------------------
30+
31+
_cpp_module = None
32+
_import_error = None
33+
34+
35+
def _resolve_module_name(config) -> str:
36+
import os
37+
cli = config.getoption("--cpp-module", default=None)
38+
if cli:
39+
return cli
40+
env = os.environ.get("SHAPELYCPP_MODULE")
41+
if env:
42+
return env
43+
return "shapelycpp"
44+
45+
46+
def get_cpp_module(request=None):
47+
"""Return the compiled shapelycpp C++ module (lazy, cached)."""
48+
global _cpp_module, _import_error
49+
50+
if _cpp_module is not None:
51+
return _cpp_module
52+
if _import_error is not None:
53+
raise _import_error
54+
55+
if request is not None:
56+
modname = _resolve_module_name(request.config)
57+
else:
58+
import os
59+
modname = os.environ.get("SHAPELYCPP_MODULE", "shapelycpp")
60+
61+
import importlib
62+
try:
63+
_cpp_module = importlib.import_module(modname)
64+
except ImportError as e:
65+
_import_error = e
66+
raise
67+
return _cpp_module
68+
69+
70+
# -- Fixtures ------------------------------------------------------------------
71+
72+
73+
@pytest.fixture(scope="session")
74+
def cpp():
75+
"""Session-scoped C++ module fixture."""
76+
return get_cpp_module()
77+
78+
79+
@pytest.fixture
80+
def rtol(request):
81+
return request.config.getoption("--rtol", default=1e-10)
82+
83+
84+
@pytest.fixture
85+
def atol(request):
86+
return request.config.getoption("--atol", default=1e-10)
87+
88+
89+
@pytest.fixture(params=[np.float64, np.float32], ids=["float64", "float32"])
90+
def dtype(request):
91+
"""Parametrized fixture: each test runs once with float64 and once with float32."""
92+
return request.param
93+
94+
95+
@pytest.fixture(params=[np.float64, np.float32], ids=["float64", "float32"])
96+
def make(cpp, request):
97+
"""Parametrized fixture providing float64/float32 geometry factories and classes."""
98+
if request.param == np.float64:
99+
return {
100+
'point': cpp.point,
101+
'linestring': cpp.linestring,
102+
'polygon': cpp.polygon,
103+
'Point': cpp.Point,
104+
'LineString': cpp.LineString,
105+
'Polygon': cpp.Polygon,
106+
'dtype': np.float64,
107+
}
108+
else:
109+
return {
110+
'point': cpp.point_f32,
111+
'linestring': cpp.linestring_f32,
112+
'polygon': cpp.polygon_f32,
113+
'Point': cpp.PointF32,
114+
'LineString': cpp.LineStringF32,
115+
'Polygon': cpp.PolygonF32,
116+
'dtype': np.float32,
117+
}

0 commit comments

Comments
 (0)