diff --git a/docs/guides/applications.md b/docs/guides/applications.md
index 111427e33..6211edbd8 100644
--- a/docs/guides/applications.md
+++ b/docs/guides/applications.md
@@ -95,7 +95,7 @@ If a shutdown hook returns `false` or throws, the launcher reports its phase, mo
diff --git a/docs/guides/applications_CN.md b/docs/guides/applications_CN.md
index 5e0048533..29e45df6a 100644
--- a/docs/guides/applications_CN.md
+++ b/docs/guides/applications_CN.md
@@ -95,7 +95,7 @@ macOS/Linux 可使用 Ctrl+C 或 `SIGTERM`,由应用主循环处理停止请
diff --git a/docs/guides/modules.md b/docs/guides/modules.md
index 84bb820e2..298345e10 100644
--- a/docs/guides/modules.md
+++ b/docs/guides/modules.md
@@ -104,9 +104,9 @@ Normal shutdown first stops managed top-level task admission and keeps pumping a
diff --git a/docs/guides/modules_CN.md b/docs/guides/modules_CN.md
index 85863f8cc..9c003d3bf 100644
--- a/docs/guides/modules_CN.md
+++ b/docs/guides/modules_CN.md
@@ -104,9 +104,9 @@ private:
diff --git a/include/ark/plugin_manager.hpp b/include/ark/plugin_manager.hpp
index 8d361ada4..2627328ca 100644
--- a/include/ark/plugin_manager.hpp
+++ b/include/ark/plugin_manager.hpp
@@ -106,6 +106,7 @@ class plugin_manager final : private noncopyable
throw std::logic_error("Plugin manager cannot start during a lifecycle call or after shutdown begins");
}
phase_guard active(*this, call_phase::starting);
+ startup_failure_.clear();
timestamp_ = timestamp;
plugin_library_dir_ = lib_dir;
@@ -241,6 +242,11 @@ class plugin_manager final : private noncopyable
return pending_tasks_unchecked();
}
+ const std::string& get_startup_failure() const {
+ check_owner();
+ return startup_failure_;
+ }
+
shutdown_state get_shutdown_state() const {
check_owner();
return shutdown_state_;
@@ -445,8 +451,11 @@ class plugin_manager final : private noncopyable
for (auto const& iter : plugins_)
{
phase_guard installing(*this, call_phase::installing);
- bool ret = load_plugin_library(iter.first);
- ARK_ASSERT_RET_VAL(ret, false);
+ startup_failure_ = "load_plugin: " + iter.first;
+ if (!load_plugin_library(iter.first))
+ {
+ return false;
+ }
}
// initialize all modules
@@ -455,12 +464,14 @@ class plugin_manager final : private noncopyable
module* module_ptr = iter.second;
ARK_ASSERT_CONTINUE(module_ptr != nullptr);
+ startup_failure_ = "init: " + iter.first;
if (!module_ptr->init())
{
return false;
}
}
+ startup_failure_.clear();
return true;
}
@@ -470,12 +481,14 @@ class plugin_manager final : private noncopyable
module* module_ptr = iter.second;
ARK_ASSERT_CONTINUE(module_ptr != nullptr);
+ startup_failure_ = "post_init: " + iter.first;
if (!module_ptr->post_init())
{
return false;
}
}
+ startup_failure_.clear();
return true;
}
@@ -485,12 +498,14 @@ class plugin_manager final : private noncopyable
module* module_ptr = iter.second;
ARK_ASSERT_CONTINUE(module_ptr != nullptr);
+ startup_failure_ = "check_config: " + iter.first;
if (!module_ptr->check_config())
{
return false;
}
}
+ startup_failure_.clear();
return true;
}
@@ -500,12 +515,14 @@ class plugin_manager final : private noncopyable
module* module_ptr = iter.second;
ARK_ASSERT_CONTINUE(module_ptr != nullptr);
+ startup_failure_ = "pre_update: " + iter.first;
if (!module_ptr->pre_update())
{
return false;
}
}
+ startup_failure_.clear();
return true;
}
@@ -590,7 +607,13 @@ class plugin_manager final : private noncopyable
{
plugin_libs_.insert(std::make_pair(plugin_name, library));
auto func = reinterpret_cast(library->get_symbol("DllEntryPlugin"));
- ARK_ASSERT_RET_VAL(func != nullptr, false);
+ if (func == nullptr)
+ {
+ ARK_LOG_ERROR("Shared library[{}] has no DllEntryPlugin entrypoint", library->get_name());
+ // Keep an already loaded handle until process exit. Static
+ // initializers may have started work before symbol validation.
+ return false;
+ }
func(this, plugin_name, logger::instance());
return true;
@@ -603,10 +626,10 @@ class plugin_manager final : private noncopyable
auto error = dlerror();
#endif
ARK_LOG_ERROR("Load shared library[{}] failed, ErrorNo[{}]", library->get_name(), error);
- ARK_ASSERT_RET_VAL(0, false);
+ // Native loading failed, so this object owns no loaded handle.
+ ARK_DELETE(library);
+ return false;
}
-
- return true;
}
auto unload_plugin_library(const std::string& plugin_name) -> bool {
@@ -681,6 +704,7 @@ class plugin_manager final : private noncopyable
shutdown_state shutdown_state_{shutdown_state::running};
std::exception_ptr shutdown_exception_;
std::string shutdown_failure_;
+ std::string startup_failure_;
// Bus id
int bus_id_{0};
diff --git a/src/app/main.cpp b/src/app/main.cpp
index 040d3d0b7..020647284 100644
--- a/src/app/main.cpp
+++ b/src/app/main.cpp
@@ -304,6 +304,16 @@ bool main_loop(std::exception_ptr* error = nullptr) {
#endif
}
+void report_startup_failure(const char* detail) {
+ std::cerr << "Application startup failed";
+ const auto& failure = plugin_manager::instance()->get_startup_failure();
+ if (!failure.empty())
+ {
+ std::cerr << " at " << failure;
+ }
+ std::cerr << ": " << detail << std::endl;
+}
+
int main(int argc, char* argv[]) {
std::chrono::milliseconds shutdown_timeout{};
if (!parse_args(argc, argv, shutdown_timeout))
@@ -324,10 +334,23 @@ int main(int argc, char* argv[]) {
return 1;
}
- if (!plugin_manager::instance()->start(
- date_time::get_now_time(), manifest.plugin_library_dir, manifest.plugin_config_dir, manifest.plugins))
+ try
+ {
+ if (!plugin_manager::instance()->start(
+ date_time::get_now_time(), manifest.plugin_library_dir, manifest.plugin_config_dir, manifest.plugins))
+ {
+ report_startup_failure("returned false");
+ return 1;
+ }
+ }
+ catch (const std::exception& error)
+ {
+ report_startup_failure(error.what());
+ return 1;
+ }
+ catch (...)
{
- std::cerr << "Application startup failed" << std::endl;
+ report_startup_failure("unknown exception");
return 1;
}
diff --git a/tests/app/missing_entry_plugin.cpp b/tests/app/missing_entry_plugin.cpp
new file mode 100644
index 000000000..0ba91718e
--- /dev/null
+++ b/tests/app/missing_entry_plugin.cpp
@@ -0,0 +1,26 @@
+// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE.
+#include
+
+#include
+
+namespace ark {
+class plugin_manager;
+}
+
+namespace {
+
+struct library_probe
+{
+ library_probe() {
+ std::cerr << "app-startup-test:library_loaded\n";
+ }
+};
+
+library_probe loaded_library;
+
+} // namespace
+
+// A loadable library deliberately omitting DllEntryPlugin.
+ARK_EXPORT_FUNC void DllExitPlugin(ark::plugin_manager*) {
+ std::cerr << "app-startup-test:uninstall\n";
+}
diff --git a/tests/app/startup_failure_plugin.cpp b/tests/app/startup_failure_plugin.cpp
index 33ef99dc2..f50303c74 100644
--- a/tests/app/startup_failure_plugin.cpp
+++ b/tests/app/startup_failure_plugin.cpp
@@ -1,6 +1,7 @@
// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE.
#include
#include
+#include
#include
#include
#include
@@ -13,7 +14,9 @@ class startup_failure_module final : public ark::module
{
public:
void fail_at(std::string phase) {
- failed_phase_ = std::move(phase);
+ const auto separator = phase.find(':');
+ behavior_ = separator == std::string::npos ? "false" : phase.substr(0, separator);
+ failed_phase_ = separator == std::string::npos ? std::move(phase) : phase.substr(separator + 1);
}
const std::string& get_name() const override {
@@ -58,10 +61,23 @@ class startup_failure_module final : public ark::module
private:
bool record(std::string_view phase) {
std::cerr << "app-startup-test:" << phase << '\n';
- return phase != failed_phase_;
+ if (phase == failed_phase_)
+ {
+ if (behavior_ == "throw")
+ {
+ throw std::runtime_error("test startup exception in " + std::string(phase));
+ }
+ if (behavior_ == "unknown")
+ {
+ throw 7;
+ }
+ return false;
+ }
+ return true;
}
std::string failed_phase_;
+ std::string behavior_;
bool updated_{};
};
@@ -73,15 +89,22 @@ startup_failure_module fixture;
ARK_EXPORT_FUNC void DllEntryPlugin(
ark::plugin_manager* manager, const std::string& plugin_name, ark::logger* shared_logger) {
ark::logger::init(shared_logger);
- fixture.fail_at(manager->get_plugin_conf(plugin_name));
+ const auto& mode = manager->get_plugin_conf(plugin_name);
+ fixture.fail_at(mode);
if (!manager->add_module(fixture.get_name(), &fixture) || !manager->add_update_module(&fixture))
{
std::cerr << "Startup test module registration failed\n";
std::abort();
}
+ if (mode == "throw:load_plugin")
+ {
+ std::cerr << "app-startup-test:load_plugin\n";
+ throw std::runtime_error("test plugin entry exception");
+ }
}
ARK_EXPORT_FUNC void DllExitPlugin(ark::plugin_manager* manager) {
+ std::cerr << "app-startup-test:uninstall\n";
manager->remove_update_module(fixture.get_name());
manager->remove_module(fixture.get_name());
}
diff --git a/tests/app/startup_failures.cmake b/tests/app/startup_failures.cmake
index 633906025..b92b095b3 100644
--- a/tests/app/startup_failures.cmake
+++ b/tests/app/startup_failures.cmake
@@ -15,17 +15,28 @@ string(REPLACE "<" "<" xml_plugin_directory "${xml_plugin_directory}")
string(REPLACE "\"" "\\\"" toml_plugin_directory "${plugin_directory}")
set(plugin_name ark_app_startup_failure_plugin)
-foreach(failed_phase init post_init check_config pre_update)
+foreach(mode false:init false:post_init false:check_config false:pre_update
+ throw:init throw:post_init throw:check_config throw:pre_update unknown:init throw:load_plugin missing_library:load_plugin missing_entry:load_plugin)
+ string(REPLACE ":" ";" parts "${mode}")
+ list(GET parts 0 behavior)
+ list(GET parts 1 failed_phase)
+ if(behavior STREQUAL "missing_library")
+ set(case_plugin ark_app_missing_library_plugin)
+ elseif(behavior STREQUAL "missing_entry")
+ set(case_plugin ark_app_missing_entry_plugin)
+ else()
+ set(case_plugin "${plugin_name}")
+ endif()
foreach(format toml app.conf)
- set(manifest "${RUNTIME_DIR}/${failed_phase}.${format}")
+ set(manifest "${RUNTIME_DIR}/${behavior}-${failed_phase}.${format}")
if(format STREQUAL "toml")
file(WRITE "${manifest}"
"[application]\nplugin_library_dir = \"${toml_plugin_directory}\"\n"
- "[[plugins]]\nname = \"${plugin_name}\"\nconfig = \"${failed_phase}\"\n")
+ "[[plugins]]\nname = \"${case_plugin}\"\nconfig = \"${mode}\"\n")
else()
file(WRITE "${manifest}"
""
- "\n")
+ "\n")
endif()
execute_process(
COMMAND "${PROGRAM}" --busid=1.0.1.1 --name=startup-error-test
@@ -42,20 +53,50 @@ foreach(failed_phase init post_init check_config pre_update)
if(NOT result STREQUAL "1")
message(FATAL_ERROR "${failed_phase}/${format}: expected normal exit 1, got ${result}\n${output}\n${errors}")
endif()
- if(NOT errors MATCHES "Application startup failed")
- message(FATAL_ERROR "${failed_phase}/${format}: missing startup diagnostic\n${errors}")
+ if(failed_phase STREQUAL "load_plugin")
+ set(owner "${case_plugin}")
+ else()
+ set(owner app_startup_failure_module)
+ endif()
+ if(NOT errors MATCHES "Application startup failed at ${failed_phase}: ${owner}")
+ message(FATAL_ERROR "${mode}/${format}: missing phase/module-or-plugin startup diagnostic\n${errors}")
+ endif()
+ string(REGEX MATCHALL "Application startup failed" diagnostics "${errors}")
+ list(LENGTH diagnostics count)
+ if(NOT count EQUAL 1)
+ message(FATAL_ERROR "${mode}/${format}: startup failure must be reported once")
endif()
- set(expected)
- foreach(phase init post_init check_config pre_update)
- list(APPEND expected "app-startup-test:${phase}")
- if(phase STREQUAL failed_phase)
- break()
+ if(behavior STREQUAL "throw")
+ if(failed_phase STREQUAL "load_plugin")
+ set(exception "test plugin entry exception")
+ else()
+ set(exception "test startup exception in ${failed_phase}")
endif()
- endforeach()
+ if(NOT errors MATCHES "${exception}")
+ message(FATAL_ERROR "${mode}/${format}: exception details were lost\n${errors}")
+ endif()
+ elseif(behavior STREQUAL "unknown" AND NOT errors MATCHES "unknown exception")
+ message(FATAL_ERROR "${mode}/${format}: unknown exception was not identified\n${errors}")
+ endif()
+ set(expected "")
+ if(behavior STREQUAL "missing_entry")
+ list(APPEND expected "app-startup-test:library_loaded")
+ elseif(behavior STREQUAL "missing_library")
+ # No module or library initializer can run for a missing file.
+ elseif(failed_phase STREQUAL "load_plugin")
+ list(APPEND expected "app-startup-test:load_plugin")
+ else()
+ foreach(phase init post_init check_config pre_update)
+ list(APPEND expected "app-startup-test:${phase}")
+ if(phase STREQUAL failed_phase)
+ break()
+ endif()
+ endforeach()
+ endif()
string(REGEX MATCHALL "app-startup-test:[a-z_]+" observed "${errors}")
- if(NOT observed STREQUAL expected)
+ if(NOT "${observed}" STREQUAL "${expected}")
message(FATAL_ERROR "${failed_phase}/${format}: unexpected lifecycle calls\nexpected=${expected}\nobserved=${observed}\n${errors}")
endif()
- message(STATUS "${failed_phase}/${format}: normal exit 1; no later phase, update or shutdown call")
+ message(STATUS "${mode}/${format}: diagnostic and normal exit 1; no later phase, update, shutdown or uninstall call")
endforeach()
endforeach()
diff --git a/tests/core/CMakeLists.txt b/tests/core/CMakeLists.txt
index 96db53914..adbd2f00c 100644
--- a/tests/core/CMakeLists.txt
+++ b/tests/core/CMakeLists.txt
@@ -113,13 +113,16 @@ if(ARK_BUILD_CONFIG)
add_library(ark_app_startup_failure_plugin SHARED ../app/startup_failure_plugin.cpp)
set_property(TARGET ark_app_startup_failure_plugin PROPERTY LINK_LIBRARIES ark::core)
set_target_properties(ark_app_startup_failure_plugin PROPERTIES PREFIX "")
+ add_library(ark_app_missing_entry_plugin SHARED ../app/missing_entry_plugin.cpp)
+ set_property(TARGET ark_app_missing_entry_plugin PROPERTY LINK_LIBRARIES ark::core)
+ set_target_properties(ark_app_missing_entry_plugin PROPERTIES PREFIX "")
add_custom_target(check-app-startup-failures
COMMAND ${CMAKE_COMMAND} "-DPROGRAM=$"
"-DPLUGIN_DIR=$"
"-DSOURCE_ROOT=${PROJECT_SOURCE_DIR}"
"-DRUNTIME_DIR=${CMAKE_CURRENT_BINARY_DIR}/startup failure runtime"
-P "${PROJECT_SOURCE_DIR}/tests/app/startup_failures.cmake"
- DEPENDS app ark_app_startup_failure_plugin
+ DEPENDS app ark_app_startup_failure_plugin ark_app_missing_entry_plugin
USES_TERMINAL)
add_dependencies(check-core check-app-startup-failures)
diff --git a/tests/core/README.md b/tests/core/README.md
index 135a0c66f..c9da8a26e 100644
--- a/tests/core/README.md
+++ b/tests/core/README.md
@@ -31,6 +31,8 @@ launcher against missing, malformed and mistyped manifests. It requires normal
exit code 1 with a manifest diagnostic, including on Windows, and is included in
`check-core`.
+`check-app-startup-failures` uses the real launcher and a test DSO for all four startup phases, each returning false or throwing, with both TOML and XML manifests. It also checks a non-standard exception, a throwing plugin entrypoint, a missing library and a loadable DSO with no `DllEntryPlugin`. The missing-entry fixture confirms native library initialization occurred before failure. All 24 children must report one exact phase/module-or-plugin diagnostic, preserve available exception details, and exit normally with code 1 without later startup calls, update, normal shutdown or uninstall. Core tests check retained failure context, unchanged exception propagation, first failure across multiple modules and owner-thread access. These tests establish failure reporting and process exit, not partial-start resource cleanup or same-process recovery.
+
`check-kernel-initialization` loads real metadata and static data from
`resource/meta/`. It checks four kernel/config initialization orders with both
resource-relative paths and legacy paths beginning with `/`, for eight combinations.
diff --git a/tests/core/README_CN.md b/tests/core/README_CN.md
index 70bec67a2..0d5bda96d 100644
--- a/tests/core/README_CN.md
+++ b/tests/core/README_CN.md
@@ -27,6 +27,8 @@ cmake --build core-build --target check-core
字段类型错误的清单,要求正常返回退出码 1 并输出清单诊断。该入口覆盖 Windows,
并纳入 `check-core`。
+`check-app-startup-failures` 使用真实启动器和测试动态库,覆盖四个启动阶段各自返回 false 或抛出异常,并分别读取 TOML 和 XML 清单;还验证非标准异常、插件入口抛出异常、库文件缺失,以及可加载但没有 `DllEntryPlugin` 的动态库。缺少入口的测试库会记录自身初始化,确认库已加载后才发生失败。24 个子进程均须输出一次准确的阶段、模块或插件诊断,保留可用异常信息,以正常退出码 1 结束,且不执行后续启动调用、update、正常关闭或卸载。核心用例检查失败上下文、原始异常传播、多模块中的首次失败停止及所属线程访问。这些测试证明失败报告与进程退出,不证明部分启动资源清理或同进程恢复。
+
`check-kernel-initialization` 从 `resource/meta/` 加载真实元数据和静态数据,
分别使用资源相对路径及以 `/` 开头的旧路径,验证内核与配置模块的四种初始化顺序,共八种组合。
diff --git a/tests/core/business_runtime.cpp b/tests/core/business_runtime.cpp
index c2b7d8193..2f8e29871 100644
--- a/tests/core/business_runtime.cpp
+++ b/tests/core/business_runtime.cpp
@@ -348,6 +348,7 @@ TEST_CASE("Manager owner and active registry checks reject mutations before they
rejects([&] { manager.get_tasks(module); });
rejects([&] { manager.create_task_group(extra); });
rejects([&] { manager.pending_tasks(); });
+ rejects([&] { manager.get_startup_failure(); });
rejects([&] { manager.get_shutdown_state(); });
rejects([&] { manager.get_shutdown_exception(); });
rejects([&] { manager.get_shutdown_failure(); });
@@ -361,7 +362,7 @@ TEST_CASE("Manager owner and active registry checks reject mutations before they
rejects([&] { tasks.start(ready_task()); });
});
worker.join();
- CHECK(rejected == 15);
+ CHECK(rejected == 16);
module.on_update = [&] {
CHECK_THROWS_AS(manager.update(1), std::logic_error);
CHECK_THROWS_AS(manager.remove_module(module.get_name()), std::logic_error);
diff --git a/tests/core/plugin_manager_startup.cpp b/tests/core/plugin_manager_startup.cpp
index 7d3139641..3b105ad29 100644
--- a/tests/core/plugin_manager_startup.cpp
+++ b/tests/core/plugin_manager_startup.cpp
@@ -1,6 +1,7 @@
// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE.
#include
#include
+#include
#include
#include "../network/doctest.h"
@@ -20,11 +21,20 @@ enum class startup_phase : std::size_t
constexpr std::array phase_names{"init", "post_init", "check_config", "pre_update"};
+enum class failure_mode
+{
+ false_result,
+ standard_exception,
+ unknown_exception
+};
+
class counted_module final : public ark::module
{
public:
- explicit counted_module(startup_phase failed_phase = startup_phase::none)
- : failed_phase_(failed_phase) {
+ explicit counted_module(
+ startup_phase failed_phase = startup_phase::none, failure_mode mode = failure_mode::false_result)
+ : failed_phase_(failed_phase)
+ , mode_(mode) {
}
bool init() override {
@@ -43,15 +53,44 @@ class counted_module final : public ark::module
return record(startup_phase::pre_update);
}
+ bool update() override {
+ ++late_calls;
+ return true;
+ }
+
+ bool pre_shut() override {
+ ++late_calls;
+ return true;
+ }
+
+ bool shut() override {
+ ++late_calls;
+ return true;
+ }
+
std::array calls{};
+ unsigned late_calls{};
private:
bool record(startup_phase phase) {
++calls[static_cast(phase)];
- return phase != failed_phase_;
+ if (phase == failed_phase_)
+ {
+ if (mode_ == failure_mode::standard_exception)
+ {
+ throw std::runtime_error("startup module exception");
+ }
+ if (mode_ == failure_mode::unknown_exception)
+ {
+ throw 91;
+ }
+ return false;
+ }
+ return true;
}
startup_phase failed_phase_;
+ failure_mode mode_;
};
class unconfigured_logger_scope
@@ -84,7 +123,9 @@ TEST_CASE("Plugin startup calls each successful module phase once") {
REQUIRE(manager.add_module("counted-module-" + std::to_string(index), &modules[index]));
}
+ CHECK(manager.get_startup_failure().empty());
CHECK(start(manager));
+ CHECK(manager.get_startup_failure().empty());
for (const auto& module : modules)
{
CHECK(module.calls == std::array{1, 1, 1, 1});
@@ -100,6 +141,8 @@ TEST_CASE("Plugin startup returns false without repeating a failed phase or runn
REQUIRE(manager.add_module("counted-module-single", &module));
CHECK_FALSE(start(manager));
+ CHECK(manager.get_startup_failure() == std::string(phase_names[failed]) + ": counted-module-single");
+ CHECK(module.late_calls == 0);
for (std::size_t phase = 0; phase < phase_names.size(); ++phase)
{
CHECK(module.calls[phase] == (phase <= failed ? 1u : 0u));
@@ -110,26 +153,43 @@ TEST_CASE("Plugin startup returns false without repeating a failed phase or runn
TEST_CASE("Plugin startup stops a phase at its first failure independently of module iteration order") {
for (std::size_t failed = 0; failed < phase_names.size(); ++failed)
{
- CAPTURE(std::string(phase_names[failed]));
- ark::plugin_manager manager;
- std::array modules{counted_module(static_cast(failed)),
- counted_module(static_cast(failed)), counted_module(static_cast(failed))};
- for (std::size_t index = 0; index < modules.size(); ++index)
+ for (const auto mode : {failure_mode::false_result, failure_mode::standard_exception})
{
- REQUIRE(manager.add_module("counted-module-" + std::to_string(index), &modules[index]));
- }
+ CAPTURE(std::string(phase_names[failed]));
+ ark::plugin_manager manager;
+ std::array modules{counted_module(static_cast(failed), mode),
+ counted_module(static_cast(failed), mode),
+ counted_module(static_cast(failed), mode)};
+ for (std::size_t index = 0; index < modules.size(); ++index)
+ {
+ REQUIRE(manager.add_module("counted-module-" + std::to_string(index), &modules[index]));
+ }
- CHECK_FALSE(start(manager));
- for (std::size_t phase = 0; phase < phase_names.size(); ++phase)
- {
- unsigned phase_calls = 0;
- for (const auto& module : modules)
+ if (mode == failure_mode::false_result)
+ {
+ CHECK_FALSE(start(manager));
+ }
+ else
{
- CHECK(module.calls[phase] <= 1);
- phase_calls += module.calls[phase];
+ CHECK_THROWS_WITH_AS(start(manager), "startup module exception", std::runtime_error);
+ }
+ for (std::size_t phase = 0; phase < phase_names.size(); ++phase)
+ {
+ unsigned phase_calls = 0;
+ for (const auto& module : modules)
+ {
+ CHECK(module.calls[phase] <= 1);
+ phase_calls += module.calls[phase];
+ if (phase == failed && module.calls[phase] == 1)
+ {
+ const auto index = static_cast(&module - modules.data());
+ CHECK(manager.get_startup_failure() ==
+ std::string(phase_names[failed]) + ": counted-module-" + std::to_string(index));
+ }
+ }
+ const auto expected = phase < failed ? modules.size() : phase == failed ? 1u : 0u;
+ CHECK(phase_calls == expected);
}
- const auto expected = phase < failed ? modules.size() : phase == failed ? 1u : 0u;
- CHECK(phase_calls == expected);
}
}
}
@@ -145,3 +205,30 @@ TEST_CASE("Plugin startup returns false without a configured logger") {
CHECK_FALSE(started);
CHECK(module.calls == std::array{1, 0, 0, 0});
}
+
+TEST_CASE("Startup exceptions preserve the first module and phase without cleanup or exception translation") {
+ for (std::size_t failed = 0; failed < phase_names.size(); ++failed)
+ {
+ for (const auto mode : {failure_mode::standard_exception, failure_mode::unknown_exception})
+ {
+ CAPTURE(std::string(phase_names[failed]));
+ ark::plugin_manager manager;
+ counted_module module(static_cast(failed), mode);
+ REQUIRE(manager.add_module("throwing-module", &module));
+ if (mode == failure_mode::standard_exception)
+ {
+ CHECK_THROWS_WITH_AS(start(manager), "startup module exception", std::runtime_error);
+ }
+ else
+ {
+ CHECK_THROWS_AS(start(manager), int);
+ }
+ CHECK(manager.get_startup_failure() == std::string(phase_names[failed]) + ": throwing-module");
+ CHECK(module.late_calls == 0);
+ for (std::size_t phase = 0; phase < phase_names.size(); ++phase)
+ {
+ CHECK(module.calls[phase] == (phase <= failed ? 1u : 0u));
+ }
+ }
+ }
+}