diff --git a/src/config_path.h.in b/src/config_path.h.in
index 19bda36870..a3d36f4b73 100644
--- a/src/config_path.h.in
+++ b/src/config_path.h.in
@@ -2,3 +2,4 @@
#define GPHOX_CONFIG_SEARCH_PATHS ".:config:@GPHOX_INSTALL_FULL_DATADIR@/config"
#define GPHOX_PTX_DIR "@CMAKE_INSTALL_FULL_LIBDIR@"
+#define GPHOX_TEST_GEOM_DIR "@PROJECT_SOURCE_DIR@/tests/geom"
diff --git a/tests/geom/sphere_phicut_quarter_shell.gdml b/tests/geom/sphere_phicut_quarter_shell.gdml
new file mode 100644
index 0000000000..ebfc2e1b35
--- /dev/null
+++ b/tests/geom/sphere_phicut_quarter_shell.gdml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/u4/tests/CMakeLists.txt b/u4/tests/CMakeLists.txt
index 91b7469045..ede142e658 100644
--- a/u4/tests/CMakeLists.txt
+++ b/u4/tests/CMakeLists.txt
@@ -62,8 +62,9 @@ set(TEST_SOURCES
G4ThreeVectorTest.cc
-
U4PhysicsTableTest.cc
+
+ SpherePhiCutQuarterShellTest.cc
)
set(EXPECTED_TO_FAIL_SOURCES
@@ -81,6 +82,7 @@ foreach(SRC ${TEST_SOURCES})
get_filename_component(TGT ${SRC} NAME_WE)
add_executable(${TGT} ${SRC})
target_link_libraries(${TGT} U4)
+ target_include_directories(${TGT} PRIVATE ${CMAKE_BINARY_DIR}/src)
add_test(
NAME ${name}.${TGT}
diff --git a/u4/tests/SpherePhiCutQuarterShellTest.cc b/u4/tests/SpherePhiCutQuarterShellTest.cc
new file mode 100644
index 0000000000..7c3aeb2041
--- /dev/null
+++ b/u4/tests/SpherePhiCutQuarterShellTest.cc
@@ -0,0 +1,246 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+using plog::error;
+using plog::fatal;
+using plog::info;
+
+#include "G4BooleanSolid.hh"
+#include "G4IntersectionSolid.hh"
+#include "G4LogicalVolume.hh"
+#include "G4PhysicalConstants.hh"
+#include "G4Sphere.hh"
+#include "G4SubtractionSolid.hh"
+#include "G4SystemOfUnits.hh"
+#include "G4VPhysicalVolume.hh"
+#include "G4VSolid.hh"
+
+#include "config_path.h"
+
+#include "U4GDML.h"
+#include "U4Solid.h"
+#include "U4Volume.h"
+
+#include "s_csg.h"
+
+namespace
+{
+inline constexpr char testGeomFile[] = GPHOX_TEST_GEOM_DIR "/sphere_phicut_quarter_shell.gdml";
+
+enum ConvertOutcome
+{
+ CONVERT_REJECTED,
+ CONVERT_ACCEPTED,
+ CONVERT_ERROR
+};
+
+bool IsExpectedRejectSignal(int signal)
+{
+ return signal == SIGABRT || signal == SIGINT;
+}
+
+bool HasNonSpherePrimitive(const sn* nd)
+{
+ if (nd == nullptr)
+ return false;
+
+ std::set typecodes;
+ nd->typecodes(typecodes);
+
+ for (int typecode : typecodes)
+ {
+ if (CSG::IsPrimitive(typecode) == false)
+ continue;
+
+ if (typecode == CSG_SPHERE || typecode == CSG_ZSPHERE)
+ continue;
+
+ if (typecode == CSG_NOTSUPPORTED || typecode == CSG_UNDEFINED)
+ continue;
+
+ if (typecode == CSG_PHICUT || typecode == CSG_HALFSPACE)
+ return true;
+ }
+
+ return false;
+}
+
+int CountPartialPhiSpheres(const G4VSolid* solid)
+{
+ const G4Sphere* sphere = dynamic_cast(solid);
+ if (sphere)
+ {
+ double start_phi = sphere->GetStartPhiAngle() / CLHEP::radian;
+ double delta_phi = sphere->GetDeltaPhiAngle() / CLHEP::radian;
+ bool partial_phi = start_phi != 0. || delta_phi != 2. * CLHEP::pi;
+ return partial_phi ? 1 : 0;
+ }
+
+ const G4BooleanSolid* boolean = dynamic_cast(solid);
+ if (boolean == nullptr)
+ return 0;
+
+ const G4VSolid* left = boolean->GetConstituentSolid(0);
+ const G4VSolid* right = boolean->GetConstituentSolid(1);
+ return CountPartialPhiSpheres(left) + CountPartialPhiSpheres(right);
+}
+
+ConvertOutcome ConvertInChildProcess(const G4VSolid* solid)
+{
+ pid_t pid = fork();
+ if (pid < 0)
+ {
+ perror("fork");
+ return CONVERT_ERROR;
+ }
+
+ if (pid == 0)
+ {
+ s_csg* csg = new s_csg;
+ assert(csg);
+
+ int lvid = 0;
+ int depth = 0;
+ int level = 1;
+ sn* nd = U4Solid::Convert(solid, lvid, depth, level);
+ int exit_code = 4;
+ if (nd != nullptr)
+ {
+ bool has_phi_cut_representation = HasNonSpherePrimitive(nd);
+ exit_code = has_phi_cut_representation ? 0 : 5;
+ if (has_phi_cut_representation == false)
+ {
+ std::cerr
+ << "child conversion produced non-null tree without any non-sphere primitive; "
+ << "phi-cut geometry is not represented"
+ << std::endl;
+ }
+ }
+ delete nd;
+ _exit(exit_code);
+ }
+
+ int status = 0;
+ int rc = waitpid(pid, &status, 0);
+ if (rc != pid)
+ {
+ perror("waitpid");
+ return CONVERT_ERROR;
+ }
+
+ if (WIFSIGNALED(status))
+ {
+ int signal = WTERMSIG(status);
+ if (IsExpectedRejectSignal(signal))
+ {
+ std::cout << "child rejected conversion with expected signal " << signal << std::endl;
+ return CONVERT_REJECTED;
+ }
+
+ std::cerr << "child crashed with unexpected signal " << signal << std::endl;
+ return CONVERT_ERROR;
+ }
+
+ if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
+ {
+ std::cout
+ << "child converted partial-phi sphere with non-sphere primitive(s) in the CSG tree"
+ << std::endl;
+ return CONVERT_ACCEPTED;
+ }
+
+ if (WIFEXITED(status))
+ {
+ std::cerr << "child exited unexpectedly with status " << WEXITSTATUS(status) << std::endl;
+ return CONVERT_ERROR;
+ }
+
+ std::cerr << "child ended in unexpected state " << status << std::endl;
+ return CONVERT_ERROR;
+}
+} // namespace
+
+int main(int argc, char** argv)
+{
+ static plog::ConsoleAppender consoleAppender;
+ plog::init(plog::info, &consoleAppender);
+
+ const G4VPhysicalVolume* world = U4GDML::Read(testGeomFile);
+ LOG_IF(plog::fatal, world == nullptr)
+ << "failed to load GDML path " << (testGeomFile ? testGeomFile : "-");
+ if (world == nullptr)
+ return EXIT_FAILURE;
+
+ const G4VPhysicalVolume* quarter_shell_pv = U4Volume::FindPV(world, "QuarterShell_pv");
+ LOG_IF(plog::fatal, quarter_shell_pv == nullptr)
+ << "failed to find QuarterShell_pv in GDML path " << testGeomFile;
+ if (quarter_shell_pv == nullptr)
+ return EXIT_FAILURE;
+
+ const G4LogicalVolume* quarter_shell_lv = quarter_shell_pv->GetLogicalVolume();
+ LOG_IF(plog::fatal, quarter_shell_lv == nullptr)
+ << "QuarterShell_pv lacks a logical volume";
+ if (quarter_shell_lv == nullptr)
+ return EXIT_FAILURE;
+
+ const G4VSolid* quarter_shell_solid = quarter_shell_lv->GetSolid();
+ LOG_IF(plog::fatal, quarter_shell_solid == nullptr)
+ << "QuarterShell_pv lacks a solid";
+ if (quarter_shell_solid == nullptr)
+ return EXIT_FAILURE;
+
+ const G4IntersectionSolid* intersection = dynamic_cast(quarter_shell_solid);
+ LOG_IF(plog::fatal, intersection != nullptr)
+ << "test geometry unexpectedly uses a parent IntersectionSolid";
+ if (intersection != nullptr)
+ return EXIT_FAILURE;
+
+ const G4SubtractionSolid* subtraction = dynamic_cast(quarter_shell_solid);
+ LOG_IF(plog::fatal, subtraction == nullptr)
+ << "test geometry expected a subtraction shell solid " << quarter_shell_solid->GetName();
+ if (subtraction == nullptr)
+ return EXIT_FAILURE;
+
+ int partial_phi_spheres = CountPartialPhiSpheres(quarter_shell_solid);
+ LOG_IF(plog::fatal, partial_phi_spheres == 0)
+ << "test geometry expected partial-phi sphere primitives";
+ if (partial_phi_spheres == 0)
+ return EXIT_FAILURE;
+
+ ConvertOutcome outcome = ConvertInChildProcess(quarter_shell_solid);
+ switch (outcome)
+ {
+ case CONVERT_REJECTED:
+ std::cout
+ << "partial-phi sphere conversion is rejected, matching current fail-fast behavior"
+ << std::endl;
+ return EXIT_SUCCESS;
+
+ case CONVERT_ACCEPTED:
+ std::cout
+ << "partial-phi sphere conversion succeeded with a non-null CSG tree"
+ << std::endl;
+ return EXIT_SUCCESS;
+
+ case CONVERT_ERROR:
+ break;
+ }
+
+ std::cerr
+ << "SpherePhiCutQuarterShellTest could neither confirm fail-fast rejection nor "
+ << "successful conversion of the partial-phi spherical shell."
+ << std::endl;
+
+ return EXIT_FAILURE;
+}