diff --git a/nvmolkit/tests/test_substructure.py b/nvmolkit/tests/test_substructure.py index 5d3fda33..2fd6161c 100644 --- a/nvmolkit/tests/test_substructure.py +++ b/nvmolkit/tests/test_substructure.py @@ -202,6 +202,23 @@ def test_buffer_overflow_rdkit_fallback(self): assert len(results[0][0]) == 10, f"Should get all 10 matches via RDKit fallback, got {len(results[0][0])}" assert matches_equal(results[0][0], rdkit_matches), "Matches should be identical to RDKit results" + @pytest.mark.parametrize("preprocessing_threads", [1, 2, -1]) + def test_disconnected_smarts_raises(self, preprocessing_threads: int): + """Disconnected (fragment) SMARTS must raise rather than crash the process. + + Regression test for GH issue 203: with more than one preprocessing + thread the validation error escaped the OpenMP region and terminated the + process. The unsupported-query error must surface as a RuntimeError for + every preprocessing-thread count. + """ + targets = [Chem.MolFromSmiles(smi) for smi in ("CC", "CCC", "CCCC")] + queries = [Chem.MolFromSmarts("C.C")] + + config = SubstructSearchConfig(preprocessingThreads=preprocessing_threads) + + with pytest.raises(RuntimeError, match="disconnected"): + hasSubstructMatch(targets, queries, config) + def test_no_match_possible(self): """Test when no match is possible.""" targets = [Chem.MolFromSmiles("CCCC")] diff --git a/src/substruct/CMakeLists.txt b/src/substruct/CMakeLists.txt index a51f7152..e53e6db5 100644 --- a/src/substruct/CMakeLists.txt +++ b/src/substruct/CMakeLists.txt @@ -19,7 +19,7 @@ add_library(molecules molecules.cpp) target_link_libraries( molecules PUBLIC device_vector flatBitVect OpenMP::OpenMP_CXX packed_bonds device - PRIVATE ${RDKit_LIBS} nvtx) + PRIVATE ${RDKit_LIBS} nvtx openmp_helpers) add_library(substruct_kernels substruct_kernels.cu) target_link_libraries( diff --git a/src/substruct/molecules.cpp b/src/substruct/molecules.cpp index b3bc37cf..54403bf4 100644 --- a/src/substruct/molecules.cpp +++ b/src/substruct/molecules.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -33,6 +34,7 @@ #include "src/substruct/substruct_debug.h" #include "src/substruct/substruct_types.h" #include "src/utils/nvtx.h" +#include "src/utils/openmp_helpers.h" #include "src/utils/rdkit_compat.h" namespace nvMolKit { @@ -2128,6 +2130,12 @@ MoleculesHost buildQueryBatchParallel(const std::vector& mo threadBatches[t].reserve(molsPerThread, atomsPerThread); } + // addQueryToBatch validates each query and throws on unsupported inputs (e.g. + // disconnected SMARTS). An exception escaping the OpenMP region would call + // std::terminate, so capture the first failure and rethrow after the region + // joins. + detail::OpenMPExceptionRegistry exceptionRegistry; + #pragma omp parallel num_threads(numThreads) { const int tid = omp_get_thread_num(); @@ -2136,10 +2144,15 @@ MoleculesHost buildQueryBatchParallel(const std::vector& mo #pragma omp for schedule(static) for (int i = 0; i < numMols; ++i) { - const int molIdx = useSortOrder ? sortOrder[i] : i; - addQueryToBatch(molecules[molIdx], localBatch); + try { + const int molIdx = useSortOrder ? sortOrder[i] : i; + addQueryToBatch(molecules[molIdx], localBatch); + } catch (...) { + exceptionRegistry.store(std::current_exception()); + } } } + exceptionRegistry.rethrow(); MoleculesHost result; {