mirror of
https://github.com/introlab/rtabmap.git
synced 2026-10-03 16:47:47 +08:00
Added python tests
This commit is contained in:
@@ -78,6 +78,16 @@ void PyDescriptor::parseParameters(const ParametersMap & parameters)
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Pre-validate the path: PyImport_Import on a non-existent script
|
||||||
|
// fails without setting a Python exception, after which
|
||||||
|
// getPythonTraceback() dereferences NULL pointers in Py_BuildValue
|
||||||
|
// and crashes. Mirrors the check in PyDetector::PyDetector().
|
||||||
|
if(!UFile::exists(path_) || UFile::getExtension(path_).compare("py") != 0)
|
||||||
|
{
|
||||||
|
UERROR("Cannot initialize Python descriptor, the path is not valid: \"%s\"=\"%s\"",
|
||||||
|
Parameters::kPyDescriptorPath().c_str(), path_.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
std::string matcherPythonDir = UDirectory::getDir(path_);
|
std::string matcherPythonDir = UDirectory::getDir(path_);
|
||||||
if(!matcherPythonDir.empty())
|
if(!matcherPythonDir.empty())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -65,12 +65,28 @@ std::string getPythonTraceback()
|
|||||||
{
|
{
|
||||||
// Author: https://stackoverflow.com/questions/41268061/c-c-python-exception-traceback-not-being-generated
|
// Author: https://stackoverflow.com/questions/41268061/c-c-python-exception-traceback-not-being-generated
|
||||||
|
|
||||||
|
// Early-exit when there is no active Python exception: callers commonly
|
||||||
|
// log this after any Python C-API failure, but some failures (notably
|
||||||
|
// PyImport_Import returning NULL after repeated load/unload cycles) do
|
||||||
|
// not set an exception. Without this guard, PyErr_Fetch returns NULL
|
||||||
|
// triples and Py_BuildValue("OOO", NULL, NULL, NULL) below crashes.
|
||||||
|
if(!PyErr_Occurred())
|
||||||
|
{
|
||||||
|
return "<no python exception set>";
|
||||||
|
}
|
||||||
|
|
||||||
PyObject* type;
|
PyObject* type;
|
||||||
PyObject* value;
|
PyObject* value;
|
||||||
PyObject* traceback;
|
PyObject* traceback;
|
||||||
|
|
||||||
PyErr_Fetch(&type, &value, &traceback);
|
PyErr_Fetch(&type, &value, &traceback);
|
||||||
PyErr_NormalizeException(&type, &value, &traceback);
|
PyErr_NormalizeException(&type, &value, &traceback);
|
||||||
|
// PyErr_Fetch may leave value/traceback NULL even when an exception was
|
||||||
|
// active. Py_BuildValue("OOO", ...) rejects NULL slots and raises
|
||||||
|
// SystemError, so substitute Py_None for any missing component.
|
||||||
|
if(!type) { Py_INCREF(Py_None); type = Py_None; }
|
||||||
|
if(!value) { Py_INCREF(Py_None); value = Py_None; }
|
||||||
|
if(!traceback) { Py_INCREF(Py_None); traceback = Py_None; }
|
||||||
|
|
||||||
std::string fcn = "";
|
std::string fcn = "";
|
||||||
fcn += "def get_pretty_traceback(exc_type, exc_value, exc_tb):\n";
|
fcn += "def get_pretty_traceback(exc_type, exc_value, exc_tb):\n";
|
||||||
@@ -85,13 +101,22 @@ std::string getPythonTraceback()
|
|||||||
UASSERT(mod);
|
UASSERT(mod);
|
||||||
PyObject* method = PyObject_GetAttrString(mod, "get_pretty_traceback");
|
PyObject* method = PyObject_GetAttrString(mod, "get_pretty_traceback");
|
||||||
UASSERT(method);
|
UASSERT(method);
|
||||||
PyObject* outStr = PyObject_CallObject(method, Py_BuildValue("OOO", type, value, traceback));
|
PyObject* args = Py_BuildValue("OOO", type, value, traceback);
|
||||||
|
PyObject* outStr = args ? PyObject_CallObject(method, args) : nullptr;
|
||||||
std::string pretty;
|
std::string pretty;
|
||||||
if(outStr)
|
if(outStr)
|
||||||
pretty = PyBytes_AsString(PyUnicode_AsASCIIString(outStr));
|
{
|
||||||
|
PyObject* asciiStr = PyUnicode_AsASCIIString(outStr);
|
||||||
|
if(asciiStr)
|
||||||
|
{
|
||||||
|
pretty = PyBytes_AsString(asciiStr);
|
||||||
|
Py_DECREF(asciiStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_XDECREF(args);
|
||||||
Py_DECREF(method);
|
Py_DECREF(method);
|
||||||
Py_DECREF(outStr);
|
Py_XDECREF(outStr); // outStr may be NULL if PyObject_CallObject failed
|
||||||
Py_DECREF(mod);
|
Py_DECREF(mod);
|
||||||
|
|
||||||
return pretty;
|
return pretty;
|
||||||
|
|||||||
@@ -173,6 +173,36 @@ add_executable(test_localgridmaker test_localgridmaker.cpp)
|
|||||||
target_link_libraries(test_localgridmaker gtest_main rtabmap_core)
|
target_link_libraries(test_localgridmaker gtest_main rtabmap_core)
|
||||||
add_test(NAME test_localgridmaker COMMAND test_localgridmaker)
|
add_test(NAME test_localgridmaker COMMAND test_localgridmaker)
|
||||||
|
|
||||||
|
#PythonInterface / PyDetector / PyDescriptor / PyMatcher (optional Python
|
||||||
|
# support; bridges to Python feature detectors (SuperPoint), global
|
||||||
|
# descriptors (NetVLAD), and matchers (SuperGlue). Tests use numpy-only stub
|
||||||
|
# scripts so no ML weights or extra packages are needed beyond what
|
||||||
|
# WITH_PYTHON already pulls in.
|
||||||
|
IF(WITH_PYTHON AND Python3_FOUND)
|
||||||
|
# Direct tests for PythonInterface / getPythonTraceback. Links pybind11
|
||||||
|
# and Python headers so the test can drive the Python C API directly.
|
||||||
|
add_executable(test_python_interface test_python_interface.cpp)
|
||||||
|
target_link_libraries(test_python_interface gtest_main rtabmap_core pybind11::embed)
|
||||||
|
target_include_directories(test_python_interface PRIVATE ${Python3_INCLUDE_DIRS})
|
||||||
|
add_test(NAME test_python_interface COMMAND test_python_interface)
|
||||||
|
|
||||||
|
add_executable(test_pydetector test_pydetector.cpp)
|
||||||
|
target_link_libraries(test_pydetector gtest_main rtabmap_core)
|
||||||
|
add_test(NAME test_pydetector COMMAND test_pydetector)
|
||||||
|
|
||||||
|
add_executable(test_pydescriptor test_pydescriptor.cpp)
|
||||||
|
target_link_libraries(test_pydescriptor gtest_main rtabmap_core)
|
||||||
|
add_test(NAME test_pydescriptor COMMAND test_pydescriptor)
|
||||||
|
|
||||||
|
# PyMatcher's header is internal (not in the public include tree);
|
||||||
|
# expose corelib/src so the test can `#include "python/PyMatcher.h"`.
|
||||||
|
add_executable(test_pymatcher test_pymatcher.cpp)
|
||||||
|
target_link_libraries(test_pymatcher gtest_main rtabmap_core)
|
||||||
|
target_include_directories(test_pymatcher PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/corelib/src)
|
||||||
|
add_test(NAME test_pymatcher COMMAND test_pymatcher)
|
||||||
|
ENDIF(WITH_PYTHON AND Python3_FOUND)
|
||||||
|
|
||||||
#LASWriter.h (optional libLAS)
|
#LASWriter.h (optional libLAS)
|
||||||
IF(libLAS_FOUND)
|
IF(libLAS_FOUND)
|
||||||
add_executable(test_laswriter test_laswriter.cpp)
|
add_executable(test_laswriter test_laswriter.cpp)
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
// Tests for PyDescriptor: rtabmap's bridge to Python-backed global-image
|
||||||
|
// descriptors (NetVLAD and similar).
|
||||||
|
//
|
||||||
|
// We stand in a tiny numpy-only stub script for whatever a real user would
|
||||||
|
// point PyDescriptor at, so the test exercises the full pipeline (script
|
||||||
|
// load -> init(dim) -> extract(image) -> descriptor parsing) without pulling
|
||||||
|
// in heavy ML model weights. The whole file is a no-op when rtabmap is built
|
||||||
|
// without Python -- the CMakeLists.txt only registers it under
|
||||||
|
// WITH_PYTHON AND Python3_FOUND, and a defensive #ifdef matches that.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <rtabmap/core/Version.h>
|
||||||
|
|
||||||
|
#ifdef RTABMAP_PYTHON
|
||||||
|
|
||||||
|
#include <rtabmap/core/GlobalDescriptor.h>
|
||||||
|
#include <rtabmap/core/GlobalDescriptorExtractor.h>
|
||||||
|
#include <rtabmap/core/Parameters.h>
|
||||||
|
#include <rtabmap/core/SensorData.h>
|
||||||
|
#include <rtabmap/utilite/UConversion.h>
|
||||||
|
#include <rtabmap/utilite/UFile.h>
|
||||||
|
|
||||||
|
#include <opencv2/core.hpp>
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
using namespace rtabmap;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Drops a minimal descriptor script implementing the contract
|
||||||
|
// PyDescriptor.cpp expects:
|
||||||
|
// init(descriptorDim) -- called once
|
||||||
|
// extract(image) -- returns 1xDIM float32, image is an HxWxC uint8 array
|
||||||
|
// Echoes the dim that init() received plus the image dimensions into the
|
||||||
|
// first four descriptor cells so the test can pin them. Numpy-only, no heavy
|
||||||
|
// deps.
|
||||||
|
//
|
||||||
|
// Each test uses a unique filename via `tag` so the Python module cache
|
||||||
|
// reloads fresh content on each TEST().
|
||||||
|
std::string writeStubScript(int tag)
|
||||||
|
{
|
||||||
|
const std::string path = uFormat(
|
||||||
|
"/tmp/rtabmap_test_pydescriptor_%d_%d.py", getpid(), tag);
|
||||||
|
std::ofstream out(path);
|
||||||
|
out <<
|
||||||
|
"import numpy as np\n"
|
||||||
|
"\n"
|
||||||
|
"INITIALIZED = False\n"
|
||||||
|
"DIM = 0\n"
|
||||||
|
"\n"
|
||||||
|
"def init(descriptorDim):\n"
|
||||||
|
" global INITIALIZED, DIM\n"
|
||||||
|
" INITIALIZED = True\n"
|
||||||
|
" DIM = int(descriptorDim)\n"
|
||||||
|
"\n"
|
||||||
|
"def extract(image):\n"
|
||||||
|
" h = image.shape[0]\n"
|
||||||
|
" w = image.shape[1]\n"
|
||||||
|
" c = image.shape[2] if image.ndim == 3 else 1\n"
|
||||||
|
" desc = np.zeros((1, DIM), dtype=np.float32)\n"
|
||||||
|
" if DIM >= 4:\n"
|
||||||
|
" desc[0, 0] = float(DIM)\n"
|
||||||
|
" desc[0, 1] = float(h)\n"
|
||||||
|
" desc[0, 2] = float(w)\n"
|
||||||
|
" desc[0, 3] = float(c)\n"
|
||||||
|
" return desc\n";
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParametersMap baseParams(const std::string & scriptPath, int dim)
|
||||||
|
{
|
||||||
|
ParametersMap p;
|
||||||
|
p.insert(ParametersPair(Parameters::kPyDescriptorPath(), scriptPath));
|
||||||
|
p.insert(ParametersPair(Parameters::kPyDescriptorDim(), uNumber2Str(dim)));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
cv::Mat makeImage(int rows = 32, int cols = 48)
|
||||||
|
{
|
||||||
|
// PyDescriptor passes the raw image as HxWxC uint8; CV_8UC3 mirrors what
|
||||||
|
// a real RGB camera would supply.
|
||||||
|
return cv::Mat(rows, cols, CV_8UC3, cv::Scalar(0, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Full happy path: script loads, init() receives the configured dim, extract
|
||||||
|
// returns a 1xDIM float descriptor whose first cells echo DIM + image shape.
|
||||||
|
TEST(PyDescriptor, BasicExtraction)
|
||||||
|
{
|
||||||
|
const std::string scriptPath = writeStubScript(1);
|
||||||
|
const int dim = 16;
|
||||||
|
std::unique_ptr<GlobalDescriptorExtractor> extractor(
|
||||||
|
GlobalDescriptorExtractor::create(
|
||||||
|
GlobalDescriptorExtractor::kPyDescriptor,
|
||||||
|
baseParams(scriptPath, dim)));
|
||||||
|
ASSERT_NE(extractor.get(), nullptr);
|
||||||
|
EXPECT_EQ(GlobalDescriptorExtractor::kPyDescriptor, extractor->getType());
|
||||||
|
|
||||||
|
cv::Mat image = makeImage(32, 48);
|
||||||
|
SensorData data(image);
|
||||||
|
GlobalDescriptor descriptor = extractor->extract(data);
|
||||||
|
|
||||||
|
// type=1 matches kPyDescriptor (set inside PyDescriptor::extract).
|
||||||
|
EXPECT_EQ(1, descriptor.type());
|
||||||
|
ASSERT_EQ(1, descriptor.data().rows);
|
||||||
|
ASSERT_EQ(dim, descriptor.data().cols);
|
||||||
|
EXPECT_EQ(CV_32FC1, descriptor.data().type());
|
||||||
|
|
||||||
|
// Stub echoes (DIM, H, W, C) in cells [0..3].
|
||||||
|
EXPECT_NEAR(float(dim), descriptor.data().at<float>(0, 0), 1e-5f);
|
||||||
|
EXPECT_NEAR(32.0f, descriptor.data().at<float>(0, 1), 1e-5f);
|
||||||
|
EXPECT_NEAR(48.0f, descriptor.data().at<float>(0, 2), 1e-5f);
|
||||||
|
EXPECT_NEAR(3.0f, descriptor.data().at<float>(0, 3), 1e-5f);
|
||||||
|
// Trailing cells are zero-initialized.
|
||||||
|
for(int j = 4; j < dim; ++j)
|
||||||
|
{
|
||||||
|
EXPECT_NEAR(0.0f, descriptor.data().at<float>(0, j), 1e-5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
UFile::erase(scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing Kp/PyDescriptor/Dim must reach init() in the script so its returned
|
||||||
|
// descriptor width follows. We construct with one dim, then check the stub
|
||||||
|
// echoes that value back.
|
||||||
|
TEST(PyDescriptor, DimParameterReachesScript)
|
||||||
|
{
|
||||||
|
const std::string scriptPath = writeStubScript(2);
|
||||||
|
const int dim = 8;
|
||||||
|
std::unique_ptr<GlobalDescriptorExtractor> extractor(
|
||||||
|
GlobalDescriptorExtractor::create(
|
||||||
|
GlobalDescriptorExtractor::kPyDescriptor,
|
||||||
|
baseParams(scriptPath, dim)));
|
||||||
|
ASSERT_NE(extractor.get(), nullptr);
|
||||||
|
|
||||||
|
GlobalDescriptor descriptor = extractor->extract(SensorData(makeImage()));
|
||||||
|
ASSERT_EQ(dim, descriptor.data().cols);
|
||||||
|
EXPECT_NEAR(float(dim), descriptor.data().at<float>(0, 0), 1e-5f);
|
||||||
|
|
||||||
|
UFile::erase(scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When no script path is configured (empty string), PyDescriptor early-exits
|
||||||
|
// in parseParameters() with a null module. extract() then returns an empty
|
||||||
|
// descriptor.
|
||||||
|
TEST(PyDescriptor, EmptyPathReturnsEmpty)
|
||||||
|
{
|
||||||
|
std::unique_ptr<GlobalDescriptorExtractor> extractor(
|
||||||
|
GlobalDescriptorExtractor::create(
|
||||||
|
GlobalDescriptorExtractor::kPyDescriptor,
|
||||||
|
baseParams(/*scriptPath=*/"", 16)));
|
||||||
|
ASSERT_NE(extractor.get(), nullptr);
|
||||||
|
|
||||||
|
GlobalDescriptor descriptor = extractor->extract(SensorData(makeImage()));
|
||||||
|
// Default-constructed: type==-1, data empty.
|
||||||
|
EXPECT_EQ(-1, descriptor.type());
|
||||||
|
EXPECT_TRUE(descriptor.data().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// A non-existent script path is now pre-validated in parseParameters() (it
|
||||||
|
// used to crash inside getPythonTraceback when PyImport_Import returned NULL
|
||||||
|
// without setting an exception). extractor stays valid and extract returns
|
||||||
|
// an empty descriptor.
|
||||||
|
TEST(PyDescriptor, MissingPathReturnsEmpty)
|
||||||
|
{
|
||||||
|
const std::string scriptPath = uFormat(
|
||||||
|
"/tmp/rtabmap_test_pydescriptor_does_not_exist_%d.py", getpid());
|
||||||
|
std::unique_ptr<GlobalDescriptorExtractor> extractor(
|
||||||
|
GlobalDescriptorExtractor::create(
|
||||||
|
GlobalDescriptorExtractor::kPyDescriptor,
|
||||||
|
baseParams(scriptPath, 16)));
|
||||||
|
ASSERT_NE(extractor.get(), nullptr);
|
||||||
|
|
||||||
|
GlobalDescriptor descriptor = extractor->extract(SensorData(makeImage()));
|
||||||
|
EXPECT_EQ(-1, descriptor.type());
|
||||||
|
EXPECT_TRUE(descriptor.data().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract() requires a non-empty raw image; when given an empty SensorData
|
||||||
|
// it logs an error and returns an empty descriptor.
|
||||||
|
TEST(PyDescriptor, EmptyImageReturnsEmpty)
|
||||||
|
{
|
||||||
|
const std::string scriptPath = writeStubScript(3);
|
||||||
|
std::unique_ptr<GlobalDescriptorExtractor> extractor(
|
||||||
|
GlobalDescriptorExtractor::create(
|
||||||
|
GlobalDescriptorExtractor::kPyDescriptor,
|
||||||
|
baseParams(scriptPath, 16)));
|
||||||
|
ASSERT_NE(extractor.get(), nullptr);
|
||||||
|
|
||||||
|
GlobalDescriptor descriptor = extractor->extract(SensorData());
|
||||||
|
EXPECT_EQ(-1, descriptor.type());
|
||||||
|
EXPECT_TRUE(descriptor.data().empty());
|
||||||
|
|
||||||
|
UFile::erase(scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // RTABMAP_PYTHON
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// Tests for PyDetector: rtabmap's bridge to Python-backed local-feature
|
||||||
|
// detectors (SuperPoint, custom networks, etc.).
|
||||||
|
//
|
||||||
|
// We stand in a tiny numpy-only stub script for whatever the user would
|
||||||
|
// normally point PyDetector at, so the test exercises the full pipeline
|
||||||
|
// (script load -> init() -> detect() -> keypoint/descriptor parsing) without
|
||||||
|
// pulling in heavy ML model weights. The whole file is a no-op when rtabmap
|
||||||
|
// is built without Python -- the CMakeLists.txt only registers it when
|
||||||
|
// WITH_PYTHON AND Python3_FOUND, and a defensive #ifdef matches that.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <rtabmap/core/Version.h>
|
||||||
|
|
||||||
|
#ifdef RTABMAP_PYTHON
|
||||||
|
|
||||||
|
#include <rtabmap/core/Features2d.h>
|
||||||
|
#include <rtabmap/core/Parameters.h>
|
||||||
|
#include <rtabmap/utilite/UConversion.h>
|
||||||
|
#include <rtabmap/utilite/UFile.h>
|
||||||
|
|
||||||
|
#include <opencv2/core.hpp>
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
using namespace rtabmap;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Drops a minimal detector script implementing the contract PyDetector.cpp
|
||||||
|
// expects:
|
||||||
|
// init(cuda) -- called once
|
||||||
|
// detect(imageBuffer) -- returns (Nx3 float32 [x, y, response], NxDIM float32)
|
||||||
|
// Three hard-coded keypoints at (w/4, h/4), (w/2, h/2), (3w/4, 3h/4) with
|
||||||
|
// descending responses; descriptor rows filled with (row_index + 1) so the
|
||||||
|
// test can identify them. Numpy-only, no heavy deps.
|
||||||
|
//
|
||||||
|
// Each test uses a unique filename via `tag` so the Python module cache
|
||||||
|
// reloads fresh content on each TEST().
|
||||||
|
std::string writeStubScript(int tag)
|
||||||
|
{
|
||||||
|
const std::string path = uFormat(
|
||||||
|
"/tmp/rtabmap_test_pydetector_%d_%d.py", getpid(), tag);
|
||||||
|
std::ofstream out(path);
|
||||||
|
out <<
|
||||||
|
"import numpy as np\n"
|
||||||
|
"\n"
|
||||||
|
"INITIALIZED = False\n"
|
||||||
|
"CUDA_ARG = None\n"
|
||||||
|
"DESCRIPTOR_DIM = 8\n"
|
||||||
|
"\n"
|
||||||
|
"def init(cuda):\n"
|
||||||
|
" global INITIALIZED, CUDA_ARG\n"
|
||||||
|
" INITIALIZED = True\n"
|
||||||
|
" CUDA_ARG = int(cuda)\n"
|
||||||
|
"\n"
|
||||||
|
"def detect(image):\n"
|
||||||
|
" h, w = image.shape\n"
|
||||||
|
" pts = np.array([\n"
|
||||||
|
" [w * 0.25, h * 0.25, 0.9],\n"
|
||||||
|
" [w * 0.50, h * 0.50, 0.8],\n"
|
||||||
|
" [w * 0.75, h * 0.75, 0.7],\n"
|
||||||
|
" ], dtype=np.float32)\n"
|
||||||
|
" desc = np.zeros((3, DESCRIPTOR_DIM), dtype=np.float32)\n"
|
||||||
|
" for i in range(3):\n"
|
||||||
|
" desc[i, :] = float(i + 1)\n"
|
||||||
|
" return pts, desc\n";
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParametersMap baseParams(const std::string & scriptPath)
|
||||||
|
{
|
||||||
|
ParametersMap p;
|
||||||
|
p.insert(ParametersPair(Parameters::kPyDetectorPath(), scriptPath));
|
||||||
|
p.insert(ParametersPair(Parameters::kPyDetectorCuda(), "false"));
|
||||||
|
p.insert(ParametersPair(Parameters::kKpMaxFeatures(), "100"));
|
||||||
|
p.insert(ParametersPair(Parameters::kKpSSC(), "false"));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
cv::Mat makeImage(int rows = 64, int cols = 64)
|
||||||
|
{
|
||||||
|
// PyDetector requires CV_8UC1; uniform content is fine for the stub
|
||||||
|
// which ignores pixel values.
|
||||||
|
return cv::Mat(rows, cols, CV_8UC1, cv::Scalar(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Full happy path: the script loads, returns 3 keypoints + descriptors, and
|
||||||
|
// the keypoint positions, responses, and descriptor rows survive end to end.
|
||||||
|
TEST(PyDetector, BasicDetection)
|
||||||
|
{
|
||||||
|
const std::string scriptPath = writeStubScript(1);
|
||||||
|
std::unique_ptr<Feature2D> detector(Feature2D::create(
|
||||||
|
Feature2D::kFeaturePyDetector, baseParams(scriptPath)));
|
||||||
|
ASSERT_NE(detector.get(), nullptr);
|
||||||
|
EXPECT_EQ(Feature2D::kFeaturePyDetector, detector->getType());
|
||||||
|
|
||||||
|
cv::Mat image = makeImage(64, 64);
|
||||||
|
std::vector<cv::KeyPoint> kpts = detector->generateKeypoints(image);
|
||||||
|
ASSERT_EQ(3u, kpts.size());
|
||||||
|
|
||||||
|
// Stub points: (w/4, h/4), (w/2, h/2), (3w/4, 3h/4) with responses
|
||||||
|
// 0.9, 0.8, 0.7.
|
||||||
|
EXPECT_NEAR(16.0f, kpts[0].pt.x, 1e-3f);
|
||||||
|
EXPECT_NEAR(16.0f, kpts[0].pt.y, 1e-3f);
|
||||||
|
EXPECT_NEAR(32.0f, kpts[1].pt.x, 1e-3f);
|
||||||
|
EXPECT_NEAR(48.0f, kpts[2].pt.x, 1e-3f);
|
||||||
|
EXPECT_NEAR(0.9f, kpts[0].response, 1e-5f);
|
||||||
|
EXPECT_NEAR(0.8f, kpts[1].response, 1e-5f);
|
||||||
|
EXPECT_NEAR(0.7f, kpts[2].response, 1e-5f);
|
||||||
|
|
||||||
|
cv::Mat descriptors = detector->generateDescriptors(image, kpts);
|
||||||
|
EXPECT_EQ(3, descriptors.rows);
|
||||||
|
EXPECT_EQ(8, descriptors.cols);
|
||||||
|
EXPECT_EQ(CV_32FC1, descriptors.type());
|
||||||
|
for(int r = 0; r < descriptors.rows; ++r)
|
||||||
|
{
|
||||||
|
EXPECT_NEAR(float(r + 1), descriptors.at<float>(r, 0), 1e-5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
UFile::erase(scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PyDetector logs an error and silently returns no keypoints when the
|
||||||
|
// script path doesn't exist -- the constructor doesn't throw. Pin that
|
||||||
|
// contract so callers can safely construct without pre-checking the path.
|
||||||
|
TEST(PyDetector, MissingPathReturnsEmpty)
|
||||||
|
{
|
||||||
|
const std::string scriptPath = uFormat(
|
||||||
|
"/tmp/rtabmap_test_pydetector_does_not_exist_%d.py", getpid());
|
||||||
|
std::unique_ptr<Feature2D> detector(Feature2D::create(
|
||||||
|
Feature2D::kFeaturePyDetector, baseParams(scriptPath)));
|
||||||
|
ASSERT_NE(detector.get(), nullptr);
|
||||||
|
|
||||||
|
cv::Mat image = makeImage(32, 32);
|
||||||
|
std::vector<cv::KeyPoint> kpts = detector->generateKeypoints(image);
|
||||||
|
EXPECT_TRUE(kpts.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// A non-empty mask removes keypoints whose (x, y) falls on a 0 pixel. The
|
||||||
|
// stub returns one keypoint at (48, 48); we mask everything from row/col 33
|
||||||
|
// onward so it gets dropped while the two earlier points survive.
|
||||||
|
TEST(PyDetector, MaskFiltersKeypoints)
|
||||||
|
{
|
||||||
|
const std::string scriptPath = writeStubScript(2);
|
||||||
|
std::unique_ptr<Feature2D> detector(Feature2D::create(
|
||||||
|
Feature2D::kFeaturePyDetector, baseParams(scriptPath)));
|
||||||
|
ASSERT_NE(detector.get(), nullptr);
|
||||||
|
|
||||||
|
cv::Mat image = makeImage(64, 64);
|
||||||
|
cv::Mat mask(image.size(), CV_8UC1, cv::Scalar(255));
|
||||||
|
mask(cv::Rect(33, 33, mask.cols - 33, mask.rows - 33)).setTo(0);
|
||||||
|
std::vector<cv::KeyPoint> kpts = detector->generateKeypoints(image, mask);
|
||||||
|
EXPECT_EQ(2u, kpts.size());
|
||||||
|
|
||||||
|
UFile::erase(scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kp/MaxFeatures caps the returned keypoint list inside generateKeypointsImpl
|
||||||
|
// (PyDetector calls limitKeypoints with its own descriptor matrix at the end).
|
||||||
|
// The stub returns 3 keypoints; cap at 2 and verify the rest are dropped.
|
||||||
|
TEST(PyDetector, MaxFeaturesCap)
|
||||||
|
{
|
||||||
|
const std::string scriptPath = writeStubScript(3);
|
||||||
|
ParametersMap p = baseParams(scriptPath);
|
||||||
|
p[Parameters::kKpMaxFeatures()] = "2";
|
||||||
|
std::unique_ptr<Feature2D> detector(
|
||||||
|
Feature2D::create(Feature2D::kFeaturePyDetector, p));
|
||||||
|
ASSERT_NE(detector.get(), nullptr);
|
||||||
|
|
||||||
|
cv::Mat image = makeImage(64, 64);
|
||||||
|
std::vector<cv::KeyPoint> kpts = detector->generateKeypoints(image);
|
||||||
|
EXPECT_EQ(2u, kpts.size());
|
||||||
|
|
||||||
|
UFile::erase(scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // RTABMAP_PYTHON
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
// Tests for PyMatcher: rtabmap's bridge to Python-backed descriptor matchers
|
||||||
|
// (SuperGlue, OANet, etc.). PyMatcher is an internal class (not in the public
|
||||||
|
// include tree), so the test pulls in its private header via the corelib/src
|
||||||
|
// include path wired up in CMakeLists.txt.
|
||||||
|
//
|
||||||
|
// We stand in a tiny numpy-only stub script for what would otherwise be a
|
||||||
|
// heavy ML model. The whole file is a no-op when rtabmap is built without
|
||||||
|
// Python -- the CMakeLists.txt only registers it under
|
||||||
|
// WITH_PYTHON AND Python3_FOUND, and a defensive #ifdef matches that.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <rtabmap/core/Version.h>
|
||||||
|
|
||||||
|
#ifdef RTABMAP_PYTHON
|
||||||
|
|
||||||
|
#include "python/PyMatcher.h" // private header (see CMakeLists.txt include dirs)
|
||||||
|
|
||||||
|
#include <rtabmap/utilite/UConversion.h>
|
||||||
|
#include <rtabmap/utilite/UFile.h>
|
||||||
|
|
||||||
|
#include <opencv2/core.hpp>
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
using namespace rtabmap;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Drops a minimal matcher script implementing the contract PyMatcher.cpp
|
||||||
|
// expects:
|
||||||
|
// init(descriptorDim, matchThreshold, iterations, cuda, model) -- called once
|
||||||
|
// match(kptsFrom, kptsTo, scoresFrom, scoresTo, descriptorsFrom,
|
||||||
|
// descriptorsTo, imageWidth, imageHeight)
|
||||||
|
// -> returns Nx2 int32 (queryIdx, trainIdx) pairs.
|
||||||
|
//
|
||||||
|
// Our stub returns a perfect 1:1 mapping for min(rowsFrom, rowsTo) rows
|
||||||
|
// (i -> i), echoing the count so the test can pin both the pipeline plumbing
|
||||||
|
// and the array-shape parsing in PyMatcher::match. Numpy-only, no heavy deps.
|
||||||
|
//
|
||||||
|
// Each test uses a unique filename via `tag` so the Python module cache
|
||||||
|
// reloads fresh content on each TEST().
|
||||||
|
std::string writeStubScript(int tag)
|
||||||
|
{
|
||||||
|
const std::string path = uFormat(
|
||||||
|
"/tmp/rtabmap_test_pymatcher_%d_%d.py", getpid(), tag);
|
||||||
|
std::ofstream out(path);
|
||||||
|
out <<
|
||||||
|
"import numpy as np\n"
|
||||||
|
"\n"
|
||||||
|
"INITIALIZED = False\n"
|
||||||
|
"INIT_DIM = 0\n"
|
||||||
|
"INIT_THRESHOLD = 0.0\n"
|
||||||
|
"INIT_ITERATIONS = 0\n"
|
||||||
|
"INIT_CUDA = 0\n"
|
||||||
|
"INIT_MODEL = ''\n"
|
||||||
|
"\n"
|
||||||
|
"def init(descriptorDim, matchThreshold, iterations, cuda, model):\n"
|
||||||
|
" global INITIALIZED, INIT_DIM, INIT_THRESHOLD, INIT_ITERATIONS\n"
|
||||||
|
" global INIT_CUDA, INIT_MODEL\n"
|
||||||
|
" INITIALIZED = True\n"
|
||||||
|
" INIT_DIM = int(descriptorDim)\n"
|
||||||
|
" INIT_THRESHOLD = float(matchThreshold)\n"
|
||||||
|
" INIT_ITERATIONS = int(iterations)\n"
|
||||||
|
" INIT_CUDA = int(cuda)\n"
|
||||||
|
" INIT_MODEL = str(model)\n"
|
||||||
|
"\n"
|
||||||
|
"def match(kptsFrom, kptsTo, scoresFrom, scoresTo,\n"
|
||||||
|
" descriptorsFrom, descriptorsTo, imageWidth, imageHeight):\n"
|
||||||
|
" nFrom = descriptorsFrom.shape[0]\n"
|
||||||
|
" nTo = descriptorsTo.shape[0]\n"
|
||||||
|
" n = min(nFrom, nTo)\n"
|
||||||
|
" # Perfect 1:1 mapping: query i <-> train i.\n"
|
||||||
|
" matches = np.zeros((n, 2), dtype=np.int32)\n"
|
||||||
|
" for i in range(n):\n"
|
||||||
|
" matches[i, 0] = i\n"
|
||||||
|
" matches[i, 1] = i\n"
|
||||||
|
" return matches\n";
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build N descriptors of dimension dim. Cell (i, j) = i + 0.01*j so the test
|
||||||
|
// can identify them, though the stub doesn't actually use the content.
|
||||||
|
cv::Mat makeDescriptors(int n, int dim)
|
||||||
|
{
|
||||||
|
cv::Mat d(n, dim, CV_32FC1);
|
||||||
|
for(int i = 0; i < n; ++i)
|
||||||
|
for(int j = 0; j < dim; ++j)
|
||||||
|
d.at<float>(i, j) = float(i) + 0.01f * float(j);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<cv::KeyPoint> makeKeypoints(int n)
|
||||||
|
{
|
||||||
|
std::vector<cv::KeyPoint> kpts;
|
||||||
|
kpts.reserve(n);
|
||||||
|
for(int i = 0; i < n; ++i)
|
||||||
|
{
|
||||||
|
kpts.emplace_back(float(i * 4), float(i * 4), 8.0f, -1.0f, 0.5f);
|
||||||
|
}
|
||||||
|
return kpts;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Full happy path: script loads, init() receives all 5 args, match() returns
|
||||||
|
// a 1:1 mapping that survives back into cv::DMatch entries.
|
||||||
|
TEST(PyMatcher, BasicMatching)
|
||||||
|
{
|
||||||
|
const std::string scriptPath = writeStubScript(1);
|
||||||
|
const int dim = 8;
|
||||||
|
PyMatcher matcher(scriptPath,
|
||||||
|
/*matchThreshold=*/0.2f,
|
||||||
|
/*iterations=*/20,
|
||||||
|
/*cuda=*/false,
|
||||||
|
/*model=*/"indoor");
|
||||||
|
EXPECT_EQ(scriptPath, matcher.path());
|
||||||
|
EXPECT_FLOAT_EQ(0.2f, matcher.matchThreshold());
|
||||||
|
EXPECT_EQ(20, matcher.iterations());
|
||||||
|
EXPECT_FALSE(matcher.cuda());
|
||||||
|
EXPECT_EQ("indoor", matcher.model());
|
||||||
|
|
||||||
|
cv::Mat descFrom = makeDescriptors(3, dim);
|
||||||
|
cv::Mat descTo = makeDescriptors(3, dim);
|
||||||
|
std::vector<cv::KeyPoint> kptsFrom = makeKeypoints(3);
|
||||||
|
std::vector<cv::KeyPoint> kptsTo = makeKeypoints(3);
|
||||||
|
|
||||||
|
std::vector<cv::DMatch> matches = matcher.match(
|
||||||
|
descFrom, descTo, kptsFrom, kptsTo, cv::Size(640, 480));
|
||||||
|
ASSERT_EQ(3u, matches.size());
|
||||||
|
for(int i = 0; i < 3; ++i)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(i, matches[i].queryIdx);
|
||||||
|
EXPECT_EQ(i, matches[i].trainIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
UFile::erase(scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asymmetric descriptor counts: stub returns min(nFrom, nTo) matches. Pin
|
||||||
|
// that the C++ parser handles non-square Nx2 arrays correctly.
|
||||||
|
TEST(PyMatcher, AsymmetricCounts)
|
||||||
|
{
|
||||||
|
const std::string scriptPath = writeStubScript(2);
|
||||||
|
const int dim = 4;
|
||||||
|
PyMatcher matcher(scriptPath, 0.2f, 20, false, "indoor");
|
||||||
|
|
||||||
|
cv::Mat descFrom = makeDescriptors(5, dim);
|
||||||
|
cv::Mat descTo = makeDescriptors(3, dim);
|
||||||
|
std::vector<cv::KeyPoint> kptsFrom = makeKeypoints(5);
|
||||||
|
std::vector<cv::KeyPoint> kptsTo = makeKeypoints(3);
|
||||||
|
|
||||||
|
std::vector<cv::DMatch> matches = matcher.match(
|
||||||
|
descFrom, descTo, kptsFrom, kptsTo, cv::Size(320, 240));
|
||||||
|
EXPECT_EQ(3u, matches.size()); // limited by the smaller side
|
||||||
|
|
||||||
|
UFile::erase(scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A non-existent script path is pre-validated by the constructor; match()
|
||||||
|
// then short-circuits because pModule_ is null.
|
||||||
|
TEST(PyMatcher, MissingPathReturnsEmpty)
|
||||||
|
{
|
||||||
|
const std::string scriptPath = uFormat(
|
||||||
|
"/tmp/rtabmap_test_pymatcher_does_not_exist_%d.py", getpid());
|
||||||
|
PyMatcher matcher(scriptPath, 0.2f, 20, false, "indoor");
|
||||||
|
|
||||||
|
cv::Mat descFrom = makeDescriptors(2, 8);
|
||||||
|
cv::Mat descTo = makeDescriptors(2, 8);
|
||||||
|
std::vector<cv::KeyPoint> kptsFrom = makeKeypoints(2);
|
||||||
|
std::vector<cv::KeyPoint> kptsTo = makeKeypoints(2);
|
||||||
|
|
||||||
|
std::vector<cv::DMatch> matches = matcher.match(
|
||||||
|
descFrom, descTo, kptsFrom, kptsTo, cv::Size(64, 64));
|
||||||
|
EXPECT_TRUE(matches.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// match() requires same descriptor dim on both sides; mismatched cols hit
|
||||||
|
// the "Invalid inputs" guard and return empty.
|
||||||
|
TEST(PyMatcher, MismatchedDescriptorDimReturnsEmpty)
|
||||||
|
{
|
||||||
|
const std::string scriptPath = writeStubScript(3);
|
||||||
|
PyMatcher matcher(scriptPath, 0.2f, 20, false, "indoor");
|
||||||
|
|
||||||
|
cv::Mat descFrom = makeDescriptors(2, 8);
|
||||||
|
cv::Mat descTo = makeDescriptors(2, 16);
|
||||||
|
std::vector<cv::KeyPoint> kptsFrom = makeKeypoints(2);
|
||||||
|
std::vector<cv::KeyPoint> kptsTo = makeKeypoints(2);
|
||||||
|
|
||||||
|
std::vector<cv::DMatch> matches = matcher.match(
|
||||||
|
descFrom, descTo, kptsFrom, kptsTo, cv::Size(64, 64));
|
||||||
|
EXPECT_TRUE(matches.empty());
|
||||||
|
|
||||||
|
UFile::erase(scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// match() also rejects non-CV_32F descriptors -- the input-guard in
|
||||||
|
// PyMatcher::match insists on float descriptors.
|
||||||
|
TEST(PyMatcher, NonFloatDescriptorsReturnEmpty)
|
||||||
|
{
|
||||||
|
const std::string scriptPath = writeStubScript(4);
|
||||||
|
PyMatcher matcher(scriptPath, 0.2f, 20, false, "indoor");
|
||||||
|
|
||||||
|
cv::Mat descFrom = cv::Mat::zeros(2, 8, CV_8UC1);
|
||||||
|
cv::Mat descTo = cv::Mat::zeros(2, 8, CV_8UC1);
|
||||||
|
std::vector<cv::KeyPoint> kptsFrom = makeKeypoints(2);
|
||||||
|
std::vector<cv::KeyPoint> kptsTo = makeKeypoints(2);
|
||||||
|
|
||||||
|
std::vector<cv::DMatch> matches = matcher.match(
|
||||||
|
descFrom, descTo, kptsFrom, kptsTo, cv::Size(64, 64));
|
||||||
|
EXPECT_TRUE(matches.empty());
|
||||||
|
|
||||||
|
UFile::erase(scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero-area imageSize is rejected by the input guard.
|
||||||
|
TEST(PyMatcher, ZeroImageSizeReturnsEmpty)
|
||||||
|
{
|
||||||
|
const std::string scriptPath = writeStubScript(5);
|
||||||
|
PyMatcher matcher(scriptPath, 0.2f, 20, false, "indoor");
|
||||||
|
|
||||||
|
cv::Mat descFrom = makeDescriptors(2, 8);
|
||||||
|
cv::Mat descTo = makeDescriptors(2, 8);
|
||||||
|
std::vector<cv::KeyPoint> kptsFrom = makeKeypoints(2);
|
||||||
|
std::vector<cv::KeyPoint> kptsTo = makeKeypoints(2);
|
||||||
|
|
||||||
|
std::vector<cv::DMatch> matches = matcher.match(
|
||||||
|
descFrom, descTo, kptsFrom, kptsTo, cv::Size(0, 0));
|
||||||
|
EXPECT_TRUE(matches.empty());
|
||||||
|
|
||||||
|
UFile::erase(scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // RTABMAP_PYTHON
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// Tests for PythonInterface and its companion getPythonTraceback() helper.
|
||||||
|
// Mostly regression coverage for the NULL-safety fixes in getPythonTraceback
|
||||||
|
// (early-return on no exception, Py_None substitution for missing fetch
|
||||||
|
// components, NULL-safe DECREFs).
|
||||||
|
//
|
||||||
|
// The whole file is a no-op when rtabmap is built without Python -- the
|
||||||
|
// CMakeLists.txt only registers it under WITH_PYTHON AND Python3_FOUND, and
|
||||||
|
// a defensive #ifdef matches that.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <rtabmap/core/Version.h>
|
||||||
|
|
||||||
|
#ifdef RTABMAP_PYTHON
|
||||||
|
|
||||||
|
#include <rtabmap/core/PythonInterface.h>
|
||||||
|
|
||||||
|
#include <pybind11/embed.h>
|
||||||
|
#include <Python.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace rtabmap;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// All tests need a live interpreter; instance() lazily initializes it.
|
||||||
|
class PythonInterfaceTest : public ::testing::Test
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
void SetUp() override
|
||||||
|
{
|
||||||
|
PythonInterface::instance("test_python_interface");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// instance() must return the same object every call and never re-initialize
|
||||||
|
// the embedded interpreter. Pinning this so any future refactor can't
|
||||||
|
// accidentally regress to "construct a new interpreter per call" (which
|
||||||
|
// pybind11's scoped_interpreter does not support in the same process).
|
||||||
|
TEST_F(PythonInterfaceTest, InstanceIsSingleton)
|
||||||
|
{
|
||||||
|
PythonInterface & a = PythonInterface::instance("test_a");
|
||||||
|
PythonInterface & b = PythonInterface::instance("test_b");
|
||||||
|
EXPECT_EQ(&a, &b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No active Python exception -> the helper returns a clear sentinel instead
|
||||||
|
// of crashing inside Py_BuildValue("OOO", NULL, NULL, NULL). The sentinel
|
||||||
|
// makes it obvious in logs that there was nothing useful to report.
|
||||||
|
TEST_F(PythonInterfaceTest, TracebackEmptyWhenNoException)
|
||||||
|
{
|
||||||
|
pybind11::gil_scoped_acquire acquire;
|
||||||
|
PyErr_Clear();
|
||||||
|
ASSERT_FALSE(PyErr_Occurred());
|
||||||
|
|
||||||
|
EXPECT_EQ("<no python exception set>", getPythonTraceback());
|
||||||
|
}
|
||||||
|
|
||||||
|
// With a real exception set, the formatter returns a non-empty string that
|
||||||
|
// includes the original message. getPythonTraceback consumes the exception
|
||||||
|
// (PyErr_Fetch clears it), so PyErr_Occurred is false afterward.
|
||||||
|
TEST_F(PythonInterfaceTest, TracebackFormattedAfterException)
|
||||||
|
{
|
||||||
|
pybind11::gil_scoped_acquire acquire;
|
||||||
|
PyErr_Clear();
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "test error message");
|
||||||
|
|
||||||
|
const std::string trace = getPythonTraceback();
|
||||||
|
EXPECT_FALSE(trace.empty());
|
||||||
|
EXPECT_NE("<no python exception set>", trace);
|
||||||
|
EXPECT_NE(std::string::npos, trace.find("test error message"));
|
||||||
|
EXPECT_FALSE(PyErr_Occurred());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeated calls on the no-exception path must stay stable. The original
|
||||||
|
// crashes (Py_DECREF on NULL outStr, Py_BuildValue on NULL slots) surfaced
|
||||||
|
// non-deterministically after several invocations -- this loop would have
|
||||||
|
// reliably tripped them.
|
||||||
|
TEST_F(PythonInterfaceTest, RepeatedCallsAreStable)
|
||||||
|
{
|
||||||
|
pybind11::gil_scoped_acquire acquire;
|
||||||
|
for(int i = 0; i < 25; ++i)
|
||||||
|
{
|
||||||
|
PyErr_Clear();
|
||||||
|
EXPECT_EQ("<no python exception set>", getPythonTraceback());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same robustness check with an exception each time.
|
||||||
|
for(int i = 0; i < 25; ++i)
|
||||||
|
{
|
||||||
|
PyErr_SetString(PyExc_ValueError, "boom");
|
||||||
|
const std::string trace = getPythonTraceback();
|
||||||
|
EXPECT_FALSE(trace.empty());
|
||||||
|
EXPECT_NE("<no python exception set>", trace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // RTABMAP_PYTHON
|
||||||
Reference in New Issue
Block a user