Compare commits

..
Author SHA1 Message Date
SoftFever 6a7e8e784c Require numpy for the plugin tests in CI 2026-09-28 14:48:30 +08:00
SoftFever c09902aa44 Merge branch 'main' into feature/enable-unit-tests-for-plugins 2026-09-28 12:40:41 +08:00
SoftFever bd3140d202 fix Windows 2026-09-28 11:40:52 +08:00
SoftFever 88586eea44 enable python unit test 2026-09-09 00:10:02 +08:00
17 changed files with 111 additions and 224 deletions
+8
View File
@@ -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
+2
View File
@@ -52,4 +52,6 @@ internal_docs/
__pycache__/
*.pyc
*.opc
/.test/
docs/superpowers/
ctest_results.xml
+48 -1
View File
@@ -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).
+2 -2
View File
@@ -1692,9 +1692,9 @@ void Layer::make_ironing()
ironing_params.just_infill = false;
// ORCA: Get filament-specific overrides if configured, otherwise use process values
size_t extruder_idx = ironing_params.extruder - 1;
ironing_params.line_spacing = std::max(IRONING_SPACING_MIN, !config.filament_ironing_spacing.is_nil(extruder_idx)
ironing_params.line_spacing = (!config.filament_ironing_spacing.is_nil(extruder_idx)
? config.filament_ironing_spacing.get_at(extruder_idx)
: config.ironing_spacing.value);
: config.ironing_spacing);
ironing_params.inset = (!config.filament_ironing_inset.is_nil(extruder_idx)
? config.filament_ironing_inset.get_at(extruder_idx)
: config.ironing_inset);
-5
View File
@@ -22,9 +22,6 @@ void FillConcentric::_fill_surface_single(
coord_t min_spacing = scale_(this->spacing) * params.multiline;
coord_t distance = coord_t(min_spacing / params.density);
// A non-positive step never shrinks the region, so the inset loop below would not end.
if (min_spacing <= 0 || distance <= 0)
return;
if (params.density > 0.9999f && !params.dont_adjust) {
distance = this->_adjust_solid_spacing(bounding_box.size()(0), distance);
@@ -111,8 +108,6 @@ void FillConcentric::_fill_surface_single(const FillParams& params,
// no rotation is supported for this infill pattern
Point bbox_size = expolygon.contour.bounding_box().size();
coord_t min_spacing = scaled<coord_t>(this->spacing);
if (min_spacing <= 0)
return;
if (params.density > 0.9999f && !params.dont_adjust) {
coord_t loops_count = std::max(bbox_size.x(), bbox_size.y()) / min_spacing + 1;
+5 -10
View File
@@ -2504,8 +2504,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = path;
ctx.cancellation_check = [print]() { return print->canceled(); };
@@ -2557,8 +2556,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
}
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\n" + err_msg;
ctx.cancellation_check = [print]() { return print->canceled(); };
@@ -2582,8 +2580,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
boost::nowide::remove(path_tmp.c_str());
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\n" + ex.what();
ctx.cancellation_check = [print]() { return print->canceled(); };
@@ -2696,8 +2693,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
if (ret) {
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
ctx.cancellation_check = [print]() { return print->canceled(); };
@@ -2716,8 +2712,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = path;
ctx.cancellation_check = [print]() { return print->canceled(); };
+5 -10
View File
@@ -2717,8 +2717,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
@@ -3345,8 +3344,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
@@ -4952,8 +4950,7 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
{
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = file;
ctx.cancellation_check = [this]() { return canceled(); };
@@ -4983,8 +4980,7 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = file + "\n" + ex.what();
ctx.cancellation_check = [this]() { return canceled(); };
@@ -4998,8 +4994,7 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = file;
ctx.cancellation_check = [this]() { return canceled(); };
-4
View File
@@ -178,10 +178,6 @@ enum class IroningType {
Count,
};
// Smallest usable ironing line spacing. Anything tighter yields an unprintable number of lines,
// and zero stops the fillers from making progress.
constexpr double IRONING_SPACING_MIN = 0.05;
//BBS
enum class WallInfillOrder {
InnerOuterInfill,
+1 -1
View File
@@ -64,7 +64,7 @@ struct SupportParameters {
this->ironing = object_config.support_ironing;
this->ironing_flow = support_material_interface_flow.with_height(support_material_interface_flow.height() * 0.01 * object_config.support_ironing_flow.value);
this->ironing_spacing = std::max(IRONING_SPACING_MIN, object_config.support_ironing_spacing.value);
this->ironing_spacing = object_config.support_ironing_spacing;
this->ironing_pattern = object_config.support_ironing_pattern;
// Calculate a minimum support layer height as a minimum over all extruders, but not smaller than 10um.
+2 -26
View File
@@ -210,30 +210,6 @@ void ConfigManipulation::check_filament_max_volumetric_speed(DynamicPrintConfig
}
void ConfigManipulation::check_filament_ironing_spacing(DynamicPrintConfig *config)
{
const auto *opt = config->option<ConfigOptionFloatsNullable>("filament_ironing_spacing");
if (opt == nullptr)
return;
std::vector<double> values = opt->values;
bool reset = false;
for (size_t i = 0; i < values.size(); ++i)
if (!opt->is_nil(i) && values[i] < IRONING_SPACING_MIN) {
values[i] = 0.1;
reset = true;
}
if (!reset)
return;
const wxString msg_text = _(L("Ironing spacing too small\nIt has been reset to 0.1"));
MessageDialog dialog(nullptr, msg_text, "", wxICON_WARNING | wxOK);
DynamicPrintConfig new_conf = *config;
is_msg_dlg_already_exist = true;
dialog.ShowModal();
new_conf.set_key_value("filament_ironing_spacing", new ConfigOptionFloatsNullable(values));
apply(config, &new_conf);
is_msg_dlg_already_exist = false;
}
void ConfigManipulation::check_chamber_temperature(DynamicPrintConfig* config)
{
bool support_chamber_temp_control=GUI::wxGetApp().preset_bundle->printers.get_selected_preset().config.opt_bool("support_chamber_temp_control");
@@ -356,7 +332,7 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
}
//BBS: ironing_spacing shouldn't be too small or equal to zero
if (config->opt_float("ironing_spacing") < IRONING_SPACING_MIN)
if (config->opt_float("ironing_spacing") < 0.05)
{
const wxString msg_text = _(L("Ironing spacing too small\nIt has been reset to 0.1"));
MessageDialog dialog(nullptr, msg_text, "", wxICON_WARNING | wxOK);
@@ -367,7 +343,7 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
apply(config, &new_conf);
is_msg_dlg_already_exist = false;
}
if (config->opt_float("support_ironing_spacing") < IRONING_SPACING_MIN)
if (config->opt_float("support_ironing_spacing") < 0.05)
{
const wxString msg_text = _(L("Ironing spacing too small\nIt has been reset to 0.1"));
MessageDialog dialog(nullptr, msg_text, "", wxICON_WARNING | wxOK);
-1
View File
@@ -84,7 +84,6 @@ public:
void check_nozzle_temperature_initial_layer_range(DynamicPrintConfig* config);
void check_adaptive_pressure_advance_model(DynamicPrintConfig* config);
void check_filament_max_volumetric_speed(DynamicPrintConfig *config);
void check_filament_ironing_spacing(DynamicPrintConfig *config);
void check_chamber_temperature(DynamicPrintConfig* config);
void check_chamber_minimal_temperature(DynamicPrintConfig* config);
bool check_layer_height(DynamicPrintConfig* config);
+4 -12
View File
@@ -12800,6 +12800,8 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
notification_manager->set_slicing_progress_export_possible();
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
const std::string lifecycle_job_name = this->background_process.fff_print() ?
this->background_process.fff_print()->output_filename() : std::string();
this->background_process.reset_export();
// This bool stops showing export finished notification even when process_completed_with_error is false
bool has_error = false;
@@ -12846,18 +12848,8 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
{
Slic3r::LifecycleEventContext ctx;
if (const PrintBase* print = this->background_process.current_print()) {
const Model& model = print->model();
ctx.id = std::to_string(model.id().id);
if (model.model_info)
ctx.name = model.model_info->model_name;
} else {
// Realistically Printbase* print will never be null because select_technology already asserts an active print
// and the worker thread asserts it before processing.
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": slicing completed without an active print; lifecycle event has no model ID";
}
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn :
(has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
ctx.name = lifecycle_job_name;
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string());
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
}
-1
View File
@@ -4905,7 +4905,6 @@ void TabFilament::update()
return; // ys_FIXME
m_config_manipulation.check_filament_max_volumetric_speed(m_config);
m_config_manipulation.check_filament_ironing_spacing(m_config);
m_update_cnt++;
+10 -5
View File
@@ -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;
-55
View File
@@ -747,61 +747,6 @@ TEST_CASE("A region with ironing turned off is never ironed", "[Fill]")
REQUIRE(Layer::choose_ironing_extruder(cfg, spiral_mode, /*is_topmost_layer=*/true) == -1);
}
// Ironing path count and total length in mm, over the whole object.
static std::pair<size_t, double> ironing_extent(const Print &print)
{
size_t paths = 0;
double length = 0.;
for (const Layer *layer : print.objects().front()->layers())
for (const LayerRegion *region : layer->regions())
for (const ExtrusionEntity *entity : region->fills.flatten().entities)
if (ironing_role(entity->role())) {
++paths;
length += unscale<double>(entity->length());
}
return {paths, length};
}
TEST_CASE("Ironing spacing below the minimum irons at the minimum spacing", "[Fill]")
{
const std::string pattern = GENERATE("rectilinear", "concentric");
const bool via_filament = GENERATE(false, true);
const double spacing = GENERATE(0., 0.001);
CAPTURE(pattern, via_filament, spacing);
auto ironing_for = [&pattern, via_filament](double spacing) {
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({{"ironing_type", "top"},
{"ironing_pattern", pattern},
{"layer_height", 0.2}});
// The filament override replaces the process spacing, which stays at a usable value.
if (via_filament)
config.set_deserialize_strict({{"ironing_spacing", 0.1}, {"filament_ironing_spacing", spacing}});
else
config.set_deserialize_strict({{"ironing_spacing", spacing}});
Print print;
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, config);
return ironing_extent(print);
};
const std::pair<size_t, double> clamped = ironing_for(spacing);
const std::pair<size_t, double> minimum = ironing_for(IRONING_SPACING_MIN);
REQUIRE(minimum.first > 0);
CHECK(clamped.first == minimum.first);
CHECK_THAT(clamped.second, Catch::Matchers::WithinRel(minimum.second, 1e-9));
}
TEST_CASE("Concentric fill at zero spacing returns without paths", "[Fill]")
{
std::unique_ptr<Fill> filler(Fill::new_from_type(ipConcentric));
filler->spacing = 0.;
filler->bounding_box = BoundingBox(Point(0, 0), Point::new_scale(10, 10));
FillParams params;
params.density = 1.f;
Surface surface(stTop, ExPolygon({Point(0, 0), Point::new_scale(10, 0), Point::new_scale(10, 10), Point::new_scale(0, 10)}));
CHECK(filler->fill_surface(&surface, params).empty());
}
TEST_CASE("Solid infill direction offsets every layer when no template is set", "[Fill]")
{
auto angles_for = [](int direction) {
-86
View File
@@ -15,9 +15,6 @@
#include "libslic3r/Layer.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Exception.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "test_helpers.hpp"
#include "test_utils.hpp"
@@ -25,10 +22,7 @@
#include <algorithm>
#include <fstream>
#include <iterator>
#include <memory>
#include <string_view>
#include <utility>
#include <vector>
using namespace Slic3r;
using namespace Slic3r::Test;
@@ -230,88 +224,8 @@ std::string resolved_output_name(Model& model, const std::string& format, const
return print.output_filename(filename_base);
}
struct ScopedLifecycleHook
{
explicit ScopedLifecycleHook(LifecycleHookFn hook) { set_lifecycle_hook_fn(std::move(hook)); }
~ScopedLifecycleHook() { set_lifecycle_hook_fn(nullptr); }
};
} // namespace
TEST_CASE("Slicing lifecycle events identify the model", "[Print][LifecycleEvents]")
{
struct ObservedEvent {
LifecycleEvent event;
std::string id;
std::string name;
};
std::vector<ObservedEvent> events;
ScopedLifecycleHook hook([&](LifecycleEvent event, const LifecycleEventContext& ctx) {
events.push_back({ event, ctx.id, ctx.name });
});
Print print;
Model model;
ModelInfo info;
info.model_name = "Lifecycle test model";
model.model_info = std::make_shared<ModelInfo>(std::move(info));
init_print({cube(20)}, print, model);
print.process();
ScopedTemporaryFile temp(".gcode");
print.export_gcode(temp.string(), nullptr, nullptr);
GCodeProcessorResult result;
print.export_gcode_from_previous_file(temp.string(), &result);
const std::string expected_id = std::to_string(print.model().id().id);
const std::vector<LifecycleEvent> expected_events = {
LifecycleEvent::SliceStarted,
LifecycleEvent::SliceGeometryFinished,
LifecycleEvent::GCodeExportStarted,
LifecycleEvent::GCodeExportFinished,
LifecycleEvent::GCodeExportStarted,
LifecycleEvent::GCodeExportFinished,
};
REQUIRE(events.size() == expected_events.size());
for (size_t i = 0; i < expected_events.size(); ++i) {
CHECK(events[i].event == expected_events[i]);
CHECK(events[i].id == expected_id);
CHECK(events[i].name == "Lifecycle test model");
}
}
TEST_CASE("Slicing lifecycle event name is empty without model metadata", "[Print][LifecycleEvents]")
{
std::string event_id;
std::string event_name = "unset";
ScopedLifecycleHook hook([&](LifecycleEvent event, const LifecycleEventContext& ctx) {
if (event == LifecycleEvent::SliceStarted) {
event_id = ctx.id;
event_name = ctx.name;
}
});
Print print;
Model model;
init_print({cube(20)}, print, model);
print.process();
CHECK(event_id == std::to_string(print.model().id().id));
CHECK(event_name.empty());
}
TEST_CASE("Output filenames with numeric statistics fail before slicing finishes", "[Print][Regression]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_key_value("filename_format", new ConfigOptionString("{int(total_weight*10) / 10.0}"));
Print print;
Model model;
init_print({cube(20)}, print, model, config);
CHECK_THROWS_AS(print.output_filename(), PlaceholderParserError);
}
TEST_CASE("Print: {first_object_name} names the first printable object on the plate", "[Print]")
{
Model model;
+21 -2
View File
@@ -49,9 +49,16 @@ if (WIN32)
COMMENT "Copying Python runtime for slic3rutils plugin host API tests"
VERBATIM
)
elseif (APPLE)
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
@@ -59,7 +66,7 @@ elseif (APPLE)
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_PREFIX_PATH}/libpython"
"$<TARGET_FILE_DIR:${_TEST_NAME}_tests>/python"
COMMENT "Copying Python runtime for macOS plugin host API tests"
COMMENT "Copying Python runtime for the plugin host API tests"
VERBATIM
)
elseif (FLATPAK)
@@ -76,4 +83,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 (<exe dir>/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 "$<TARGET_FILE_DIR:${_TEST_NAME}_tests>/tools/uv"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${ORCA_BUNDLED_UV_EXECUTABLE}"
"$<TARGET_FILE_DIR:${_TEST_NAME}_tests>/tools/uv/${ORCA_BUNDLED_UV_FILENAME}"
COMMENT "Copying uv for the plugin host API tests"
VERBATIM
)
endif()
orcaslicer_discover_tests(${_TEST_NAME}_tests)