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)); + } + } + } +}