diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8457a5e7f..2234ddeac0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,6 +79,36 @@ jobs: shell: bash run: make check + - name: Install Autoconf test-library prefix + if: runner.os == 'Linux' + shell: bash + run: | + set -euxo pipefail + build_root="$RUNNER_TEMP/legacy-test-component-check" + prefix="$build_root/prefix" + rm -rf "$build_root" + mkdir -p "$prefix" + make install prefix="$prefix" + + - name: Verify installed legacy test consumer + if: runner.os == 'Linux' + shell: bash + run: | + set -euxo pipefail + build_root="$RUNNER_TEMP/legacy-test-component-check" + prefix="$build_root/prefix" + + python test/package/verify-installed-legacy-test-component.py \ + --source "$GITHUB_WORKSPACE" \ + --build-root "$build_root" \ + --prefix "$prefix" + + python test/package/verify-installed-legacy-test-component.py \ + --source "$GITHUB_WORKSPACE" \ + --build-root "$build_root" \ + --prefix "$prefix" \ + --mode unsupported-header + - name: Autoconf install smoke shell: bash run: bash .github/ci/autoconf-install-smoke.sh "$RUNNER_TEMP/gecode-autoconf-install" @@ -148,6 +178,12 @@ jobs: set -euxo pipefail cmake --build . --config $BUILD_TYPE --target check + - name: Run public test harness smokes + if: matrix.os == 'ubuntu-latest' + working-directory: ${{github.workspace}}/build + shell: bash + run: ctest --build-config "$BUILD_TYPE" --output-on-failure -R '^public-(runner|int)-smoke$' + - name: Install CMake package working-directory: ${{github.workspace}}/build shell: bash @@ -169,6 +205,26 @@ jobs: core fi + - name: Verify installed test component + if: matrix.os == 'ubuntu-latest' + shell: bash + run: | + set -euxo pipefail + python test/package/verify-installed-test-component.py \ + --source "$GITHUB_WORKSPACE" \ + --build-root "$RUNNER_TEMP/test-component-check" \ + --prefix "$GITHUB_WORKSPACE/install" \ + --include-dir "/tmp/gecode-install-abs-include" \ + --lib-dir "$GITHUB_WORKSPACE/install/lib" + + python test/package/verify-installed-test-component.py \ + --source "$GITHUB_WORKSPACE" \ + --build-root "$RUNNER_TEMP/test-component-check-missing-component" \ + --prefix "$GITHUB_WORKSPACE/install" \ + --include-dir "/tmp/gecode-install-abs-include" \ + --lib-dir "$GITHUB_WORKSPACE/install/lib" \ + --expect-missing-component-failure + - name: Package version checks shell: bash run: bash .github/ci/cmake-package-version-checks.sh "$GITHUB_WORKSPACE/install" diff --git a/CMakeLists.txt b/CMakeLists.txt index e9d0600f6b..9fface316a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -814,7 +814,7 @@ ${CONFIG_OUT}") include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/GecodeSources.cmake) if(GECODE_ENABLE_FAULT_INJECTION) list(APPEND GECODE_SUPPORT_SOURCES gecode/support/failpoint.cpp) - set(GECODE_FAULT_TEST_SOURCES test/test.cpp test/fault.cpp) + set(GECODE_FAULT_TEST_SOURCES ${GECODE_TEST_MAIN_SOURCE} test/fault.cpp) endif() # --------------------------------------------------------------------------- @@ -1354,8 +1354,132 @@ if(GECODE_ENABLE_FLATZINC) endif() if(BUILD_TESTING) - set(GECODE_CAN_BUILD_TESTS TRUE) - foreach(required search int minimodel driver) + set(GECODE_CAN_BUILD_TEST_HARNESS TRUE) + foreach(required search int) + if(NOT TARGET gecode${required}) + set(GECODE_CAN_BUILD_TEST_HARNESS FALSE) + endif() + endforeach() + + if(GECODE_CAN_BUILD_TEST_HARNESS) + add_library(gecodetest STATIC ${GECODE_TEST_CORE_SOURCES}) + target_compile_features(gecodetest PUBLIC cxx_std_17) + target_include_directories(gecodetest + PUBLIC + $ + $ + $) + if(GECODE_VISIBILITY_COMPILE_OPTION) + target_compile_options(gecodetest PRIVATE ${GECODE_VISIBILITY_COMPILE_OPTION}) + endif() + set_target_properties(gecodetest PROPERTIES + OUTPUT_NAME "${GECODE_LIB_PREFIX}gecodetest${GECODE_LIB_SUFFIX}") + target_link_libraries(gecodetest PUBLIC gecodesearch gecodeint) + add_library(Gecode::gecodetest ALIAS gecodetest) + list(APPEND GECODE_INSTALL_TARGETS gecodetest) + list(APPEND GECODE_EXPORT_TARGETS gecodetest) + + add_library(gecodetestint STATIC ${GECODE_TEST_INT_SOURCES}) + target_compile_features(gecodetestint PUBLIC cxx_std_17) + target_include_directories(gecodetestint + PUBLIC + $ + $ + $) + if(GECODE_VISIBILITY_COMPILE_OPTION) + target_compile_options(gecodetestint PRIVATE ${GECODE_VISIBILITY_COMPILE_OPTION}) + endif() + set_target_properties(gecodetestint PROPERTIES + OUTPUT_NAME "${GECODE_LIB_PREFIX}gecodetestint${GECODE_LIB_SUFFIX}") + target_link_libraries(gecodetestint PUBLIC gecodetest) + add_library(Gecode::gecodetestint ALIAS gecodetestint) + list(APPEND GECODE_INSTALL_TARGETS gecodetestint) + list(APPEND GECODE_EXPORT_TARGETS gecodetestint) + + if(TARGET gecodeset) + add_library(gecodetestset STATIC ${GECODE_TEST_SET_SOURCES}) + target_compile_features(gecodetestset PUBLIC cxx_std_17) + target_include_directories(gecodetestset + PUBLIC + $ + $ + $) + if(GECODE_VISIBILITY_COMPILE_OPTION) + target_compile_options(gecodetestset PRIVATE ${GECODE_VISIBILITY_COMPILE_OPTION}) + endif() + set_target_properties(gecodetestset PROPERTIES + OUTPUT_NAME "${GECODE_LIB_PREFIX}gecodetestset${GECODE_LIB_SUFFIX}") + target_link_libraries(gecodetestset PUBLIC gecodetestint gecodeset) + add_library(Gecode::gecodetestset ALIAS gecodetestset) + list(APPEND GECODE_INSTALL_TARGETS gecodetestset) + list(APPEND GECODE_EXPORT_TARGETS gecodetestset) + endif() + + if(TARGET gecodefloat) + add_library(gecodetestfloat STATIC ${GECODE_TEST_FLOAT_SOURCES}) + target_compile_features(gecodetestfloat PUBLIC cxx_std_17) + target_include_directories(gecodetestfloat + PUBLIC + $ + $ + $) + if(GECODE_VISIBILITY_COMPILE_OPTION) + target_compile_options(gecodetestfloat PRIVATE ${GECODE_VISIBILITY_COMPILE_OPTION}) + endif() + set_target_properties(gecodetestfloat PROPERTIES + OUTPUT_NAME "${GECODE_LIB_PREFIX}gecodetestfloat${GECODE_LIB_SUFFIX}") + target_link_libraries(gecodetestfloat PUBLIC gecodetest gecodefloat) + add_library(Gecode::gecodetestfloat ALIAS gecodetestfloat) + list(APPEND GECODE_INSTALL_TARGETS gecodetestfloat) + list(APPEND GECODE_EXPORT_TARGETS gecodetestfloat) + endif() + if(GECODE_INSTALL) + foreach(gecode_install_component IN LISTS GECODE_LIBRARY_COMPONENTS) + if(TARGET gecode${gecode_install_component}_${GECODE_DEFAULT_LINK_VARIANT}) + add_dependencies(gecodetest gecode${gecode_install_component}_${GECODE_DEFAULT_LINK_VARIANT}) + endif() + endforeach() + endif() + + add_executable(public-runner-smoke EXCLUDE_FROM_ALL ${GECODE_TEST_PUBLIC_RUNNER_SMOKE_SOURCE}) + target_link_libraries(public-runner-smoke PRIVATE gecodetest) + add_test(NAME public-runner-smoke COMMAND public-runner-smoke) + + add_executable(public-int-smoke EXCLUDE_FROM_ALL ${GECODE_TEST_PUBLIC_INT_SMOKE_SOURCE}) + target_link_libraries(public-int-smoke PRIVATE gecodetestint) + add_test(NAME public-int-smoke COMMAND public-int-smoke) + + set(GECODE_PUBLIC_TEST_SMOKE_TARGETS public-runner-smoke public-int-smoke) + if(TARGET gecodetestset) + add_executable(public-set-smoke EXCLUDE_FROM_ALL ${GECODE_TEST_PUBLIC_SET_SMOKE_SOURCE}) + target_link_libraries(public-set-smoke PRIVATE gecodetestset) + add_test(NAME public-set-smoke COMMAND public-set-smoke) + list(APPEND GECODE_PUBLIC_TEST_SMOKE_TARGETS public-set-smoke) + endif() + if(TARGET gecodetestfloat) + add_executable(public-float-smoke EXCLUDE_FROM_ALL ${GECODE_TEST_PUBLIC_FLOAT_SMOKE_SOURCE}) + target_link_libraries(public-float-smoke PRIVATE gecodetestfloat) + add_test(NAME public-float-smoke COMMAND public-float-smoke) + list(APPEND GECODE_PUBLIC_TEST_SMOKE_TARGETS public-float-smoke) + endif() + + add_test(NAME build-public-test-smokes + COMMAND ${CMAKE_COMMAND} --build "${CMAKE_BINARY_DIR}" + --target ${GECODE_PUBLIC_TEST_SMOKE_TARGETS} --config $ + ) + set_tests_properties(build-public-test-smokes PROPERTIES + FIXTURES_SETUP public-test-smokes-built + RESOURCE_LOCK gecode-test-build + ) + set_tests_properties(${GECODE_PUBLIC_TEST_SMOKE_TARGETS} PROPERTIES + FIXTURES_REQUIRED public-test-smokes-built + ) + else() + message(WARNING "Skipping public test harness targets because required modules are disabled") + endif() + + set(GECODE_CAN_BUILD_TESTS ${GECODE_CAN_BUILD_TEST_HARNESS}) + foreach(required minimodel driver) if(NOT TARGET gecode${required}) set(GECODE_CAN_BUILD_TESTS FALSE) endif() @@ -1382,6 +1506,12 @@ if(BUILD_TESTING) endif() set(GECODE_TEST_SOURCES_SELECTED ${GECODE_TEST_SOURCES}) + if(TARGET gecodetestset) + list(REMOVE_ITEM GECODE_TEST_SOURCES_SELECTED test/set.cpp) + endif() + if(TARGET gecodetestfloat) + list(REMOVE_ITEM GECODE_TEST_SOURCES_SELECTED test/float.cpp) + endif() if(NOT GECODE_ENABLE_SET_VARS) list(FILTER GECODE_TEST_SOURCES_SELECTED EXCLUDE REGEX "^test/set(/|\\.cpp)") endif() @@ -1393,8 +1523,16 @@ if(BUILD_TESTING) endif() if(GECODE_CAN_BUILD_TESTS) - add_executable(gecode-test EXCLUDE_FROM_ALL ${GECODE_TEST_SOURCES_SELECTED}) + add_executable(gecode-test EXCLUDE_FROM_ALL ${GECODE_TEST_MAIN_SOURCE} ${GECODE_TEST_SOURCES_SELECTED}) set(GECODE_TEST_LINK_LIBS gecodeminimodel) + if(TARGET gecodetestset) + list(APPEND GECODE_TEST_LINK_LIBS gecodetestset) + else() + list(APPEND GECODE_TEST_LINK_LIBS gecodetestint) + endif() + if(TARGET gecodetestfloat) + list(APPEND GECODE_TEST_LINK_LIBS gecodetestfloat) + endif() if(GECODE_ENABLE_FLATZINC) list(APPEND GECODE_TEST_LINK_LIBS gecodeflatzinc) endif() @@ -1487,7 +1625,8 @@ if(BUILD_TESTING) COMMAND ${CMAKE_COMMAND} --build "${CMAKE_BINARY_DIR}" --target ${GECODE_TEST_BUILD_TARGETS} --config $) set_tests_properties(build-gecode-test PROPERTIES - FIXTURES_SETUP "${GECODE_TEST_BUILD_FIXTURES}") + FIXTURES_SETUP "${GECODE_TEST_BUILD_FIXTURES}" + RESOURCE_LOCK gecode-test-build) add_test(NAME test COMMAND gecode-test ${GECODE_CHECK_ARGS}) set_tests_properties(test PROPERTIES FIXTURES_REQUIRED gecode-test-built) @@ -1643,6 +1782,26 @@ if(GECODE_INSTALL) install(FILES ${GECODE_VAR_TYPE_HPP} ${GECODE_VAR_IMP_HPP} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/gecode/kernel/) endif() + if(TARGET gecodetest AND TARGET gecodetestint) + install(FILES + ${CMAKE_CURRENT_SOURCE_DIR}/test/test.hh + ${CMAKE_CURRENT_SOURCE_DIR}/test/test.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/test/int.hh + ${CMAKE_CURRENT_SOURCE_DIR}/test/int.hpp + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/test) + if(TARGET gecodetestset) + install(FILES + ${CMAKE_CURRENT_SOURCE_DIR}/test/set.hh + ${CMAKE_CURRENT_SOURCE_DIR}/test/set.hpp + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/test) + endif() + if(TARGET gecodetestfloat) + install(FILES + ${CMAKE_CURRENT_SOURCE_DIR}/test/float.hh + ${CMAKE_CURRENT_SOURCE_DIR}/test/float.hpp + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/test) + endif() + endif() if(GECODE_ENABLE_FLATZINC) install(DIRECTORY gecode/flatzinc/mznlib/ @@ -1664,6 +1823,18 @@ if(GECODE_INSTALL) if(GECODE_ENABLE_FLOAT_VARS AND GECODE_ENABLE_MPFR AND MPFR_FOUND) set(GECODE_PACKAGE_NEEDS_MPFR ON) endif() + set(GECODE_PACKAGE_HAS_TEST_COMPONENT OFF) + set(GECODE_PACKAGE_TEST_TYPES "") + if(TARGET gecodetest AND TARGET gecodetestint) + set(GECODE_PACKAGE_HAS_TEST_COMPONENT ON) + set(GECODE_PACKAGE_TEST_TYPES int) + if(TARGET gecodetestset) + list(APPEND GECODE_PACKAGE_TEST_TYPES set) + endif() + if(TARGET gecodetestfloat) + list(APPEND GECODE_PACKAGE_TEST_TYPES float) + endif() + endif() set(GECODE_PACKAGE_QT_MAJOR "") set(GECODE_PACKAGE_QT_COMPONENTS "") set(GECODE_PACKAGE_QT_VERSION "") diff --git a/Makefile.in b/Makefile.in index b446405125..b887199987 100755 --- a/Makefile.in +++ b/Makefile.in @@ -1087,12 +1087,13 @@ else STATICTARGETS= endif -export LIBTARGETS = $(DLLTARGETS) $(STATICTARGETS) +export LIBTARGETS = $(DLLTARGETS) $(STATICTARGETS) $(TESTPUBLICINSTALLLIBTARGETS) ifeq "@need_soname@" "yes" export LIBLINKTARGETS = \ $(DLLTARGETS:%$(DLLSUFFIX)=%$(SOSUFFIX)) \ - $(DLLTARGETS:%$(DLLSUFFIX)=%$(SOLINKSUFFIX)) + $(DLLTARGETS:%$(DLLSUFFIX)=%$(SOLINKSUFFIX)) \ + $(TESTPUBLICINSTALLLIBLINKTARGETS) else export LIBLINKTARGETS = endif @@ -1100,8 +1101,8 @@ endif PDBS = $(ALLLIB:%$(LIBSUFFIX)=%$(PDBSUFFIX)) ifeq "@INSTALLLIBS@" "yes" -LIBLIBTARGETS = $(ALLLIB) -PDBTARGETS = $(PDBS) +LIBLIBTARGETS = $(ALLLIB) $(TESTPUBLICINSTALLLIBLIBTARGETS) +PDBTARGETS = $(PDBS) $(TESTPUBLICINSTALLPDBTARGETS) else LIBLIBTARGETS = PDBTARGETS = @@ -1276,7 +1277,29 @@ BLACKBOXEXECSRC = test/flatzinc/blackbox-exec.cpp BLACKBOXDLLSRC = test/flatzinc/blackbox-dll.cpp BLACKBOXSRC = $(BLACKBOXEXECSRC) $(BLACKBOXDLLSRC) -TESTSRC0 = test/test.cpp test/afc.cpp test/ldsb.cpp test/region.cpp \ +TESTCORESRC = test/test.cpp +TESTCOREOBJ = $(TESTCORESRC:%.cpp=%$(OBJSUFFIX)) +TESTMAINSRC = test/test-main.cpp +TESTMAINOBJ = $(TESTMAINSRC:%.cpp=%$(OBJSUFFIX)) +TESTINTSEAMSRC = test/int.cpp +TESTINTSEAMOBJ = $(TESTINTSEAMSRC:%.cpp=%$(OBJSUFFIX)) +TESTSETSEAMSRC = test/set.cpp +TESTSETSEAMOBJ = $(TESTSETSEAMSRC:%.cpp=%$(OBJSUFFIX)) +TESTFLOATSEAMSRC = test/float.cpp +TESTFLOATSEAMOBJ = $(TESTFLOATSEAMSRC:%.cpp=%$(OBJSUFFIX)) +TESTPUBLICRUNNERSMOKESRC = test/public-runner-smoke.cpp +TESTPUBLICRUNNERSMOKEOBJ = $(TESTPUBLICRUNNERSMOKESRC:%.cpp=%$(OBJSUFFIX)) +TESTPUBLICINTSMOKESRC = test/public-int-smoke.cpp +TESTPUBLICINTSMOKEOBJ = $(TESTPUBLICINTSMOKESRC:%.cpp=%$(OBJSUFFIX)) +TESTPUBLICSETSMOKESRC = test/public-set-smoke.cpp +TESTPUBLICSETSMOKEOBJ = $(TESTPUBLICSETSMOKESRC:%.cpp=%$(OBJSUFFIX)) +TESTPUBLICFLOATSMOKESRC = test/public-float-smoke.cpp +TESTPUBLICFLOATSMOKEOBJ = $(TESTPUBLICFLOATSMOKESRC:%.cpp=%$(OBJSUFFIX)) +TESTAUXOBJ = $(TESTMAINOBJ) $(TESTPUBLICRUNNERSMOKEOBJ) \ + $(TESTPUBLICINTSMOKEOBJ) $(TESTPUBLICSETSMOKEOBJ) \ + $(TESTPUBLICFLOATSMOKEOBJ) + +TESTSRC0 = $(TESTCORESRC) test/afc.cpp test/ldsb.cpp test/region.cpp \ test/groups.cpp # FailPoint is CMake-only; keep the Autoconf test executable fault-free. @@ -1289,6 +1312,99 @@ TESTHDR0 = \ test.hh test.hpp int.hh int.hpp set.hh set.hpp float.hh float.hpp \ branch.hh assign.hh flatzinc.hh TESTHDR = $(TESTHDR0:%=test/%) + +TESTPUBLICINSTALLHDR = +TESTPUBLICINSTALLLIBTARGETS = +TESTPUBLICINSTALLLIBLINKTARGETS = +TESTPUBLICINSTALLLIBLIBTARGETS = +TESTPUBLICINSTALLPDBTARGETS = +TESTSETSTATICLIB = +TESTFLOATSTATICLIB = + +ifeq "@enable_search@" "yes" +ifeq "@enable_int_vars@" "yes" +TESTPUBLICINSTALLHDR = test/test.hh test/test.hpp test/int.hh test/int.hpp + +export TESTCOREDLL = $(LIBPREFIX)test$(DLLSUFFIX) +export TESTCORELIB = $(LIBPREFIX)test$(LIBSUFFIX) +ifeq "@need_soname@" "yes" +export TESTCORESONAME = \ + @WLSONAME@$(LIBPREFIX)test$(SOSUFFIX) +else +export TESTCORESONAME = +endif +export TESTCORESTATICLIB = $(LIBPREFIX)test$(STATICLIBSUFFIX) +export LINKTESTCORE = $(LINKPREFIX)test$(LINKSUFFIX) +ifeq "@enable_resource@" "yes" +export TESTCORERC = $(LIBPREFIX)test$(RCSUFFIX) +export TESTCORERES = $(LIBPREFIX)test$(RESSUFFIX) +else +export TESTCORERES = +export TESTCORERC = +endif + +export TESTINTDLL = $(LIBPREFIX)testint$(DLLSUFFIX) +export TESTINTLIB = $(LIBPREFIX)testint$(LIBSUFFIX) +ifeq "@need_soname@" "yes" +export TESTINTSONAME = \ + @WLSONAME@$(LIBPREFIX)testint$(SOSUFFIX) +else +export TESTINTSONAME = +endif +export TESTINTSTATICLIB = $(LIBPREFIX)testint$(STATICLIBSUFFIX) +export LINKTESTINT = $(LINKPREFIX)testint$(LINKSUFFIX) +ifeq "@enable_resource@" "yes" +export TESTINTRC = $(LIBPREFIX)testint$(RCSUFFIX) +export TESTINTRES = $(LIBPREFIX)testint$(RESSUFFIX) +else +export TESTINTRES = +export TESTINTRC = +endif + +TESTPUBLICINSTALLLIBTARGETS += $(TESTCORESTATICLIB) $(TESTINTSTATICLIB) +ifeq "@enable_set_vars@" "yes" +TESTPUBLICINSTALLHDR += test/set.hh test/set.hpp +TESTSETSTATICLIB = $(LIBPREFIX)testset$(STATICLIBSUFFIX) +TESTPUBLICINSTALLLIBTARGETS += $(TESTSETSTATICLIB) +endif +ifeq "@enable_float_vars@" "yes" +TESTPUBLICINSTALLHDR += test/float.hh test/float.hpp +TESTFLOATSTATICLIB = $(LIBPREFIX)testfloat$(STATICLIBSUFFIX) +TESTPUBLICINSTALLLIBTARGETS += $(TESTFLOATSTATICLIB) +endif +else +export TESTCOREDLL = +export TESTCORELIB = +export TESTCORESONAME = +export TESTCORESTATICLIB = +export LINKTESTCORE = +export TESTCORERES = +export TESTCORERC = +export TESTINTDLL = +export TESTINTLIB = +export TESTINTSONAME = +export TESTINTSTATICLIB = +export LINKTESTINT = +export TESTINTRES = +export TESTINTRC = +endif +else +export TESTCOREDLL = +export TESTCORELIB = +export TESTCORESONAME = +export TESTCORESTATICLIB = +export LINKTESTCORE = +export TESTCORERES = +export TESTCORERC = +export TESTINTDLL = +export TESTINTLIB = +export TESTINTSONAME = +export TESTINTSTATICLIB = +export LINKTESTINT = +export TESTINTRES = +export TESTINTRC = +endif + TESTOBJ = $(INTTESTOBJ) $(SETTESTOBJ) $(FLOATTESTOBJ) \ $(FLATZINCTESTOBJ:%.cpp=%$(OBJSUFFIX)) \ $(BRANCHTESTSRC0:%.cpp=%$(OBJSUFFIX)) \ @@ -1297,6 +1413,21 @@ TESTOBJ = $(INTTESTOBJ) $(SETTESTOBJ) $(FLOATTESTOBJ) \ $(TESTSRC0:%.cpp=%$(OBJSUFFIX)) TESTSBJ = $(TESTOBJ:%$(OBJSUFFIX)=%$(SBJSUFFIX)) TESTEXE = test/test$(EXESUFFIX) +TESTPUBLICRUNNERSMOKEEXE = test/public-runner-smoke$(EXESUFFIX) +TESTPUBLICINTSMOKEEXE = test/public-int-smoke$(EXESUFFIX) +ifeq "@enable_set_vars@" "yes" +TESTPUBLICSETSMOKEEXE = test/public-set-smoke$(EXESUFFIX) +else +TESTPUBLICSETSMOKEEXE = +endif +ifeq "@enable_float_vars@" "yes" +TESTPUBLICFLOATSMOKEEXE = test/public-float-smoke$(EXESUFFIX) +else +TESTPUBLICFLOATSMOKEEXE = +endif +TESTPUBLICSMOKEEXES = $(TESTPUBLICRUNNERSMOKEEXE) \ + $(TESTPUBLICINTSMOKEEXE) $(TESTPUBLICSETSMOKEEXE) \ + $(TESTPUBLICFLOATSMOKEEXE) BLACKBOXEXECOBJ = $(BLACKBOXEXECSRC:%.cpp=%$(OBJSUFFIX)) BLACKBOXDLLOBJ = $(BLACKBOXDLLSRC:%.cpp=%$(OBJSUFFIX)) BLACKBOXEXEC = test/flatzinc/blackbox-exec$(EXESUFFIX) @@ -1342,10 +1473,9 @@ compilelib: mkcompiledirs compileexamples: $(EXAMPLEEXE) test: mkcompiledirs $(BLACKBOXFIXTURES) - @$(MAKE) $(VARIMP) $(TESTEXE) + @$(MAKE) $(VARIMP) $(TESTEXE) $(TESTPUBLICSMOKEEXES) CHECKTESTS = Branch::Int::Dense::3 \ - FlatZinc::magic_square \ Int::Arithmetic::Abs \ Int::Arithmetic::ArgMax \ Int::Arithmetic::Max::Nary \ @@ -1356,14 +1486,18 @@ CHECKTESTS = Branch::Int::Dense::3 \ Int::Linear::Bool::Int::Lq \ Int::MiniModel::LinExpr::Bool::352 \ NoGoods::Queens \ - Search::DFS::Sol::Binary::Nary::Binary::1::1::1 \ + Search::DFS::Sol::Binary::Nary::Binary::1::1::1 + +ifeq "@enable_set_vars@" "yes" +CHECKTESTS += \ Set::Dom::Dom::Gr \ Set::RelOp::ConstSSI::Union \ Set::Sequence::SeqU1 \ Set::Wait +endif ifeq "@enable_flatzinc@" "yes" -CHECKTESTS += FlatZinc::Options FlatZinc::blackbox +CHECKTESTS += FlatZinc::magic_square FlatZinc::Options FlatZinc::blackbox BLACKBOXCHECKENV = \ GECODE_TEST_BLACKBOX_EXEC=$(abspath $(BLACKBOXEXEC)) \ GECODE_TEST_BLACKBOX_DLL=$(abspath $(BLACKBOXDLL)) \ @@ -1388,6 +1522,9 @@ endif # A basic integrity test check: test + @for t in $(TESTPUBLICSMOKEEXES); do \ + $(RUNENVIRONMENT) $$t || exit 1; \ + done @for t in $(REQUIREDFLOATCHECKTESTS); do \ case " $(CHECKTESTS) " in *" $$t "*) ;; \ *) echo "Required Float check test is missing: $$t" >&2; exit 1 ;; \ @@ -1438,12 +1575,12 @@ VIS = @ALLVIS@ VISDEP = $(VIS) \ $(top_srcdir)/misc/genvarimp.py Makefile -gecode/kernel/var-type.hpp: $(VISDEP) +gecode/kernel/var-type.hpp: $(VISDEP) | mkcompiledirs $(UVRUN) $(top_srcdir)/misc/genvarimp.py -typehpp $(VIS) > $@ -gecode/kernel/var-imp.hpp: $(VISDEP) +gecode/kernel/var-imp.hpp: $(VISDEP) | mkcompiledirs $(UVRUN) $(top_srcdir)/misc/genvarimp.py -header $(VIS) > $@ -$(ALLOBJ) $(ALLSBJ) $(TESTOBJ) $(TESTSBJ): $(VARIMP) +$(ALLOBJ) $(ALLSBJ) $(TESTOBJ) $(TESTSBJ) $(TESTAUXOBJ): $(VARIMP) | mkcompiledirs # # Object targets @@ -1555,27 +1692,27 @@ $(GISTMOCSRC:%.cpp=%$(SBJSUFFIX)): gecode/gist/moc_%$(SBJSUFFIX): gecode/gist/mo @COMPILESBJ@$@ @CXXIN@$< .PRECIOUS: gecode/gist/moc_%.cpp -gecode/gist/moc_%.cpp: $(top_srcdir)/gecode/gist/%.hh +gecode/gist/moc_%.cpp: $(top_srcdir)/gecode/gist/%.hh | mkcompiledirs $(MOC) $(CPPFLAGS) -pgecode/gist $< -o $@ .PRECIOUS: gecode/flatzinc/%.cpp gecode/flatzinc/%.hh ifeq "@HAVE_FLEXBISON@" "yes" gecode/flatzinc/lexer.yy.cpp: \ $(top_srcdir)/gecode/flatzinc/lexer.lxx \ - $(top_srcdir)/gecode/flatzinc/parser.tab.hpp + $(top_srcdir)/gecode/flatzinc/parser.tab.hpp | mkcompiledirs flex -ogecode/flatzinc/lexer.yy.cpp \ $(top_srcdir)/gecode/flatzinc/lexer.lxx gecode/flatzinc/parser.tab.hpp gecode/flatzinc/parser.tab.cpp: \ - $(top_srcdir)/gecode/flatzinc/parser.yxx + $(top_srcdir)/gecode/flatzinc/parser.yxx | mkcompiledirs bison -t -o gecode/flatzinc/parser.tab.cpp -d $< else ifneq "$(top_srcdir)" "." -gecode/flatzinc/lexer.yy.cpp: $(top_srcdir)/gecode/flatzinc/lexer.yy.cpp +gecode/flatzinc/lexer.yy.cpp: $(top_srcdir)/gecode/flatzinc/lexer.yy.cpp | mkcompiledirs cp $(@:%=$(top_srcdir)/%) $@ -gecode/flatzinc/parser.tab.hpp: $(top_srcdir)/gecode/flatzinc/parser.tab.hpp +gecode/flatzinc/parser.tab.hpp: $(top_srcdir)/gecode/flatzinc/parser.tab.hpp | mkcompiledirs cp $(@:%=$(top_srcdir)/%) $@ -gecode/flatzinc/parser.tab.cpp: $(top_srcdir)/gecode/flatzinc/parser.tab.cpp +gecode/flatzinc/parser.tab.cpp: $(top_srcdir)/gecode/flatzinc/parser.tab.cpp | mkcompiledirs cp $(@:%=$(top_srcdir)/%) $@ endif endif @@ -1680,6 +1817,18 @@ $(FLATZINCDLL): $(FLATZINCOBJ) $(SUPPORTDLL) $(KERNELDLL) $(SEARCHDLL) \ $(LINKDL) @LINKOUTPUT@$(FLATZINCDLL) $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOLINKSUFFIX)) $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOSUFFIX)) +$(TESTCOREDLL): $(TESTCOREOBJ) $(SUPPORTDLL) $(KERNELDLL) $(SEARCHDLL) $(INTDLL) + $(CXX) $(DLLFLAGS) $(TESTCOREOBJ) $(TESTCORESONAME) \ + @DLLPATH@ $(LINKSUPPORT) $(LINKKERNEL) $(LINKSEARCH) $(LINKINT) \ + @LINKOUTPUT@$(TESTCOREDLL) + $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOLINKSUFFIX)) + $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOSUFFIX)) +$(TESTINTDLL): $(TESTINTSEAMOBJ) $(TESTCOREDLL) + $(CXX) $(DLLFLAGS) $(TESTINTSEAMOBJ) $(TESTINTSONAME) \ + @DLLPATH@ $(LINKTESTCORE) \ + @LINKOUTPUT@$(TESTINTDLL) + $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOLINKSUFFIX)) + $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOSUFFIX)) else export LINKALL = @@ -1789,6 +1938,25 @@ $(FLATZINCDLL) $(FLATZINCLIB): $(FLATZINCOBJ) $(FLATZINCRES) \ $(FIXMANIFEST) $(FLATZINCDLL).manifest $(MANIFEST) -manifest $(FLATZINCDLL).manifest \ -outputresource:$(FLATZINCDLL)\;2 +ifeq "@enable_resource@" "yes" +$(TESTCORERC): + $(RCGEN) $(TESTCOREDLL) $(TESTCORESRC) test/test.hh test/test.hpp > $@ +$(TESTINTRC): + $(RCGEN) $(TESTINTDLL) $(TESTINTSEAMSRC) test/test.hh test/test.hpp test/int.hh test/int.hpp > $@ +endif +$(TESTCOREDLL) $(TESTCORELIB): $(TESTCOREOBJ) $(TESTCORERES) \ + $(SUPPORTDLL) $(KERNELDLL) $(SEARCHDLL) $(INTDLL) + $(CXX) $(DLLFLAGS) $(TESTCOREOBJ) $(TESTCORERES) \ + @DLLPATH@ @LINKOUTPUT@$(TESTCOREDLL) $(GLDFLAGS) + $(FIXMANIFEST) $(TESTCOREDLL).manifest + $(MANIFEST) -manifest $(TESTCOREDLL).manifest \ + -outputresource:$(TESTCOREDLL)\;2 +$(TESTINTDLL) $(TESTINTLIB): $(TESTINTSEAMOBJ) $(TESTINTRES) $(TESTCOREDLL) + $(CXX) $(DLLFLAGS) $(TESTINTSEAMOBJ) $(TESTINTRES) \ + @DLLPATH@ @LINKOUTPUT@$(TESTINTDLL) $(GLDFLAGS) + $(FIXMANIFEST) $(TESTINTDLL).manifest + $(MANIFEST) -manifest $(TESTINTDLL).manifest \ + -outputresource:$(TESTINTDLL)\;2 endif # @@ -1825,6 +1993,22 @@ $(GISTSTATICLIB): $(GISTOBJ) $(FLATZINCSTATICLIB): $(FLATZINCOBJ) $(AR) $(ARFLAGS) $@ $(FLATZINCOBJ) $(RANLIB) $@ +$(TESTCORESTATICLIB): $(TESTCOREOBJ) + $(AR) $(ARFLAGS) $@ $(TESTCOREOBJ) + $(RANLIB) $@ +$(TESTINTSTATICLIB): $(TESTINTSEAMOBJ) + $(AR) $(ARFLAGS) $@ $(TESTINTSEAMOBJ) + $(RANLIB) $@ +ifeq "@enable_set_vars@" "yes" +$(TESTSETSTATICLIB): $(TESTSETSEAMOBJ) + $(AR) $(ARFLAGS) $@ $(TESTSETSEAMOBJ) + $(RANLIB) $@ +endif +ifeq "@enable_float_vars@" "yes" +$(TESTFLOATSTATICLIB): $(TESTFLOATSEAMOBJ) + $(AR) $(ARFLAGS) $@ $(TESTFLOATSEAMOBJ) + $(RANLIB) $@ +endif # # Mac OS X Framework @@ -1881,18 +2065,80 @@ endif ifeq "@enable_resource@" "yes" TESTRES = $(TESTEXE).res +TESTPUBLICRUNNERSMOKERES = $(TESTPUBLICRUNNERSMOKEEXE).res +TESTPUBLICINTSMOKERES = $(TESTPUBLICINTSMOKEEXE).res +ifeq "@enable_set_vars@" "yes" +TESTPUBLICSETSMOKERES = $(TESTPUBLICSETSMOKEEXE).res +else +TESTPUBLICSETSMOKERES = +endif +ifeq "@enable_float_vars@" "yes" +TESTPUBLICFLOATSMOKERES = $(TESTPUBLICFLOATSMOKEEXE).res +else +TESTPUBLICFLOATSMOKERES = +endif $(TESTEXE).rc: - $(RCGEN) $(TESTEXE) $(TESTSRC) $(TESTHDR) > $@ + $(RCGEN) $(TESTEXE) $(TESTSRC) $(TESTHDR) $(TESTMAINSRC) > $@ +$(TESTPUBLICRUNNERSMOKEEXE).rc: + $(RCGEN) $(TESTPUBLICRUNNERSMOKEEXE) $(TESTPUBLICRUNNERSMOKESRC) test/test.hh test/test.hpp > $@ +$(TESTPUBLICINTSMOKEEXE).rc: + $(RCGEN) $(TESTPUBLICINTSMOKEEXE) $(TESTPUBLICINTSMOKESRC) test/test.hh test/test.hpp test/int.hh test/int.hpp > $@ +ifeq "@enable_set_vars@" "yes" +$(TESTPUBLICSETSMOKEEXE).rc: + $(RCGEN) $(TESTPUBLICSETSMOKEEXE) $(TESTPUBLICSETSMOKESRC) test/test.hh test/test.hpp test/int.hh test/int.hpp test/set.hh test/set.hpp > $@ +endif +ifeq "@enable_float_vars@" "yes" +$(TESTPUBLICFLOATSMOKEEXE).rc: + $(RCGEN) $(TESTPUBLICFLOATSMOKEEXE) $(TESTPUBLICFLOATSMOKESRC) test/test.hh test/test.hpp test/float.hh test/float.hpp > $@ +endif +$(TESTEXE).rc $(TESTPUBLICRUNNERSMOKEEXE).rc $(TESTPUBLICINTSMOKEEXE).rc \ + $(TESTPUBLICSETSMOKEEXE:%=%.rc) $(TESTPUBLICFLOATSMOKEEXE:%=%.rc): | mkcompiledirs else TESTRES = +TESTPUBLICRUNNERSMOKERES = +TESTPUBLICINTSMOKERES = +TESTPUBLICSETSMOKERES = +TESTPUBLICFLOATSMOKERES = endif -$(TESTEXE): $(TESTOBJ) $(TESTRES) $(ALLLIB) - $(CXX) @EXEOUTPUT@$@ $(TESTOBJ) $(TESTRES) $(DLLPATH) $(CXXFLAGS) \ +$(TESTEXE): $(TESTMAINOBJ) $(TESTOBJ) $(TESTRES) $(ALLLIB) + $(CXX) @EXEOUTPUT@$@ $(TESTMAINOBJ) $(TESTOBJ) $(TESTRES) $(DLLPATH) $(CXXFLAGS) \ $(LINKALL) $(GLDFLAGS) $(LINKQT) $(LINKCPPROFILER) $(FIXMANIFEST) $@.manifest $(DLLSUFFIX) $(MANIFEST) -manifest $@.manifest -outputresource:$@\;1 -$(BLACKBOXEXECOBJ) $(BLACKBOXDLLOBJ): | mkcompiledirs +$(TESTOBJ) $(TESTAUXOBJ) $(BLACKBOXEXECOBJ) $(BLACKBOXDLLOBJ): | mkcompiledirs + +$(TESTPUBLICRUNNERSMOKEEXE): $(TESTPUBLICRUNNERSMOKEOBJ) $(TESTCOREOBJ) $(TESTPUBLICRUNNERSMOKERES) $(ALLLIB) + $(CXX) @EXEOUTPUT@$@ $(TESTPUBLICRUNNERSMOKEOBJ) $(TESTCOREOBJ) $(TESTPUBLICRUNNERSMOKERES) \ + $(DLLPATH) $(CXXFLAGS) \ + $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) $(GLDFLAGS) + $(FIXMANIFEST) $@.manifest $(DLLSUFFIX) + $(MANIFEST) -manifest $@.manifest -outputresource:$@\;1 + +$(TESTPUBLICINTSMOKEEXE): $(TESTPUBLICINTSMOKEOBJ) $(TESTINTSEAMOBJ) $(TESTCOREOBJ) $(TESTPUBLICINTSMOKERES) $(ALLLIB) + $(CXX) @EXEOUTPUT@$@ $(TESTPUBLICINTSMOKEOBJ) $(TESTINTSEAMOBJ) $(TESTCOREOBJ) $(TESTPUBLICINTSMOKERES) \ + $(DLLPATH) $(CXXFLAGS) \ + $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) $(GLDFLAGS) + $(FIXMANIFEST) $@.manifest $(DLLSUFFIX) + $(MANIFEST) -manifest $@.manifest -outputresource:$@\;1 + +ifeq "@enable_set_vars@" "yes" +$(TESTPUBLICSETSMOKEEXE): $(TESTPUBLICSETSMOKEOBJ) $(TESTSETSEAMOBJ) $(TESTINTSEAMOBJ) $(TESTCOREOBJ) $(TESTPUBLICSETSMOKERES) $(ALLLIB) + $(CXX) @EXEOUTPUT@$@ $(TESTPUBLICSETSMOKEOBJ) $(TESTSETSEAMOBJ) $(TESTINTSEAMOBJ) $(TESTCOREOBJ) $(TESTPUBLICSETSMOKERES) \ + $(DLLPATH) $(CXXFLAGS) \ + $(LINKSET) $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) $(GLDFLAGS) + $(FIXMANIFEST) $@.manifest $(DLLSUFFIX) + $(MANIFEST) -manifest $@.manifest -outputresource:$@\;1 +endif + +ifeq "@enable_float_vars@" "yes" +$(TESTPUBLICFLOATSMOKEEXE): $(TESTPUBLICFLOATSMOKEOBJ) $(TESTFLOATSEAMOBJ) $(TESTCOREOBJ) $(TESTPUBLICFLOATSMOKERES) $(ALLLIB) + $(CXX) @EXEOUTPUT@$@ $(TESTPUBLICFLOATSMOKEOBJ) $(TESTFLOATSEAMOBJ) $(TESTCOREOBJ) $(TESTPUBLICFLOATSMOKERES) \ + $(DLLPATH) $(CXXFLAGS) \ + $(LINKFLOAT) $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) $(GLDFLAGS) + $(FIXMANIFEST) $@.manifest $(DLLSUFFIX) + $(MANIFEST) -manifest $@.manifest -outputresource:$@\;1 +endif $(BLACKBOXEXEC): $(BLACKBOXEXECOBJ) $(CXX) @EXEOUTPUT@$@ $(BLACKBOXEXECOBJ) $(CXXFLAGS) @@ -2043,7 +2289,7 @@ endif endif -doinstallheaders: $(INSTALLHDR:%=$(top_srcdir)/%) $(EXTRA_HEADERS) $(VARIMPHDR) $(FLATZINCCONFIG) +doinstallheaders: $(INSTALLHDR:%=$(top_srcdir)/%) $(EXTRA_HEADERS) $(VARIMPHDR) $(FLATZINCCONFIG) $(TESTPUBLICINSTALLHDR:%=$(top_srcdir)/%) mkdir -p $(DESTDIR)$(includedir) && \ (cd $(top_srcdir) && tar cf - $(INSTALLHDR)) | \ (cd $(DESTDIR)$(includedir) && tar xf -) && \ @@ -2053,6 +2299,12 @@ doinstallheaders: $(INSTALLHDR:%=$(top_srcdir)/%) $(EXTRA_HEADERS) $(VARIMPHDR) for_extraheaders="$(EXTRA_HEADERS)" && \ for f in $$for_extraheaders; do \ cp $$f $(DESTDIR)$(includedir)/$$f; done && \ + for_publictestheaders="$(TESTPUBLICINSTALLHDR)" && \ + if test -n "$$for_publictestheaders"; then \ + mkdir -p $(DESTDIR)$(includedir)/test; \ + fi && \ + for f in $$for_publictestheaders; do \ + cp $(top_srcdir)/$$f $(DESTDIR)$(includedir)/test; done && \ for_flatconf="$(FLATZINCCONFIG)" && \ if test -n "$$for_flatconf"; then \ mkdir -p $(DESTDIR)$(datadir)/minizinc/solvers && \ @@ -2160,7 +2412,8 @@ clean: $(RMF) doxygen.log doxygen.hh doxygen.conf.use stat.hh \ changelog.hh doxygen.hh license.hh header.html $(RMF) $(ALLOBJ) $(ALLSBJ) $(ALLOBJ:%$(OBJSUFFIX)=%.pdb) - $(RMF) $(TESTOBJ) $(TESTSBJ) $(TESTOBJ:%$(OBJSUFFIX)=%.pdb) + $(RMF) $(TESTOBJ) $(TESTAUXOBJ) $(TESTSBJ) \ + $(TESTOBJ:%$(OBJSUFFIX)=%.pdb) $(TESTAUXOBJ:%$(OBJSUFFIX)=%.pdb) $(RMF) $(BLACKBOXEXECOBJ) $(BLACKBOXDLLOBJ) \ $(BLACKBOXEXECOBJ:%$(OBJSUFFIX)=%.pdb) \ $(BLACKBOXDLLOBJ:%$(OBJSUFFIX)=%.pdb) @@ -2177,6 +2430,14 @@ clean: $(EXAMPLEEXE:%=%.rc) $(EXAMPLEEXE:%=%.res) $(RMF) $(TESTEXE:%.exe=%.pdb) $(TESTEXE:%=%.manifest) \ $(TESTEXE:%=%.rc) $(TESTEXE:%=%.res) + $(RMF) $(TESTPUBLICRUNNERSMOKEEXE:%.exe=%.pdb) $(TESTPUBLICRUNNERSMOKEEXE:%=%.manifest) \ + $(TESTPUBLICRUNNERSMOKEEXE:%=%.rc) $(TESTPUBLICRUNNERSMOKEEXE:%=%.res) + $(RMF) $(TESTPUBLICINTSMOKEEXE:%.exe=%.pdb) $(TESTPUBLICINTSMOKEEXE:%=%.manifest) \ + $(TESTPUBLICINTSMOKEEXE:%=%.rc) $(TESTPUBLICINTSMOKEEXE:%=%.res) + $(RMF) $(TESTPUBLICSETSMOKEEXE:%.exe=%.pdb) $(TESTPUBLICSETSMOKEEXE:%=%.manifest) \ + $(TESTPUBLICSETSMOKEEXE:%=%.rc) $(TESTPUBLICSETSMOKEEXE:%=%.res) + $(RMF) $(TESTPUBLICFLOATSMOKEEXE:%.exe=%.pdb) $(TESTPUBLICFLOATSMOKEEXE:%=%.manifest) \ + $(TESTPUBLICFLOATSMOKEEXE:%=%.rc) $(TESTPUBLICFLOATSMOKEEXE:%=%.res) $(RMF) $(BLACKBOXEXEC:%.exe=%.pdb) $(BLACKBOXEXEC:%=%.manifest) \ $(BLACKBOXDLL:%$(DLLSUFFIX)=%$(LIBSUFFIX)) \ $(BLACKBOXDLL:%$(DLLSUFFIX)=%$(PDBSUFFIX)) \ @@ -2195,11 +2456,15 @@ veryclean: clean $(LIBTARGETS:%$(DLLSUFFIX)=%$(SOSUFFIX)) $(RMF) $(EXAMPLEEXE) $(RMF) $(TESTEXE) + $(RMF) $(TESTPUBLICRUNNERSMOKEEXE) + $(RMF) $(TESTPUBLICINTSMOKEEXE) + $(RMF) $(TESTPUBLICSETSMOKEEXE) + $(RMF) $(TESTPUBLICFLOATSMOKEEXE) $(RMF) $(BLACKBOXFIXTURES) $(RMF) $(FLATZINCEXE) $(RMF) doc ChangeLog - $(RMF) $(ALLOBJ:%$(OBJSUFFIX)=%.gcno) $(TESTOBJ:%$(OBJSUFFIX)=%.gcno) - $(RMF) $(ALLOBJ:%$(OBJSUFFIX)=%.gcda) $(TESTOBJ:%$(OBJSUFFIX)=%.gcda) + $(RMF) $(ALLOBJ:%$(OBJSUFFIX)=%.gcno) $(TESTOBJ:%$(OBJSUFFIX)=%.gcno) $(TESTAUXOBJ:%$(OBJSUFFIX)=%.gcno) + $(RMF) $(ALLOBJ:%$(OBJSUFFIX)=%.gcda) $(TESTOBJ:%$(OBJSUFFIX)=%.gcda) $(TESTAUXOBJ:%$(OBJSUFFIX)=%.gcda) distclean: veryclean $(RMF) $(VARIMP) diff --git a/changelog.in b/changelog.in index 5356b24303..79375c5e56 100755 --- a/changelog.in +++ b/changelog.in @@ -73,6 +73,15 @@ Date: unreleased [DESCRIPTION] This is the development changelog for the next Gecode release. +[ENTRY] +Module: test +What: new +Rank: major +[DESCRIPTION] +Install reusable core, integer, set, and float test-harness libraries and +headers for CMake and Autoconf builds, allowing downstream propagator projects +to register and run Gecode-style tests for every configured variable family. + [RELEASE] Version: 6.4.0 Date: 2026-07-15 diff --git a/cmake/GecodeConfig.cmake.in b/cmake/GecodeConfig.cmake.in index 4099c3b2bd..98aff4b67b 100644 --- a/cmake/GecodeConfig.cmake.in +++ b/cmake/GecodeConfig.cmake.in @@ -12,8 +12,13 @@ set(_gecode_supported_components minimodel driver flatzinc - gist) + gist + test) set(_gecode_built_components "@GECODE_LIBRARY_COMPONENTS@") +set(_gecode_test_types "@GECODE_PACKAGE_TEST_TYPES@") +if("@GECODE_PACKAGE_HAS_TEST_COMPONENT@" STREQUAL "ON") + list(APPEND _gecode_built_components test) +endif() foreach(_gecode_component IN LISTS _gecode_supported_components) if(_gecode_component IN_LIST _gecode_built_components) @@ -23,27 +28,64 @@ foreach(_gecode_component IN LISTS _gecode_supported_components) endif() set(Gecode_${_gecode_component}_FOUND ${_gecode_component_found}) set(Gecode_gecode${_gecode_component}_FOUND ${_gecode_component_found}) + if(_gecode_component STREQUAL test) + set(Gecode_gecodetestint_FOUND ${_gecode_component_found}) + foreach(_gecode_test_type IN LISTS _gecode_test_types) + set(Gecode_gecodetest${_gecode_test_type}_FOUND ${_gecode_component_found}) + endforeach() + endif() endforeach() set(_gecode_dependency_roots) set(Gecode_LIBRARIES) +set(_gecode_missing_components) +set(_gecode_unsupported_components) if(Gecode_FIND_COMPONENTS) foreach(_gecode_requested_component IN LISTS Gecode_FIND_COMPONENTS) set(_gecode_canonical_component "${_gecode_requested_component}") + set(_gecode_requested_test_type "") if(_gecode_canonical_component MATCHES "^gecode(.+)$") set(_gecode_canonical_component "${CMAKE_MATCH_1}") endif() + if(_gecode_canonical_component MATCHES "^test(int|set|float)$") + set(_gecode_requested_test_type "${CMAKE_MATCH_1}") + set(_gecode_canonical_component test) + endif() if(_gecode_canonical_component IN_LIST _gecode_supported_components) - if(_gecode_canonical_component IN_LIST _gecode_built_components) + if(_gecode_canonical_component IN_LIST _gecode_built_components AND + ("${_gecode_requested_test_type}" STREQUAL "" OR + _gecode_requested_test_type IN_LIST _gecode_test_types)) list(APPEND _gecode_dependency_roots ${_gecode_canonical_component}) - list(APPEND Gecode_LIBRARIES "Gecode::gecode${_gecode_canonical_component}") + if(_gecode_canonical_component STREQUAL test) + list(APPEND Gecode_LIBRARIES Gecode::gecodetest) + foreach(_gecode_test_type IN LISTS _gecode_test_types) + list(APPEND Gecode_LIBRARIES Gecode::gecodetest${_gecode_test_type}) + endforeach() + else() + list(APPEND Gecode_LIBRARIES "Gecode::gecode${_gecode_canonical_component}") + endif() + else() + list(APPEND _gecode_missing_components ${_gecode_requested_component}) + endif() + else() + list(APPEND _gecode_unsupported_components ${_gecode_requested_component}) + if(NOT DEFINED Gecode_${_gecode_requested_component}_FOUND) + set(Gecode_${_gecode_requested_component}_FOUND FALSE) endif() - elseif(NOT DEFINED Gecode_${_gecode_requested_component}_FOUND) - set(Gecode_${_gecode_requested_component}_FOUND FALSE) endif() endforeach() list(REMOVE_DUPLICATES _gecode_dependency_roots) list(REMOVE_DUPLICATES Gecode_LIBRARIES) + if(_gecode_unsupported_components) + list(JOIN _gecode_unsupported_components ", " _gecode_unsupported_components_text) + list(JOIN _gecode_supported_components ", " _gecode_supported_components_text) + set(Gecode_NOT_FOUND_MESSAGE + "Unsupported Gecode components requested: ${_gecode_unsupported_components_text}. Supported components: ${_gecode_supported_components_text}") + elseif(_gecode_missing_components) + list(JOIN _gecode_missing_components ", " _gecode_missing_components_text) + set(Gecode_NOT_FOUND_MESSAGE + "Requested Gecode components are unavailable in this installation: ${_gecode_missing_components_text}") + endif() else() set(_gecode_dependency_roots ${_gecode_built_components}) set(Gecode_LIBRARIES Gecode::gecode) @@ -78,6 +120,13 @@ while(_gecode_dependency_queue) set(_gecode_component_dependencies int search minimodel gist) elseif(_gecode_dependency_component STREQUAL flatzinc) set(_gecode_component_dependencies minimodel driver gist) + elseif(_gecode_dependency_component STREQUAL test) + set(_gecode_component_dependencies search int) + foreach(_gecode_test_type IN LISTS _gecode_test_types) + if(NOT _gecode_test_type STREQUAL int) + list(APPEND _gecode_component_dependencies ${_gecode_test_type}) + endif() + endforeach() endif() foreach(_gecode_component_dependency IN LISTS _gecode_component_dependencies) if(_gecode_component_dependency IN_LIST _gecode_built_components) @@ -187,8 +236,13 @@ unset(_gecode_dependency_component) unset(_gecode_dependency_queue) unset(_gecode_dependency_roots) unset(_gecode_needs_qt) +unset(_gecode_missing_components) +unset(_gecode_missing_components_text) unset(_gecode_package_prefix_dir) unset(_gecode_requested_component) unset(_gecode_supported_components) +unset(_gecode_supported_components_text) +unset(_gecode_unsupported_components) +unset(_gecode_unsupported_components_text) check_required_components(Gecode) diff --git a/cmake/GecodeSources.cmake b/cmake/GecodeSources.cmake index 0c9defc0f1..5d07c2777c 100644 --- a/cmake/GecodeSources.cmake +++ b/cmake/GecodeSources.cmake @@ -244,6 +244,28 @@ set(GECODE_FLATZINC_SOURCES gecode/flatzinc/registry.cpp ) +set(GECODE_TEST_CORE_SOURCES + test/test.cpp +) + +set(GECODE_TEST_INT_SOURCES + test/int.cpp +) + +set(GECODE_TEST_SET_SOURCES + test/set.cpp +) + +set(GECODE_TEST_FLOAT_SOURCES + test/float.cpp +) + +set(GECODE_TEST_MAIN_SOURCE test/test-main.cpp) +set(GECODE_TEST_PUBLIC_RUNNER_SMOKE_SOURCE test/public-runner-smoke.cpp) +set(GECODE_TEST_PUBLIC_INT_SMOKE_SOURCE test/public-int-smoke.cpp) +set(GECODE_TEST_PUBLIC_SET_SMOKE_SOURCE test/public-set-smoke.cpp) +set(GECODE_TEST_PUBLIC_FLOAT_SMOKE_SOURCE test/public-float-smoke.cpp) + set(GECODE_TEST_SOURCES test/afc.cpp test/array.cpp @@ -376,7 +398,6 @@ set(GECODE_TEST_SOURCES test/float/transcendental.cpp test/float/trigonometric.cpp test/groups.cpp - test/int.cpp test/int/arithmetic.cpp test/int/basic.cpp test/int/bin-packing.cpp @@ -427,7 +448,6 @@ set(GECODE_TEST_SOURCES test/set/rel-op.cpp test/set/rel.cpp test/set/sequence.cpp - test/test.cpp ) set(GECODE_TEST_BLACKBOX_EXEC_SOURCE test/flatzinc/blackbox-exec.cpp) diff --git a/docs/cmake-build.md b/docs/cmake-build.md index 9dff49d775..8af6e2dbc0 100644 --- a/docs/cmake-build.md +++ b/docs/cmake-build.md @@ -169,7 +169,7 @@ target_link_libraries(app PRIVATE Gecode::gecode) Canonical component names: - `support`, `kernel`, `search`, `int`, `set`, `float`, `minimodel`, `driver`, - `flatzinc`, `gist` + `flatzinc`, `gist`, `test` Example: @@ -179,6 +179,24 @@ add_executable(app main.cpp) target_link_libraries(app PRIVATE Gecode::gecodedriver) ``` +### Test component for custom propagators + +A Gecode installation configured with `BUILD_TESTING=ON` exports the `test` +component. It lets downstream projects check custom integer, Boolean, set, and +float propagators with Gecode's test runner. + +- Always available targets: `Gecode::gecodetest`, `Gecode::gecodetestint` +- Set target when configured: `Gecode::gecodetestset` +- Float target when configured: `Gecode::gecodetestfloat` +- Installed headers follow the same configuration: `test/test.*`, `test/int.*`, + `test/set.*`, and `test/float.*` +- Public runner entrypoint: `Test::run_registered_tests(argc, argv)` +- Package check: `python test/package/verify-installed-test-component.py ...` + +See [Testing custom propagators](./public-test-harness.md) for a complete test, +consistency and reification controls, failure reproduction, and direct linking +without CMake package metadata. + Legacy component spellings are also accepted in `COMPONENTS`: - `gecodesupport`, `gecodekernel`, `gecodesearch`, `gecodeint`, `gecodeset`, @@ -234,6 +252,7 @@ Deprecation horizon: - `find_package(Gecode COMPONENTS ...)` fails: - Verify requested component is enabled in the installed build. - For optional modules (`flatzinc`, `gist`, `float` with MPFR), ensure dependencies were available. + - The `test` component requires `BUILD_TESTING` to be true in the installed package's configuration. - Qt/Gist issues: - `GECODE_ENABLE_QT=ON` and `GECODE_ENABLE_GIST=ON` are requirement modes; use `AUTO` when dependency discovery should be best-effort. diff --git a/docs/public-test-harness.md b/docs/public-test-harness.md new file mode 100644 index 0000000000..e2f4f3ef57 --- /dev/null +++ b/docs/public-test-harness.md @@ -0,0 +1,254 @@ +# Testing custom propagators + +Gecode's test harness can check a custom propagator against a small, independent +specification. The test supplies two things: a predicate that says which complete +assignments are valid, and a function that posts the propagator. The harness +compares them while exercising propagation, cloning, pruning, and search. + +The installed API covers the core runner and every configured variable family: + +- `Gecode::gecodetest` provides test registration and the runner. +- `Gecode::gecodetestint` provides `Test::Int::Test` and links to the core runner. +- `Gecode::gecodetestset` provides `Test::Set::SetTest` when set variables are enabled. +- `Gecode::gecodetestfloat` provides `Test::Float::Test` when float variables are enabled. +- `test/test.hh` and `test/test.hpp` declare the core API. +- `test/int.hh` and `test/int.hpp` declare the integer helpers. +- `test/set.hh` and `test/set.hpp` declare the set helpers when configured. +- `test/float.hh` and `test/float.hpp` declare the float helpers when configured. + +The Boolean tests use the integer helper, matching Gecode's `BoolVar` interface. +The branch, assign, and FlatZinc helpers are internal to Gecode's test suite. + +## Install the test component + +The test component requires the search and integer modules. Set and float helpers +follow `GECODE_ENABLE_SET_VARS` and `GECODE_ENABLE_FLOAT_VARS`. With CMake, enable +testing when configuring Gecode: + +```bash +cmake -S . -B build -DBUILD_TESTING=ON +cmake --build build +cmake --install build --prefix /path/to/gecode +``` + +The Autoconf build installs the same configured helper families when search and +integer variables are enabled: + +```bash +mkdir -p build/legacy +cd build/legacy +../../configure --disable-qt --disable-gist --disable-doc-search --disable-examples +make -j4 +make install prefix=/path/to/gecode +``` + +See [the CMake build guide](./cmake-build.md) for package lookup and other build +options. + +## Link a test executable with CMake + +An installed-package consumer needs the `test` component and the helper target +for its variable family: + +```cmake +cmake_minimum_required(VERSION 3.21) +project(custom_propagator_tests LANGUAGES CXX) + +find_package(Gecode CONFIG REQUIRED COMPONENTS test) + +add_executable(custom-propagator-test custom-propagator-test.cpp) +target_compile_features(custom-propagator-test PRIVATE cxx_std_17) +target_link_libraries(custom-propagator-test PRIVATE Gecode::gecodetestint) +``` + +Use `Gecode::gecodetestset` for `Test::Set::SetTest` and +`Gecode::gecodetestfloat` for `Test::Float::Test`. Each target carries the core +runner and the corresponding Gecode module as transitive dependencies. + +The target names are identical when Gecode is brought in with +`add_subdirectory` or `FetchContent`. Set `BUILD_TESTING` to a true CMake value +before adding Gecode so that the test targets exist. + +If CMake cannot locate an installed package, set one of these variables: + +- `CMAKE_PREFIX_PATH=/path/to/gecode` +- `Gecode_ROOT=/path/to/gecode` +- `Gecode_DIR=/path/to/gecode/lib/cmake/Gecode` + +## Write the test + +The following test checks an equality propagator over two variables with domain +`0..1`. Two assignments satisfy equality and two violate it, so an empty or +incorrect posting function cannot pass. + +```c++ +#include + +#include + +namespace { + +void post_equal(Gecode::Space& home, Gecode::IntVar x, Gecode::IntVar y) { + // Replace this body with the posting function for the propagator under test. + Gecode::rel(home, x, Gecode::IRT_EQ, y); +} + +class EqualityTest final : public ::Test::Int::Test { +public: + EqualityTest() + : ::Test::Int::Test("Package::Equality", 2, 0, 1) {} + + bool solution(const ::Test::Int::Assignment& assignment) const override { + return assignment[0] == assignment[1]; + } + + void post(Gecode::Space& home, Gecode::IntVarArray& x) override { + post_equal(home, x[0], x[1]); + } +} equality_test; + +} // namespace + +int main(int argc, char* argv[]) { + return Test::run_registered_tests(argc, argv); +} +``` + +`solution()` is the specification. Keep it independent of the propagator code. +For a complicated constraint, a direct calculation over the assigned values is +usually a better oracle than calling another version of the same propagator. + +The constructor arguments give the test its name, arity, and common variable +domain. Override `assignment()` when the variables need different domains or a +custom assignment generator. + +`post()` receives fresh variables in a test space. It should call the same +posting function that users of the propagator call. The harness checks the +posted constraint against the oracle across complete assignments and under +partial-domain modifications. + +The test object registers during construction. Give it static lifetime, as in +the example. Destruction does not unregister it. Registered objects must remain +alive until all calls to `run_registered_tests` have returned, and runner calls +must not overlap. + +## Consistency and reification + +The final constructor arguments select reification and the integer propagation +level: + +```c++ +EqualityTest() + : ::Test::Int::Test("Package::Equality", 2, 0, 1, + false, Gecode::IPL_DOM) {} +``` + +`IPL_DOM` also selects the domain-consistency check. A subclass can set the +protected `contest` member to `CTL_NONE`, `CTL_BOUNDS_D`, or `CTL_BOUNDS_Z` when +the propagator promises a different consistency level. + +Pass `true` as the reification argument for a reified propagator and override +the reified posting function: + +```c++ +void post(Gecode::Space& home, Gecode::IntVarArray& x, + Gecode::Reify r) override { + Gecode::rel(home, x[0], Gecode::IRT_EQ, x[1], r); +} +``` + +The protected `rms` bit mask restricts the tested modes to `RM_EQV`, `RM_IMP`, +or `RM_PMI` when the propagator supports only part of the reification API. +`testsearch` and `testfix` can disable the corresponding checks for constraints +where those checks do not apply. + +## Set and float tests + +A set test derives from `Test::Set::SetTest`. Its constructor specifies the +number of set variables, their common least upper bound, whether reification is +supported, and the number of accompanying integer variables. `solution()` sees +each set as a bit pattern over that bound; `CountableSetRanges` converts the bit +pattern to ranges when the oracle needs set operations. `post()` receives both +the set and integer arrays. + +A float test derives from `Test::Float::Test`. Its constructor specifies the +arity, interval, sampling step, assignment strategy, and reification support. +Its oracle returns `MT_TRUE`, `MT_FALSE`, or `MT_MAYBE`, which accounts for +interval assignments. The static `cmp()` helper implements the same +three-valued interpretation for Gecode float relations. + +Both families use `run_registered_tests()` and the command-line controls shown +below. Their protected `rms`, `testsearch`, `testfix`, and `testsubsumed` members +select checks that match the propagator's contract. + +## Run and reproduce tests + +List the registered tests: + +```bash +./custom-propagator-test -list +``` + +Run the equality test once and stop on its first error: + +```bash +./custom-propagator-test -test Package::Equality -iter 1 -stop true +``` + +A failure reports the random seed and test name. Use both values to reproduce +the run: + +```bash +./custom-propagator-test \ + -test Package::Equality \ + -seed 12345 \ + -iter 1 \ + -threads 1 \ + -log \ + -stop true +``` + +`-log` prints the buffered test log on failure and cannot be combined with a +multi-threaded run. `-help` prints the complete option list. Help and malformed +options terminate the process, so the runner belongs in a test executable +rather than an embedding library. + +## Link without CMake package metadata + +The Autoconf installation provides one static library for each configured +helper family. A direct compiler invocation using integer, set, and float tests +must name their dependency closure: + +```bash +c++ -std=c++17 -I/include custom-propagator-test.cpp \ + -L/lib \ + -lgecodetestfloat -lgecodetestset -lgecodetestint -lgecodetest \ + -lgecodefloat -lgecodeset -lgecodesearch -lgecodeint \ + -lgecodekernel -lgecodesupport +``` + +Omit the set or float test library and its Gecode module when that variable +family is disabled or unused. + +Use the configured `libdir` or `sharedlibdir` instead of `/lib` when the +installation uses a different library directory. + +The installation does not include `gecode-test` or the `public-*-smoke` +executables. Those programs check Gecode's build and are not part of the +downstream interface. + +## Check an installation + +The package checks build and run the sample consumer against an installed +prefix: + +```bash +python test/package/verify-installed-test-component.py \ + --source . \ + --build-root build/package-check \ + --prefix /path/to/gecode +``` + +For a nonstandard layout, pass `--include-dir` and `--lib-dir`. The Autoconf +package check uses `verify-installed-legacy-test-component.py` with the same +`--source`, `--build-root`, and `--prefix` arguments. diff --git a/test/package/public-test-component/CMakeLists.txt b/test/package/public-test-component/CMakeLists.txt new file mode 100644 index 0000000000..2370ad111b --- /dev/null +++ b/test/package/public-test-component/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.21) + +project(gecode_public_test_component_consumer LANGUAGES CXX) + +option(REQUIRE_UNKNOWN_COMPONENT "Request an unsupported Gecode component to verify diagnostics" OFF) + +set(CMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY ON) +set(CMAKE_FIND_USE_PACKAGE_REGISTRY OFF) + +set(_gecode_components test) +if(REQUIRE_UNKNOWN_COMPONENT) + list(APPEND _gecode_components unsupported_component) +endif() + +find_package(Gecode CONFIG REQUIRED COMPONENTS ${_gecode_components}) + +add_executable(consumer-smoke consumer-smoke.cpp) +target_compile_features(consumer-smoke PRIVATE cxx_std_17) +target_link_libraries(consumer-smoke PRIVATE Gecode::gecodetestint) +if(TARGET Gecode::gecodetestset) + target_compile_definitions(consumer-smoke PRIVATE GECODE_PACKAGE_HAS_SET) + target_link_libraries(consumer-smoke PRIVATE Gecode::gecodetestset) +endif() +if(TARGET Gecode::gecodetestfloat) + target_compile_definitions(consumer-smoke PRIVATE GECODE_PACKAGE_HAS_FLOAT) + target_link_libraries(consumer-smoke PRIVATE Gecode::gecodetestfloat) +endif() diff --git a/test/package/public-test-component/consumer-smoke.cpp b/test/package/public-test-component/consumer-smoke.cpp new file mode 100644 index 0000000000..8bcdf80813 --- /dev/null +++ b/test/package/public-test-component/consumer-smoke.cpp @@ -0,0 +1,76 @@ +#include +#ifdef GECODE_PACKAGE_HAS_SET +#include +#endif +#ifdef GECODE_PACKAGE_HAS_FLOAT +#include +#endif + +#include +#ifdef GECODE_PACKAGE_HAS_SET +#include +#endif +#ifdef GECODE_PACKAGE_HAS_FLOAT +#include +#endif + +namespace { + +void post_equal(Gecode::Space& home, Gecode::IntVar x, Gecode::IntVar y) { + Gecode::rel(home, x, Gecode::IRT_EQ, y); +} + +class EqualityTest final : public ::Test::Int::Test { +public: + EqualityTest() + : ::Test::Int::Test("Package::Equality", 2, 0, 1) {} + + bool solution(const ::Test::Int::Assignment& assignment) const override { + return assignment[0] == assignment[1]; + } + + void post(Gecode::Space& home, Gecode::IntVarArray& x) override { + post_equal(home, x[0], x[1]); + } +} equality_test; + +#ifdef GECODE_PACKAGE_HAS_SET +class SingletonSetTest final : public ::Test::Set::SetTest { +public: + SingletonSetTest() + : ::Test::Set::SetTest("Package::Singleton", 1, Gecode::IntSet(0, 1)) {} + + bool solution(const ::Test::Set::SetAssignment& assignment) const override { + return assignment[0] == 1; + } + + void post(Gecode::Space& home, Gecode::SetVarArray& x, + Gecode::IntVarArray&) override { + Gecode::dom(home, x[0], Gecode::SRT_EQ, Gecode::IntSet(0, 0)); + } +} singleton_set_test; +#endif + +#ifdef GECODE_PACKAGE_HAS_FLOAT +class FloatEqualityTest final : public ::Test::Float::Test { +public: + FloatEqualityTest() + : ::Test::Float::Test("Package::Equality", 2, 0.0, 1.0, 1.0, + ::Test::Float::CPLT_ASSIGNMENT, false) {} + + ::Test::Float::MaybeType + solution(const ::Test::Float::Assignment& assignment) const override { + return cmp(assignment[0], Gecode::FRT_EQ, assignment[1]); + } + + void post(Gecode::Space& home, Gecode::FloatVarArray& x) override { + Gecode::rel(home, x[0], Gecode::FRT_EQ, x[1]); + } +} float_equality_test; +#endif + +} // namespace + +int main(int argc, char* argv[]) { + return Test::run_registered_tests(argc, argv); +} diff --git a/test/package/verify-installed-legacy-test-component.py b/test/package/verify-installed-legacy-test-component.py new file mode 100644 index 0000000000..103aff6994 --- /dev/null +++ b/test/package/verify-installed-legacy-test-component.py @@ -0,0 +1,452 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import os +import shlex +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +VERIFIER_PREFIX = "[verify-installed-legacy-test-component]" +EXPECTED_TEST_NAMES = [ + "Float::Package::Equality", + "Int::Package::Equality", + "Set::Package::Singleton", +] +LIBRARIES = [ + "gecodetestfloat", + "gecodetestset", + "gecodetestint", + "gecodetest", + "gecodefloat", + "gecodeset", + "gecodesearch", + "gecodeint", + "gecodekernel", + "gecodesupport", +] + + +def format_command(command: list[str]) -> str: + return shlex.join(command) + + + +def write_stream(stream, text: str) -> None: + if text: + stream.write(text) + if not text.endswith("\n"): + stream.write("\n") + + + +def fail_phase( + phase: str, + message: str, + *, + command: list[str] | None = None, + cwd: Path | None = None, + result: subprocess.CompletedProcess[str] | None = None, + env_summary: dict[str, str] | None = None, +) -> "NoReturn": + sys.stderr.write(f"{VERIFIER_PREFIX} {phase}: FAIL - {message}\n") + if command is not None: + sys.stderr.write(f"{VERIFIER_PREFIX} {phase}: command: {format_command(command)}\n") + if cwd is not None: + sys.stderr.write(f"{VERIFIER_PREFIX} {phase}: cwd: {cwd}\n") + if env_summary is not None: + sys.stderr.write( + f"{VERIFIER_PREFIX} {phase}: runtime-env: {json.dumps(env_summary, sort_keys=True)}\n" + ) + if result is not None: + sys.stderr.write(f"{VERIFIER_PREFIX} {phase}: exit: {result.returncode}\n") + write_stream(sys.stderr, f"{VERIFIER_PREFIX} {phase}: stdout:\n{result.stdout}") + write_stream(sys.stderr, f"{VERIFIER_PREFIX} {phase}: stderr:\n{result.stderr}") + raise SystemExit(1) + + + +def assert_phase(condition: bool, phase: str, message: str) -> None: + if not condition: + fail_phase(phase, message) + + + +def run_phase( + phase: str, + command: list[str], + *, + cwd: Path, + env: dict[str, str] | None = None, + expect_success: bool = True, + env_summary: dict[str, str] | None = None, +) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + command, + cwd=cwd, + env=env, + text=True, + capture_output=True, + ) + if expect_success and result.returncode != 0: + fail_phase( + phase, + "command failed", + command=command, + cwd=cwd, + result=result, + env_summary=env_summary, + ) + if not expect_success and result.returncode == 0: + fail_phase( + phase, + "command unexpectedly succeeded", + command=command, + cwd=cwd, + result=result, + env_summary=env_summary, + ) + sys.stdout.write(f"{VERIFIER_PREFIX} {phase}: ok\n") + return result + + + +def resolve_library_dir(prefix: Path) -> Path: + phase = "inputs" + required = [ + "libgecodetest.a", + "libgecodetestint.a", + "libgecodetestset.a", + "libgecodetestfloat.a", + ] + candidates: dict[str, list[Path]] = {} + for filename in required: + matches = sorted(path.resolve() for path in prefix.rglob(filename) if path.is_file()) + assert_phase(matches, phase, f"missing installed library: {prefix / filename}") + candidates[filename] = matches + + lib_dirs = {path.parent for matches in candidates.values() for path in matches} + assert_phase(len(lib_dirs) == 1, phase, f"expected one installed library directory, found: {sorted(str(path) for path in lib_dirs)}") + return next(iter(lib_dirs)) + + + +def build_runtime_env(prefix_lib: Path) -> tuple[dict[str, str], dict[str, str]]: + env = os.environ.copy() + prefix_lib_str = str(prefix_lib) + env["DYLD_LIBRARY_PATH"] = prefix_lib_str + env["LD_LIBRARY_PATH"] = prefix_lib_str + env_summary = { + "DYLD_LIBRARY_PATH": env["DYLD_LIBRARY_PATH"], + "LD_LIBRARY_PATH": env["LD_LIBRARY_PATH"], + } + return env, env_summary + + + +def create_workspace(build_root: Path, source: Path, *, mode: str) -> tuple[tempfile.TemporaryDirectory[str], Path, Path]: + artifact_dir = build_root / "installed-legacy-verifier" + artifact_dir.mkdir(parents=True, exist_ok=True) + temp_dir = tempfile.TemporaryDirectory(prefix=f"{build_root.name}-{mode}-") + workspace = Path(temp_dir.name).resolve() + assert_phase(not workspace.is_relative_to(source), "workspace", f"workspace must stay outside the source tree: {workspace}") + (artifact_dir / "last-workspace.txt").write_text(f"{workspace}\n") + (artifact_dir / "last-mode.txt").write_text(f"{mode}\n") + sys.stdout.write(f"{VERIFIER_PREFIX} workspace: path={workspace}\n") + return temp_dir, workspace, artifact_dir + + + +def resolve_consumer_binary(binary: Path) -> Path: + if binary.exists(): + return binary + exe_binary = binary.with_suffix(binary.suffix + ".exe") + if exe_binary.exists(): + return exe_binary + fail_phase("build", f"missing built consumer binary: {binary}") + + + +def assert_compiler_command_surface( + command: list[str], + *, + source: Path, + workspace: Path, + prefix: Path, + prefix_include: Path, + expected_library_dirs: list[Path], +) -> None: + phase = "include-leakage" + workspace = workspace.resolve() + prefix = prefix.resolve() + prefix_include = prefix_include.resolve() + source = source.resolve() + expected_library_dirs = [path.resolve() for path in expected_library_dirs] + + include_dirs: list[Path] = [] + library_dirs: list[Path] = [] + workspace_sources: list[Path] = [] + output_paths: list[Path] = [] + i = 0 + while i < len(command): + argument = command[i] + include_path: str | None = None + library_path: str | None = None + output_path: str | None = None + if argument == "-I": + i += 1 + include_path = command[i] + elif argument.startswith("-I") and argument != "-I": + include_path = argument[2:] + elif argument == "-L": + i += 1 + library_path = command[i] + elif argument.startswith("-L") and argument != "-L": + library_path = argument[2:] + elif argument == "-o": + i += 1 + output_path = command[i] + elif argument.endswith((".cpp", ".cc", ".cxx", ".c")): + candidate = Path(argument) + candidate = (workspace / candidate).resolve() if not candidate.is_absolute() else candidate.resolve() + workspace_sources.append(candidate) + i += 1 + + if include_path is not None: + candidate = Path(include_path) + include_dirs.append((workspace / candidate).resolve() if not candidate.is_absolute() else candidate.resolve()) + if library_path is not None: + candidate = Path(library_path) + library_dirs.append((workspace / candidate).resolve() if not candidate.is_absolute() else candidate.resolve()) + if output_path is not None: + candidate = Path(output_path) + output_paths.append((workspace / candidate).resolve() if not candidate.is_absolute() else candidate.resolve()) + + assert_phase(include_dirs == [prefix_include], phase, f"unexpected include directories in compiler command: {[str(path) for path in include_dirs]}") + assert_phase(library_dirs == expected_library_dirs, phase, f"unexpected library directories in compiler command: {[str(path) for path in library_dirs]}") + assert_phase(workspace_sources, phase, "compiler command did not reference any workspace source files") + assert_phase(output_paths, phase, "compiler command did not set an output path") + assert_phase( + all(path.is_relative_to(workspace) for path in [*workspace_sources, *output_paths]), + phase, + f"compiler command referenced non-workspace source/output paths: {[str(path) for path in [*workspace_sources, *output_paths]]}", + ) + + candidate_paths = [*include_dirs, *library_dirs, *workspace_sources, *output_paths] + disallowed_source_paths = [ + path + for path in candidate_paths + if path.is_relative_to(source) and not path.is_relative_to(prefix) and not path.is_relative_to(workspace) + ] + assert_phase( + not disallowed_source_paths, + phase, + f"compiler command referenced disallowed source-tree paths: {[str(path) for path in disallowed_source_paths]}", + ) + sys.stdout.write( + f"{VERIFIER_PREFIX} {phase}: command={format_command(command)} include={prefix_include} lib-dirs={[str(path) for path in expected_library_dirs]}\n" + ) + + + +def run_prefix_surface(source: Path, prefix: Path) -> None: + command = [ + sys.executable, + str((source / "test/verify-legacy-install-surface.py").resolve()), + "--prefix", + str(prefix), + ] + result = run_phase("prefix-surface", command, cwd=source) + write_stream(sys.stdout, result.stdout) + write_stream(sys.stderr, result.stderr) + + + +def write_positive_consumer(source: Path, workspace: Path) -> Path: + source_fixture = (source / "test/package/public-test-component/consumer-smoke.cpp").resolve() + destination = workspace / "consumer-smoke.cpp" + shutil.copy2(source_fixture, destination) + return destination + + + +def compiler_command_prefix() -> list[str]: + cxx = os.environ.get("CXX", "c++") + return shlex.split(cxx) + + + +def build_positive_consumer( + source: Path, + workspace: Path, + prefix: Path, + prefix_lib: Path, + artifact_dir: Path, +) -> Path: + consumer_source = write_positive_consumer(source, workspace) + consumer_binary = workspace / "consumer-smoke" + command = [ + *compiler_command_prefix(), + "-std=c++17", + f"-I{prefix / 'include'}", + str(consumer_source), + f"-L{prefix_lib}", + *[f"-l{name}" for name in LIBRARIES], + "-o", + str(consumer_binary), + ] + assert_compiler_command_surface( + command, + source=source, + workspace=workspace, + prefix=prefix, + prefix_include=prefix / "include", + expected_library_dirs=[prefix_lib], + ) + (artifact_dir / "last-compile-command.txt").write_text(f"{format_command(command)}\n") + result = run_phase("build", command, cwd=workspace) + write_stream(sys.stdout, result.stdout) + write_stream(sys.stderr, result.stderr) + binary = resolve_consumer_binary(consumer_binary) + sys.stdout.write(f"{VERIFIER_PREFIX} build: binary={binary}\n") + return binary + + + +def run_list_phase(consumer_binary: Path, workspace: Path, runtime_env: dict[str, str], env_summary: dict[str, str]) -> None: + result = run_phase( + "list", + [str(consumer_binary), "-list"], + cwd=workspace, + env=runtime_env, + env_summary=env_summary, + ) + for test_name in EXPECTED_TEST_NAMES: + assert_phase(test_name in result.stdout, "list", f"-list output missing {test_name!r}") + sys.stdout.write(f"{VERIFIER_PREFIX} list: discovered={EXPECTED_TEST_NAMES}\n") + + + +def run_filtered_phase( + consumer_binary: Path, + workspace: Path, + runtime_env: dict[str, str], + env_summary: dict[str, str], +) -> None: + result = run_phase( + "filtered-run", + [str(consumer_binary), "-test", "Package", "-iter", "1", "-stop", "true"], + cwd=workspace, + env=runtime_env, + env_summary=env_summary, + ) + for test_name in EXPECTED_TEST_NAMES: + assert_phase(test_name in result.stdout, "filtered-run", f"filtered run did not print {test_name!r}") + assert_phase("+" in result.stdout, "filtered-run", "filtered run did not report success") + sys.stdout.write(f"{VERIFIER_PREFIX} filtered-run: executed={EXPECTED_TEST_NAMES}\n") + + + +def write_unsupported_consumer(workspace: Path) -> Path: + source = workspace / "unsupported-header.cpp" + source.write_text( + "#include \n\n" + "int main() {\n" + " return 0;\n" + "}\n" + ) + return source + + + +def assert_missing_header_diagnostic(result: subprocess.CompletedProcess[str]) -> None: + phase = "unsupported-header" + combined_output = f"{result.stdout}\n{result.stderr}" + assert_phase("test/branch.hh" in combined_output, phase, "missing expected unsupported-header diagnostic target") + assert_phase( + "file not found" in combined_output or "No such file or directory" in combined_output, + phase, + "missing missing-header diagnostic wording", + ) + sys.stdout.write(f"{VERIFIER_PREFIX} {phase}: diagnostic=test/branch.hh missing\n") + + + +def run_unsupported_header_mode( + source: Path, + workspace: Path, + prefix: Path, + artifact_dir: Path, +) -> None: + unsupported_source = write_unsupported_consumer(workspace) + unsupported_object = workspace / "unsupported-header.o" + command = [ + *compiler_command_prefix(), + "-std=c++17", + f"-I{prefix / 'include'}", + str(unsupported_source), + "-c", + "-o", + str(unsupported_object), + ] + assert_compiler_command_surface( + command, + source=source, + workspace=workspace, + prefix=prefix, + prefix_include=prefix / "include", + expected_library_dirs=[], + ) + (artifact_dir / "last-runtime-env.json").write_text("{}\n") + (artifact_dir / "last-compile-command.txt").write_text(f"{format_command(command)}\n") + result = run_phase("unsupported-header", command, cwd=workspace, expect_success=False) + assert_missing_header_diagnostic(result) + + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--source", required=True) + parser.add_argument("--build-root", required=True) + parser.add_argument("--prefix", required=True) + parser.add_argument("--mode", choices=["positive", "unsupported-header"], default="positive") + return parser.parse_args() + + + +def main() -> int: + args = parse_args() + source = Path(args.source).resolve() + build_root = Path(args.build_root).resolve() + prefix = Path(args.prefix).resolve() + + assert_phase(source.is_dir(), "inputs", f"missing source tree: {source}") + assert_phase(prefix.is_dir(), "inputs", f"missing installed prefix: {prefix}") + build_root.mkdir(parents=True, exist_ok=True) + + prefix_lib = resolve_library_dir(prefix) + run_prefix_surface(source, prefix) + + temp_dir, workspace, artifact_dir = create_workspace(build_root, source, mode=args.mode) + with temp_dir: + if args.mode == "unsupported-header": + run_unsupported_header_mode(source, workspace, prefix, artifact_dir) + return 0 + + consumer_binary = build_positive_consumer(source, workspace, prefix, prefix_lib, artifact_dir) + runtime_env, env_summary = build_runtime_env(prefix_lib) + (artifact_dir / "last-runtime-env.json").write_text(json.dumps(env_summary, indent=2, sort_keys=True) + "\n") + sys.stdout.write( + f"{VERIFIER_PREFIX} runtime-env: {json.dumps(env_summary, sort_keys=True)}\n" + ) + run_list_phase(consumer_binary, workspace, runtime_env, env_summary) + run_filtered_phase(consumer_binary, workspace, runtime_env, env_summary) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/package/verify-installed-test-component.py b/test/package/verify-installed-test-component.py new file mode 100644 index 0000000000..c82a204244 --- /dev/null +++ b/test/package/verify-installed-test-component.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import shlex +import shutil +import subprocess +import sys +from pathlib import Path + + +CORE_HEADERS = [ + "test/int.hh", + "test/int.hpp", + "test/test.hh", + "test/test.hpp", +] +EXPECTED_METADATA = [ + "cmake/Gecode/GecodeConfig.cmake", + "cmake/Gecode/GecodeTargets.cmake", +] +CORE_TEST_NAMES = ["Int::Package::Equality"] +VERIFIER_PREFIX = "[verify-installed-test-component]" + + +def format_command(command: list[str]) -> str: + return shlex.join(command) + + +def write_stream(stream, text: str) -> None: + if text: + stream.write(text) + if not text.endswith("\n"): + stream.write("\n") + + +def fail_phase( + phase: str, + message: str, + *, + command: list[str] | None = None, + cwd: Path | None = None, + result: subprocess.CompletedProcess[str] | None = None, +) -> "NoReturn": + sys.stderr.write(f"{VERIFIER_PREFIX} {phase}: FAIL - {message}\n") + if command is not None: + sys.stderr.write(f"{VERIFIER_PREFIX} {phase}: command: {format_command(command)}\n") + if cwd is not None: + sys.stderr.write(f"{VERIFIER_PREFIX} {phase}: cwd: {cwd}\n") + if result is not None: + sys.stderr.write(f"{VERIFIER_PREFIX} {phase}: exit: {result.returncode}\n") + write_stream(sys.stderr, f"{VERIFIER_PREFIX} {phase}: stdout:\n{result.stdout}") + write_stream(sys.stderr, f"{VERIFIER_PREFIX} {phase}: stderr:\n{result.stderr}") + raise SystemExit(1) + + +def run_phase( + phase: str, + command: list[str], + *, + cwd: Path | None = None, + expect_success: bool = True, +) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + command, + cwd=cwd, + text=True, + capture_output=True, + ) + if expect_success and result.returncode != 0: + fail_phase(phase, "command failed", command=command, cwd=cwd, result=result) + if not expect_success and result.returncode == 0: + fail_phase(phase, "command unexpectedly succeeded", command=command, cwd=cwd, result=result) + sys.stdout.write(f"{VERIFIER_PREFIX} {phase}: ok\n") + return result + + +def assert_phase(condition: bool, phase: str, message: str) -> None: + if not condition: + fail_phase(phase, message) + + +def assert_prefix_surface(include_dir: Path, lib_dir: Path) -> list[str]: + phase = "prefix-surface" + targets = (lib_dir / "cmake/Gecode/GecodeTargets.cmake").read_text() + has_set = "Gecode::gecodetestset" in targets + has_float = "Gecode::gecodetestfloat" in targets + expected_headers = list(CORE_HEADERS) + expected_test_names = list(CORE_TEST_NAMES) + if has_set: + expected_headers.extend(["test/set.hh", "test/set.hpp"]) + expected_test_names.append("Set::Package::Singleton") + if has_float: + expected_headers.extend(["test/float.hh", "test/float.hpp"]) + expected_test_names.append("Float::Package::Equality") + expected_headers.sort() + expected_test_names.sort() + + for rel in expected_headers: + path = include_dir / rel + assert_phase(path.exists(), phase, f"missing installed path: {path}") + for rel in EXPECTED_METADATA: + path = lib_dir / rel + assert_phase(path.exists(), phase, f"missing installed path: {path}") + + installed_test_headers = sorted( + path.relative_to(include_dir).as_posix() + for path in (include_dir / "test").glob("*") + if path.is_file() + ) + assert_phase(installed_test_headers == expected_headers, phase, f"unexpected installed test headers: {installed_test_headers}") + + assert_phase("Gecode::gecodetest" in targets, phase, "missing Gecode::gecodetest export") + assert_phase("Gecode::gecodetestint" in targets, phase, "missing Gecode::gecodetestint export") + + sys.stdout.write(f"{VERIFIER_PREFIX} {phase}: ok\n") + return expected_test_names + + +def iter_include_dirs(entry: dict[str, object]) -> list[Path]: + directory = Path(str(entry["directory"])).resolve() + arguments = entry.get("arguments") + if arguments is None: + arguments = shlex.split(str(entry["command"])) + + include_dirs: list[Path] = [] + i = 0 + while i < len(arguments): + argument = str(arguments[i]) + include_path: str | None = None + if argument == "-I" or argument == "-isystem": + i += 1 + include_path = str(arguments[i]) + elif argument.startswith("-I") and argument != "-I": + include_path = argument[2:] + elif argument.startswith("-isystem") and argument != "-isystem": + include_path = argument[len("-isystem") :] + + if include_path: + path = Path(include_path) + include_dirs.append((directory / path).resolve() if not path.is_absolute() else path.resolve()) + i += 1 + + return include_dirs + + +def assert_no_source_tree_include_leakage( + source: Path, + consumer_build: Path, + prefix: Path, + include_dir: Path, +) -> None: + phase = "include-leakage" + compile_commands_path = consumer_build / "compile_commands.json" + assert_phase(compile_commands_path.exists(), phase, f"missing compile commands: {compile_commands_path}") + + compile_commands = json.loads(compile_commands_path.read_text()) + assert_phase(bool(compile_commands), phase, "expected compile commands for installed-package consumer") + + source_root = source.resolve() + consumer_build = consumer_build.resolve() + prefix = prefix.resolve() + installed_include = include_dir.resolve() + + leaked_include_dirs: set[str] = set() + saw_prefix_include = False + for entry in compile_commands: + for include_dir in iter_include_dirs(entry): + if include_dir == installed_include or include_dir.is_relative_to(installed_include): + saw_prefix_include = True + if ( + include_dir.is_relative_to(source_root) + and not include_dir.is_relative_to(consumer_build) + and not include_dir.is_relative_to(prefix) + ): + leaked_include_dirs.add(include_dir.as_posix()) + + assert_phase(saw_prefix_include, phase, "consumer compile flags never referenced the installed prefix include directory") + assert_phase(not leaked_include_dirs, phase, f"source-tree include leakage detected: {sorted(leaked_include_dirs)}") + + sys.stdout.write(f"{VERIFIER_PREFIX} {phase}: ok\n") + + +def configure_consumer( + source: Path, + build_root: Path, + prefix: Path, + cmake_dir: Path, + *, + require_unknown_component: bool, +) -> tuple[Path, Path, subprocess.CompletedProcess[str]]: + consumer_source = source / "test/package/public-test-component" + consumer_build = build_root / "consumer" + shutil.rmtree(build_root, ignore_errors=True) + consumer_build.mkdir(parents=True, exist_ok=True) + + configure_command = [ + "cmake", + "-S", + str(consumer_source), + "-B", + str(consumer_build), + "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", + f"-DGecode_DIR={cmake_dir}", + f"-DCMAKE_PREFIX_PATH={prefix}", + ] + if require_unknown_component: + configure_command.append("-DREQUIRE_UNKNOWN_COMPONENT=ON") + + result = run_phase( + "configure-missing-component" if require_unknown_component else "configure", + configure_command, + expect_success=not require_unknown_component, + ) + return consumer_source, consumer_build, result + + +def assert_missing_component_failure(result: subprocess.CompletedProcess[str]) -> None: + phase = "configure-missing-component" + combined_output = f"{result.stdout}\n{result.stderr}" + assert_phase( + "Unsupported Gecode components requested: unsupported_component" in combined_output, + phase, + "missing unsupported-component diagnostic in configure failure output", + ) + sys.stdout.write(f"{VERIFIER_PREFIX} {phase}: diagnostic ok\n") + + +def build_consumer(consumer_build: Path) -> Path: + run_phase("build", ["cmake", "--build", str(consumer_build), "--target", "consumer-smoke"]) + consumer_binary = consumer_build / "consumer-smoke" + if not consumer_binary.exists(): + consumer_binary = consumer_build / "consumer-smoke.exe" + assert_phase(consumer_binary.exists(), "build", f"missing built consumer binary: {consumer_binary}") + sys.stdout.write(f"{VERIFIER_PREFIX} build: binary={consumer_binary}\n") + return consumer_binary + + +def run_list_phase(consumer_binary: Path, expected_test_names: list[str]) -> None: + result = run_phase("list", [str(consumer_binary), "-list"]) + for test_name in expected_test_names: + assert_phase(test_name in result.stdout, "list", f"-list output missing {test_name!r}") + sys.stdout.write(f"{VERIFIER_PREFIX} list: discovered={expected_test_names}\n") + + +def run_filtered_phase(consumer_binary: Path, expected_test_names: list[str]) -> None: + result = run_phase( + "filtered-run", + [str(consumer_binary), "-test", "Package", "-iter", "1", "-stop", "true"], + ) + for test_name in expected_test_names: + assert_phase(test_name in result.stdout, "filtered-run", f"filtered run did not print {test_name!r}") + assert_phase("+" in result.stdout, "filtered-run", "filtered run did not report success") + sys.stdout.write(f"{VERIFIER_PREFIX} filtered-run: executed={expected_test_names}\n") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--source", required=True) + parser.add_argument("--build-root", required=True) + parser.add_argument("--prefix", required=True) + parser.add_argument("--include-dir") + parser.add_argument("--lib-dir") + parser.add_argument("--expect-missing-component-failure", action="store_true") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + source = Path(args.source).resolve() + build_root = Path(args.build_root).resolve() + prefix = Path(args.prefix).resolve() + include_dir = Path(args.include_dir).resolve() if args.include_dir else prefix / "include" + lib_dir = Path(args.lib_dir).resolve() if args.lib_dir else prefix / "lib" + + expected_test_names = assert_prefix_surface(include_dir, lib_dir) + + _, consumer_build, configure_result = configure_consumer( + source, + build_root, + prefix, + lib_dir / "cmake/Gecode", + require_unknown_component=args.expect_missing_component_failure, + ) + + if args.expect_missing_component_failure: + assert_missing_component_failure(configure_result) + return 0 + + assert_no_source_tree_include_leakage(source, consumer_build, prefix, include_dir) + consumer_binary = build_consumer(consumer_build) + run_list_phase(consumer_binary, expected_test_names) + run_filtered_phase(consumer_binary, expected_test_names) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/public-float-smoke.cpp b/test/public-float-smoke.cpp new file mode 100644 index 0000000000..f22777b307 --- /dev/null +++ b/test/public-float-smoke.cpp @@ -0,0 +1,76 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Christian Schulte + * + * Contributing authors: + * Mikael Lagerkvist + * + * Copyright: + * Christian Schulte, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.org + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + + +#include "test/float.hh" + +#include +#include + +namespace { + + int float_runs = 0; + + class FloatSmokeTest : public Test::Float::Test { + public: + FloatSmokeTest(void) + : ::Test::Float::Test("Public::Smoke",1,0.0,1.0,1.0, + ::Test::Float::CPLT_ASSIGNMENT,false) {} + + ::Test::Float::MaybeType + solution(const ::Test::Float::Assignment&) const override { + return ::Test::Float::MT_TRUE; + } + + void post(Gecode::Space&, Gecode::FloatVarArray&) override { + float_runs++; + } + } float_smoke_test; + +} + +int +main(int argc, char* argv[]) { + const int result = Test::run_registered_tests(argc,argv); + if ((result != EXIT_SUCCESS) || (float_runs == 0)) { + std::cerr << "public-float-smoke: helper-backed test did not run" + << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +// STATISTICS: test-float diff --git a/test/public-int-smoke.cpp b/test/public-int-smoke.cpp new file mode 100644 index 0000000000..8539a6acef --- /dev/null +++ b/test/public-int-smoke.cpp @@ -0,0 +1,131 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Christian Schulte + * + * Contributing authors: + * Mikael Lagerkvist + * + * Copyright: + * Christian Schulte, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.org + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "test/int.hh" + +#include +#include +#include +#include +#include +#include + +namespace { + + int abs_runs = 0; + + class AbsSmokeTest : public Test::Int::Test { + public: + AbsSmokeTest(void) + : ::Test::Int::Test("Public::Smoke::Abs",2,-2,2) {} + + bool solution(const ::Test::Int::Assignment& x) const override { + const int abs_x0 = (x[0] < 0) ? -x[0] : x[0]; + return abs_x0 == x[1]; + } + + void post(Gecode::Space& home, Gecode::IntVarArray& x) override { + abs_runs++; + Gecode::abs(home, x[0], x[1]); + } + } abs_smoke_test; + + bool require(bool condition, const std::string& message) { + if (!condition) { + std::cerr << "public-int-smoke: " << message << std::endl; + return false; + } + return true; + } + + int run_and_capture(std::initializer_list args, std::string& output) { + std::vector argv; + argv.reserve(args.size()); + for (const char* arg : args) + argv.push_back(const_cast(arg)); + + std::ostringstream captured; + std::streambuf* old = std::cout.rdbuf(captured.rdbuf()); + const int rc = Test::run_registered_tests(static_cast(argv.size()), argv.data()); + std::cout.rdbuf(old); + output = captured.str(); + return rc; + } + +} + +int +main(void) { + const std::string test_name = "Int::Public::Smoke::Abs"; + + std::string list_output; + if (!require(run_and_capture({"public-int-smoke", "-list"}, list_output) == EXIT_SUCCESS, + "-list should succeed")) { + return EXIT_FAILURE; + } + if (!require(list_output.find(test_name) != std::string::npos, + "-list output missing integer smoke test")) { + return EXIT_FAILURE; + } + if (!require(abs_runs == 0, + "-list should not execute integer smoke tests")) { + return EXIT_FAILURE; + } + + std::string run_output; + if (!require(run_and_capture({"public-int-smoke", "-test", "Int::Public::Smoke::Abs", + "-iter", "1", "-stop", "true"}, + run_output) == EXIT_SUCCESS, + "filtered integer smoke run should succeed")) { + return EXIT_FAILURE; + } + if (!require(run_output.find(test_name) != std::string::npos, + "filtered integer smoke run did not print the selected test")) { + return EXIT_FAILURE; + } + if (!require(run_output.find('+') != std::string::npos, + "filtered integer smoke run did not report success")) { + return EXIT_FAILURE; + } + if (!require(abs_runs > 0, + "filtered integer smoke run did not execute the helper-backed test")) { + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + +// STATISTICS: test-int diff --git a/test/public-runner-smoke.cpp b/test/public-runner-smoke.cpp new file mode 100644 index 0000000000..e359bc3ce2 --- /dev/null +++ b/test/public-runner-smoke.cpp @@ -0,0 +1,172 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Christian Schulte + * + * Contributing authors: + * Mikael Lagerkvist + * + * Copyright: + * Christian Schulte, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.org + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "test/test.hh" + +#include +#include +#include +#include +#include +#include + +namespace { + + int passing_runs = 0; + int failing_runs = 0; + + class PassingSmokeTest : public Test::Base { + public: + PassingSmokeTest(void) + : Test::Base("Smoke::A-Pass") {} + + bool run(void) override { + passing_runs++; + return true; + } + } passing_smoke_test; + + class FailingSmokeTest : public Test::Base { + public: + FailingSmokeTest(void) + : Test::Base("Smoke::B-Fail") {} + + bool run(void) override { + failing_runs++; + return false; + } + } failing_smoke_test; + + bool require(bool condition, const std::string& message) { + if (!condition) { + std::cerr << "public-runner-smoke: " << message << std::endl; + return false; + } + return true; + } + + int run_and_capture(std::initializer_list args, std::string& output) { + std::vector argv; + argv.reserve(args.size()); + for (const char* arg : args) { + argv.push_back(const_cast(arg)); + } + + std::ostringstream captured; + std::streambuf* old = std::cout.rdbuf(captured.rdbuf()); + const int rc = Test::run_registered_tests(static_cast(argv.size()), argv.data()); + std::cout.rdbuf(old); + output = captured.str(); + return rc; + } + +} + +int +main(void) { + std::string list_output; + if (!require(run_and_capture({"public-runner-smoke", "-list"}, list_output) == EXIT_SUCCESS, + "-list should succeed")) { + return EXIT_FAILURE; + } + const std::string pass_name = "Smoke::A-Pass"; + const std::string fail_name = "Smoke::B-Fail"; + const std::size_t pass_pos = list_output.find(pass_name); + const std::size_t fail_pos = list_output.find(fail_name); + if (!require(pass_pos != std::string::npos, "-list output missing pass test")) { + return EXIT_FAILURE; + } + if (!require(fail_pos != std::string::npos, "-list output missing fail test")) { + return EXIT_FAILURE; + } + if (!require(pass_pos < fail_pos, "-list output is not alphabetically ordered")) { + return EXIT_FAILURE; + } + if (!require(passing_runs == 0 && failing_runs == 0, + "-list should not execute any registered tests")) { + return EXIT_FAILURE; + } + + std::string pass_output; + if (!require(run_and_capture({"public-runner-smoke", "-test", "Smoke::A-Pass", "-iter", "1", "-stop", "true"}, + pass_output) == EXIT_SUCCESS, + "filtered passing run should succeed")) { + return EXIT_FAILURE; + } + if (!require(pass_output.find(pass_name) != std::string::npos, + "filtered passing run did not print the selected test")) { + return EXIT_FAILURE; + } + if (!require(pass_output.find(fail_name) == std::string::npos, + "filtered passing run executed an unselected test")) { + return EXIT_FAILURE; + } + if (!require(pass_output.find('+') != std::string::npos, + "filtered passing run did not report success")) { + return EXIT_FAILURE; + } + if (!require(passing_runs == 1 && failing_runs == 0, + "filtered passing run counts are wrong")) { + return EXIT_FAILURE; + } + + std::string fail_output; + if (!require(run_and_capture({"public-runner-smoke", "-test", "Smoke::B-Fail", "-iter", "1", "-stop", "true"}, + fail_output) == EXIT_FAILURE, + "filtered failing run should fail")) { + return EXIT_FAILURE; + } + if (!require(fail_output.find(fail_name) != std::string::npos, + "filtered failing run did not print the selected test")) { + return EXIT_FAILURE; + } + if (!require(fail_output.find("Options: -seed ") != std::string::npos, + "filtered failing run did not preserve seed diagnostics")) { + return EXIT_FAILURE; + } + if (!require(fail_output.find("-test Smoke::B-Fail") != std::string::npos, + "filtered failing run did not preserve test diagnostics")) { + return EXIT_FAILURE; + } + if (!require(passing_runs == 1 && failing_runs == 1, + "filtered failing run counts are wrong")) { + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + +// STATISTICS: test-core diff --git a/test/public-set-smoke.cpp b/test/public-set-smoke.cpp new file mode 100644 index 0000000000..5eb87b7077 --- /dev/null +++ b/test/public-set-smoke.cpp @@ -0,0 +1,76 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Christian Schulte + * + * Contributing authors: + * Mikael Lagerkvist + * + * Copyright: + * Christian Schulte, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.org + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + + +#include "test/set.hh" + +#include +#include +#include + +namespace { + + int set_runs = 0; + + class SetSmokeTest : public Test::Set::SetTest { + public: + SetSmokeTest(void) + : Test::Set::SetTest("Public::Smoke",1,Gecode::IntSet(0,1)) {} + + bool solution(const Test::Set::SetAssignment&) const override { + return true; + } + + void post(Gecode::Space&, Gecode::SetVarArray&, + Gecode::IntVarArray&) override { + set_runs++; + } + } set_smoke_test; + +} + +int +main(int argc, char* argv[]) { + const int result = Test::run_registered_tests(argc,argv); + if ((result != EXIT_SUCCESS) || (set_runs == 0)) { + std::cerr << "public-set-smoke: helper-backed test did not run" + << std::endl; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +// STATISTICS: test-set diff --git a/test/test-main.cpp b/test/test-main.cpp new file mode 100644 index 0000000000..e1451d93fa --- /dev/null +++ b/test/test-main.cpp @@ -0,0 +1,51 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Christian Schulte + * + * Contributing authors: + * Mikael Lagerkvist + * + * Copyright: + * Christian Schulte, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.org + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "test/test.hh" + +#ifdef GECODE_HAS_MTRACE +#include +#endif + +int +main(int argc, char* argv[]) { +#ifdef GECODE_HAS_MTRACE + mtrace(); +#endif + return Test::run_registered_tests(argc, argv); +} + +// STATISTICS: test-core diff --git a/test/test.cpp b/test/test.cpp index 5d808d8dd2..4912fa558a 100644 --- a/test/test.cpp +++ b/test/test.cpp @@ -46,7 +46,6 @@ #include #include #include -#include namespace Test { @@ -94,7 +93,7 @@ namespace Test { Options opt; - void report_error(const std::string& name, unsigned int seed, Options& options, std::ostream& ostream) { + void report_error(const std::string& name, unsigned int seed, const Options& options, std::ostream& ostream) { ostream << "Options: -seed " << seed; if (options.fixprob != Test::Options::deffixprob) ostream << " -fixprob " << options.fixprob; @@ -207,7 +206,7 @@ namespace Test { exit(EXIT_FAILURE); } - bool Options::is_test_name_matching(const std::string& test_name) { + bool Options::is_test_name_matching(const std::string& test_name) const { if (!testpat.empty()) { bool positive_patterns = false; bool match_found = false; @@ -260,7 +259,7 @@ namespace Test { ostream.flush(); } else { ostream << "-" << std::endl; - report_error(test->name(), seed, opt, ostream); + report_error(test->name(), seed, options, ostream); return false; } } @@ -268,9 +267,9 @@ namespace Test { return true; } catch (Gecode::Exception& e) { ostream << "Exception in \"Gecode::" << e.what() - << "." << std::endl - << "Stopping..." << std::endl; - report_error(test->name(), options.seed, opt, ostream); + << "." << std::endl + << "Stopping..." << std::endl; + report_error(test->name(), options.seed, options, ostream); return false; } } @@ -282,7 +281,7 @@ namespace Test { for (auto test : tests) { unsigned int test_seed = seed_sequence.next(); if (!run_test(test, test_seed, options, std::cout)) { - if (opt.stop) { + if (options.stop) { return EXIT_FAILURE; } else { result = EXIT_FAILURE; @@ -439,59 +438,56 @@ namespace Test { } }; - /// Run all the tests with the supplied options i parallel. + /// Run all the tests with the supplied options in parallel. int run_tests_parallel(const std::vector& tests, const Options& options) { using namespace Gecode::Support; RandomGenerator seed_sequence(options.seed); - TestExecutionControl tec(tests, options, opt.threads); + TestExecutionControl tec(tests, options, options.threads); - for (unsigned int i = 0; i < opt.threads; ++i) { + for (unsigned int i = 0; i < options.threads; ++i) { Thread::run(new TestExecutor(tec, seed_sequence.next())); } tec.await_test_runners_completed(); return tec.get_result(); } -} - -int -main(int argc, char* argv[]) { - using namespace Test; -#ifdef GECODE_HAS_MTRACE - mtrace(); -#endif - - opt.parse(argc, argv); - - Base::sort(); - - if (opt.list) { - for (Base* t = Base::tests() ; t != nullptr; t = t->next() ) { - std::cout << t->name() << std::endl; + std::vector select_tests(const Options& options) { + std::vector tests; + bool started = options.start_from == nullptr; + for (Base* t = Base::tests(); t != nullptr; t = t->next()) { + if (!started) { + if (t->name().find(options.start_from) != std::string::npos) { + started = true; + } else { + continue; + } + } + if (options.is_test_name_matching(t->name())) { + tests.emplace_back(t); + } } - exit(EXIT_SUCCESS); + return tests; } - std::vector tests; - bool started = opt.start_from == nullptr ? true : false; - for (Base* t = Base::tests() ; t != nullptr; t = t->next() ) { - if (!started) { - if (t->name().find(opt.start_from) != std::string::npos) { - started = true; - } else { - continue; + int run_registered_tests(int argc, char* argv[]) { + opt = Options(); + opt.parse(argc, argv); + + Base::sort(); + + if (opt.list) { + for (Base* t = Base::tests(); t != nullptr; t = t->next()) { + std::cout << t->name() << std::endl; } + return EXIT_SUCCESS; } - if (opt.is_test_name_matching(t->name())) { - tests.emplace_back(t); - } - } - if (opt.threads > 1) { - return run_tests_parallel(tests, opt); - } else { + const std::vector tests = select_tests(opt); + if (opt.threads > 1) { + return run_tests_parallel(tests, opt); + } return run_tests(tests, opt); } } diff --git a/test/test.hh b/test/test.hh index 9092165615..53e605f89c 100755 --- a/test/test.hh +++ b/test/test.hh @@ -45,6 +45,7 @@ #include #include #include +#include /// General test support namespace Test { @@ -111,7 +112,7 @@ namespace Test { void parse(int argc, char* argv[]); /// True iff a test name should be executed according to the patterns. With no patterns, always true. - bool is_test_name_matching(const std::string& test_name); + bool is_test_name_matching(const std::string& test_name) const; }; /// The options @@ -163,16 +164,21 @@ namespace Test { /// Random number generator mutable Gecode::Support::RandomGenerator _rand; }; + + /** + * \brief Run the tests in the process-wide registry + * + * Tests register during construction. They must remain alive until every + * runner call has returned; destruction does not remove a test from the + * process-wide registry. Runner calls must not overlap. Option parsing exits + * the process after printing help or reporting a malformed option. + * \relates Test::Base + */ + int run_registered_tests(int argc, char* argv[]); //@} } -/** - * \brief Main function - * \relates Test::Base - */ -int main(int argc, char* argv[]); - /** * \brief Print indentation * \relates Test::ind diff --git a/test/verify-legacy-install-surface.py b/test/verify-legacy-install-surface.py new file mode 100644 index 0000000000..dcdb411d4c --- /dev/null +++ b/test/verify-legacy-install-surface.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +VERIFIER_PREFIX = "[verify-legacy-install-surface]" +CORE_HEADERS = ["int.hh", "int.hpp", "test.hh", "test.hpp"] +UNSUPPORTED_HEADERS = [ + "assign.hh", + "branch.hh", + "flatzinc.hh", +] +UNSUPPORTED_BINARIES = [ + "gecode-test", + "public-int-smoke", + "public-runner-smoke", +] +EXPECTED_STATIC_LIBS = { + "gecodetest": "libgecodetest.a", + "gecodetestint": "libgecodetestint.a", +} +def fail_phase(phase: str, message: str) -> "NoReturn": + sys.stderr.write(f"{VERIFIER_PREFIX} {phase}: FAIL - {message}\n") + raise SystemExit(1) + + + +def assert_phase(condition: bool, phase: str, message: str) -> None: + if not condition: + fail_phase(phase, message) + + + +def is_installed_library_path(prefix: Path, path: Path) -> bool: + relative_path = path.relative_to(prefix) + return any(part.startswith("lib") for part in relative_path.parts[:-1]) and "cmake" not in relative_path.parts + + + +def configured_types(prefix: Path) -> set[str]: + library_names = {path.name for path in prefix.rglob("*") if path.is_file()} + result = {"int"} + if any(name.startswith("libgecodeset.") for name in library_names): + result.add("set") + if any(name.startswith("libgecodefloat.") for name in library_names): + result.add("float") + return result + + +def verify_prefix_surface(prefix: Path, types: set[str]) -> None: + phase = "prefix-surface" + include_test_dir = prefix / "include" / "test" + assert_phase(include_test_dir.is_dir(), phase, f"missing installed test include dir: {include_test_dir}") + + installed_headers = sorted(path.name for path in include_test_dir.iterdir() if path.is_file()) + expected_headers = list(CORE_HEADERS) + for variable_type in ("set", "float"): + if variable_type in types: + expected_headers.extend([f"{variable_type}.hh", f"{variable_type}.hpp"]) + missing_headers = [name for name in expected_headers if name not in installed_headers] + unexpected_headers = [name for name in installed_headers if name not in expected_headers] + assert_phase( + not missing_headers and not unexpected_headers, + phase, + f"missing expected headers: {missing_headers}; unexpected installed headers: {unexpected_headers}", + ) + sys.stdout.write(f"{VERIFIER_PREFIX} {phase}: headers={installed_headers}\n") + + + +def verify_harness_libs(prefix: Path, types: set[str]) -> None: + phase = "harness-libs" + expected_locations: list[str] = [] + + expected_static_libs = dict(EXPECTED_STATIC_LIBS) + for variable_type in ("set", "float"): + if variable_type in types: + expected_static_libs[f"gecodetest{variable_type}"] = f"libgecodetest{variable_type}.a" + + for library_name, expected_filename in expected_static_libs.items(): + matches = sorted( + path.relative_to(prefix).as_posix() + for path in prefix.rglob(expected_filename) + if path.is_file() and is_installed_library_path(prefix, path) + ) + assert_phase(matches, phase, f"missing installed {library_name} static library: {expected_filename}") + assert_phase( + len(matches) == 1, + phase, + f"expected exactly one installed {library_name} static library, found: {matches}", + ) + expected_locations.extend(matches) + + unexpected_harness_artifacts = sorted( + path.relative_to(prefix).as_posix() + for path in prefix.rglob("*") + if path.is_file() + and is_installed_library_path(prefix, path) + and path.name.startswith("libgecodetest") + and path.name not in expected_static_libs.values() + ) + assert_phase( + not unexpected_harness_artifacts, + phase, + f"unexpected installed harness artifacts: {unexpected_harness_artifacts}", + ) + sys.stdout.write(f"{VERIFIER_PREFIX} {phase}: artifacts={sorted(expected_locations)}\n") + + + +def verify_unsupported_surface(prefix: Path, types: set[str]) -> None: + phase = "unsupported-surface" + include_test_dir = prefix / "include" / "test" + unsupported_headers = list(UNSUPPORTED_HEADERS) + for variable_type in ("set", "float"): + if variable_type not in types: + unsupported_headers.extend([f"{variable_type}.hh", f"{variable_type}.hpp"]) + unexpected_headers = [name for name in unsupported_headers if (include_test_dir / name).exists()] + assert_phase(not unexpected_headers, phase, f"unexpected public helper headers installed: {unexpected_headers}") + + unexpected_binaries = sorted( + path.relative_to(prefix).as_posix() + for path in prefix.rglob("*") + if path.is_file() and (path.name in UNSUPPORTED_BINARIES or path.stem in UNSUPPORTED_BINARIES) + ) + assert_phase(not unexpected_binaries, phase, f"unexpected internal test binaries installed: {unexpected_binaries}") + sys.stdout.write(f"{VERIFIER_PREFIX} {phase}: headers-ok binaries-ok\n") + + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--prefix", required=True) + return parser.parse_args() + + + +def main() -> int: + args = parse_args() + prefix = Path(args.prefix).resolve() + + assert_phase(prefix.is_dir(), "inputs", f"missing installed prefix: {prefix}") + + types = configured_types(prefix) + verify_prefix_surface(prefix, types) + verify_harness_libs(prefix, types) + verify_unsupported_surface(prefix, types) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())