diff --git a/CMakeLists.txt b/CMakeLists.txt index b13fd73..98384cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -327,6 +327,26 @@ target_link_libraries(script_engine_tests PRIVATE Vorbis::vorbis ) add_test(NAME script_engine_tests COMMAND script_engine_tests) + +add_executable(bgfx_gui_service_tests + tests/test_bgfx_gui_service.cpp + src/services/impl/bgfx_gui_service.cpp +) +target_include_directories(bgfx_gui_service_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") +target_link_libraries(bgfx_gui_service_tests PRIVATE + ${SDL_TARGET} + ${SDL3CPP_RENDER_STACK_LIBS} + ${SDL3CPP_FREETYPE_LIBS} + lunasvg::lunasvg +) +if(TARGET shaderc::shaderc) + target_link_libraries(bgfx_gui_service_tests PRIVATE shaderc::shaderc) +elseif(TARGET shaderc::shaderc_combined) + target_link_libraries(bgfx_gui_service_tests PRIVATE shaderc::shaderc_combined) +else() + message(FATAL_ERROR "shaderc CMake target not found") +endif() +add_test(NAME bgfx_gui_service_tests COMMAND bgfx_gui_service_tests) endif() add_executable(gxm_backend_tests diff --git a/scripts/dev_commands.py b/scripts/dev_commands.py index 8cbc245..93ffbf6 100755 --- a/scripts/dev_commands.py +++ b/scripts/dev_commands.py @@ -315,6 +315,35 @@ def build(args: argparse.Namespace) -> None: run_argvs([cmd], args.dry_run, env_overrides=vita_env) +def tests(args: argparse.Namespace) -> None: + """Build (optional) and run ctest for a given build directory.""" + build_dir = _as_build_dir(args.build_dir, DEFAULT_BUILD_DIR) + vita_env = _resolve_vita_env_for_build(build_dir) + argvs: list[list[str]] = [] + + if args.build_first: + build_cmd: list[str] = ["cmake", "--build", build_dir] + if args.config: + build_cmd.extend(["--config", args.config]) + if args.target: + build_cmd.extend(["--target", args.target]) + build_tool_args = _strip_leading_double_dash(args.build_tool_args) + if build_tool_args: + build_cmd.append("--") + build_cmd.extend(build_tool_args) + argvs.append(build_cmd) + + ctest_cmd: list[str] = ["ctest", "--output-on-failure", "--test-dir", build_dir] + if args.config: + ctest_cmd.extend(["-C", args.config]) + ctest_args = _strip_leading_double_dash(args.ctest_args) + if ctest_args: + ctest_cmd.extend(ctest_args) + argvs.append(ctest_cmd) + + run_argvs(argvs, args.dry_run, env_overrides=vita_env) + + def _cmd_one_liner_vcvars_then(bat: str, arch: str, then_parts: Sequence[str]) -> list[str]: """ Construct a command to call a Visual Studio environment setup batch file and @@ -552,7 +581,13 @@ def gui(args: argparse.Namespace) -> None: layout.addRow("Build Type:", self.build_type_combo) self.target_combo = QComboBox() - self.target_combo.addItems(["sdl3_app", "all"]) + self.target_combo.addItems([ + "sdl3_app", + "all", + "script_engine_tests", + "gxm_backend_tests", + "bgfx_gui_service_tests", + ]) layout.addRow("Target:", self.target_combo) buttons = QDialogButtonBox( @@ -1139,6 +1174,10 @@ def gui(args: argparse.Namespace) -> None: build_action.triggered.connect(self.run_build) dev_menu.addAction(build_action) + tests_action = QAction("Run Tests", self) + tests_action.triggered.connect(self.run_tests) + dev_menu.addAction(tests_action) + sync_action = QAction("Sync Assets", self) sync_action.triggered.connect(self.sync_assets) dev_menu.addAction(sync_action) @@ -1702,6 +1741,25 @@ return {{ vita_env = _resolve_vita_env_for_build(build_dir) self.run_command(cmd, env_overrides=vita_env) + def run_tests(self): + """Build (optional) and run tests""" + if self.preset != "default": + build_dir = f"build-{self.preset.split('-')[0]}" # e.g., build-vita + else: + build_dir = GENERATOR_DEFAULT_DIR.get(self.generator, DEFAULT_BUILD_DIR) + cmd = [ + sys.executable, __file__, "tests", + "--build-dir", build_dir, + "--config", self.build_type, + "--target", "all" + ] + vita_env = None + if self.preset in VITA_PRESETS: + vita_env = _vita_env_overrides(f"preset {self.preset}") + else: + vita_env = _resolve_vita_env_for_build(build_dir) + self.run_command(cmd, env_overrides=vita_env) + def sync_assets(self): """Sync assets into the active build directory""" if self.preset != "default": @@ -1785,6 +1843,40 @@ def main() -> int: ), ) bld.set_defaults(func=build) + tst = subparsers.add_parser("tests", help="build (optional) and run ctest") + tst.add_argument( + "--build-dir", default=DEFAULT_BUILD_DIR, help="which directory to test" + ) + tst.add_argument( + "--config", default="Release", help="configuration for multi-config generators" + ) + tst.add_argument( + "--target", + default="all", + help="target to build before tests (use --no-build to skip)", + ) + tst.add_argument( + "--no-build", + action="store_true", + help="skip build step and only run tests", + ) + tst.add_argument( + "--build-tool-args", + nargs=argparse.REMAINDER, + help=( + "extra args forwarded to the underlying build tool after `--` " + "(prefix with '--' before the tool args if needed)" + ), + ) + tst.add_argument( + "--ctest-args", + nargs=argparse.REMAINDER, + help=( + "extra arguments forwarded to ctest " + "(prefix with '--' before ctest flags if needed)" + ), + ) + tst.set_defaults(func=tests, build_first=True) msvc = subparsers.add_parser( "msvc-quick", help="run a VS env setup + follow-on command (README one-liner style)" ) @@ -1849,6 +1941,8 @@ def main() -> int: ) guip.set_defaults(func=gui) args = parser.parse_args() + if hasattr(args, "no_build") and args.no_build: + args.build_first = False try: args.func(args) except subprocess.CalledProcessError as exc: diff --git a/tests/test_bgfx_gui_service.cpp b/tests/test_bgfx_gui_service.cpp new file mode 100644 index 0000000..ba61c4a --- /dev/null +++ b/tests/test_bgfx_gui_service.cpp @@ -0,0 +1,213 @@ +#define private public +#include "services/impl/bgfx_gui_service.hpp" +#undef private + +#include "services/interfaces/i_config_service.hpp" +#include "services/interfaces/i_logger.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace { +class TestLogger : public sdl3cpp::services::ILogger { +public: + void SetLevel(sdl3cpp::services::LogLevel level) override { + level_ = level; + } + + sdl3cpp::services::LogLevel GetLevel() const override { + return level_; + } + + void SetOutputFile(const std::string& filename) override { + (void)filename; + } + + void SetMaxLinesPerFile(size_t maxLines) override { + (void)maxLines; + } + + void EnableConsoleOutput(bool enable) override { + consoleEnabled_ = enable; + } + + void Log(sdl3cpp::services::LogLevel level, const std::string& message) override { + if (level < level_) { + return; + } + entries_.emplace_back(level, message); + if (consoleEnabled_) { + std::cout << message << '\n'; + } + } + + void Trace(const std::string& message) override { + Log(sdl3cpp::services::LogLevel::TRACE, message); + } + + void Trace(const std::string& className, + const std::string& methodName, + const std::string& args = "", + const std::string& message = "") override { + std::string formattedMessage = className + "::" + methodName; + if (!args.empty()) { + formattedMessage += "(" + args + ")"; + } + if (!message.empty()) { + formattedMessage += ": " + message; + } + Log(sdl3cpp::services::LogLevel::TRACE, formattedMessage); + } + + void Debug(const std::string& message) override { + Log(sdl3cpp::services::LogLevel::DEBUG, message); + } + + void Info(const std::string& message) override { + Log(sdl3cpp::services::LogLevel::INFO, message); + } + + void Warn(const std::string& message) override { + Log(sdl3cpp::services::LogLevel::WARN, message); + } + + void Error(const std::string& message) override { + Log(sdl3cpp::services::LogLevel::ERROR, message); + } + + void TraceFunction(const std::string& funcName) override { + Trace("Entering " + funcName); + } + + void TraceVariable(const std::string& name, const std::string& value) override { + Trace(name + " = " + value); + } + + void TraceVariable(const std::string& name, int value) override { + TraceVariable(name, std::to_string(value)); + } + + void TraceVariable(const std::string& name, size_t value) override { + TraceVariable(name, std::to_string(value)); + } + + void TraceVariable(const std::string& name, bool value) override { + TraceVariable(name, std::string(value ? "true" : "false")); + } + + void TraceVariable(const std::string& name, float value) override { + TraceVariable(name, std::to_string(value)); + } + + void TraceVariable(const std::string& name, double value) override { + TraceVariable(name, std::to_string(value)); + } + + bool HasErrorSubstring(const std::string& fragment) const { + for (const auto& entry : entries_) { + if (entry.first == sdl3cpp::services::LogLevel::ERROR && + entry.second.find(fragment) != std::string::npos) { + return true; + } + } + return false; + } + +private: + sdl3cpp::services::LogLevel level_ = sdl3cpp::services::LogLevel::TRACE; + bool consoleEnabled_ = false; + std::vector> entries_; +}; + +class StubConfigService : public sdl3cpp::services::IConfigService { +public: + uint32_t GetWindowWidth() const override { return 1; } + uint32_t GetWindowHeight() const override { return 1; } + std::filesystem::path GetScriptPath() const override { return {}; } + bool IsLuaDebugEnabled() const override { return false; } + std::string GetWindowTitle() const override { return ""; } + const sdl3cpp::services::InputBindings& GetInputBindings() const override { return inputBindings_; } + const sdl3cpp::services::MouseGrabConfig& GetMouseGrabConfig() const override { return mouseGrabConfig_; } + const sdl3cpp::services::BgfxConfig& GetBgfxConfig() const override { return bgfxConfig_; } + const sdl3cpp::services::MaterialXConfig& GetMaterialXConfig() const override { return materialXConfig_; } + const std::vector& GetMaterialXMaterialConfigs() const override { + return materialXMaterials_; + } + const sdl3cpp::services::GuiFontConfig& GetGuiFontConfig() const override { return guiFontConfig_; } + const std::string& GetConfigJson() const override { return configJson_; } + + void DisableFreeType() { + guiFontConfig_.useFreeType = false; + } + +private: + sdl3cpp::services::InputBindings inputBindings_{}; + sdl3cpp::services::MouseGrabConfig mouseGrabConfig_{}; + sdl3cpp::services::BgfxConfig bgfxConfig_{}; + sdl3cpp::services::MaterialXConfig materialXConfig_{}; + std::vector materialXMaterials_{}; + sdl3cpp::services::GuiFontConfig guiFontConfig_{}; + std::string configJson_{}; +}; + +void Assert(bool condition, const std::string& message, int& failures) { + if (!condition) { + std::cerr << "test failure: " << message << '\n'; + ++failures; + } +} +} // namespace + +int main() { + int failures = 0; + + bgfx::Init init{}; + init.type = bgfx::RendererType::Noop; + init.resolution.width = 1; + init.resolution.height = 1; + init.resolution.reset = BGFX_RESET_NONE; + + if (!bgfx::init(init)) { + std::cerr << "test failure: bgfx init failed (Noop renderer)" << '\n'; + return 1; + } + + try { + auto logger = std::make_shared(); + auto configService = std::make_shared(); + configService->DisableFreeType(); + + sdl3cpp::services::impl::BgfxGuiService service(configService, logger); + service.InitializeResources(); + + Assert(bgfx::isValid(service.program_), "GUI shader program should link", failures); + Assert(bgfx::isValid(service.whiteTexture_), "white texture should be created", failures); + + if (!bgfx::isValid(service.program_) && + !logger->HasErrorSubstring("bgfx::createProgram failed to link shaders")) { + Assert(false, "missing bgfx::createProgram link failure log", failures); + } + + service.Shutdown(); + } catch (const std::exception& ex) { + std::cerr << "exception during bgfx gui service tests: " << ex.what() << '\n'; + bgfx::shutdown(); + return 1; + } + + bgfx::shutdown(); + + if (failures == 0) { + std::cout << "bgfx_gui_service_tests: PASSED" << '\n'; + } else { + std::cerr << "bgfx_gui_service_tests: FAILED (" << failures << " errors)" << '\n'; + } + + return failures; +}