diff --git a/.github/workflows/linux-eic-shell.yml b/.github/workflows/linux-eic-shell.yml index c43b0d1..31a3811 100644 --- a/.github/workflows/linux-eic-shell.yml +++ b/.github/workflows/linux-eic-shell.yml @@ -65,7 +65,10 @@ jobs: run: | CXX="${{ matrix.CXX }}" cmake -B build -S . \ -DCMAKE_BUILD_TYPE=${{ matrix.CMAKE_BUILD_TYPE }} \ + -DUSE_GEOCAD=ON \ -DCMAKE_INSTALL_PREFIX=install + cmake --build build -j $(getconf _NPROCESSORS_ONLN) + ctest --test-dir build --output-on-failure --no-tests=error -j $(getconf _NPROCESSORS_ONLN) cmake --build build -j $(getconf _NPROCESSORS_ONLN) --target install - name: Compress install directory run: tar -caf install.tar.zst install/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d0e6ea..bb1e83b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,7 @@ endif() # Use GNU install dirs include(GNUInstallDirs) +include(CTest) # Build type if (NOT CMAKE_BUILD_TYPE) diff --git a/src/geocad/CMakeLists.txt b/src/geocad/CMakeLists.txt index 4b5950d..16eb834 100644 --- a/src/geocad/CMakeLists.txt +++ b/src/geocad/CMakeLists.txt @@ -66,3 +66,7 @@ install(TARGETS GeoCad RUNTIME DESTINATION bin INCLUDES DESTINATION include ) + +if(BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/src/geocad/tests/CMakeLists.txt b/src/geocad/tests/CMakeLists.txt new file mode 100644 index 0000000..4589d01 --- /dev/null +++ b/src/geocad/tests/CMakeLists.txt @@ -0,0 +1,16 @@ +set(geocad_unit_test npdet_geocad_unit_partial_tree) +add_executable(${geocad_unit_test} unit_partial_tree.cxx) +target_link_libraries(${geocad_unit_test} + PRIVATE GeoCad ROOT::Geom +) +add_test(NAME ${geocad_unit_test} COMMAND ${geocad_unit_test}) + +set(geocad_integration_test npdet_geocad_integration_create_step) +add_executable(${geocad_integration_test} integration_create_step.cxx) +target_link_libraries(${geocad_integration_test} + PRIVATE GeoCad ROOT::Geom +) +add_test( + NAME ${geocad_integration_test} + COMMAND ${geocad_integration_test} ${CMAKE_CURRENT_BINARY_DIR}/npdet_geocad_test_geometry.stp +) diff --git a/src/geocad/tests/integration_create_step.cxx b/src/geocad/tests/integration_create_step.cxx new file mode 100644 index 0000000..6ba8ad9 --- /dev/null +++ b/src/geocad/tests/integration_create_step.cxx @@ -0,0 +1,46 @@ +#include +#include +#include + +#include "TGeoManager.h" +#include "TGeoMaterial.h" +#include "TGeoMedium.h" +#include "TGeoToStep.h" +#include "TGeoVolume.h" + +namespace fs = std::filesystem; + +namespace { +TGeoManager* make_test_geometry() { + auto* geom = new TGeoManager("npdet_geo_test", "npdet geocad integration test"); + auto* mat = new TGeoMaterial("Vacuum", 0, 0, 0); + auto* med = new TGeoMedium("Vacuum", 1, mat); + + auto* top = geom->MakeBox("top", med, 100., 100., 100.); + auto* child = geom->MakeBox("child", med, 10., 10., 10.); + top->AddNode(child, 1); + geom->SetTopVolume(top); + geom->CloseGeometry(); + return geom; +} +} // namespace + +int main(int argc, char* argv[]) { + if (argc != 2) { + std::cerr << "Expected output path argument\n"; + return 1; + } + + const fs::path out_path = argv[1]; + fs::remove(out_path); + + TGeoToStep converter(make_test_geometry()); + converter.CreateGeometry(out_path.string().c_str(), 2, 1.0); + + if (!fs::exists(out_path) || fs::file_size(out_path) == 0) { + std::cerr << "STEP output file was not created\n"; + return 1; + } + + return 0; +} diff --git a/src/geocad/tests/unit_partial_tree.cxx b/src/geocad/tests/unit_partial_tree.cxx new file mode 100644 index 0000000..1121543 --- /dev/null +++ b/src/geocad/tests/unit_partial_tree.cxx @@ -0,0 +1,41 @@ +#include + +#include "src/TOCCToStep.h" +#include "TGeoManager.h" +#include "TGeoMaterial.h" +#include "TGeoMedium.h" +#include "TGeoVolume.h" + +namespace { +TGeoManager* make_test_geometry() { + auto* geom = new TGeoManager("npdet_geo_test", "npdet geocad unit test"); + auto* mat = new TGeoMaterial("Vacuum", 0, 0, 0); + auto* med = new TGeoMedium("Vacuum", 1, mat); + + auto* top = geom->MakeBox("top", med, 100., 100., 100.); + auto* child = geom->MakeBox("child", med, 10., 10., 10.); + top->AddNode(child, 1); + geom->SetTopVolume(top); + geom->CloseGeometry(); + return geom; +} +} // namespace + +int main() { + auto* geom = make_test_geometry(); + + TOCCToStep converter; + converter.OCCShapeCreation(geom, 1.0); + + const bool found_child = converter.OCCPartialTreeCreation(geom, "child", 2); + const bool found_fake = converter.OCCPartialTreeCreation(geom, "does_not_exist", 2); + + delete geom; + + if (!found_child || found_fake) { + std::cerr << "Unexpected partial tree lookup results\n"; + return 1; + } + + return 0; +} diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index ac37e29..1ee8fc9 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -39,7 +39,7 @@ install(TARGETS ${exe_name} # npdet_to_step needs the (formerly part of ROOT) GeoCad library if(TARGET GeoCad) set(exe_name npdet_to_step) - add_executable(${exe_name} src/${exe_name}.cxx src/settings.cxx) + add_executable(${exe_name} src/${exe_name}.cxx src/settings.cxx src/npdet_to_step_cli.cxx) target_include_directories(${exe_name} PRIVATE include ) target_compile_features(${exe_name} @@ -98,6 +98,10 @@ install(TARGETS ${exe_name} EXPORT NPDetTargets RUNTIME DESTINATION bin ) +if(BUILD_TESTING) + add_subdirectory(tests) +endif() + # ------------------------------------ # dd_web_display # ------------------------------------ diff --git a/src/tools/src/npdet_to_step.cxx b/src/tools/src/npdet_to_step.cxx index 5af09c1..bc50658 100644 --- a/src/tools/src/npdet_to_step.cxx +++ b/src/tools/src/npdet_to_step.cxx @@ -1,4 +1,3 @@ -#include #include #include #include @@ -6,212 +5,37 @@ namespace fs = std::filesystem; #include -#include #include #include -#include "clipp.h" -using namespace clipp; - -#include "settings.h" #include "TGeoToStep.h" +#include "npdet_to_step_cli.h" +#include "settings.h" void run_part_mode(const settings& s); //______________________________________________________________________________ -template -void print_usage(T cli, const char* argv0 ){ - std::cout << "Usage:\n" << usage_lines(cli, argv0) - << "\nOptions:\n" << documentation(cli) << '\n'; -} -//______________________________________________________________________________ - -template -void print_man_page(T cli, const char* argv0 ){ - //all formatting options (with their default values) - auto fmt = clipp::doc_formatting{} - .first_column(8) //column where usage lines and documentation starts - .doc_column(20) //parameter docstring start col - .indent_size(4) //indent of documentation lines for children of a documented group - .line_spacing(0) //number of empty lines after single documentation lines - .paragraph_spacing(1) //number of empty lines before and after paragraphs - .flag_separator(", ") //between flags of the same parameter - .param_separator(" ") //between parameters - .group_separator(" ") //between groups (in usage) - .alternative_param_separator("|") //between alternative flags - .alternative_group_separator(" | ") //between alternative groups - .surround_group("(", ")") //surround groups with these - .surround_alternatives("(", ")") //surround group of alternatives with these - .surround_alternative_flags("", "") //surround alternative flags with these - .surround_joinable("(", ")") //surround group of joinable flags with these - .surround_optional("[", "]") //surround optional parameters with these - .surround_repeat("", "..."); //surround repeatable parameters with these - //.surround_value("<", ">") //surround values with these - //.empty_label("") //used if parameter has no flags and no label - //.max_alternative_flags_in_usage(1) //max. # of flags per parameter in usage - //.max_alternative_flags_in_doc(2) //max. # of flags per parameter in detailed documentation - //.split_alternatives(true) //split usage into several lines for large alternatives - //.alternatives_min_split_size(3) //min. # of parameters for separate usage line - //.merge_alternative_flags_with_common_prefix(false) //-ab(cdxy|xy) instead of -abcdxy|-abxy - //.merge_joinable_flags_with_common_prefix(true); //-abc instead of -a -b -c - - auto mp = make_man_page(cli, argv0, fmt); - mp.prepend_section("DESCRIPTION", "Geometry tool for converting compact files to STEP (cad) files."); - mp.append_section("EXAMPLES", " $ npdet_to_step list compact.xml"); - std::cout << mp << "\n"; -} -//______________________________________________________________________________ - -settings cmdline_settings(int argc, char* argv[]) -{ - settings s; - - //auto listMode = "list mode:" % ( - // command("list").set(s.selected,mode::list) % "list detectors and print info about geometry ", - // option("-v","--verbose") - // ); - auto listMode = "list mode:" % repeatable( - command("list").set(s.selected,mode::list) % "list detectors and print info about geometry ", - repeatable( - option("-l","--level").set(s.level_set) - & value("level",s.part_level) - & value("name")([&](const std::string& p) - { - s.part_name = p; - if(!s.level_set) { s.part_level = -1; } - s.part_name_levels[p] = s.part_level; - s.level_set = false; - }) % "Part/Node name (must be child of top node)" - ) - ); - - auto partMode = "part mode:" % repeatable( - command("part").set(s.selected,mode::part) % "Select only the first level nodes by name", - repeatable( - option("-l","--level").set(s.level_set) & value("level",s.part_level) % "Maximum level navigated to for part", - value("name")([&](const std::string& p) - { - s.part_name = p; - if(!s.level_set) { s.part_level = -1; } - s.part_name_levels[p] = s.part_level; - s.level_set = false; - }) % "Part/Node name (must be child of top node)" - ) - ); - - auto lastOpt = " options:" % ( - option("-h", "--help").set(s.selected, mode::help) % "show help", - option("-g","--global_level") & integer("level",s.global_level), - option("-o","--output") & value("out",s.outfile), - value("file",s.infile).if_missing([]{ std::cout << "You need to provide an input xml filename as the last argument!\n"; } ) - % "input xml file", - option("-u","--unit-factor") & value("unit",s.tgeo_length_unit_in_mm) % "conversion factor of the length unit to mm" - ); - - auto helpMode = command("help").set(s.selected, mode::help); - - auto cli = ( - helpMode | (partMode | listMode , lastOpt) - ); - - assert( cli.flags_are_prefix_free() ); - auto res = parse(argc, argv, cli); - - //auto doc_label = [](const parameter& p) { - // if(!p.flags().empty()) return p.flags().front(); - // if(!p.label().empty()) return p.label(); - // return doc_string{""}; - //}; - //std::string wrong; - - //auto& os = std::cout; - //std::cout << "args -> parameter mapping:\n"; - //for(const auto& m : res) { - // os << "#" << m.index() << " " << m.arg() << " -> "; - // auto p = m.param(); - // if(p) { - // os << doc_label(*p) << " \t"; - // if(m.repeat() > 0) { - // os << (m.bad_repeat() ? "[bad repeat " : "[repeat ") - // << m.repeat() << "]"; - // } - // if(m.blocked()) os << " [blocked]"; - // if(m.conflict()) os << " [conflict]"; - // os << '\n'; - // } - // else { - // os << " [unmapped]\n"; - // } - //} - - //std::cout << "missing parameters:\n"; - //for(const auto& m : res.missing()) { - // auto p = m.param(); - // if(p) { - // os << doc_label(*p) << " \t"; - // os << " [missing after " << m.after_index() << "]\n"; - // } - //} - - //std::cout << "wrong " << wrong << std::endl; - //if(res.unmapped_args_count()) { std::cout << "a\n"; } - //if(res.any_bad_repeat()) { std::cout << "b\n"; } - //if(res.any_blocked()) { std::cout << "c\n"; } - //if(res.any_conflict()) { std::cout << "d\n"; } - - if(s.selected == mode::help) { - print_man_page(cli,argv[0]); - return s; - } - - if( res.any_error() ) { - s.success = false; - std::cout << make_man_page(cli, argv[0]).prepend_section("error: ", - " The best thing since sliced bread."); - return s; - } - - s.success = true; - - return s; -} -//______________________________________________________________________________ - - -int main (int argc, char *argv[]) { - - settings s = cmdline_settings(argc,argv); - if( !s.success ) { +int main(int argc, char* argv[]) { + settings s = cmdline_settings(argc, argv); + if (!s.success) { return 1; } - if(s.selected == mode::help) { + if (s.selected == mode::help) { return 0; } - // -------------------------------------- - // CLI Checks - if( !fs::exists(fs::path(s.infile)) ) { + if (!fs::exists(fs::path(s.infile))) { std::cerr << "file, " << s.infile << ", does not exist\n"; return 1; } - auto has_suffix = [&](const std::string &str, const std::string &suffix) { - return str.size() >= suffix.size() && - str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; - }; - if( !has_suffix(s.outfile,".stp") ) { - s.outfile += ".stp"; - } + s.outfile = ensure_step_extension(s.outfile); - // Make things quite - gErrorIgnoreLevel = kWarning;// kPrint, kInfo, kWarning, + gErrorIgnoreLevel = kWarning; dd4hep::setPrintLevel(dd4hep::WARNING); - // --------------------------------------------- - // Run modes - // - switch(s.selected) { + switch (s.selected) { case mode::list: run_list_mode(s); break; @@ -226,29 +50,21 @@ int main (int argc, char *argv[]) { } //______________________________________________________________________________ - - -void run_part_mode(const settings& s) -{ +void run_part_mode(const settings& s) { dd4hep::setPrintLevel(dd4hep::WARNING); gErrorIgnoreLevel = kWarning; - // ------------------------- - // Get the DD4hep instance - // Load the compact XML file dd4hep::Detector& detector = dd4hep::Detector::getInstance(); detector.fromCompact(s.infile); - TGeoToStep * mygeom= new TGeoToStep( &(detector.manager()) ); - if( s.part_name_levels.size() > 1 ) { - mygeom->CreatePartialGeometry( s.part_name_levels, s.outfile.c_str(), s.tgeo_length_unit_in_mm ); - } else if( s.part_name_levels.size() == 1 ) { - // loop of 1 - for(const auto& [n,l] : s.part_name_levels){ - mygeom->CreatePartialGeometry( n.c_str(), l, s.outfile.c_str(), s.tgeo_length_unit_in_mm ); + TGeoToStep* mygeom = new TGeoToStep(&(detector.manager())); + if (s.part_name_levels.size() > 1) { + mygeom->CreatePartialGeometry(s.part_name_levels, s.outfile.c_str(), s.tgeo_length_unit_in_mm); + } else if (s.part_name_levels.size() == 1) { + for (const auto& [n, l] : s.part_name_levels) { + mygeom->CreatePartialGeometry(n.c_str(), l, s.outfile.c_str(), s.tgeo_length_unit_in_mm); } } else { mygeom->CreateGeometry(s.outfile.c_str(), s.global_level, s.tgeo_length_unit_in_mm); } } - diff --git a/src/tools/src/npdet_to_step_cli.cxx b/src/tools/src/npdet_to_step_cli.cxx new file mode 100644 index 0000000..3f5fefb --- /dev/null +++ b/src/tools/src/npdet_to_step_cli.cxx @@ -0,0 +1,94 @@ +#include "npdet_to_step_cli.h" + +#include +#include +#include + +#include "clipp.h" +using namespace clipp; + +template +void print_man_page(T cli, const char* argv0) { + auto fmt = clipp::doc_formatting{} + .first_column(8) + .doc_column(20) + .indent_size(4) + .line_spacing(0) + .paragraph_spacing(1) + .flag_separator(", ") + .param_separator(" ") + .group_separator(" ") + .alternative_param_separator("|") + .alternative_group_separator(" | ") + .surround_group("(", ")") + .surround_alternatives("(", ")") + .surround_alternative_flags("", "") + .surround_joinable("(", ")") + .surround_optional("[", "]") + .surround_repeat("", "..."); + + auto mp = make_man_page(cli, argv0, fmt); + mp.prepend_section("DESCRIPTION", "Geometry tool for converting compact files to STEP (cad) files."); + mp.append_section("EXAMPLES", " $ npdet_to_step list compact.xml"); + std::cout << mp << "\n"; +} + +settings cmdline_settings(int argc, char* argv[]) { + settings s; + + auto listMode = "list mode:" % repeatable( + command("list").set(s.selected, mode::list) % "list detectors and print info about geometry ", + repeatable(option("-l", "--level").set(s.level_set) & value("level", s.part_level) & + value("name")([&](const std::string& p) { + s.part_name = p; + if (!s.level_set) { + s.part_level = -1; + } + s.part_name_levels[p] = s.part_level; + s.level_set = false; + }) % "Part/Node name (must be child of top node)")); + + auto partMode = "part mode:" % repeatable( + command("part").set(s.selected, mode::part) % "Select only the first level nodes by name", + repeatable(option("-l", "--level").set(s.level_set) & value("level", s.part_level) % "Maximum level navigated to for part", + value("name")([&](const std::string& p) { + s.part_name = p; + if (!s.level_set) { + s.part_level = -1; + } + s.part_name_levels[p] = s.part_level; + s.level_set = false; + }) % "Part/Node name (must be child of top node)")); + + auto lastOpt = " options:" % ( + option("-h", "--help").set(s.selected, mode::help) % "show help", + option("-g", "--global_level") & integer("level", s.global_level), + option("-o", "--output") & value("out", s.outfile), + value("file", s.infile).if_missing( + [] { std::cout << "You need to provide an input xml filename as the last argument!\n"; }) % + "input xml file", + option("-u", "--unit-factor") & value("unit", s.tgeo_length_unit_in_mm) % "conversion factor of the length unit to mm"); + + auto helpMode = command("help").set(s.selected, mode::help); + + auto cli = (helpMode | (partMode | listMode, lastOpt)); + + assert(cli.flags_are_prefix_free()); + auto res = parse(argc, argv, cli); + + if (s.selected == mode::help) { + s.success = true; + print_man_page(cli, argv[0]); + return s; + } + + if (res.any_error()) { + s.success = false; + std::cout << make_man_page(cli, argv[0]).prepend_section("error: ", " The best thing since sliced bread."); + return s; + } + + s.success = true; + + return s; +} diff --git a/src/tools/src/npdet_to_step_cli.h b/src/tools/src/npdet_to_step_cli.h new file mode 100644 index 0000000..196f969 --- /dev/null +++ b/src/tools/src/npdet_to_step_cli.h @@ -0,0 +1,8 @@ +#ifndef NPDET_TO_STEP_CLI_H +#define NPDET_TO_STEP_CLI_H + +#include "settings.h" + +settings cmdline_settings(int argc, char* argv[]); + +#endif diff --git a/src/tools/src/settings.h b/src/tools/src/settings.h index 79813e3..81b3a1d 100644 --- a/src/tools/src/settings.h +++ b/src/tools/src/settings.h @@ -36,4 +36,14 @@ struct settings { void print_daughter_nodes(TGeoNode* node, int print_depth) ; void run_list_mode(const settings& s); +inline std::string ensure_step_extension(std::string output_path) { + static const std::string extension = ".stp"; + if (output_path.size() >= extension.size() && + output_path.compare(output_path.size() - extension.size(), extension.size(), extension) == 0) { + return output_path; + } + output_path += extension; + return output_path; +} + #endif diff --git a/src/tools/tests/CMakeLists.txt b/src/tools/tests/CMakeLists.txt index 8e677db..84417db 100644 --- a/src/tools/tests/CMakeLists.txt +++ b/src/tools/tests/CMakeLists.txt @@ -1,15 +1,26 @@ -# ---------------------------------------------- -# Tests - -# InitialState -set(test_name simple) -add_executable(${test_name} tests/${test_name}.cxx) -target_include_directories(${test_name} PUBLIC - $ - $ - $ - ) +set(test_name npdet_tools_unit_settings) +add_executable(${test_name} unit_settings.cxx) +target_include_directories(${test_name} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../src +) target_link_libraries(${test_name} - PRIVATE insanePhysics - PRIVATE Catch) + PRIVATE ROOT::Geom +) add_test(NAME ${test_name} COMMAND ${test_name}) + +if(TARGET npdet_to_step) + add_test( + NAME tools_integration_npdet_to_step_help + COMMAND $ help + ) + set_tests_properties(tools_integration_npdet_to_step_help + PROPERTIES PASS_REGULAR_EXPRESSION "Geometry tool for converting compact files to STEP") + + add_test( + NAME tools_integration_npdet_to_step_missing_input + COMMAND $ list /tmp/npsim_missing_compact.xml + ) + set_tests_properties(tools_integration_npdet_to_step_missing_input + PROPERTIES PASS_REGULAR_EXPRESSION "does not exist") +endif() diff --git a/src/tools/tests/simple.cxx b/src/tools/tests/simple.cxx deleted file mode 100644 index 303f3a0..0000000 --- a/src/tools/tests/simple.cxx +++ /dev/null @@ -1,13 +0,0 @@ -#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this in one cpp file -#include "catch2/catch.hpp" - -unsigned int Factorial( unsigned int number ) { - return number <= 1 ? number : Factorial(number-1)*number; -} - -TEST_CASE( "Factorials are computed", "[factorial]" ) { - REQUIRE( Factorial(1) == 1 ); - REQUIRE( Factorial(2) == 2 ); - REQUIRE( Factorial(3) == 6 ); - REQUIRE( Factorial(10) == 3628800 ); -} diff --git a/src/tools/tests/unit_settings.cxx b/src/tools/tests/unit_settings.cxx new file mode 100644 index 0000000..1b0fe91 --- /dev/null +++ b/src/tools/tests/unit_settings.cxx @@ -0,0 +1,19 @@ +#include +#include + +#include "settings.h" + +int main() { + bool ok = true; + + ok = ok && (ensure_step_extension("detector") == "detector.stp"); + ok = ok && (ensure_step_extension("detector.stp") == "detector.stp"); + ok = ok && (ensure_step_extension("a.long.name.step") == "a.long.name.step.stp"); + + if (!ok) { + std::cerr << "settings unit checks failed\n"; + return 1; + } + + return 0; +}