diff --git a/.github/ci/generated-source-freshness.sh b/.github/ci/generated-source-freshness.sh index 92cce3ae6f..5be8629767 100755 --- a/.github/ci/generated-source-freshness.sh +++ b/.github/ci/generated-source-freshness.sh @@ -139,9 +139,13 @@ ensure_autoconf() { mkdir -p "$tools_root" autoconf_build_root=$(mktemp -d "$temp_root/gecode-autoconf-build.XXXXXX") archive="$autoconf_build_root/autoconf-$autoconf_version.tar.gz" - curl --fail --silent --show-error --location --retry 3 \ - --output "$archive" \ - "https://ftp.gnu.org/gnu/autoconf/autoconf-$autoconf_version.tar.gz" + if ! curl --fail --silent --show-error --location --retry 3 \ + --connect-timeout 15 --max-time 120 --output "$archive" \ + "https://mirrors.kernel.org/gnu/autoconf/autoconf-$autoconf_version.tar.gz"; then + curl --fail --silent --show-error --location --retry 3 \ + --connect-timeout 15 --max-time 120 --output "$archive" \ + "https://ftp.gnu.org/gnu/autoconf/autoconf-$autoconf_version.tar.gz" + fi if command -v shasum >/dev/null 2>&1; then printf '%s %s\n' "$autoconf_sha256" "$archive" | shasum -a 256 -c - else diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8457a5e7f..f67f858bf7 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-.*-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-unsupported-component" \ + --prefix "$GITHUB_WORKSPACE/install" \ + --include-dir "/tmp/gecode-install-abs-include" \ + --lib-dir "$GITHUB_WORKSPACE/install/lib" \ + --expect-unsupported-component-failure + - name: Package version checks shell: bash run: bash .github/ci/cmake-package-version-checks.sh "$GITHUB_WORKSPACE/install" @@ -405,6 +461,10 @@ jobs: shell: pwsh run: cmake --build "${{ github.workspace }}\\build" --config ${{ env.BUILD_TYPE }} --target check + - name: Run public test harness smokes + shell: pwsh + run: ctest --test-dir "${{ github.workspace }}\\build" -C ${{ env.BUILD_TYPE }} --output-on-failure -R '^public-.*-smoke$' + - name: Install CMake package shell: pwsh run: cmake --install "${{ github.workspace }}\\build" --config ${{ env.BUILD_TYPE }} --prefix "${{ github.workspace }}\\install" @@ -413,6 +473,18 @@ jobs: shell: pwsh run: .\.github\ci\cmake-consumer-smoke.ps1 "${{ github.workspace }}\install" + - name: Installed test-library consumer + shell: pwsh + run: | + $prefix = "${{ github.workspace }}\install" + $build = "$env:RUNNER_TEMP\test-library-consumer" + cmake -S test/package/public-test-component -B $build "-DCMAKE_PREFIX_PATH=$prefix" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cmake --build $build --config $env:BUILD_TYPE + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $env:PATH = "$prefix\bin;$env:PATH" + ctest --test-dir $build -C $env:BUILD_TYPE --output-on-failure + build-cmake-windows-vcpkg: runs-on: windows-2022 diff --git a/CMakeLists.txt b/CMakeLists.txt index 65f4dd0913..459b5b6606 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() # --------------------------------------------------------------------------- @@ -1128,7 +1128,11 @@ endif() function(add_gecode_component_library lib) string(TOUPPER ${lib} libupper) - set(sources ${GECODE_${libupper}_SOURCES}) + if(ARGN) + set(sources ${ARGN}) + else() + set(sources ${GECODE_${libupper}_SOURCES}) + endif() set(component_output_name "${GECODE_LIB_PREFIX}gecode${lib}${GECODE_LIB_SUFFIX}") if(lib STREQUAL "float" AND NOT (GECODE_ENABLE_MPFR AND MPFR_FOUND)) # Keep in sync with Make behavior: skip empty MPFR-only source when MPFR is absent. @@ -1354,8 +1358,66 @@ 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_gecode_component_library(test ${GECODE_TEST_CORE_SOURCES}) + gecode_link_component(test search int) + add_gecode_component_library(testint ${GECODE_TEST_INT_SOURCES}) + gecode_link_component(testint test) + if(TARGET gecodeset) + add_gecode_component_library(testset ${GECODE_TEST_SET_SOURCES}) + gecode_link_component(testset testint set) + endif() + if(TARGET gecodefloat) + add_gecode_component_library(testfloat ${GECODE_TEST_FLOAT_SOURCES}) + gecode_link_component(testfloat test float) + 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 +1444,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 +1461,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() @@ -1489,7 +1565,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) @@ -1645,6 +1722,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/ @@ -1666,6 +1763,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 eb3658cafb..ca23749e27 100755 --- a/Makefile.in +++ b/Makefile.in @@ -1088,12 +1088,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 @@ -1101,8 +1102,8 @@ endif PDBS = $(ALLLIB:%$(LIBSUFFIX)=%$(PDBSUFFIX)) ifeq "@INSTALLLIBS@" "yes" -LIBLIBTARGETS = $(ALLLIB) -PDBTARGETS = $(PDBS) +LIBLIBTARGETS = $(ALLLIB) $(TESTPUBLICINSTALLLIBLIBTARGETS) +PDBTARGETS = $(PDBS) $(TESTPUBLICINSTALLPDBTARGETS) else LIBLIBTARGETS = PDBTARGETS = @@ -1277,7 +1278,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. @@ -1290,14 +1313,148 @@ 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/%) -TESTOBJ = $(INTTESTOBJ) $(SETTESTOBJ) $(FLOATTESTOBJ) \ + +TESTPUBLICINSTALLHDR = +TESTPUBLICINSTALLLIBTARGETS = +TESTPUBLICINSTALLLIBLINKTARGETS = +TESTPUBLICINSTALLLIBLIBTARGETS = +TESTPUBLICINSTALLPDBTARGETS = +TESTPUBLICLIBTARGETS = +TESTHARNESSOBJ = +ifeq "@enable_search@" "yes" +ifeq "@enable_int_vars@" "yes" +TESTPUBLICINSTALLHDR += test/test.hh test/test.hpp +TESTHARNESSOBJ += $(TESTCOREOBJ) +export TESTCOREDLL = $(LIBPREFIX)test$(DLLSUFFIX) +export TESTCORELIB = $(LIBPREFIX)test$(LIBSUFFIX) +export TESTCORESTATICLIB = $(LIBPREFIX)test$(STATICLIBSUFFIX) +export LINKTESTCORE = $(LINKPREFIX)test$(LINKSUFFIX) +ifeq "@need_soname@" "yes" +TESTCORESONAME = @WLSONAME@$(LIBPREFIX)test$(SOSUFFIX) +endif +ifeq "@enable_resource@" "yes" +TESTCORERC = $(LIBPREFIX)test$(RCSUFFIX) +TESTCORERES = $(LIBPREFIX)test$(RESSUFFIX) +endif +ifeq "@BUILDDLL@" "yes" +TESTPUBLICINSTALLLIBTARGETS += $(TESTCOREDLL) +TESTPUBLICINSTALLLIBLINKTARGETS += $(TESTCOREDLL:%$(DLLSUFFIX)=%$(SOLINKSUFFIX)) $(TESTCOREDLL:%$(DLLSUFFIX)=%$(SOSUFFIX)) +TESTPUBLICINSTALLLIBLIBTARGETS += $(TESTCORELIB) +TESTPUBLICINSTALLPDBTARGETS += $(TESTCORELIB:%$(LIBSUFFIX)=%$(PDBSUFFIX)) +TESTPUBLICLIBTARGETS += $(TESTCORELIB) +else +TESTPUBLICLIBTARGETS += $(TESTCORESTATICLIB) +endif +ifeq "@BUILDSTATIC@" "yes" +TESTPUBLICINSTALLLIBTARGETS += $(TESTCORESTATICLIB) +endif +TESTPUBLICINSTALLHDR += test/int.hh test/int.hpp +TESTHARNESSOBJ += $(TESTINTSEAMOBJ) +export TESTINTDLL = $(LIBPREFIX)testint$(DLLSUFFIX) +export TESTINTLIB = $(LIBPREFIX)testint$(LIBSUFFIX) +export TESTINTSTATICLIB = $(LIBPREFIX)testint$(STATICLIBSUFFIX) +export LINKTESTINT = $(LINKPREFIX)testint$(LINKSUFFIX) +ifeq "@need_soname@" "yes" +TESTINTSONAME = @WLSONAME@$(LIBPREFIX)testint$(SOSUFFIX) +endif +ifeq "@enable_resource@" "yes" +TESTINTRC = $(LIBPREFIX)testint$(RCSUFFIX) +TESTINTRES = $(LIBPREFIX)testint$(RESSUFFIX) +endif +ifeq "@BUILDDLL@" "yes" +TESTPUBLICINSTALLLIBTARGETS += $(TESTINTDLL) +TESTPUBLICINSTALLLIBLINKTARGETS += $(TESTINTDLL:%$(DLLSUFFIX)=%$(SOLINKSUFFIX)) $(TESTINTDLL:%$(DLLSUFFIX)=%$(SOSUFFIX)) +TESTPUBLICINSTALLLIBLIBTARGETS += $(TESTINTLIB) +TESTPUBLICINSTALLPDBTARGETS += $(TESTINTLIB:%$(LIBSUFFIX)=%$(PDBSUFFIX)) +TESTPUBLICLIBTARGETS += $(TESTINTLIB) +else +TESTPUBLICLIBTARGETS += $(TESTINTSTATICLIB) +endif +ifeq "@BUILDSTATIC@" "yes" +TESTPUBLICINSTALLLIBTARGETS += $(TESTINTSTATICLIB) +endif +ifeq "@enable_set_vars@" "yes" +TESTPUBLICINSTALLHDR += test/set.hh test/set.hpp +TESTHARNESSOBJ += $(TESTSETSEAMOBJ) +export TESTSETDLL = $(LIBPREFIX)testset$(DLLSUFFIX) +export TESTSETLIB = $(LIBPREFIX)testset$(LIBSUFFIX) +export TESTSETSTATICLIB = $(LIBPREFIX)testset$(STATICLIBSUFFIX) +export LINKTESTSET = $(LINKPREFIX)testset$(LINKSUFFIX) +ifeq "@need_soname@" "yes" +TESTSETSONAME = @WLSONAME@$(LIBPREFIX)testset$(SOSUFFIX) +endif +ifeq "@enable_resource@" "yes" +TESTSETRC = $(LIBPREFIX)testset$(RCSUFFIX) +TESTSETRES = $(LIBPREFIX)testset$(RESSUFFIX) +endif +ifeq "@BUILDDLL@" "yes" +TESTPUBLICINSTALLLIBTARGETS += $(TESTSETDLL) +TESTPUBLICINSTALLLIBLINKTARGETS += $(TESTSETDLL:%$(DLLSUFFIX)=%$(SOLINKSUFFIX)) $(TESTSETDLL:%$(DLLSUFFIX)=%$(SOSUFFIX)) +TESTPUBLICINSTALLLIBLIBTARGETS += $(TESTSETLIB) +TESTPUBLICINSTALLPDBTARGETS += $(TESTSETLIB:%$(LIBSUFFIX)=%$(PDBSUFFIX)) +TESTPUBLICLIBTARGETS += $(TESTSETLIB) +else +TESTPUBLICLIBTARGETS += $(TESTSETSTATICLIB) +endif +ifeq "@BUILDSTATIC@" "yes" +TESTPUBLICINSTALLLIBTARGETS += $(TESTSETSTATICLIB) +endif +endif +ifeq "@enable_float_vars@" "yes" +TESTPUBLICINSTALLHDR += test/float.hh test/float.hpp +TESTHARNESSOBJ += $(TESTFLOATSEAMOBJ) +export TESTFLOATDLL = $(LIBPREFIX)testfloat$(DLLSUFFIX) +export TESTFLOATLIB = $(LIBPREFIX)testfloat$(LIBSUFFIX) +export TESTFLOATSTATICLIB = $(LIBPREFIX)testfloat$(STATICLIBSUFFIX) +export LINKTESTFLOAT = $(LINKPREFIX)testfloat$(LINKSUFFIX) +ifeq "@need_soname@" "yes" +TESTFLOATSONAME = @WLSONAME@$(LIBPREFIX)testfloat$(SOSUFFIX) +endif +ifeq "@enable_resource@" "yes" +TESTFLOATRC = $(LIBPREFIX)testfloat$(RCSUFFIX) +TESTFLOATRES = $(LIBPREFIX)testfloat$(RESSUFFIX) +endif +ifeq "@BUILDDLL@" "yes" +TESTPUBLICINSTALLLIBTARGETS += $(TESTFLOATDLL) +TESTPUBLICINSTALLLIBLINKTARGETS += $(TESTFLOATDLL:%$(DLLSUFFIX)=%$(SOLINKSUFFIX)) $(TESTFLOATDLL:%$(DLLSUFFIX)=%$(SOSUFFIX)) +TESTPUBLICINSTALLLIBLIBTARGETS += $(TESTFLOATLIB) +TESTPUBLICINSTALLPDBTARGETS += $(TESTFLOATLIB:%$(LIBSUFFIX)=%$(PDBSUFFIX)) +TESTPUBLICLIBTARGETS += $(TESTFLOATLIB) +else +TESTPUBLICLIBTARGETS += $(TESTFLOATSTATICLIB) +endif +ifeq "@BUILDSTATIC@" "yes" +TESTPUBLICINSTALLLIBTARGETS += $(TESTFLOATSTATICLIB) +endif +endif +endif +endif +TESTPUBLICLINK = $(LINKTESTFLOAT) $(LINKTESTSET) $(LINKTESTINT) $(LINKTESTCORE) + +TESTOBJ0 = $(INTTESTOBJ) $(SETTESTOBJ) $(FLOATTESTOBJ) \ $(FLATZINCTESTOBJ:%.cpp=%$(OBJSUFFIX)) \ $(BRANCHTESTSRC0:%.cpp=%$(OBJSUFFIX)) \ $(SEARCHTESTSRC0:%.cpp=%$(OBJSUFFIX)) \ $(ARRAYTESTSRC0:%.cpp=%$(OBJSUFFIX)) \ $(TESTSRC0:%.cpp=%$(OBJSUFFIX)) +TESTOBJ = $(filter-out $(TESTHARNESSOBJ),$(TESTOBJ0)) 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) @@ -1343,10 +1500,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 \ @@ -1359,14 +1515,18 @@ CHECKTESTS = Branch::Int::Dense::3 \ NoGoods::Queens \ Search::PBS::ConstructionFailure \ Search::WorkerControl:: \ - 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)) \ @@ -1391,6 +1551,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 ;; \ @@ -1441,12 +1604,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) $(TESTHARNESSOBJ): $(VARIMP) | mkcompiledirs # # Object targets @@ -1523,6 +1686,22 @@ examples/%$(SBJSUFFIX): $(top_srcdir)/examples/%.cpp @COMPILESBJ@$@ @CXXIN@$< +$(TESTCOREOBJ): $(top_srcdir)/test/test.cpp + $(CXX) $(CXXFLAGS) $(subst GECODE_BUILD_SUPPORT,GECODE_BUILD_TEST,@GECODE_BUILD_SUPPORT_FLAG@) \ + @COMPILEOBJ@$@ @CXXIN@$< + +$(TESTINTSEAMOBJ): $(top_srcdir)/test/int.cpp + $(CXX) $(CXXFLAGS) $(subst GECODE_BUILD_SUPPORT,GECODE_BUILD_TESTINT,@GECODE_BUILD_SUPPORT_FLAG@) \ + @COMPILEOBJ@$@ @CXXIN@$< + +$(TESTSETSEAMOBJ): $(top_srcdir)/test/set.cpp + $(CXX) $(CXXFLAGS) $(subst GECODE_BUILD_SUPPORT,GECODE_BUILD_TESTSET,@GECODE_BUILD_SUPPORT_FLAG@) \ + @COMPILEOBJ@$@ @CXXIN@$< + +$(TESTFLOATSEAMOBJ): $(top_srcdir)/test/float.cpp + $(CXX) $(CXXFLAGS) $(subst GECODE_BUILD_SUPPORT,GECODE_BUILD_TESTFLOAT,@GECODE_BUILD_SUPPORT_FLAG@) \ + @COMPILEOBJ@$@ @CXXIN@$< + test/%$(OBJSUFFIX): $(top_srcdir)/test/%.cpp $(CXX) $(CXXFLAGS) $(EXAMPLES_EXTRA_CXXFLAGS) \ @COMPILEOBJ@$@ @CXXIN@$< @@ -1558,27 +1737,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 @@ -1683,6 +1862,34 @@ $(FLATZINCDLL): $(FLATZINCOBJ) $(SUPPORTDLL) $(KERNELDLL) $(SEARCHDLL) \ $(LINKDL) @LINKOUTPUT@$(FLATZINCDLL) $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOLINKSUFFIX)) $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOSUFFIX)) +ifneq "$(TESTCOREDLL)" "" +$(TESTCOREDLL): $(TESTCOREOBJ) $(SEARCHDLL) $(INTDLL) $(KERNELDLL) $(SUPPORTDLL) + $(CXX) $(DLLFLAGS) $(TESTCOREOBJ) $(TESTCORESONAME) \ + @DLLPATH@ $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) @LINKOUTPUT@$(TESTCOREDLL) + $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOLINKSUFFIX)) + $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOSUFFIX)) +endif +ifneq "$(TESTINTDLL)" "" +$(TESTINTDLL): $(TESTINTSEAMOBJ) $(TESTCOREDLL) $(INTDLL) + $(CXX) $(DLLFLAGS) $(TESTINTSEAMOBJ) $(TESTINTSONAME) \ + @DLLPATH@ $(LINKTESTCORE) $(LINKINT) $(LINKSEARCH) $(LINKKERNEL) $(LINKSUPPORT) @LINKOUTPUT@$(TESTINTDLL) + $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOLINKSUFFIX)) + $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOSUFFIX)) +endif +ifneq "$(TESTSETDLL)" "" +$(TESTSETDLL): $(TESTSETSEAMOBJ) $(TESTINTDLL) $(TESTCOREDLL) $(SETDLL) $(INTDLL) + $(CXX) $(DLLFLAGS) $(TESTSETSEAMOBJ) $(TESTSETSONAME) \ + @DLLPATH@ $(LINKTESTINT) $(LINKTESTCORE) $(LINKSET) $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) @LINKOUTPUT@$(TESTSETDLL) + $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOLINKSUFFIX)) + $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOSUFFIX)) +endif +ifneq "$(TESTFLOATDLL)" "" +$(TESTFLOATDLL): $(TESTFLOATSEAMOBJ) $(TESTCOREDLL) $(FLOATDLL) $(INTDLL) + $(CXX) $(DLLFLAGS) $(TESTFLOATSEAMOBJ) $(TESTFLOATSONAME) \ + @DLLPATH@ $(LINKTESTCORE) $(LINKFLOAT) $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) @LINKOUTPUT@$(TESTFLOATDLL) + $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOLINKSUFFIX)) + $(CREATELINK) $@ $(@:%$(DLLSUFFIX)=%$(SOSUFFIX)) +endif else export LINKALL = @@ -1792,6 +1999,50 @@ $(FLATZINCDLL) $(FLATZINCLIB): $(FLATZINCOBJ) $(FLATZINCRES) \ $(FIXMANIFEST) $(FLATZINCDLL).manifest $(MANIFEST) -manifest $(FLATZINCDLL).manifest \ -outputresource:$(FLATZINCDLL)\;2 +ifneq "$(TESTCOREDLL)" "" +ifeq "@enable_resource@" "yes" +$(TESTCORERC): | mkcompiledirs + $(RCGEN) $(TESTCOREDLL) test/test.cpp test/test.hh test/test.hpp > $@ +endif +$(TESTCOREDLL) $(TESTCORELIB): $(TESTCOREOBJ) $(TESTCORERES) $(SEARCHDLL) $(INTDLL) $(KERNELDLL) $(SUPPORTDLL) + $(CXX) $(DLLFLAGS) $(TESTCOREOBJ) $(TESTCORERES) \ + @DLLPATH@ @LINKOUTPUT@$(TESTCOREDLL) $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) $(GLDFLAGS) + $(FIXMANIFEST) $(TESTCOREDLL).manifest + $(MANIFEST) -manifest $(TESTCOREDLL).manifest -outputresource:$(TESTCOREDLL)\;2 +endif +ifneq "$(TESTINTDLL)" "" +ifeq "@enable_resource@" "yes" +$(TESTINTRC): | mkcompiledirs + $(RCGEN) $(TESTINTDLL) test/int.cpp test/int.hh test/int.hpp > $@ +endif +$(TESTINTDLL) $(TESTINTLIB): $(TESTINTSEAMOBJ) $(TESTINTRES) $(TESTCOREDLL) $(INTDLL) + $(CXX) $(DLLFLAGS) $(TESTINTSEAMOBJ) $(TESTINTRES) \ + @DLLPATH@ @LINKOUTPUT@$(TESTINTDLL) $(LINKTESTCORE) $(LINKINT) $(LINKSEARCH) $(LINKKERNEL) $(LINKSUPPORT) $(GLDFLAGS) + $(FIXMANIFEST) $(TESTINTDLL).manifest + $(MANIFEST) -manifest $(TESTINTDLL).manifest -outputresource:$(TESTINTDLL)\;2 +endif +ifneq "$(TESTSETDLL)" "" +ifeq "@enable_resource@" "yes" +$(TESTSETRC): | mkcompiledirs + $(RCGEN) $(TESTSETDLL) test/set.cpp test/set.hh test/set.hpp > $@ +endif +$(TESTSETDLL) $(TESTSETLIB): $(TESTSETSEAMOBJ) $(TESTSETRES) $(TESTINTDLL) $(TESTCOREDLL) $(SETDLL) $(INTDLL) + $(CXX) $(DLLFLAGS) $(TESTSETSEAMOBJ) $(TESTSETRES) \ + @DLLPATH@ @LINKOUTPUT@$(TESTSETDLL) $(LINKTESTINT) $(LINKTESTCORE) $(LINKSET) $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) $(GLDFLAGS) + $(FIXMANIFEST) $(TESTSETDLL).manifest + $(MANIFEST) -manifest $(TESTSETDLL).manifest -outputresource:$(TESTSETDLL)\;2 +endif +ifneq "$(TESTFLOATDLL)" "" +ifeq "@enable_resource@" "yes" +$(TESTFLOATRC): | mkcompiledirs + $(RCGEN) $(TESTFLOATDLL) test/float.cpp test/float.hh test/float.hpp > $@ +endif +$(TESTFLOATDLL) $(TESTFLOATLIB): $(TESTFLOATSEAMOBJ) $(TESTFLOATRES) $(TESTCOREDLL) $(FLOATDLL) $(INTDLL) + $(CXX) $(DLLFLAGS) $(TESTFLOATSEAMOBJ) $(TESTFLOATRES) \ + @DLLPATH@ @LINKOUTPUT@$(TESTFLOATDLL) $(LINKTESTCORE) $(LINKFLOAT) $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) $(GLDFLAGS) + $(FIXMANIFEST) $(TESTFLOATDLL).manifest + $(MANIFEST) -manifest $(TESTFLOATDLL).manifest -outputresource:$(TESTFLOATDLL)\;2 +endif endif # @@ -1828,6 +2079,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 @@ -1884,18 +2151,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 = -endif -$(TESTEXE): $(TESTOBJ) $(TESTRES) $(ALLLIB) - $(CXX) @EXEOUTPUT@$@ $(TESTOBJ) $(TESTRES) $(DLLPATH) $(CXXFLAGS) \ - $(LINKALL) $(GLDFLAGS) $(LINKQT) $(LINKCPPROFILER) +TESTPUBLICRUNNERSMOKERES = +TESTPUBLICINTSMOKERES = +TESTPUBLICSETSMOKERES = +TESTPUBLICFLOATSMOKERES = +endif +$(TESTEXE): $(TESTMAINOBJ) $(TESTOBJ) $(TESTRES) $(ALLLIB) $(TESTPUBLICLIBTARGETS) + $(CXX) @EXEOUTPUT@$@ $(TESTMAINOBJ) $(TESTOBJ) $(TESTRES) $(DLLPATH) $(CXXFLAGS) \ + $(TESTPUBLICLINK) $(LINKALL) $(GLDFLAGS) $(LINKQT) $(LINKCPPROFILER) + $(FIXMANIFEST) $@.manifest $(DLLSUFFIX) + $(MANIFEST) -manifest $@.manifest -outputresource:$@\;1 + +$(TESTOBJ) $(TESTAUXOBJ) $(BLACKBOXEXECOBJ) $(BLACKBOXDLLOBJ): | mkcompiledirs + +$(TESTPUBLICRUNNERSMOKEEXE): $(TESTPUBLICRUNNERSMOKEOBJ) $(TESTPUBLICLIBTARGETS) $(TESTPUBLICRUNNERSMOKERES) $(ALLLIB) + $(CXX) @EXEOUTPUT@$@ $(TESTPUBLICRUNNERSMOKEOBJ) $(TESTPUBLICRUNNERSMOKERES) \ + $(DLLPATH) $(CXXFLAGS) \ + $(LINKTESTCORE) $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) $(GLDFLAGS) $(FIXMANIFEST) $@.manifest $(DLLSUFFIX) $(MANIFEST) -manifest $@.manifest -outputresource:$@\;1 -$(BLACKBOXEXECOBJ) $(BLACKBOXDLLOBJ): | mkcompiledirs +$(TESTPUBLICINTSMOKEEXE): $(TESTPUBLICINTSMOKEOBJ) $(TESTPUBLICLIBTARGETS) $(TESTPUBLICINTSMOKERES) $(ALLLIB) + $(CXX) @EXEOUTPUT@$@ $(TESTPUBLICINTSMOKEOBJ) $(TESTPUBLICINTSMOKERES) \ + $(DLLPATH) $(CXXFLAGS) \ + $(LINKTESTINT) $(LINKTESTCORE) $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) $(GLDFLAGS) + $(FIXMANIFEST) $@.manifest $(DLLSUFFIX) + $(MANIFEST) -manifest $@.manifest -outputresource:$@\;1 + +ifeq "@enable_set_vars@" "yes" +$(TESTPUBLICSETSMOKEEXE): $(TESTPUBLICSETSMOKEOBJ) $(TESTPUBLICLIBTARGETS) $(TESTPUBLICSETSMOKERES) $(ALLLIB) + $(CXX) @EXEOUTPUT@$@ $(TESTPUBLICSETSMOKEOBJ) $(TESTPUBLICSETSMOKERES) \ + $(DLLPATH) $(CXXFLAGS) \ + $(LINKTESTSET) $(LINKTESTINT) $(LINKTESTCORE) $(LINKSET) $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) $(GLDFLAGS) + $(FIXMANIFEST) $@.manifest $(DLLSUFFIX) + $(MANIFEST) -manifest $@.manifest -outputresource:$@\;1 +endif + +ifeq "@enable_float_vars@" "yes" +$(TESTPUBLICFLOATSMOKEEXE): $(TESTPUBLICFLOATSMOKEOBJ) $(TESTPUBLICLIBTARGETS) $(TESTPUBLICFLOATSMOKERES) $(ALLLIB) + $(CXX) @EXEOUTPUT@$@ $(TESTPUBLICFLOATSMOKEOBJ) $(TESTPUBLICFLOATSMOKERES) \ + $(DLLPATH) $(CXXFLAGS) \ + $(LINKTESTFLOAT) $(LINKTESTCORE) $(LINKFLOAT) $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) $(GLDFLAGS) + $(FIXMANIFEST) $@.manifest $(DLLSUFFIX) + $(MANIFEST) -manifest $@.manifest -outputresource:$@\;1 +endif $(BLACKBOXEXEC): $(BLACKBOXEXECOBJ) $(CXX) @EXEOUTPUT@$@ $(BLACKBOXEXECOBJ) $(CXXFLAGS) @@ -2046,7 +2375,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 -) && \ @@ -2056,6 +2385,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 && \ @@ -2163,7 +2498,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) $(TESTHARNESSOBJ) $(TESTAUXOBJ) $(TESTSBJ) \ + $(TESTOBJ:%$(OBJSUFFIX)=%.pdb) $(TESTAUXOBJ:%$(OBJSUFFIX)=%.pdb) $(RMF) $(BLACKBOXEXECOBJ) $(BLACKBOXDLLOBJ) \ $(BLACKBOXEXECOBJ:%$(OBJSUFFIX)=%.pdb) \ $(BLACKBOXDLLOBJ:%$(OBJSUFFIX)=%.pdb) @@ -2180,6 +2516,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)) \ @@ -2198,11 +2542,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 e5808a7e76..1614dca4a3 100755 --- a/changelog.in +++ b/changelog.in @@ -73,6 +73,17 @@ 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. +Support shared and static harness libraries using the configured Gecode +library variants. Add exact replay of a failed test iteration with `-replay`. + [ENTRY] Module: int @@ -85,6 +96,7 @@ overload checking. The algorithm is described in: S. Cloutier, C.-G. Quimper, Augmenting the Cumulative Overload Check with Integral Resource Usage Reasoning, CP 2026. +[ENTRY] Module: search What: new Rank: major diff --git a/cmake/GecodeConfig.cmake.in b/cmake/GecodeConfig.cmake.in index 4099c3b2bd..e570a4c2cd 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,66 @@ 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)) + set(Gecode_${_gecode_requested_component}_FOUND TRUE) 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() + set(Gecode_${_gecode_requested_component}_FOUND FALSE) + 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 +122,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 +238,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 c0d389f116..eb013c2682 100644 --- a/cmake/GecodeSources.cmake +++ b/cmake/GecodeSources.cmake @@ -245,6 +245,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 @@ -377,7 +399,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 @@ -428,7 +449,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..c4ce8a172b 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,49 @@ 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.*` + +```cmake +find_package(Gecode CONFIG REQUIRED COMPONENTS test) +add_executable(custom-propagator-test custom-propagator-test.cpp) +target_link_libraries(custom-propagator-test PRIVATE Gecode::gecodetestint) +``` + +Use `Gecode::gecodetestset` or `Gecode::gecodetestfloat` for those variable +families. The targets supply the runner and their Gecode dependencies. With +`add_subdirectory` or `FetchContent`, enable `BUILD_TESTING` before adding Gecode. +When both library variants are installed, targets ending in `_shared` or +`_static` select a variant explicitly. Use one variant throughout the test +executable so registration and execution share the same registry. + +See the test API in [`test/int.hh`](../test/int.hh) and +[`test/test.hh`](../test/test.hh) for an example, consistency controls, and +`Test::run_registered_tests`. The runner's `-help` lists execution and replay +options. + +Autoconf also installs the configured test helpers when search and integer +variables are enabled. For direct linking, list the used helpers before their +Gecode dependencies, for example: + +```bash +c++ -std=c++17 -I/include custom-propagator-test.cpp \ + -L/lib -lgecodetestint -lgecodetest -lgecodesearch \ + -lgecodeint -lgecodekernel -lgecodesupport +``` + +Use the installation's configured library directory. Set and float helpers +also require their corresponding Gecode libraries. + Legacy component spellings are also accepted in `COMPONENTS`: - `gecodesupport`, `gecodekernel`, `gecodesearch`, `gecodeint`, `gecodeset`, @@ -234,6 +277,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/doxygen/doxygen.hh.in b/doxygen/doxygen.hh.in index 69ae6ec5fa..0f4d67a1ee 100755 --- a/doxygen/doxygen.hh.in +++ b/doxygen/doxygen.hh.in @@ -161,6 +161,9 @@ /** * \defgroup TaskTest Testing + * + * See Test::Int::Test for writing an integer or Boolean propagator test and + * Test::run_registered_tests for registration, execution, and failure replay. * \ingroup Task */ diff --git a/gecode/search/worker-control.cpp b/gecode/search/worker-control.cpp index 5969163834..6a4cb53ad3 100644 --- a/gecode/search/worker-control.cpp +++ b/gecode/search/worker-control.cpp @@ -289,3 +289,5 @@ namespace Gecode { namespace Search { } }} + +// STATISTICS: search-other diff --git a/gecode/search/worker-control.hh b/gecode/search/worker-control.hh index c8e9905342..cbfa606207 100644 --- a/gecode/search/worker-control.hh +++ b/gecode/search/worker-control.hh @@ -77,3 +77,5 @@ namespace Gecode { namespace Search { }} #endif + +// STATISTICS: search-other diff --git a/test/float.hh b/test/float.hh index 84ccb562b6..4576978c90 100644 --- a/test/float.hh +++ b/test/float.hh @@ -42,6 +42,20 @@ #include +/* Configure linking, as for the Gecode variable libraries. */ +#if !defined(GECODE_STATIC_LIBS) && \ + (defined(__CYGWIN__) || defined(__MINGW32__) || defined(_MSC_VER)) +#ifdef GECODE_BUILD_TESTFLOAT +#define GECODE_TESTFLOAT_EXPORT __declspec( dllexport ) +#else +#define GECODE_TESTFLOAT_EXPORT __declspec( dllimport ) +#endif +#elif defined(GECODE_GCC_HAS_CLASS_VISIBILITY) +#define GECODE_TESTFLOAT_EXPORT __attribute__ ((visibility("default"))) +#else +#define GECODE_TESTFLOAT_EXPORT +#endif + namespace Test { /// Testing domain floats @@ -64,7 +78,7 @@ namespace Test { EXTEND_ASSIGNMENT }; - class Test; + class GECODE_TESTFLOAT_EXPORT Test; /** * \defgroup TaskTestFloat Testing domain floats @@ -77,7 +91,7 @@ namespace Test { */ //@{ /// %Base class for assignments - class Assignment { + class GECODE_TESTFLOAT_EXPORT Assignment { protected: int n; ///< Number of variables Gecode::FloatVal d; ///< Domain for each variable @@ -99,7 +113,7 @@ namespace Test { }; /// Generate all assignments - class CpltAssignment : public Assignment { + class GECODE_TESTFLOAT_EXPORT CpltAssignment : public Assignment { protected: Gecode::FloatVal* dsv; ///< Iterator for each variable Gecode::FloatNum step; ///< Step for next assignment @@ -119,7 +133,7 @@ namespace Test { }; /// Generate all assignments except the last variable and complete it to get a solution - class ExtAssignment : public Assignment { + class GECODE_TESTFLOAT_EXPORT ExtAssignment : public Assignment { protected: const Test* curPb; ///< Current problem used to complete assignment Gecode::FloatVal* dsv; ///< Iterator for each variable @@ -142,7 +156,7 @@ namespace Test { /// Generate random selection of assignments - class RandomAssignment : public Assignment { + class GECODE_TESTFLOAT_EXPORT RandomAssignment : public Assignment { protected: Gecode::FloatVal* vals; ///< The current values for the variables int a; ///< How many assignments still to be generated @@ -164,7 +178,7 @@ namespace Test { }; /// Space for executing tests - class TestSpace : public Gecode::Space { + class GECODE_TESTFLOAT_EXPORT TestSpace : public Gecode::Space { public: /// Initial domain Gecode::FloatVal d; @@ -242,7 +256,7 @@ namespace Test { * \brief %Base class for tests with float constraints * */ - class Test : public Base { + class GECODE_TESTFLOAT_EXPORT Test : public Base { protected: /// Number of variables int arity; @@ -254,7 +268,7 @@ namespace Test { AssignmentType assignmentType; /// Does the constraint also exist as reified constraint bool reified; - /// Which reification modes are supported + /// Supported reification modes, encoded as a bit mask of 1 << Gecode::RM_* int rms; /// Whether to perform search test bool testsearch; @@ -298,7 +312,7 @@ namespace Test { /// Complete the current assignment to get a feasible one (which satisfies all constraint). /// If such an assignment is computed, it returns true, false otherwise virtual bool extendAssignment(Assignment& a) const; - /// Check for solution + /// Check an interval assignment using MT_TRUE, MT_FALSE, or MT_MAYBE; see cmp() virtual MaybeType solution(const Assignment&) const = 0; /// Test if \a ts is subsumed or not (i.e. if there is no more propagator unless /// the assignment is an extended assignment. @@ -337,7 +351,7 @@ namespace Test { //@} /// Iterator for float relation types - class FloatRelTypes { + class GECODE_TESTFLOAT_EXPORT FloatRelTypes { private: /// Array of relation types static const Gecode::FloatRelType frts[6]; @@ -363,11 +377,10 @@ namespace Test { * \brief Print assignment \a * \relates Assignment */ -std::ostream& operator<<(std::ostream& os, const Test::Float::Assignment& a); +GECODE_TESTFLOAT_EXPORT std::ostream& operator<<(std::ostream& os, const Test::Float::Assignment& a); #include "test/float.hpp" #endif // STATISTICS: test-float - diff --git a/test/int.hh b/test/int.hh index 2adc40dab3..6e36158718 100755 --- a/test/int.hh +++ b/test/int.hh @@ -40,6 +40,20 @@ #include +/* Configure linking, as for the Gecode variable libraries. */ +#if !defined(GECODE_STATIC_LIBS) && \ + (defined(__CYGWIN__) || defined(__MINGW32__) || defined(_MSC_VER)) +#ifdef GECODE_BUILD_TESTINT +#define GECODE_TESTINT_EXPORT __declspec( dllexport ) +#else +#define GECODE_TESTINT_EXPORT __declspec( dllimport ) +#endif +#elif defined(GECODE_GCC_HAS_CLASS_VISIBILITY) +#define GECODE_TESTINT_EXPORT __attribute__ ((visibility("default"))) +#else +#define GECODE_TESTINT_EXPORT +#endif + namespace Test { /// Testing finite domain integers @@ -56,7 +70,7 @@ namespace Test { */ //@{ /// %Base class for assignments - class Assignment { + class GECODE_TESTINT_EXPORT Assignment { protected: int n; ///< Number of variables Gecode::IntSet d; ///< Domain for each variable @@ -76,7 +90,7 @@ namespace Test { }; /// Generate all assignments - class CpltAssignment : public Assignment { + class GECODE_TESTINT_EXPORT CpltAssignment : public Assignment { protected: Gecode::IntSetValues* dsv; ///< Iterator for each variable public: @@ -93,7 +107,7 @@ namespace Test { }; /// Generate random selection of assignments - class RandomAssignment : public Assignment { + class GECODE_TESTINT_EXPORT RandomAssignment : public Assignment { protected: int* vals; ///< The current values for the variables int a; ///< How many assignments still to be generated @@ -113,7 +127,7 @@ namespace Test { }; /// Generate random selection of assignments - class RandomMixAssignment : public Assignment { + class GECODE_TESTINT_EXPORT RandomMixAssignment : public Assignment { protected: int* vals; ///< The current values for the variables int a; ///< How many assignments still to be generated @@ -143,10 +157,10 @@ namespace Test { CTL_BOUNDS_Z, ///< Test for bounds(z)-consistency }; - class Test; + class GECODE_TESTINT_EXPORT Test; /// Space for executing tests - class TestSpace : public Gecode::Space { + class GECODE_TESTINT_EXPORT TestSpace : public Gecode::Space { public: /// Initial domain Gecode::IntSet d; @@ -219,8 +233,41 @@ namespace Test { /** * \brief %Base class for tests with integer constraints * + * Override solution() with an independent predicate over complete + * assignments and post() with the propagator's public posting function. + * The harness compares them while exercising propagation, cloning, + * pruning, and search. Register a test with static lifetime: + * \code + * #include + * + * class EqualityTest : public ::Test::Int::Test { + * public: + * EqualityTest() : ::Test::Int::Test("Package::Equality", 2, 0, 1) {} + * bool solution(const ::Test::Int::Assignment& a) const override { + * return a[0] == a[1]; + * } + * void post(Gecode::Space& home, Gecode::IntVarArray& x) override { + * Gecode::rel(home, x[0], Gecode::IRT_EQ, x[1]); + * } + * } equality_test; + * + * int main(int argc, char* argv[]) { + * return Test::run_registered_tests(argc, argv); + * } + * \endcode + * + * All test variables start with the constructor's common domain. + * For per-variable restrictions, post them with the constraint and + * include them in solution(). Use the complete assignment generator + * when checking the full relation. + * + * IPL_DOM initializes contest to CTL_DOMAIN; other levels initialize + * it to CTL_NONE. Set contest to CTL_BOUNDS_D or CTL_BOUNDS_Z only when + * the propagator promises that consistency. For reified constraints, + * enable reification in the constructor and override the reified post(). + * \see Test::run_registered_tests */ - class Test : public Base { + class GECODE_TESTINT_EXPORT Test : public Base { protected: /// Number of variables int arity; @@ -228,7 +275,7 @@ namespace Test { Gecode::IntSet dom; /// Does the constraint also exist as reified constraint bool reified; - /// Which reification modes are supported + /// Supported reification modes, encoded as a bit mask of 1 << Gecode::RM_* int rms; /// Propagation level Gecode::IntPropLevel ipl; @@ -253,8 +300,8 @@ namespace Test { * * Constructs a test with prefix \a p, name \a s, arity \a a, * and variable domain \a d. Also tests for a reified - * constraint, if \a r is true. The propagation level is - * maintained for convenience. + * constraint, if \a r is true. The propagation level + * also selects the initial consistency check (see class documentation). */ Test(const std::string& p, const std::string& s, int a, const Gecode::IntSet& d, bool r=false, @@ -264,8 +311,8 @@ namespace Test { * * Constructs a test with name \a s, arity \a a, and variable * domain \a d. Also tests for a reified constraint, - * if \a r is true. The propagation level is - * maintained for convenience. + * if \a r is true. The propagation level + * also selects the initial consistency check (see class documentation). */ Test(const std::string& s, int a, const Gecode::IntSet& d, bool r=false, @@ -276,7 +323,7 @@ namespace Test { * Constructs a test with prefix \a p, name \a s, arity \a a, * and variable domain \a min ... \a max. Also tests for * a reified constraint, if \a r is true. The propagation - * level is maintained for convenience. + * level also selects the initial consistency check (see class documentation). */ Test(const std::string& p, const std::string& s, int a, int min, int max, bool r=false, @@ -286,13 +333,17 @@ namespace Test { * * Constructs a test with name \a s, arity \a a, variable * domain \a min ... \a max. Also tests for a reified constraint, - * if \a r is true. The propagation level is - * maintained for convenience. + * if \a r is true. The propagation level + * also selects the initial consistency check (see class documentation). */ Test(const std::string& s, int a, int min, int max, bool r=false, Gecode::IntPropLevel i=Gecode::IPL_DEF); - /// Create assignment + /** \brief Create the assignment generator (complete by default) + * + * An override changes the assignments checked, not the variable + * domains in the test space. + */ virtual Assignment* assignment(void) const; /// Check for solution virtual bool solution(const Assignment&) const = 0; @@ -329,7 +380,7 @@ namespace Test { //@} /// Iterator for simple integer propagation levels - class IntPropLevels { + class GECODE_TESTINT_EXPORT IntPropLevels { private: /// Array of propagation levels static const Gecode::IntPropLevel ipls[3]; @@ -347,7 +398,7 @@ namespace Test { }; /// Iterator for basic and advanced integer propagation levels - class IntPropBasicAdvanced { + class GECODE_TESTINT_EXPORT IntPropBasicAdvanced { private: /// Array of propagation levels static const Gecode::IntPropLevel ipls[3]; @@ -365,7 +416,7 @@ namespace Test { }; /// Iterator for integer relation types - class IntRelTypes { + class GECODE_TESTINT_EXPORT IntRelTypes { private: /// Array of relation types static const Gecode::IntRelType irts[6]; @@ -385,7 +436,7 @@ namespace Test { }; /// Iterator for Boolean operation types - class BoolOpTypes { + class GECODE_TESTINT_EXPORT BoolOpTypes { private: /// Array of operation types static const Gecode::BoolOpType bots[5]; @@ -409,11 +460,10 @@ namespace Test { * \brief Print assignment \a * \relates Assignment */ -std::ostream& operator<<(std::ostream& os, const Test::Int::Assignment& a); +GECODE_TESTINT_EXPORT std::ostream& operator<<(std::ostream& os, const ::Test::Int::Assignment& a); #include "test/int.hpp" #endif // STATISTICS: test-int - diff --git a/test/package/public-test-component/CMakeLists.txt b/test/package/public-test-component/CMakeLists.txt new file mode 100644 index 0000000000..0eb0c56a2a --- /dev/null +++ b/test/package/public-test-component/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.21) + +project(gecode_public_test_component_consumer LANGUAGES CXX) + +enable_testing() + +option(REQUIRE_UNKNOWN_COMPONENT "Request an unsupported Gecode component to verify diagnostics" OFF) +set(GECODE_TEST_COMPONENT test CACHE STRING "Gecode test component to request") + +set(CMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY ON) +set(CMAKE_FIND_USE_PACKAGE_REGISTRY OFF) + +set(_gecode_components ${GECODE_TEST_COMPONENT}) +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() + +add_test(NAME public-test-component + COMMAND consumer-smoke -test Package -iter 1 -stop true) 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..65637f8447 --- /dev/null +++ b/test/package/verify-installed-legacy-test-component.py @@ -0,0 +1,465 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import os +import re +import shlex +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +VERIFIER_PREFIX = "[verify-installed-legacy-test-component]" + + +def configured_types(prefix: Path) -> list[str]: + config = (prefix / "include/gecode/support/config.hpp").read_text() + return [family for family in ("int", "set", "float") + if re.search(r"^#define GECODE_HAS_" + family.upper() + r"_VARS\b", + config, re.MULTILINE)] + + +def expected_test_names(types: list[str]) -> list[str]: + names = {"int": "Int::Package::Equality", "set": "Set::Package::Singleton", + "float": "Float::Package::Equality"} + return [names[family] for family in types] + + +def libraries(types: list[str]) -> list[str]: + return ([f"gecodetest{family}" for family in reversed(types)] + + ["gecodetest"] + + [f"gecode{family}" for family in reversed(types) if family != "int"] + + ["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, types: list[str]) -> Path: + phase = "inputs" + required = ["libgecodetest"] + [f"libgecodetest{family}" for family in types] + 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( + prefix_include in include_dirs, + phase, + f"missing installed include directory in compiler command: {prefix_include}", + ) + missing_library_dirs = [path for path in expected_library_dirs if path not in library_dirs] + assert_phase( + not missing_library_dirs, + phase, + f"missing installed library directories in compiler command: {[str(path) for path in missing_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: + types = configured_types(prefix) + consumer_source = write_positive_consumer(source, workspace) + consumer_binary = workspace / "consumer-smoke" + command = [ + *compiler_command_prefix(), + "-std=c++17", + *[f"-DGECODE_PACKAGE_HAS_{family.upper()}" for family in types], + f"-I{prefix / 'include'}", + str(consumer_source), + f"-L{prefix_lib}", + *[f"-l{name}" for name in libraries(types)], + "-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], names: list[str]) -> None: + result = run_phase( + "list", + [str(consumer_binary), "-list"], + cwd=workspace, + env=runtime_env, + env_summary=env_summary, + ) + for test_name in names: + assert_phase(test_name in result.stdout, "list", f"-list output missing {test_name!r}") + sys.stdout.write(f"{VERIFIER_PREFIX} list: discovered={names}\n") + + + +def run_filtered_phase( + consumer_binary: Path, + workspace: Path, + runtime_env: dict[str, str], + env_summary: dict[str, str], + names: list[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 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={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) + + types = configured_types(prefix) + names = expected_test_names(types) + prefix_lib = resolve_library_dir(prefix, types) + 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, names) + run_filtered_phase(consumer_binary, workspace, runtime_env, env_summary, names) + 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..9c9e8985b6 --- /dev/null +++ b/test/package/verify-installed-test-component.py @@ -0,0 +1,310 @@ +#!/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, + test_component: str = "test", +) -> tuple[Path, Path, subprocess.CompletedProcess[str]]: + consumer_source = source / "test/package/public-test-component" + consumer_build = build_root / "consumer" + shutil.rmtree(consumer_build, 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}", + f"-DGECODE_TEST_COMPONENT={test_component}", + ] + if require_unknown_component: + configure_command.append("-DREQUIRE_UNKNOWN_COMPONENT=ON") + + result = run_phase( + "configure-unsupported-component" if require_unknown_component else "configure", + configure_command, + expect_success=not require_unknown_component, + ) + return consumer_source, consumer_build, result + + +def assert_unsupported_component_failure(result: subprocess.CompletedProcess[str]) -> None: + phase = "configure-unsupported-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-unsupported-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_unsupported_component_failure, + ) + + if args.expect_unsupported_component_failure: + assert_unsupported_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) + + configure_consumer( + source, + build_root / "typed-component", + prefix, + lib_dir / "cmake/Gecode", + require_unknown_component=False, + test_component="testint", + ) + 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..c2bbb45466 --- /dev/null +++ b/test/public-runner-smoke.cpp @@ -0,0 +1,206 @@ +/* -*- 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; + + unsigned int observed_seed = 0; + int replay_runs = 0; + class ReplaySmokeTest : public Test::Base { + public: + ReplaySmokeTest(void) : Test::Base("Smoke::Replay") {} + bool run(void) override { + if (++replay_runs == 1) { + // Leave a state above the seed setter's modulus for the next iteration. + _rand.seed(44488); + (void) _rand.next(); + return true; + } + observed_seed = _rand.seed(); + (void) _rand(10); + return false; + } + } replay_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: -replay ") != 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; + } + + std::string replay_output; + if (!require(run_and_capture({"public-runner-smoke", "-test", "Smoke::Replay", + "-seed", "12345", "-iter", "2"}, + replay_output) == EXIT_FAILURE && replay_runs == 2, + "seed-sensitive test should fail on its second iteration")) + return EXIT_FAILURE; + const std::string seed = std::to_string(observed_seed); + if (!require(replay_output.find("Options: -replay " + seed) != std::string::npos, + "failure should report the iteration's initial random state")) + return EXIT_FAILURE; + if (!require(run_and_capture({"public-runner-smoke", "-test", "Smoke::Replay", + "-replay", seed.c_str()}, replay_output) == EXIT_FAILURE && + std::to_string(observed_seed) == seed, + "replay should restore the reported random state")) + 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/set.hh b/test/set.hh index 29f74bdaf6..8aae22c6f7 100644 --- a/test/set.hh +++ b/test/set.hh @@ -40,6 +40,20 @@ #include "test/test.hh" #include "test/int.hh" +/* Configure linking, as for the Gecode variable libraries. */ +#if !defined(GECODE_STATIC_LIBS) && \ + (defined(__CYGWIN__) || defined(__MINGW32__) || defined(_MSC_VER)) +#ifdef GECODE_BUILD_TESTSET +#define GECODE_TESTSET_EXPORT __declspec( dllexport ) +#else +#define GECODE_TESTSET_EXPORT __declspec( dllimport ) +#endif +#elif defined(GECODE_GCC_HAS_CLASS_VISIBILITY) +#define GECODE_TESTSET_EXPORT __attribute__ ((visibility("default"))) +#else +#define GECODE_TESTSET_EXPORT +#endif + namespace Test { /// Testing finite sets @@ -57,7 +71,7 @@ namespace Test { //@{ /// Value iterator producing subsets of an IntSet - class CountableSetValues { + class GECODE_TESTSET_EXPORT CountableSetValues { private: Gecode::IntSetValues dv; int cur; @@ -95,7 +109,7 @@ namespace Test { }; /// Range iterator producing subsets of an IntSet - class CountableSetRanges + class GECODE_TESTSET_EXPORT CountableSetRanges : public Gecode::Iter::Values::ToRanges { private: /// The corresponding value iterator @@ -115,7 +129,7 @@ namespace Test { }; /// Iterate all subsets of a given set - class CountableSet { + class GECODE_TESTSET_EXPORT CountableSet { private: /// The superset Gecode::IntSet d; @@ -139,7 +153,7 @@ namespace Test { }; /// Generate all set assignments - class SetAssignment { + class GECODE_TESTSET_EXPORT SetAssignment { private: /// Arity int n; @@ -160,7 +174,7 @@ namespace Test { bool operator()(void) const { return !done; } /// Move to next assignment void next(Gecode::Support::RandomGenerator& rand); - /// Return value for variable \a i + /// Return the set at \a i as a bit pattern over lub; see CountableSetRanges int operator[](int i) const { assert((i>=0) && (i + * + * 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..5fcac5b55d 100644 --- a/test/test.cpp +++ b/test/test.cpp @@ -46,7 +46,6 @@ #include #include #include -#include namespace Test { @@ -93,9 +92,11 @@ namespace Test { Base::~Base() = default; Options opt; + const int Options::defiter; + const unsigned int Options::deffixprob; - void report_error(const std::string& name, unsigned int seed, Options& options, std::ostream& ostream) { - ostream << "Options: -seed " << seed; + void report_error(const std::string& name, unsigned int seed, const Options& options, std::ostream& ostream) { + ostream << "Options: -replay " << seed; if (options.fixprob != Test::Options::deffixprob) ostream << " -fixprob " << options.fixprob; ostream << " -test " << name << std::endl; @@ -119,6 +120,8 @@ namespace Test { << std::endl << "\t\tor \"time\" for a random seed based on " << "current time" << std::endl + << "\t-replay (unsigned int)\n" + << "\t\treplay the exact -test name once, on one thread\n" << "\t-fixprob (unsigned int) default: " << fixprob << std::endl << "\t\t1/fixprob is the probability of computing a fixpoint" @@ -156,7 +159,8 @@ namespace Test { } else { threads = argument; } - } else if (!strcmp(argv[i],"-seed")) { + } else if (!strcmp(argv[i],"-seed") || !strcmp(argv[i],"-replay")) { + replay = !strcmp(argv[i],"-replay"); if (++i == argc) goto missing; if (!strcmp(argv[i],"time")) { seed = static_cast(time(nullptr)); @@ -195,6 +199,16 @@ namespace Test { i++; } + if (replay) { + if (testpat.size() != 1 || testpat[0].first != MT_ANY || + start_from != nullptr) { + std::cerr << "-replay requires one exact -test name." << std::endl; + exit(EXIT_FAILURE); + } + threads = 1; + iter = 1; + } + if (threads > 1 && log) { std::cerr << "Logging and multi threading can not be used jointly." << std::endl; exit(EXIT_FAILURE); @@ -207,7 +221,9 @@ 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 (replay) + return testpat.size() == 1 && test_name == testpat[0].second; if (!testpat.empty()) { bool positive_patterns = false; bool match_found = false; @@ -249,18 +265,21 @@ namespace Test { /// Run a single test, returning true iff the test succeeded bool run_test(Base* test, unsigned int test_seed, const Options& options, std::ostream& ostream) { + unsigned int seed = options.replay ? options.seed : test_seed; try { ostream << test->name() << " "; ostream.flush(); - test->_rand.seed(test_seed); + test->_rand.seed(seed); for (unsigned int i = options.iter; i--;) { - unsigned int seed = test->_rand.seed(); + // Normalize evolved states so the reported seed restores this iteration. + test->_rand.seed(test->_rand.seed()); + seed = test->_rand.seed(); if (test->run()) { ostream << '+'; ostream.flush(); } else { ostream << "-" << std::endl; - report_error(test->name(), seed, opt, ostream); + report_error(test->name(), seed, options, ostream); return false; } } @@ -268,9 +287,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(), seed, options, ostream); return false; } } @@ -282,7 +301,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 +458,60 @@ 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(); } -} + 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); + } + } + return tests; + } -int -main(int argc, char* argv[]) { - using namespace Test; -#ifdef GECODE_HAS_MTRACE - mtrace(); -#endif - - opt.parse(argc, argv); + int run_registered_tests(int argc, char* argv[]) { + opt = Options(); + opt.parse(argc, argv); - Base::sort(); + Base::sort(); - if (opt.list) { - for (Base* t = Base::tests() ; t != nullptr; t = t->next() ) { - std::cout << t->name() << std::endl; + if (opt.list) { + for (Base* t = Base::tests(); t != nullptr; t = t->next()) { + std::cout << t->name() << std::endl; + } + return EXIT_SUCCESS; } - exit(EXIT_SUCCESS); - } - 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; - } + const std::vector tests = select_tests(opt); + if (opt.replay && tests.size() != 1) { + std::cerr << "-replay requires exactly one registered test." << std::endl; + return EXIT_FAILURE; } - if (opt.is_test_name_matching(t->name())) { - tests.emplace_back(t); + if (opt.threads > 1) { + return run_tests_parallel(tests, opt); } - } - - if (opt.threads > 1) { - return run_tests_parallel(tests, opt); - } else { return run_tests(tests, opt); } } diff --git a/test/test.hh b/test/test.hh index 9092165615..99f772f3f5 100755 --- a/test/test.hh +++ b/test/test.hh @@ -45,6 +45,21 @@ #include #include #include +#include + +/* Configure linking, as for the Gecode variable libraries. */ +#if !defined(GECODE_STATIC_LIBS) && \ + (defined(__CYGWIN__) || defined(__MINGW32__) || defined(_MSC_VER)) +#ifdef GECODE_BUILD_TEST +#define GECODE_TEST_EXPORT __declspec( dllexport ) +#else +#define GECODE_TEST_EXPORT __declspec( dllimport ) +#endif +#elif defined(GECODE_GCC_HAS_CLASS_VISIBILITY) +#define GECODE_TEST_EXPORT __attribute__ ((visibility("default"))) +#else +#define GECODE_TEST_EXPORT +#endif /// General test support namespace Test { @@ -61,10 +76,10 @@ namespace Test { * The olog is buffered and printed only if an error has * occurred. */ - extern std::ostringstream olog; + extern GECODE_TEST_EXPORT std::ostringstream olog; /// Simple class for describing indentation - class ind { + class GECODE_TEST_EXPORT ind { public: /// Which indentation level int l; @@ -80,12 +95,14 @@ namespace Test { }; /// Commandline options - class Options { + class GECODE_TEST_EXPORT Options { public: /// Number of threads to use unsigned int threads; /// The random seed to be used unsigned int seed; + /// Replay one named test from its reported random state + bool replay; /// Number of iterations for each test unsigned int iter; /// Default number of iterations @@ -110,15 +127,20 @@ namespace Test { /// Parse commandline arguments 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); + /** + * \brief Test whether a test name should be executed + * + * Replay requires one exact test name. Otherwise, match the configured + * patterns; with no patterns, every test name matches. + */ + bool is_test_name_matching(const std::string& test_name) const; }; /// The options - extern Options opt; + extern GECODE_TEST_EXPORT Options opt; /// %Base class for all tests to be run - class Base { + class GECODE_TEST_EXPORT Base { private: /// Name of the test std::string _name; @@ -163,21 +185,31 @@ 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. + * + * Use -help to list options and -list to list registered tests. Failure + * reports provide a -replay state and exact -test name. Replay runs that + * test once on one thread, starting from the failing iteration's state. + * -log prints the buffered log on failure and requires one thread. + * \relates Test::Base + */ + GECODE_TEST_EXPORT 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 */ -std::ostream& +GECODE_TEST_EXPORT std::ostream& operator<<(std::ostream& os, const Test::ind& i); #include "test/test.hpp" diff --git a/test/test.hpp b/test/test.hpp index f7f56488e7..5c3f45e2d7 100755 --- a/test/test.hpp +++ b/test/test.hpp @@ -39,7 +39,7 @@ namespace Test { */ inline Options::Options(void) - : threads(1), seed(0), iter(defiter), fixprob(deffixprob), + : threads(1), seed(0), replay(false), iter(defiter), fixprob(deffixprob), stop(true), log(false), testpat(), start_from(nullptr), list(false) {} diff --git a/test/verify-legacy-install-surface.py b/test/verify-legacy-install-surface.py new file mode 100644 index 0000000000..6322896ed6 --- /dev/null +++ b/test/verify-legacy-install-surface.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import re +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", +] +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]: + config = (prefix / "include/gecode/support/config.hpp").read_text() + return {family for family in ("int", "set", "float") + if re.search(r"^#define GECODE_HAS_" + family.upper() + r"_VARS\b", + config, re.MULTILINE)} + + +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] = [] + + libraries = ["gecodetest"] + [f"gecodetest{family}" for family in sorted(types)] + for library in libraries: + matches = sorted(path.relative_to(prefix).as_posix() + for path in prefix.rglob(f"lib{library}.*") + if path.is_file() and is_installed_library_path(prefix, path)) + assert_phase(matches, phase, f"missing installed {library} library") + expected_locations.extend(matches) + sys.stdout.write(f"{VERIFIER_PREFIX} {phase}: artifacts={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())