diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index f5850d9fd7..41ef0b1f4b 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -44,6 +44,14 @@ jobs: uses: actions/download-artifact@v8 with: name: ${{ inputs.artifact }} + # run_unit_tests.sh installs the plugin tests' numpy with the uv the build stages + # beside them; the Windows arm64 build bundles none, so put one on PATH there. + - name: Install uv + if: runner.os == 'Windows' && runner.arch == 'ARM64' + uses: astral-sh/setup-uv@v10.2.0 + with: + version: "0.11.21" # ORCA_UV_VERSION in CMakeLists.txt + enable-cache: false - uses: lukka/get-cmake@latest with: cmakeVersion: "~4.3.0" # use most recent 4.3.x version diff --git a/.gitignore b/.gitignore index b029afa180..5224ecad6b 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,6 @@ internal_docs/ __pycache__/ *.pyc *.opc -docs/superpowers/ \ No newline at end of file +/.test/ +docs/superpowers/ +ctest_results.xml diff --git a/scripts/run_unit_tests.sh b/scripts/run_unit_tests.sh index 1d3f0046fd..c4433df04d 100755 --- a/scripts/run_unit_tests.sh +++ b/scripts/run_unit_tests.sh @@ -2,7 +2,8 @@ # This file is made to support the unit tests workflow. # It should only require the directories build/tests, scripts/, and tests/ to function, -# and cmake (with ctest) installed. +# and cmake (with ctest) installed -- plus network access to PyPI whenever numpy has to +# be installed into a freshly built test tree (see below). # (otherwise, update the workflow too, but try to avoid to keep things self-contained) # # Usage: run_unit_tests.sh [TEST_DIR] [BUILD_CONFIG] @@ -18,6 +19,52 @@ cd "${ROOT_DIR}" || exit 1 TEST_DIR="${1:-build/tests}" BUILD_CONFIG="${2:-}" +# The slic3rutils plugin-host tests build numpy arrays through the CPython copied next +# to the test binary (see tests/slic3rutils/CMakeLists.txt), which ships no numpy. +# Install it with the uv staged beside that runtime -- the tool the app installs plugin +# dependencies with -- straight into the interpreter's own site-packages: no pip needed +# in the runtime, no PYTHONPATH. Re-checked every run because a rebuild of the test +# target re-copies the runtime; needs network whenever it installs. Pinned so a numpy +# release cannot change results on its own. +NUMPY_VERSION="2.5.3" + +# Without numpy those tests assert the numpy-absent error path instead, so a local run +# only warns. Under CI it fails the run, which would otherwise stay green while silently +# dropping the array coverage. (The Flatpak leg runs this inside `flatpak build`, whose +# minimal environment has no CI, and its offline build stages no uv, so numpy stays +# best-effort there.) +numpy_unavailable() { + if [ -n "${CI:-}" ]; then + echo "error: $1" >&2 + exit 1 + fi + echo "warning: $1; the numpy-backed binding tests will cover only the numpy-absent path." +} + +has_pinned_numpy() { + "${python_exe}" -c "import sys, numpy; sys.exit(numpy.__version__ != '${NUMPY_VERSION}')" >/dev/null 2>&1 +} + +find_args=("${TEST_DIR}" \( -path '*/python/bin/python3' -o -path '*/python/python.exe' \)) +# Multi-config trees hold one copy per configuration; only bootstrap the one being run. +[ -n "${BUILD_CONFIG}" ] && find_args+=(-path "*/${BUILD_CONFIG}/*") +python_exe="$(find "${find_args[@]}" -print -quit 2>/dev/null)" + +if [ -z "${python_exe}" ]; then + numpy_unavailable "no bundled Python under ${TEST_DIR}" +elif ! has_pinned_numpy; then + uv_exe="${python_exe%/python/*}/tools/uv/uv" + # Builds that bundle no uv (Windows arm64) fall back to one on PATH. + [ -x "${uv_exe}" ] || uv_exe="$(command -v uv)" + echo "Installing numpy ${NUMPY_VERSION} into the embedded test interpreter (${python_exe})..." + if [ -z "${uv_exe}" ]; then + numpy_unavailable "no uv staged beside the tests or on PATH" + elif ! "${uv_exe}" pip install --python "${python_exe}" --only-binary :all: "numpy==${NUMPY_VERSION}" \ + || ! has_pinned_numpy; then + numpy_unavailable "could not install numpy ${NUMPY_VERSION} into ${python_exe}" + fi +fi + # Run the whole suite, excluding tests tagged [NotWorking] and tests labelled RequiresApp, # which run the built orca-slicer binary that this directory does not contain. # --no-tests=error fails the job if the filter matches nothing (instead of passing green). diff --git a/src/slic3r/plugin/PythonInterpreter.cpp b/src/slic3r/plugin/PythonInterpreter.cpp index 3d180c2f2a..d82bf52a13 100644 --- a/src/slic3r/plugin/PythonInterpreter.cpp +++ b/src/slic3r/plugin/PythonInterpreter.cpp @@ -345,16 +345,21 @@ boost::filesystem::path find_bundled_python_home() fs::path bundle_python = fs::path(resources_dir()).parent_path() / "MacOS" / "python"; if (valid_python_home(bundle_python)) return bundle_python; -#elif defined(_WIN32) - fs::path exe_python = boost::dll::program_location().parent_path() / "python"; - if (valid_python_home(exe_python)) - return exe_python; -#else +#elif !defined(_WIN32) fs::path linux_python = fs::path(resources_dir()).parent_path() / "lib" / "python"; if (valid_python_home(linux_python)) return linux_python; #endif + // Next to the executable: the Windows install layout, and the runtime copied + // beside every platform's unit-test binary (tests/slic3rutils/CMakeLists.txt). + // The CI test runner only receives the build/tests tree, so the candidates + // below -- all of which point into the deps or install trees -- never resolve + // there. + fs::path exe_python = boost::dll::program_location().parent_path() / "python"; + if (valid_python_home(exe_python)) + return exe_python; + fs::path configured_python = ORCA_BUNDLED_PYTHON_ROOT; if (!configured_python.empty() && valid_python_home(configured_python)) return configured_python; diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 58262eef99..be796e814b 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -51,9 +51,16 @@ if (WIN32) COMMENT "Copying Python runtime for slic3rutils plugin host API tests" VERBATIM ) -elseif (APPLE) - target_link_options(${_TEST_NAME}_tests PRIVATE - "LINKER:-rpath,@executable_path/python/lib") +elseif (NOT FLATPAK) + # The CI unit-test runner only receives the build/tests tree, so both the + # interpreter and the libpython the test binary links have to travel next to + # the executable; find_bundled_python_home() picks the copy up from there. + if (APPLE) + target_link_options(${_TEST_NAME}_tests PRIVATE + "LINKER:-rpath,@executable_path/python/lib") + else () + set_property(TARGET ${_TEST_NAME}_tests APPEND PROPERTY BUILD_RPATH "$ORIGIN/python/lib") + endif () add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E rm -rf @@ -61,7 +68,7 @@ elseif (APPLE) COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_PREFIX_PATH}/libpython" "$/python" - COMMENT "Copying Python runtime for macOS plugin host API tests" + COMMENT "Copying Python runtime for the plugin host API tests" VERBATIM ) elseif (FLATPAK) @@ -78,4 +85,16 @@ elseif (FLATPAK) ) endif() +# scripts/run_unit_tests.sh installs the tests' numpy into that runtime with this uv, +# staged where the app build tree keeps it (/tools/uv, see src/CMakeLists.txt). +if (ORCA_BUNDLED_UV_EXECUTABLE) + add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "$/tools/uv" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${ORCA_BUNDLED_UV_EXECUTABLE}" + "$/tools/uv/${ORCA_BUNDLED_UV_FILENAME}" + COMMENT "Copying uv for the plugin host API tests" + VERBATIM + ) +endif() + orcaslicer_discover_tests(${_TEST_NAME}_tests)