From 3e09dec613958ec34c73f27fafca073e62ebec3a Mon Sep 17 00:00:00 2001 From: Zedhugh Chen Date: Tue, 24 Mar 2026 02:40:57 +0800 Subject: [PATCH] =?UTF-8?q?config:=20=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=8A=A0=E8=BD=BD=E6=B5=81=E7=A8=8B=E5=92=8C?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/defaults.c | 33 +++++ src/config/defaults.h | 5 + src/config/loader.c | 30 ++-- src/config/runtime_config.c | 178 ++++++++++++++++++++++ src/config/runtime_config.h | 6 + tests/CMakeLists.txt | 1 + tests/config/CMakeLists.txt | 74 ++++++++++ tests/config/config_loader_test.c | 173 ++++++++++++++++++++++ tests/config/fixture_valid_config.c | 28 ++++ tests/config/fixture_without_setup.c | 1 + tests/config/helpers.h | 48 ++++++ tests/config/runtime_config_test.c | 213 +++++++++++++++++++++++++++ 12 files changed, 772 insertions(+), 18 deletions(-) create mode 100644 src/config/defaults.c create mode 100644 src/config/defaults.h create mode 100644 src/config/runtime_config.c create mode 100644 src/config/runtime_config.h create mode 100644 tests/config/CMakeLists.txt create mode 100644 tests/config/config_loader_test.c create mode 100644 tests/config/fixture_valid_config.c create mode 100644 tests/config/fixture_without_setup.c create mode 100644 tests/config/helpers.h create mode 100644 tests/config/runtime_config_test.c diff --git a/src/config/defaults.c b/src/config/defaults.c new file mode 100644 index 0000000..6ab95c7 --- /dev/null +++ b/src/config/defaults.c @@ -0,0 +1,33 @@ +#include "config/defaults.h" + +#include "base/macros.h" + +bool config_defaults_build(const zdwm_api_t *api, + zdwm_config_builder_t *builder, + const zdwm_output_info_t *outputs, + size_t output_count) { + if (!api || !builder || !outputs || output_count == 0) return false; + + zdwm_layout_id_t tile_id = api->register_layout( + builder, "tile", "[]=", "builtin tile", api->builtin_layouts.tile); + zdwm_layout_id_t monocle_id = api->register_layout( + builder, "monocle", "[M]", "builtin monocle", api->builtin_layouts.monocle); + zdwm_layout_id_t floating_id = + api->register_layout(builder, "floating", "><>", "floating", nullptr); + if (tile_id == ZDWM_LAYOUT_ID_INVALID || + monocle_id == ZDWM_LAYOUT_ID_INVALID || + floating_id == ZDWM_LAYOUT_ID_INVALID) { + return false; + } + + zdwm_layout_id_t layout_ids[] = {tile_id, monocle_id, floating_id}; + for (size_t i = 0; i < output_count; i++) { + if (api->define_workspace(builder, i, "main", layout_ids, + countof(layout_ids), + tile_id) == ZDWM_WORKSPACE_ID_INVALID) { + return false; + } + } + + return true; +} diff --git a/src/config/defaults.h b/src/config/defaults.h new file mode 100644 index 0000000..e491d93 --- /dev/null +++ b/src/config/defaults.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +zdwm_config_setup_fn config_defaults_build; diff --git a/src/config/loader.c b/src/config/loader.c index 7eb7c70..53d3405 100644 --- a/src/config/loader.c +++ b/src/config/loader.c @@ -55,26 +55,22 @@ bool config_loader_load(config_loader_t *loader, const char *override_path) { if (!loader) return false; config_loader_cleanup(loader); - char *path = nullptr; bool is_explicit = false; - if (!config_loader_resolve_path(&path, &is_explicit, override_path)) { + if (!config_loader_resolve_path(&loader->path, &is_explicit, override_path)) { return false; } - loader->path = path; + if (access(loader->path, F_OK) != 0) { + if (!is_explicit) return true; - if (access(path, F_OK) != 0) { - if (is_explicit) { - warn("config library not found: %s", path); - config_loader_cleanup(loader); - return false; - } - return true; + warn("config library not found: %s", loader->path); + config_loader_cleanup(loader); + return false; } - loader->handle = dlopen(path, RTLD_NOW | RTLD_LOCAL); + loader->handle = dlopen(loader->path, RTLD_NOW | RTLD_LOCAL); if (!loader->handle) { - warn("failed to load config library %s: %s", path, dlerror()); + warn("failed to load config library %s: %s", loader->path, dlerror()); config_loader_cleanup(loader); return false; } @@ -82,13 +78,11 @@ bool config_loader_load(config_loader_t *loader, const char *override_path) { dlerror(); loader->setup = (zdwm_config_setup_fn *)dlsym(loader->handle, "setup"); const char *error = dlerror(); - if (error) { - warn("failed to resolve setup from %s: %s", path, error); - config_loader_cleanup(loader); - return false; - } + if (!error) return true; - return true; + warn("failed to resolve setup from %s: %s", loader->path, error); + config_loader_cleanup(loader); + return false; } void config_loader_cleanup(config_loader_t *loader) { diff --git a/src/config/runtime_config.c b/src/config/runtime_config.c new file mode 100644 index 0000000..5bd0d2a --- /dev/null +++ b/src/config/runtime_config.c @@ -0,0 +1,178 @@ +#include "config/runtime_config.h" + +#include +#include + +#include "base/memory.h" +#include "config/defaults.h" +#include "config/loader.h" +#include "core/layout.h" +#include "core/runtime.h" +#include "core/wm_desc.h" + +struct zdwm_config_builder_t { + layout_registry_t layouts; + workspace_desc_t *workspaces; + size_t workspace_count; + size_t workspace_capacity; + size_t output_count; +}; + +void runtime_config_cleanup(runtime_init_desc_t *desc) { + if (!desc) return; + + if (desc->layouts.slots || desc->layouts.slot_count || + desc->layouts.slot_capacity) { + layout_registry_cleanup(&desc->layouts); + } + workspace_desc_list_cleanup(&desc->workspaces, &desc->workspace_count); + if (desc->config_module_handle) dlclose(desc->config_module_handle); + desc->config_module_handle = nullptr; +} + +static void config_builder_cleanup(zdwm_config_builder_t *builder) { + if (!builder) return; + + if (builder->layouts.slots || builder->layouts.slot_count || + builder->layouts.slot_capacity) { + layout_registry_cleanup(&builder->layouts); + } + workspace_desc_list_cleanup(&builder->workspaces, &builder->workspace_count); + builder->workspace_capacity = 0; + builder->output_count = 0; +} + +static bool config_builder_init(zdwm_config_builder_t *builder, + size_t output_count) { + if (!builder || output_count == 0) return false; + + p_clear(builder, 1); + builder->output_count = output_count; + layout_registry_init(&builder->layouts); + return true; +} + +static bool config_builder_ensure_workspace_capacity( + zdwm_config_builder_t *builder) { + if (!builder) return false; + if (builder->workspace_count < builder->workspace_capacity) return true; + + size_t capacity = next_capacity(builder->workspace_capacity); + if (!builder->workspaces) { + builder->workspaces = p_new(workspace_desc_t, capacity); + } else { + p_realloc(&builder->workspaces, capacity); + } + builder->workspace_capacity = capacity; + return true; +} + +static layout_id_t runtime_config_register_layout( + zdwm_config_builder_t *builder, const char *name, const char *symbol, + const char *description, layout_fn fn) { + if (!builder || !name || !symbol) return ZDWM_LAYOUT_ID_INVALID; + + return layout_register(&builder->layouts, name, symbol, description, fn); +} + +static workspace_id_t runtime_config_define_workspace( + zdwm_config_builder_t *builder, size_t output_index, const char *name, + const layout_id_t *layout_ids, size_t layout_count, + layout_id_t initial_layout_id) { + if (!builder) return ZDWM_WORKSPACE_ID_INVALID; + if (output_index >= builder->output_count) return ZDWM_WORKSPACE_ID_INVALID; + if (builder->workspace_count >= (size_t)ZDWM_WORKSPACE_ID_INVALID) { + return ZDWM_WORKSPACE_ID_INVALID; + } + + workspace_desc_t desc = { + .output_index = output_index, + .name = name, + .layout_ids = layout_ids, + .layout_count = layout_count, + .initial_layout_id = initial_layout_id, + }; + if (!workspace_desc_valid(&desc, builder->output_count)) { + return ZDWM_WORKSPACE_ID_INVALID; + } + + for (size_t i = 0; i < layout_count; i++) { + if (!layout_slot_get(&builder->layouts, layout_ids[i])) { + return ZDWM_WORKSPACE_ID_INVALID; + } + } + if (!config_builder_ensure_workspace_capacity(builder)) { + return ZDWM_WORKSPACE_ID_INVALID; + } + + workspace_id_t workspace_id = (workspace_id_t)builder->workspace_count; + workspace_desc_t *slot = &builder->workspaces[builder->workspace_count]; + slot->output_index = output_index; + slot->name = p_strdup(name); + slot->layout_ids = p_copy(layout_ids, layout_count); + slot->layout_count = layout_count; + slot->initial_layout_id = initial_layout_id; + builder->workspace_count++; + return workspace_id; +} + +static bool config_builder_finish(zdwm_config_builder_t *builder, + runtime_init_desc_t *out) { + if (!builder || !out) return false; + + if (!builder->layouts.slot_count || !builder->workspace_count) return false; + + if (!layout_registry_move(&builder->layouts, &out->layouts)) return false; + out->workspaces = builder->workspaces; + out->workspace_count = builder->workspace_count; + builder->workspaces = nullptr; + builder->workspace_count = 0; + builder->workspace_capacity = 0; + builder->output_count = 0; + return true; +} + +static bool runtime_config_build(zdwm_config_setup_fn *setup, + const output_info_t *outputs, + size_t output_count, + runtime_init_desc_t *out) { + if (!setup || !out) return false; + zdwm_config_builder_t builder = {0}; + if (!config_builder_init(&builder, output_count)) return false; + + zdwm_api_t api = { + .abi_version = ZDWM_CONFIG_ABI_VERSION, + .builtin_layouts = + { + .tile = nullptr, + .monocle = nullptr, + }, + .register_layout = runtime_config_register_layout, + .define_workspace = runtime_config_define_workspace, + }; + bool ok = setup(&api, &builder, outputs, output_count) && + config_builder_finish(&builder, out); + config_builder_cleanup(&builder); + return ok; +} + +bool runtime_config_load(const char *override_path, runtime_init_desc_t *out) { + if (!out || !out->outputs || out->output_count == 0) return false; + runtime_config_cleanup(out); + + config_loader_t loader = {0}; + bool ok = false; + if (!config_loader_load(&loader, override_path)) goto cleanup; + + zdwm_config_setup_fn *setup = + loader.setup ? loader.setup : config_defaults_build; + ok = runtime_config_build(setup, out->outputs, out->output_count, out); + if (ok) { + out->config_module_handle = loader.handle; + loader.handle = nullptr; + } + +cleanup: + config_loader_cleanup(&loader); + return ok; +} diff --git a/src/config/runtime_config.h b/src/config/runtime_config.h new file mode 100644 index 0000000..055a3df --- /dev/null +++ b/src/config/runtime_config.h @@ -0,0 +1,6 @@ +#pragma once + +typedef struct runtime_init_desc_t runtime_init_desc_t; + +bool runtime_config_load(const char *override_path, runtime_init_desc_t *out); +void runtime_config_cleanup(runtime_init_desc_t *desc); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fb0d203..d897565 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(backend) +add_subdirectory(config) add_subdirectory(core) diff --git a/tests/config/CMakeLists.txt b/tests/config/CMakeLists.txt new file mode 100644 index 0000000..04fa52f --- /dev/null +++ b/tests/config/CMakeLists.txt @@ -0,0 +1,74 @@ +set(CONFIG_FIXTURE_TARGET "zdwm-config-fixture") +set(CONFIG_FIXTURE_NO_SETUP_TARGET "zdwm-config-fixture-no-setup") +set(CONFIG_LOADER_TEST_APP_NAME "zdwm-config-loader-tests") +set(RUNTIME_CONFIG_TEST_APP_NAME "zdwm-runtime-config-tests") + +add_library(${CONFIG_FIXTURE_TARGET} SHARED + ${CMAKE_CURRENT_SOURCE_DIR}/fixture_valid_config.c +) +target_include_directories(${CONFIG_FIXTURE_TARGET} + PRIVATE ${INCLUDE_DIR} +) + +add_library(${CONFIG_FIXTURE_NO_SETUP_TARGET} SHARED + ${CMAKE_CURRENT_SOURCE_DIR}/fixture_without_setup.c +) + +add_executable(${CONFIG_LOADER_TEST_APP_NAME} + ${CMAKE_CURRENT_SOURCE_DIR}/config_loader_test.c + ${SOURCE_DIR}/base/log.c + ${SOURCE_DIR}/config/loader.c +) +target_include_directories(${CONFIG_LOADER_TEST_APP_NAME} SYSTEM + PRIVATE ${INCLUDE_DIR} +) +target_include_directories(${CONFIG_LOADER_TEST_APP_NAME} + PRIVATE ${SOURCE_DIR} + PRIVATE ${BUILD_DIR} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} +) +target_link_libraries(${CONFIG_LOADER_TEST_APP_NAME} + PRIVATE ${CMAKE_DL_LIBS} +) +add_dependencies(${CONFIG_LOADER_TEST_APP_NAME} + ${CONFIG_FIXTURE_TARGET} + ${CONFIG_FIXTURE_NO_SETUP_TARGET} +) + +add_executable(${RUNTIME_CONFIG_TEST_APP_NAME} + ${CMAKE_CURRENT_SOURCE_DIR}/runtime_config_test.c + ${SOURCE_DIR}/base/log.c + ${SOURCE_DIR}/config/defaults.c + ${SOURCE_DIR}/config/loader.c + ${SOURCE_DIR}/config/runtime_config.c + ${SOURCE_DIR}/core/layout.c + ${SOURCE_DIR}/core/runtime.c + ${SOURCE_DIR}/core/state.c +) +target_include_directories(${RUNTIME_CONFIG_TEST_APP_NAME} SYSTEM + PRIVATE ${INCLUDE_DIR} +) +target_include_directories(${RUNTIME_CONFIG_TEST_APP_NAME} + PRIVATE ${SOURCE_DIR} + PRIVATE ${BUILD_DIR} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} +) +target_link_libraries(${RUNTIME_CONFIG_TEST_APP_NAME} + PRIVATE ${CMAKE_DL_LIBS} +) +add_dependencies(${RUNTIME_CONFIG_TEST_APP_NAME} + ${CONFIG_FIXTURE_TARGET} + ${CONFIG_FIXTURE_NO_SETUP_TARGET} +) + +add_test(NAME ${CONFIG_LOADER_TEST_APP_NAME} + COMMAND $ + $ + $ +) + +add_test(NAME ${RUNTIME_CONFIG_TEST_APP_NAME} + COMMAND $ + $ + $ +) diff --git a/tests/config/config_loader_test.c b/tests/config/config_loader_test.c new file mode 100644 index 0000000..91fee42 --- /dev/null +++ b/tests/config/config_loader_test.c @@ -0,0 +1,173 @@ +#include "config/loader.h" + +#include +#include +#include +#include +#include + +#include "helpers.h" + +static void test_config_loader_loads_explicit_override(const char *fixture_path) { + config_test_clear_env(); + + config_loader_t loader = {0}; + assert(config_loader_load(&loader, fixture_path)); + assert(loader.path); + assert(strcmp(loader.path, fixture_path) == 0); + assert(loader.handle); + assert(loader.setup); + + config_loader_cleanup(&loader); +} + +static void test_config_loader_override_has_priority_over_env( + const char *fixture_path) { + config_test_clear_env(); + assert(setenv("ZDWM_CONFIG_LIB", "/tmp/definitely-missing-config.so", 1) == 0); + + config_loader_t loader = {0}; + assert(config_loader_load(&loader, fixture_path)); + assert(loader.path); + assert(strcmp(loader.path, fixture_path) == 0); + assert(loader.handle); + assert(loader.setup); + + config_loader_cleanup(&loader); +} + +static void test_config_loader_uses_zdwm_config_lib_env( + const char *fixture_path) { + config_test_clear_env(); + assert(setenv("ZDWM_CONFIG_LIB", fixture_path, 1) == 0); + + config_loader_t loader = {0}; + assert(config_loader_load(&loader, nullptr)); + assert(loader.path); + assert(strcmp(loader.path, fixture_path) == 0); + assert(loader.handle); + assert(loader.setup); + + config_loader_cleanup(&loader); +} + +static void test_config_loader_uses_xdg_config_home(const char *fixture_path) { + config_test_clear_env(); + + char temp_root[PATH_MAX] = {0}; + char config_dir[PATH_MAX] = {0}; + char config_path[PATH_MAX] = {0}; + + config_test_make_temp_dir(temp_root, sizeof(temp_root), + "zdwm-config-loader-xdg"); + config_test_join_path(config_dir, sizeof(config_dir), temp_root, "zdwm"); + config_test_join_path(config_path, sizeof(config_path), config_dir, + "config.so"); + config_test_mkdir(config_dir); + config_test_symlink(fixture_path, config_path); + assert(setenv("XDG_CONFIG_HOME", temp_root, 1) == 0); + + config_loader_t loader = {0}; + assert(config_loader_load(&loader, nullptr)); + assert(loader.path); + assert(strcmp(loader.path, config_path) == 0); + assert(loader.handle); + assert(loader.setup); + + config_loader_cleanup(&loader); + config_test_unlink(config_path); + config_test_rmdir(config_dir); + config_test_rmdir(temp_root); +} + +static void test_config_loader_uses_home_fallback(const char *fixture_path) { + config_test_clear_env(); + + char temp_root[PATH_MAX] = {0}; + char config_root[PATH_MAX] = {0}; + char zdwm_dir[PATH_MAX] = {0}; + char config_path[PATH_MAX] = {0}; + + config_test_make_temp_dir(temp_root, sizeof(temp_root), + "zdwm-config-loader-home"); + config_test_join_path(config_root, sizeof(config_root), temp_root, ".config"); + config_test_join_path(zdwm_dir, sizeof(zdwm_dir), config_root, "zdwm"); + config_test_join_path(config_path, sizeof(config_path), zdwm_dir, + "config.so"); + config_test_mkdir(config_root); + config_test_mkdir(zdwm_dir); + config_test_symlink(fixture_path, config_path); + assert(setenv("HOME", temp_root, 1) == 0); + + config_loader_t loader = {0}; + assert(config_loader_load(&loader, nullptr)); + assert(loader.path); + assert(strcmp(loader.path, config_path) == 0); + assert(loader.handle); + assert(loader.setup); + + config_loader_cleanup(&loader); + config_test_unlink(config_path); + config_test_rmdir(zdwm_dir); + config_test_rmdir(config_root); + config_test_rmdir(temp_root); +} + +static void test_config_loader_allows_missing_implicit_library(void) { + config_test_clear_env(); + + char temp_root[PATH_MAX] = {0}; + char expected_path[PATH_MAX] = {0}; + + config_test_make_temp_dir(temp_root, sizeof(temp_root), + "zdwm-config-loader-missing"); + config_test_join_path(expected_path, sizeof(expected_path), temp_root, + "zdwm/config.so"); + assert(setenv("XDG_CONFIG_HOME", temp_root, 1) == 0); + + config_loader_t loader = {0}; + assert(config_loader_load(&loader, nullptr)); + assert(loader.path); + assert(strcmp(loader.path, expected_path) == 0); + assert(loader.handle == nullptr); + assert(loader.setup == nullptr); + + config_loader_cleanup(&loader); + config_test_rmdir(temp_root); +} + +static void test_config_loader_rejects_missing_explicit_library(void) { + config_test_clear_env(); + + config_loader_t loader = {0}; + assert(!config_loader_load(&loader, "/tmp/definitely-missing-config.so")); + assert(loader.path == nullptr); + assert(loader.handle == nullptr); + assert(loader.setup == nullptr); +} + +static void test_config_loader_rejects_library_without_setup( + const char *fixture_path) { + config_test_clear_env(); + + config_loader_t loader = {0}; + assert(!config_loader_load(&loader, fixture_path)); + assert(loader.path == nullptr); + assert(loader.handle == nullptr); + assert(loader.setup == nullptr); +} + +int main(int argc, char **argv) { + assert(argc == 3); + + test_config_loader_loads_explicit_override(argv[1]); + test_config_loader_override_has_priority_over_env(argv[1]); + test_config_loader_uses_zdwm_config_lib_env(argv[1]); + test_config_loader_uses_xdg_config_home(argv[1]); + test_config_loader_uses_home_fallback(argv[1]); + test_config_loader_allows_missing_implicit_library(); + test_config_loader_rejects_missing_explicit_library(); + test_config_loader_rejects_library_without_setup(argv[2]); + + return 0; +} diff --git a/tests/config/fixture_valid_config.c b/tests/config/fixture_valid_config.c new file mode 100644 index 0000000..e95b1da --- /dev/null +++ b/tests/config/fixture_valid_config.c @@ -0,0 +1,28 @@ +#include + +static bool fixture_layout(const zdwm_layout_ctx_t *ctx, + zdwm_layout_result_t *out) { + if (!ctx || !out) return false; + + zdwm_layout_result_reset(out); + if (!ctx->window_ids || ctx->window_count == 0) return true; + + zdwm_layout_result_push(out, (zdwm_layout_item_t){ + .window_id = ctx->window_ids[0], + .rect = ctx->workarea, + }); + return true; +} + +bool setup(const zdwm_api_t *api, zdwm_config_builder_t *builder, + const zdwm_output_info_t *outputs, size_t output_count) { + if (!api || !builder || !outputs || output_count == 0) return false; + + zdwm_layout_id_t layout_id = + api->register_layout(builder, "test-only", "[T]", "fixture layout", + fixture_layout); + if (layout_id == ZDWM_LAYOUT_ID_INVALID) return false; + + return api->define_workspace(builder, 0, "from-so", &layout_id, 1, + layout_id) != ZDWM_WORKSPACE_ID_INVALID; +} diff --git a/tests/config/fixture_without_setup.c b/tests/config/fixture_without_setup.c new file mode 100644 index 0000000..e355136 --- /dev/null +++ b/tests/config/fixture_without_setup.c @@ -0,0 +1 @@ +int zdwm_config_fixture_without_setup_symbol(void) { return 1; } diff --git a/tests/config/helpers.h b/tests/config/helpers.h new file mode 100644 index 0000000..7979f44 --- /dev/null +++ b/tests/config/helpers.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +static inline void config_test_clear_env(void) { + assert(unsetenv("ZDWM_CONFIG_LIB") == 0); + assert(unsetenv("XDG_CONFIG_HOME") == 0); + assert(unsetenv("HOME") == 0); +} + +static inline void config_test_make_temp_dir(char *path, size_t path_size, + const char *prefix) { + int len = snprintf(path, path_size, "/tmp/%s-XXXXXX", prefix); + assert(len > 0); + assert((size_t)len < path_size); + assert(mkdtemp(path) == path); +} + +static inline void config_test_join_path(char *out, size_t out_size, + const char *base, + const char *suffix) { + int len = snprintf(out, out_size, "%s/%s", base, suffix); + assert(len > 0); + assert((size_t)len < out_size); +} + +static inline void config_test_mkdir(const char *path) { + assert(mkdir(path, 0700) == 0); +} + +static inline void config_test_symlink(const char *target, const char *path) { + assert(symlink(target, path) == 0); +} + +static inline void config_test_unlink(const char *path) { + assert(unlink(path) == 0); +} + +static inline void config_test_rmdir(const char *path) { + assert(rmdir(path) == 0); +} diff --git a/tests/config/runtime_config_test.c b/tests/config/runtime_config_test.c new file mode 100644 index 0000000..0cb7807 --- /dev/null +++ b/tests/config/runtime_config_test.c @@ -0,0 +1,213 @@ +#include "config/runtime_config.h" + +#include +#include +#include +#include +#include + +#include "core/layout.h" +#include "core/runtime.h" +#include "helpers.h" + +struct backend_t { + int unused; +}; + +void backend_destroy(backend_t *backend) { free(backend); } + +static backend_t *test_backend_create(void) { + return calloc(1, sizeof(backend_t)); +} + +static void assert_default_layouts(const runtime_init_desc_t *desc) { + assert(desc); + assert(layout_registry_count(&desc->layouts) == 3); + + const layout_slot_t *tile = layout_registry_at(&desc->layouts, 0); + const layout_slot_t *monocle = layout_registry_at(&desc->layouts, 1); + const layout_slot_t *floating = layout_registry_at(&desc->layouts, 2); + + assert(tile); + assert(monocle); + assert(floating); + assert(strcmp(tile->name, "tile") == 0); + assert(strcmp(monocle->name, "monocle") == 0); + assert(strcmp(floating->name, "floating") == 0); +} + +static void test_runtime_config_loads_user_library(const char *fixture_path) { + config_test_clear_env(); + + output_info_t outputs[] = { + { + .name = "HDMI-A-1", + .geometry = {.x = 0, .y = 0, .width = 1920, .height = 1080}, + }, + }; + + runtime_init_desc_t desc = { + .outputs = outputs, + .output_count = 1, + }; + assert(runtime_config_load(fixture_path, &desc)); + assert(desc.config_module_handle != nullptr); + assert(layout_registry_count(&desc.layouts) == 1); + assert(desc.workspace_count == 1); + assert(strcmp(desc.workspaces[0].name, "from-so") == 0); + assert(desc.workspaces[0].output_index == 0); + assert(desc.workspaces[0].layout_count == 1); + assert(desc.workspaces[0].initial_layout_id == 0); + + const layout_slot_t *layout = layout_registry_at(&desc.layouts, 0); + assert(layout); + assert(strcmp(layout->name, "test-only") == 0); + assert(strcmp(layout->symbol, "[T]") == 0); + + runtime_config_cleanup(&desc); +} + +static void test_runtime_config_keeps_module_loaded_after_runtime_init( + const char *fixture_path) { + config_test_clear_env(); + + output_info_t outputs[] = { + { + .name = "HDMI-A-1", + .geometry = {.x = 0, .y = 0, .width = 1920, .height = 1080}, + }, + }; + window_id_t window_ids[] = {42}; + + runtime_init_desc_t desc = { + .backend = test_backend_create(), + .outputs = outputs, + .output_count = 1, + }; + assert(desc.backend); + assert(runtime_config_load(fixture_path, &desc)); + + runtime_t runtime = {0}; + assert(runtime_init(&runtime, &desc)); + assert(desc.config_module_handle == nullptr); + runtime_init_desc_cleanup(&desc); + + assert(runtime.config_module_handle != nullptr); + + layout_fn layout = layout_get(&runtime.layouts, 0); + assert(layout != nullptr); + + layout_result_t result = {0}; + layout_result_init(&result); + + layout_ctx_t ctx = { + .workspace_id = 0, + .focused_window_id = window_ids[0], + .workarea = {.x = 11, .y = 22, .width = 333, .height = 444}, + .window_ids = window_ids, + .window_count = 1, + }; + assert(layout(&ctx, &result)); + assert(result.item_count == 1); + assert(result.items[0].window_id == window_ids[0]); + assert(result.items[0].rect.x == ctx.workarea.x); + assert(result.items[0].rect.y == ctx.workarea.y); + assert(result.items[0].rect.width == ctx.workarea.width); + assert(result.items[0].rect.height == ctx.workarea.height); + + layout_result_cleanup(&result); + runtime_shutdown(&runtime); + assert(runtime.config_module_handle == nullptr); +} + +static void test_runtime_config_falls_back_to_defaults_when_implicit_library_is_missing( + void) { + config_test_clear_env(); + + output_info_t outputs[] = { + { + .name = "eDP-1", + .geometry = {.x = 0, .y = 0, .width = 1920, .height = 1080}, + }, + { + .name = "DP-1", + .geometry = {.x = 1920, .y = 0, .width = 2560, .height = 1440}, + }, + }; + + char temp_root[PATH_MAX] = {0}; + config_test_make_temp_dir(temp_root, sizeof(temp_root), + "zdwm-runtime-config-missing"); + assert(setenv("XDG_CONFIG_HOME", temp_root, 1) == 0); + + runtime_init_desc_t desc = { + .outputs = outputs, + .output_count = 2, + }; + assert(runtime_config_load(nullptr, &desc)); + assert(desc.config_module_handle == nullptr); + assert_default_layouts(&desc); + assert(desc.workspace_count == 2); + assert(strcmp(desc.workspaces[0].name, "main") == 0); + assert(strcmp(desc.workspaces[1].name, "main") == 0); + assert(desc.workspaces[0].output_index == 0); + assert(desc.workspaces[1].output_index == 1); + + runtime_config_cleanup(&desc); + config_test_rmdir(temp_root); +} + +static void test_runtime_config_rejects_missing_explicit_library(void) { + config_test_clear_env(); + + output_info_t outputs[] = { + { + .name = "HDMI-A-1", + .geometry = {.x = 0, .y = 0, .width = 1920, .height = 1080}, + }, + }; + + runtime_init_desc_t desc = { + .outputs = outputs, + .output_count = 1, + }; + assert(!runtime_config_load("/tmp/definitely-missing-config.so", &desc)); + assert(layout_registry_count(&desc.layouts) == 0); + assert(desc.workspace_count == 0); + assert(desc.workspaces == nullptr); + assert(desc.config_module_handle == nullptr); +} + +static void test_runtime_config_rejects_library_without_setup( + const char *fixture_path) { + config_test_clear_env(); + + output_info_t outputs[] = { + { + .name = "HDMI-A-1", + .geometry = {.x = 0, .y = 0, .width = 1920, .height = 1080}, + }, + }; + + runtime_init_desc_t desc = { + .outputs = outputs, + .output_count = 1, + }; + assert(!runtime_config_load(fixture_path, &desc)); + assert(layout_registry_count(&desc.layouts) == 0); + assert(desc.workspace_count == 0); + assert(desc.workspaces == nullptr); + assert(desc.config_module_handle == nullptr); +} + +int main(int argc, char **argv) { + assert(argc == 3); + + test_runtime_config_loads_user_library(argv[1]); + test_runtime_config_keeps_module_loaded_after_runtime_init(argv[1]); + test_runtime_config_falls_back_to_defaults_when_implicit_library_is_missing(); + test_runtime_config_rejects_missing_explicit_library(); + test_runtime_config_rejects_library_without_setup(argv[2]); + + return 0; +}