mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-01 23:32:10 +00:00
fix: unit test shutting down pythonintepreter
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
#include <libslic3r/Utils.hpp>
|
#include <libslic3r/Utils.hpp>
|
||||||
#include <slic3r/plugin/PluginConfig.hpp>
|
#include <slic3r/plugin/PluginConfig.hpp>
|
||||||
#include <slic3r/plugin/PluginManager.hpp>
|
#include <slic3r/plugin/PluginManager.hpp>
|
||||||
|
#include <slic3r/plugin/PythonInterpreter.hpp>
|
||||||
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
||||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||||
|
|
||||||
@@ -21,19 +22,33 @@ using json = nlohmann::json;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
void ensure_python_initialized()
|
// Brings the plugin system up for the duration of one test and tears it down at the end — same
|
||||||
|
// idiom as ScopedPluginManager in test_plugin_lifecycle.cpp, and for the same reason: shutdown()
|
||||||
|
// logs via boost::log, so it must run before boost::log tears down its thread-local storage, not be
|
||||||
|
// left to a static destructor at process exit.
|
||||||
|
//
|
||||||
|
// Needed here (unlike a bare pybind11::scoped_interpreter): has_config_ui()/get_config_ui()/
|
||||||
|
// get_name()/etc. all cross PyPluginCommonTrampoline's ORCA_PY_OVERRIDE_AUDITED, which refuses to
|
||||||
|
// call into Python unless PythonInterpreter::instance() itself reports initialized (see
|
||||||
|
// PythonGILState). A bare interpreter never sets that flag, so every trampoline call would report
|
||||||
|
// "Python interpreter is shutting down" even though Python was perfectly alive.
|
||||||
|
//
|
||||||
|
// initialize() leaves the GIL released (production code re-acquires it per call via PythonGILState);
|
||||||
|
// every TEST_CASE below pairs this with a py::gil_scoped_acquire, declared second so it releases
|
||||||
|
// before this destructor's shutdown() runs.
|
||||||
|
struct ScopedPluginManager
|
||||||
{
|
{
|
||||||
// As in test_plugin_host_api.cpp: `orca` is compiled into this binary, so a bare interpreter is
|
bool initialized = PluginManager::instance().initialize();
|
||||||
// enough and does not need the bundled Python home.
|
|
||||||
if (!Py_IsInitialized()) {
|
~ScopedPluginManager()
|
||||||
static py::scoped_interpreter interpreter;
|
{
|
||||||
(void) interpreter;
|
PluginManager::instance().shutdown();
|
||||||
|
PythonInterpreter::instance().shutdown();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
py::module_ import_orca_module()
|
py::module_ import_orca_module()
|
||||||
{
|
{
|
||||||
ensure_python_initialized();
|
|
||||||
(void) PythonPluginBridge::instance(); // force the embedded module registration into the binary
|
(void) PythonPluginBridge::instance(); // force the embedded module registration into the binary
|
||||||
return py::module_::import("orca");
|
return py::module_::import("orca");
|
||||||
}
|
}
|
||||||
@@ -86,6 +101,11 @@ PluginCapabilityId capability_id(PluginCapabilityType type, const char* name, co
|
|||||||
|
|
||||||
TEST_CASE("Capability config API is exposed on every Python capability", "[PluginConfig][Python]")
|
TEST_CASE("Capability config API is exposed on every Python capability", "[PluginConfig][Python]")
|
||||||
{
|
{
|
||||||
|
ScopedPluginManager plugin_system; // declared first: destroyed last
|
||||||
|
if (!plugin_system.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down
|
||||||
|
|
||||||
py::module_ orca = import_orca_module();
|
py::module_ orca = import_orca_module();
|
||||||
REQUIRE(py::hasattr(orca, "PythonPluginBase"));
|
REQUIRE(py::hasattr(orca, "PythonPluginBase"));
|
||||||
|
|
||||||
@@ -107,6 +127,11 @@ TEST_CASE("Capability config API is exposed on every Python capability", "[Plugi
|
|||||||
|
|
||||||
TEST_CASE("get_config returns only cap_config and save_config persists it", "[PluginConfig][Python]")
|
TEST_CASE("get_config returns only cap_config and save_config persists it", "[PluginConfig][Python]")
|
||||||
{
|
{
|
||||||
|
ScopedPluginManager plugin_system; // declared first: destroyed last
|
||||||
|
if (!plugin_system.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down
|
||||||
|
|
||||||
ScopedDataDir data_dir_guard("plugin-config-py-roundtrip");
|
ScopedDataDir data_dir_guard("plugin-config-py-roundtrip");
|
||||||
host_config().load(); // reset the singleton's in-memory store against the empty temp dir
|
host_config().load(); // reset the singleton's in-memory store against the empty temp dir
|
||||||
|
|
||||||
@@ -138,6 +163,11 @@ TEST_CASE("get_config returns only cap_config and save_config persists it", "[Pl
|
|||||||
|
|
||||||
TEST_CASE("save_config rejects a string that is not valid JSON", "[PluginConfig][Python]")
|
TEST_CASE("save_config rejects a string that is not valid JSON", "[PluginConfig][Python]")
|
||||||
{
|
{
|
||||||
|
ScopedPluginManager plugin_system; // declared first: destroyed last
|
||||||
|
if (!plugin_system.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down
|
||||||
|
|
||||||
ScopedDataDir data_dir_guard("plugin-config-py-badjson");
|
ScopedDataDir data_dir_guard("plugin-config-py-badjson");
|
||||||
host_config().load();
|
host_config().load();
|
||||||
|
|
||||||
@@ -152,6 +182,11 @@ TEST_CASE("save_config rejects a string that is not valid JSON", "[PluginConfig]
|
|||||||
|
|
||||||
TEST_CASE("Saving one capability's config does not touch another's", "[PluginConfig][Python]")
|
TEST_CASE("Saving one capability's config does not touch another's", "[PluginConfig][Python]")
|
||||||
{
|
{
|
||||||
|
ScopedPluginManager plugin_system; // declared first: destroyed last
|
||||||
|
if (!plugin_system.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down
|
||||||
|
|
||||||
ScopedDataDir data_dir_guard("plugin-config-py-isolation");
|
ScopedDataDir data_dir_guard("plugin-config-py-isolation");
|
||||||
host_config().load();
|
host_config().load();
|
||||||
|
|
||||||
@@ -188,6 +223,11 @@ TEST_CASE("Saving one capability's config does not touch another's", "[PluginCon
|
|||||||
|
|
||||||
TEST_CASE("Config API refuses a capability the host never materialized", "[PluginConfig][Python]")
|
TEST_CASE("Config API refuses a capability the host never materialized", "[PluginConfig][Python]")
|
||||||
{
|
{
|
||||||
|
ScopedPluginManager plugin_system; // declared first: destroyed last
|
||||||
|
if (!plugin_system.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down
|
||||||
|
|
||||||
ScopedDataDir data_dir_guard("plugin-config-py-unowned");
|
ScopedDataDir data_dir_guard("plugin-config-py-unowned");
|
||||||
host_config().load();
|
host_config().load();
|
||||||
|
|
||||||
@@ -202,6 +242,11 @@ TEST_CASE("Config API refuses a capability the host never materialized", "[Plugi
|
|||||||
|
|
||||||
TEST_CASE("Custom config UI hooks dispatch to the Python override", "[PluginConfig][Python]")
|
TEST_CASE("Custom config UI hooks dispatch to the Python override", "[PluginConfig][Python]")
|
||||||
{
|
{
|
||||||
|
ScopedPluginManager plugin_system; // declared first: destroyed last
|
||||||
|
if (!plugin_system.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down
|
||||||
|
|
||||||
py::object cap = make_capability("CustomUiCap",
|
py::object cap = make_capability("CustomUiCap",
|
||||||
" def get_name(self): return 'cap_a'\n"
|
" def get_name(self): return 'cap_a'\n"
|
||||||
" def has_config_ui(self): return True\n"
|
" def has_config_ui(self): return True\n"
|
||||||
@@ -216,6 +261,11 @@ TEST_CASE("Custom config UI hooks dispatch to the Python override", "[PluginConf
|
|||||||
|
|
||||||
TEST_CASE("A capability that omits the config UI hooks gets the default editor", "[PluginConfig][Python]")
|
TEST_CASE("A capability that omits the config UI hooks gets the default editor", "[PluginConfig][Python]")
|
||||||
{
|
{
|
||||||
|
ScopedPluginManager plugin_system; // declared first: destroyed last
|
||||||
|
if (!plugin_system.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down
|
||||||
|
|
||||||
ScopedDataDir data_dir_guard("plugin-config-py-bare");
|
ScopedDataDir data_dir_guard("plugin-config-py-bare");
|
||||||
host_config().load();
|
host_config().load();
|
||||||
|
|
||||||
@@ -234,6 +284,11 @@ TEST_CASE("A capability that omits the config UI hooks gets the default editor",
|
|||||||
|
|
||||||
TEST_CASE("get_default_config supplies the value Restore defaults writes back", "[PluginConfig][Python]")
|
TEST_CASE("get_default_config supplies the value Restore defaults writes back", "[PluginConfig][Python]")
|
||||||
{
|
{
|
||||||
|
ScopedPluginManager plugin_system; // declared first: destroyed last
|
||||||
|
if (!plugin_system.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down
|
||||||
|
|
||||||
SECTION("not overridden -> an empty config")
|
SECTION("not overridden -> an empty config")
|
||||||
{
|
{
|
||||||
// Already "restore defaults" for a capability that keeps its stored config sparse and applies
|
// Already "restore defaults" for a capability that keeps its stored config sparse and applies
|
||||||
@@ -291,6 +346,11 @@ TEST_CASE("get_default_config supplies the value Restore defaults writes back",
|
|||||||
|
|
||||||
TEST_CASE("Restoring defaults overwrites only the target capability", "[PluginConfig][Python]")
|
TEST_CASE("Restoring defaults overwrites only the target capability", "[PluginConfig][Python]")
|
||||||
{
|
{
|
||||||
|
ScopedPluginManager plugin_system; // declared first: destroyed last
|
||||||
|
if (!plugin_system.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down
|
||||||
|
|
||||||
ScopedDataDir data_dir_guard("plugin-config-py-restore");
|
ScopedDataDir data_dir_guard("plugin-config-py-restore");
|
||||||
host_config().load();
|
host_config().load();
|
||||||
|
|
||||||
@@ -313,6 +373,11 @@ TEST_CASE("Restoring defaults overwrites only the target capability", "[PluginCo
|
|||||||
|
|
||||||
TEST_CASE("A raising get_default_config leaves the stored config untouched", "[PluginConfig][Python]")
|
TEST_CASE("A raising get_default_config leaves the stored config untouched", "[PluginConfig][Python]")
|
||||||
{
|
{
|
||||||
|
ScopedPluginManager plugin_system; // declared first: destroyed last
|
||||||
|
if (!plugin_system.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down
|
||||||
|
|
||||||
ScopedDataDir data_dir_guard("plugin-config-py-restore-raise");
|
ScopedDataDir data_dir_guard("plugin-config-py-restore-raise");
|
||||||
host_config().load();
|
host_config().load();
|
||||||
|
|
||||||
@@ -333,6 +398,11 @@ TEST_CASE("A raising get_default_config leaves the stored config untouched", "[P
|
|||||||
|
|
||||||
TEST_CASE("A raising config UI hook surfaces as an exception the host can catch", "[PluginConfig][Python]")
|
TEST_CASE("A raising config UI hook surfaces as an exception the host can catch", "[PluginConfig][Python]")
|
||||||
{
|
{
|
||||||
|
ScopedPluginManager plugin_system; // declared first: destroyed last
|
||||||
|
if (!plugin_system.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down
|
||||||
|
|
||||||
py::object cap = make_capability("RaisingCap",
|
py::object cap = make_capability("RaisingCap",
|
||||||
" def get_name(self): return 'cap_a'\n"
|
" def get_name(self): return 'cap_a'\n"
|
||||||
" def has_config_ui(self): return True\n"
|
" def has_config_ui(self): return True\n"
|
||||||
@@ -351,6 +421,11 @@ TEST_CASE("A raising config UI hook surfaces as an exception the host can catch"
|
|||||||
|
|
||||||
TEST_CASE("A config UI hook returning the wrong type does not crash the host", "[PluginConfig][Python]")
|
TEST_CASE("A config UI hook returning the wrong type does not crash the host", "[PluginConfig][Python]")
|
||||||
{
|
{
|
||||||
|
ScopedPluginManager plugin_system; // declared first: destroyed last
|
||||||
|
if (!plugin_system.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down
|
||||||
|
|
||||||
// has_config_ui() is plugin-authored, so it can return anything; the host must survive the call.
|
// has_config_ui() is plugin-authored, so it can return anything; the host must survive the call.
|
||||||
py::object cap = make_capability("BadTypeCap",
|
py::object cap = make_capability("BadTypeCap",
|
||||||
" def get_name(self): return 'cap_a'\n"
|
" def get_name(self): return 'cap_a'\n"
|
||||||
|
|||||||
@@ -22,13 +22,16 @@ using json = nlohmann::json;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Shut both singletons down while boost::log is still alive; left to their static
|
// Brings the plugin manager up and shuts both singletons down while boost::log is still alive;
|
||||||
// destructors, shutdown()'s logging runs after boost::log tears down its thread-local
|
// left to their static destructors, shutdown()'s logging runs after boost::log tears down its
|
||||||
// storage and crashes the process on exit (same reason ScopedPluginManager exists in
|
// thread-local storage and crashes the process on exit (same reason ScopedPluginManager exists in
|
||||||
// test_plugin_lifecycle.cpp). No initialize() needed: discovery and the cloud-metadata
|
// test_plugin_lifecycle.cpp). initialize() IS needed here: the plugin under test is a
|
||||||
// merge never touch Python, but manager shutdown instantiates the interpreter singleton.
|
// slicing-pipeline script, so discover_plugins() brings up the Python interpreter to parse it,
|
||||||
|
// same as any other plugin.
|
||||||
struct ScopedManagerShutdown
|
struct ScopedManagerShutdown
|
||||||
{
|
{
|
||||||
|
bool initialized = PluginManager::instance().initialize();
|
||||||
|
|
||||||
~ScopedManagerShutdown()
|
~ScopedManagerShutdown()
|
||||||
{
|
{
|
||||||
PluginManager::instance().shutdown();
|
PluginManager::instance().shutdown();
|
||||||
@@ -63,7 +66,9 @@ print('ok')
|
|||||||
TEST_CASE("cloud metadata refresh preserves a plugin's stored config", "[PluginCloudMetadata]")
|
TEST_CASE("cloud metadata refresh preserves a plugin's stored config", "[PluginCloudMetadata]")
|
||||||
{
|
{
|
||||||
ScopedManagerShutdown manager_shutdown_guard; // declared first: destroyed last
|
ScopedManagerShutdown manager_shutdown_guard; // declared first: destroyed last
|
||||||
ScopedDataDir data_dir_guard("cloud-meta-config");
|
if (!manager_shutdown_guard.initialized)
|
||||||
|
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||||
|
ScopedDataDir data_dir_guard("cloud-meta-config");
|
||||||
|
|
||||||
// A locally-installed cloud plugin: package .py plus an install-state sidecar carrying the
|
// A locally-installed cloud plugin: package .py plus an install-state sidecar carrying the
|
||||||
// cloud identity. Discovery derives plugin_key from the cloud UUID.
|
// cloud identity. Discovery derives plugin_key from the cloud UUID.
|
||||||
|
|||||||
Reference in New Issue
Block a user