diff --git a/doc/decisions/README.md b/doc/decisions/README.md index fe268903feb..9b06bfe25d6 100644 --- a/doc/decisions/README.md +++ b/doc/decisions/README.md @@ -46,7 +46,7 @@ section here. ## In Progress -- [Global Plugins](global_plugins.md) (@mpranj) +- [Hooks](hooks.md) (@atmaxinger) - [Ensure](ensure.md) (@kodebach) - [Capabilities](capabilities.md) (@markus2330) - [Error Semantics](error_semantics.md) (API) diff --git a/doc/decisions/array.md b/doc/decisions/array.md index 1df5c91bc04..fe32d44a632 100644 --- a/doc/decisions/array.md +++ b/doc/decisions/array.md @@ -108,7 +108,7 @@ The `spec` plugin should check if it is a valid array, i.e.: ## Related Decisions -- [Global Plugins](global_plugins.md) +- [Hooks](hooks.md) - [Global Validation](global_validation.md) - [Base Names](base_name.md) - [Metadata in Spec Namespace](spec_metadata.md) diff --git a/doc/decisions/ensure.md b/doc/decisions/ensure.md index c46d47ad7c6..8c64ba60102 100644 --- a/doc/decisions/ensure.md +++ b/doc/decisions/ensure.md @@ -83,6 +83,6 @@ invocations. Contract `KeySet`s only contain `Key`s below ## Related Decisions -- [Global Plugins](global_plugins.md) +- [Hooks](hooks.md) ## Notes diff --git a/doc/decisions/global_plugins.md b/doc/decisions/hooks.md similarity index 73% rename from doc/decisions/global_plugins.md rename to doc/decisions/hooks.md index 8c46459ad22..512d28c14f5 100644 --- a/doc/decisions/global_plugins.md +++ b/doc/decisions/hooks.md @@ -1,7 +1,14 @@ -# Global Plugins +# Hooks in KDB ## Problem +Some components of `kdbGet`/`kdbSet` should be optional. +We use the plugin system for that. +However, some of these cases cannot be tied to a mountpoint. +This was the idea of global plugins, but that idea proved problematic. + +In the old global plugins implementation: + - Notification does not happen once after final commit, but for every plugin - Problems in spec plugin @@ -11,21 +18,23 @@ We need to clean up and simplify the placement. ## Constraints -- Plugin interface should be the same. Many plugins, e.g. dbus, should work +- Plugin interface should be the same. Many plugins, where appropriate, e.g. dbus, should work as global plugins w/o any change in code (i.e. only changes in contract) + - Global plugins might depend on specific applications or specific mount points (it should be possible to enforce global plugins for specific applications). ## Assumptions -- Elektra is useful with following types of global plugins: +- Elektra is useful with following types of plugins: - mmap - spec - gopts - receiving of notifications (internalnotification) - sending of notifications (dbus, ...) + - recording of changes - There are not too many types of global plugins, not more than 10 ## Considered Alternatives @@ -44,6 +53,8 @@ These hooks are not shared, so no `list` plugin is needed. Installed plugins will be used. +In the beginning, we'll hardcode the names of the plugins. For changing those plugins symlinks will have to be used. + ## Rationale - allows adding more types of plugins later, also post-1.0 @@ -57,6 +68,7 @@ Installed plugins will be used. - remove `list` plugin - remove plugins that stop working or disallow global positioning for them - call `spec` as needed several times +- remove current global plugins mechanism ## Related Decisions diff --git a/doc/dev/hooks.md b/doc/dev/hooks.md new file mode 100644 index 00000000000..3891879754f --- /dev/null +++ b/doc/dev/hooks.md @@ -0,0 +1,24 @@ +# Hooks + +Hooks are central points in the KDB lifecycle, where specialized plugins are called. + +## Selecting which Plugin will be Used for a Specific Hook + +Currently, the names of the plugins are hard-coded. +This [decision](../decisions/hooks.md) was made, because these plugins are meant to fulfil very specific purposes. +A symlink replacing the shared library file of the plugin could be used to change the implementation. + +## Interface of the hooks + +If a plugin should be able to act upon a hook, it must export all the functions that the hook requires. +These exports are of the form `system:/elektra/modules//exports/hook//`. + +For example, the `gopts` hook only requires the `get` function. A plugin that wants to act upon the `gopts` hook therefore has to export `system:/elektra/modules//exports/hook/gopts/get`. + +Other hooks (e.g. `spec`) require multiple exported functions. + +## Lifecycle + +1. Hooks are initilized within `kdbOpen` after the contract has been processed. This includes loading the plugins. +2. The appropriate hooks are called within each `kdbGet` and `kdbSet` call. +3. Hooks are deinitialized within `kdbClose`. This includes unloading the plugins. diff --git a/doc/news/_preparation_next_release.md b/doc/news/_preparation_next_release.md index c9570a2fcb3..078107fb0e3 100644 --- a/doc/news/_preparation_next_release.md +++ b/doc/news/_preparation_next_release.md @@ -164,7 +164,7 @@ _(Michael Tucek)_ ### New Backend - Improve [description of plugin framework](doc/dev/plugins-framework.md). -- <> +- Implement [hooks](doc/decisions/hooks.md). _(Maximilian Irlinger @atmaxinger)_ - <> - <> - <> diff --git a/src/include/kdbprivate.h b/src/include/kdbprivate.h index 4731401d252..3268642c9b8 100644 --- a/src/include/kdbprivate.h +++ b/src/include/kdbprivate.h @@ -99,6 +99,8 @@ typedef int (*kdbSetPtr) (Plugin * handle, KeySet * returned, Key * parentKey); typedef int (*kdbErrorPtr) (Plugin * handle, KeySet * returned, Key * parentKey); typedef int (*kdbCommitPtr) (Plugin * handle, KeySet * returned, Key * parentKey); +typedef int (*kdbHookGoptsGetPtr) (Plugin * handle, KeySet * returned, Key * parentKey); + typedef Plugin * (*OpenMapper) (const char *, const char *, KeySet *); typedef int (*CloseMapper) (Plugin *); @@ -319,6 +321,7 @@ struct _KeySet #endif }; +typedef struct _Hooks Hooks; /** * The access point to the key database. @@ -362,8 +365,16 @@ struct _KDB up their parts of the global keyset, which they do not need any more.*/ Plugin * globalPlugins[NR_GLOBAL_POSITIONS][NR_GLOBAL_SUBPOSITIONS]; - KeySet * backends; + + struct + { + struct + { + struct _Plugin* plugin; + kdbHookGoptsGetPtr kdbHookGoptsGet; + } gopts; + } hooks; }; /** @@ -564,6 +575,8 @@ int mountModules (KDB * kdb, KeySet * modules, Key * errorKey); int mountVersion (KDB * kdb, Key * errorKey); int mountGlobals (KDB * kdb, KeySet * keys, KeySet * modules, Key * errorKey); int mountBackend (KDB * kdb, const Key * mountpoint, Plugin * backend); +int initHooks (KDB * kdb, const KeySet * config, KeySet * modules, const KeySet * contract, Key * errorKey); +void freeHooks (KDB * kdb, Key * errorKey); const Key * mountGetMountpoint (KDB * handle, Key * where); Plugin * mountGetBackend (KDB * handle, Key * key); diff --git a/src/libs/elektra/CMakeLists.txt b/src/libs/elektra/CMakeLists.txt index 158deab1b27..d10f27ff886 100644 --- a/src/libs/elektra/CMakeLists.txt +++ b/src/libs/elektra/CMakeLists.txt @@ -63,6 +63,7 @@ if (BUILD_SHARED) backend.c kdb.c mount.c + hooks.c split.c trie.c plugin.c diff --git a/src/libs/elektra/global.c b/src/libs/elektra/global.c index 346f996ccf4..17925dfdfe4 100644 --- a/src/libs/elektra/global.c +++ b/src/libs/elektra/global.c @@ -20,7 +20,7 @@ int elektraGlobalGet (KDB * handle, KeySet * ks, Key * parentKey, int position, Plugin * plugin; if (handle && (plugin = handle->globalPlugins[position][subPosition])) { - ret = plugin->kdbGet (plugin, ks, parentKey); + // ret = plugin->kdbGet (plugin, ks, parentKey); } return ret; } @@ -31,7 +31,7 @@ int elektraGlobalSet (KDB * handle, KeySet * ks, Key * parentKey, int position, Plugin * plugin; if (handle && (plugin = handle->globalPlugins[position][subPosition])) { - ret = plugin->kdbSet (plugin, ks, parentKey); + // ret = plugin->kdbSet (plugin, ks, parentKey); } return ret; } @@ -42,7 +42,7 @@ int elektraGlobalError (KDB * handle, KeySet * ks, Key * parentKey, int position Plugin * plugin; if (handle && (plugin = handle->globalPlugins[position][subPosition])) { - ret = plugin->kdbError (plugin, ks, parentKey); + // ret = plugin->kdbError (plugin, ks, parentKey); } return ret; } diff --git a/src/libs/elektra/hooks.c b/src/libs/elektra/hooks.c new file mode 100644 index 00000000000..2ab23ff1808 --- /dev/null +++ b/src/libs/elektra/hooks.c @@ -0,0 +1,190 @@ +/** + * @file + * + * @brief + * + * @copyright BSD License (see LICENSE.md or https://www.libelektra.org) + */ + +#include + +/** + * Uninitializes and frees all hooks in the passed KDB handle. + * + * @param kdb the KDB handle where the hooks should be freed + * @param errorKey the key which holds errors and warnings which were issued + */ +void freeHooks (KDB * kdb, Key * errorKey) +{ + if (kdb->hooks.gopts.plugin != NULL) + { + elektraPluginClose (kdb->hooks.gopts.plugin, errorKey); + } +} + +static size_t getFunction (Plugin * plugin, const char * functionName, Key * errorKey) +{ + size_t result = elektraPluginGetFunction (plugin, functionName); + + if (result == 0) + { + ELEKTRA_ADD_INSTALLATION_WARNINGF (errorKey, "Plugin '%s' does not implement function '%s'", plugin->name, functionName); + } + + return result; +} + +static int initHooksGopts (KDB * kdb, Plugin * plugin, Key * errorKey) +{ + if (!plugin) + { + return -1; + } + + kdb->hooks.gopts.plugin = plugin; + + if ((kdb->hooks.gopts.kdbHookGoptsGet = (kdbHookGoptsGetPtr) getFunction (plugin, "hook/gopts/get", errorKey)) == NULL) + { + return -1; + } + + return 0; +} + +/** + * Extracts the config for a single plugin from the contract + * + * The config must be below system:/elektra/contract/mountglobal/ and will be moved to below user:/. + * It also must contain the key system:/elektra/contract/mountglobal/. + * + * @param pluginName The name of the plugin for which the config will be extracted + * @param contract The contract from which the config is extracted + */ +static KeySet * getPluginConfigFromContract (const char * pluginName, const KeySet * contract) +{ + KeySet * tmpContract = ksDup (contract); + KeySet * config = ksNew (0, KS_END); + + Key * mountContractRoot = keyNew ("system:/elektra/contract/mountglobal", KEY_END); + Key * pluginConfigRoot = keyNew ("user:/", KEY_END); + + for (elektraCursor it = ksFindHierarchy (tmpContract, mountContractRoot, NULL); it < ksGetSize (tmpContract); it++) + { + Key * cur = ksAtCursor (tmpContract, it); + if (keyIsDirectlyBelow (mountContractRoot, cur) == 1) + { + const char * pluginNameOfConfig = keyBaseName (cur); + + // only handle config for the specified plugin + // we might be able to replace this check by modifying the key mountContractRoot, + // but I just copied this function from the previous global plugins implementation + // and it works for now. + if (strcmp (pluginName, pluginNameOfConfig) != 0) + { + break; + } + + KeySet * pluginConfig = ksCut (tmpContract, cur); + + // increment ref count, because cur is part of pluginConfig and + // we hold a reference to cur that is still needed (via pluginName) + keyIncRef (cur); + ksRename (pluginConfig, cur, pluginConfigRoot); + ksAppend (config, pluginConfig); + + // we need to delete cur separately, because it was ksCut() from contract + // we also need to decrement the ref count, because it was incremented above + keyDecRef (cur); + keyDel (cur); + + --it; + } + } + + keyDel (mountContractRoot); + keyDel (pluginConfigRoot); + ksDel (tmpContract); + + return config; +} + +static Plugin * loadPlugin (const char * pluginName, KeySet * global, KeySet * modules, const KeySet * contract, Key * errorKey) +{ + Key * openKey = keyDup (errorKey, KEY_CP_ALL); + + KeySet * config = getPluginConfigFromContract (pluginName, contract); + + Plugin * plugin = elektraPluginOpen (pluginName, modules, config, openKey); + + if (!plugin) + { + ELEKTRA_ADD_INSTALLATION_WARNINGF (errorKey, "Could not load plugin '%s'", pluginName); + keyCopyAllMeta (errorKey, openKey); + keyDel (openKey); + return NULL; + } + + plugin->global = global; + + return plugin; +} + +static bool isGoptsEnabledByContract (const KeySet * contract) +{ + KeySet * dupContract = ksDup (contract); // We need to duplicate because contract is const, and ksLookupByName doesn't take const + bool isEnabled = ksLookupByName (dupContract, "system:/elektra/contract/mountglobal/gopts", 0) != NULL; + ksDel (dupContract); + + return isEnabled; +} + +/** + * Initializes the hooks stored in the passed KDB handle. + * If the handle already contains initialized hooks, they will be reinitialized, including unloading and loading of their plugins. + * Parameters @p config and @p contract will be used to determine which hooks to populate. + * + * @param kdb the KDB instance where the hooks should be initialized + * @param config KeySet containing the current config in @p system:/elektra namespace + * @param modules the current list of loaded modules + * @param contract the contract passed to @p kdbOpen + * @param errorKey the key which holds errors and warnings which were issued + * @return 0 on success, -1 on failure + */ +int initHooks (KDB * kdb, const KeySet * config, KeySet * modules, const KeySet * contract, Key * errorKey) +{ + bool existingError = ksLookupByName (keyMeta (errorKey), "meta:/error", 0) != NULL; + + if (!existingError) + { + // set a dummy value to block errors + // any errors that occur will be converted into warnings + keySetMeta (errorKey, "meta:/error", "blocked"); + } + + freeHooks (kdb, errorKey); + + if (isGoptsEnabledByContract (contract) && + initHooksGopts (kdb, loadPlugin ("gopts", kdb->global, modules, contract, errorKey), errorKey) != 0) + { + goto error; + } + + if (!existingError) + { + // remove dummy error again + keySetMeta (errorKey, "meta:/error", NULL); + } + + return 0; + +error: + freeHooks (kdb, errorKey); + + if (!existingError) + { + // remove dummy error again + keySetMeta (errorKey, "meta:/error", NULL); + } + + return -1; +} diff --git a/src/libs/elektra/kdb.c b/src/libs/elektra/kdb.c index 63f67f0eddb..54ec9ec6eb9 100644 --- a/src/libs/elektra/kdb.c +++ b/src/libs/elektra/kdb.c @@ -999,6 +999,15 @@ KDB * kdbOpen (const KeySet * contract, Key * errorKey) goto error; } + // TODO (atmaxinger): improve + // TODO: combine with ensureContract below + if (initHooks (handle, elektraKs, handle->modules, contract, errorKey) == -1) + { + ELEKTRA_SET_INSTALLATION_ERROR (errorKey, "Initializing hooks failed. Please see warning of concrete plugin"); + ksDel (elektraKs); + goto error; + } + // Step 5: process contract if (contract != NULL && !ensureContract (handle, contract, errorKey)) { @@ -1120,6 +1129,8 @@ int kdbClose (KDB * handle, Key * errorKey) } } + freeHooks (handle, errorKey); + if (handle->modules) { elektraModulesClose (handle->modules, errorKey); @@ -1736,7 +1747,8 @@ int kdbGet (KDB * handle, KeySet * ks, Key * parentKey) #endif // Step 1: find backends for parentKey KeySet * backends = backendsForParentKey (handle->backends, parentKey); - bool goptsActive = handle->globalPlugins[PROCGETSTORAGE][MAXONCE] != NULL; + + bool goptsActive = handle->hooks.gopts.plugin != NULL; if (goptsActive) { // HACK: for gopts; generates keys outside backend @@ -1854,17 +1866,6 @@ int kdbGet (KDB * handle, KeySet * ks, Key * parentKey) } /* TODO (kodebach): implement actual steps with new global plugins - // Step 13: run gopts (if enabled) - keyCopy (parentKey, initialParent, KEY_CP_NAME); - keySetNamespace (cascadingParent, KEY_NS_CASCADING); - set_bit (parentKey, KEY_LOCK_NAME | KEY_LOCK_VALUE); - bool goptsEnabled = false; - if (goptsEnabled && !goptsGet (dataKs, cascadingParent)) - { - clear_bit (parentKey->flags, KEY_LOCK_NAME | KEY_LOCK_VALUE); - goto error; - } - // Step 14: run spec plugin if (!specGet (dataKs, cascadingParent)) { @@ -1874,6 +1875,18 @@ int kdbGet (KDB * handle, KeySet * ks, Key * parentKey) clear_bit (parentKey->flags, KEY_LOCK_NAME | KEY_LOCK_VALUE); */ + // Step 13: run gopts (if enabled) + keyCopy (parentKey, initialParent, KEY_CP_NAME); + keySetNamespace (parentKey, KEY_NS_CASCADING); + + if (goptsActive && !handle->hooks.gopts.kdbHookGoptsGet (handle->hooks.gopts.plugin, dataKs, parentKey)) + { + clear_bit (parentKey->flags, KEY_LOCK_NAME | KEY_LOCK_VALUE); + goto error; + } + + keySetNamespace (parentKey, keyGetNamespace (initialParent)); + // Step 15: split dataKs for poststorage phase // FIXME (kodebach): handle proc:/ keys if (!backendsDivide (backends, dataKs)) diff --git a/src/libs/elektra/plugin.c b/src/libs/elektra/plugin.c index 31864ed72e8..c612f6851b2 100644 --- a/src/libs/elektra/plugin.c +++ b/src/libs/elektra/plugin.c @@ -153,7 +153,7 @@ int elektraPluginClose (Plugin * handle, Key * errorKey) * Retrieves a function exported by a plugin. * * @param plugin Plugin handle - * @param name Function name + * @param name Function name. Must be a valid key name suffix. May not contain the sequence '..' * @return Pointer to function. NULL if function not found or not enough memory available */ size_t elektraPluginGetFunction (Plugin * plugin, const char * name) @@ -161,13 +161,22 @@ size_t elektraPluginGetFunction (Plugin * plugin, const char * name) ELEKTRA_NOT_NULL (plugin); ELEKTRA_NOT_NULL (name); + if (strstr (name, "..") != NULL) + { + // The sequence ".." is contained in the name. + // For security and stability purposes we do not allow that. + ELEKTRA_LOG_WARNING ("Can't get function '%s' from plugin because '..' is not allowed in function name", name); + return 0; + } + KeySet * exports = ksNew (0, KS_END); Key * pk = keyNew ("system:/elektra/modules", KEY_END); keyAddBaseName (pk, plugin->name); plugin->kdbGet (plugin, exports, pk); ksRewind (exports); keyAddBaseName (pk, "exports"); - keyAddBaseName (pk, name); + keyAddName (pk, name); + Key * keyFunction = ksLookup (exports, pk, 0); if (!keyFunction) { diff --git a/src/plugins/gopts/gopts.c b/src/plugins/gopts/gopts.c index f57cda39ddf..ccc6bc2e271 100644 --- a/src/plugins/gopts/gopts.c +++ b/src/plugins/gopts/gopts.c @@ -43,6 +43,7 @@ int elektraGOptsGet (Plugin * handle, KeySet * returned, Key * parentKey) ksNew (30, keyNew ("system:/elektra/modules/gopts", KEY_VALUE, "gopts plugin waits for your orders", KEY_END), keyNew ("system:/elektra/modules/gopts/exports", KEY_END), keyNew ("system:/elektra/modules/gopts/exports/get", KEY_FUNC, elektraGOptsGet, KEY_END), + keyNew ("system:/elektra/modules/gopts/exports/hook/gopts/get", KEY_FUNC, elektraGOptsGet, KEY_END), #include ELEKTRA_README keyNew ("system:/elektra/modules/gopts/infos/version", KEY_VALUE, PLUGINVERSION, KEY_END), KS_END); ksAppend (returned, contract); diff --git a/tests/ctest/CMakeLists.txt b/tests/ctest/CMakeLists.txt index cad386ec362..b7c8e6853b3 100644 --- a/tests/ctest/CMakeLists.txt +++ b/tests/ctest/CMakeLists.txt @@ -37,5 +37,7 @@ target_link_elektra (test_cmerge elektra-merge) target_link_elektra (test_sha-256 elektra-ease) +target_link_elektra(test_hooks elektra-plugin) + # LibGit leaks memory set_property (TEST test_cmerge PROPERTY LABELS memleak) diff --git a/tests/ctest/test_hooks.c b/tests/ctest/test_hooks.c new file mode 100644 index 00000000000..dca13acd3a9 --- /dev/null +++ b/tests/ctest/test_hooks.c @@ -0,0 +1,185 @@ +/** + * @file + * + * @brief + * + * @copyright BSD License (see LICENSE.md or https://www.libelektra.org) + */ + +#include "../../src/libs/elektra/hooks.c" +#include + +static void test_getPluginConfigFromContract_withConfigInContract (void) +{ + printf ("Executing %s\n", __func__); + + // Arrange + KeySet * contract = ksNew (30, keyNew ("system:/elektra/something", KEY_VALUE, "3", KEY_END), + keyNew ("system:/elektra/contract/mountglobal/myPlugin", KEY_END), + keyNew ("system:/elektra/contract/mountglobal/myPlugin/someValue", KEY_VALUE, "1", KEY_END), + keyNew ("system:/elektra/contract/mountglobal/myPlugin/someOtherValue", KEY_VALUE, "2", KEY_END), + keyNew ("system:/elektra/contract/mountglobal/otherPlugin", KEY_END), + keyNew ("system:/elektra/contract/mountglobal/otherPlugin/someValue", KEY_VALUE, "1", KEY_END), + keyNew ("system:/elektra/myPlugin", KEY_END), + keyNew ("system:/elektra/myPlugin/shouldntSeeMe", KEY_VALUE, "5", KEY_END), + keyNew ("system:/elektra/record/enabled", KEY_VALUE, "0", KEY_END), KS_END); + + // Act + KeySet * result = getPluginConfigFromContract ("myPlugin", contract); + + // Assert + succeed_if (result != NULL, "result must not be NULL"); + succeed_if (ksGetSize (result) == 3, "expected resulting config to have 3 keys"); + succeed_if (ksLookupByName (result, "user:/", KDB_O_NONE) != NULL, "must contain key user:/"); + succeed_if (ksLookupByName (result, "user:/someValue", KDB_O_NONE) != NULL, "must contain key user:/someValue"); + succeed_if (ksLookupByName (result, "user:/someOtherValue", KDB_O_NONE) != NULL, "must contain key user:/someOtherValue"); + + ksDel (contract); + ksDel (result); +} + +static void test_getPluginConfigFromContract_withoutConfigInContract (void) +{ + printf ("Executing %s\n", __func__); + + // Arrange + KeySet * contract = ksNew (30, keyNew ("system:/elektra/something", KEY_VALUE, "3", KEY_END), + keyNew ("system:/elektra/contract/mountglobal/otherPlugin", KEY_END), + keyNew ("system:/elektra/contract/mountglobal/otherPlugin/someValue", KEY_VALUE, "1", KEY_END), + keyNew ("system:/elektra/myPlugin", KEY_END), + keyNew ("system:/elektra/myPlugin/shouldntSeeMe", KEY_VALUE, "5", KEY_END), + keyNew ("system:/elektra/record/enabled", KEY_VALUE, "0", KEY_END), KS_END); + + // Act + KeySet * result = getPluginConfigFromContract ("myPlugin", contract); + + // Assert + succeed_if (result != NULL, "result must not be NULL"); + succeed_if (ksGetSize (result) == 0, "expected resulting config to have 0 keys"); + + ksDel (contract); + ksDel (result); +} + +static void test_loadPlugin (void) +{ + printf ("Executing %s\n", __func__); + + // Arrange + KeySet * global = ksNew (0, KS_END); + KeySet * modules = ksNew (0, KS_END); + KeySet * contract = ksNew (30, keyNew ("system:/elektra/contract/mountglobal/gopts", KEY_END), + keyNew ("system:/elektra/contract/mountglobal/gopts/offset", KEY_VALUE, "1", KEY_END), KS_END); + + Key * errorKey = keyNew ("system:/hello", KEY_END); + + // Act + Plugin * plugin = loadPlugin ("gopts", global, modules, contract, errorKey); + + // Assert + succeed_if (plugin != NULL, "must be able to load plugin"); + succeed_if (plugin->modules == modules, "must set modules"); + succeed_if (plugin->global == global, "must set global"); + succeed_if (ksGetSize (plugin->config) == 2, "config size must be 2"); + + ksDel (global); + ksDel (modules); + ksDel (contract); + elektraPluginClose (plugin, errorKey); + keyDel (errorKey); +} + +static void test_loadPlugin_inexistent (void) +{ + printf ("Executing %s\n", __func__); + + // Arrange + KeySet * global = ksNew (0, KS_END); + KeySet * modules = ksNew (0, KS_END); + KeySet * contract = ksNew (0, KS_END); + + Key * errorKey = keyNew ("system:/hello", KEY_END); + + // Act + Plugin * plugin = loadPlugin ("thisPluginSurelyDoesNotExist123", global, modules, contract, errorKey); + + // Assert + succeed_if (plugin == NULL, "must not load plugin"); + + ksDel (global); + ksDel (modules); + ksDel (contract); + keyDel (errorKey); +} + +static void test_isGoptsEnabledByContract (bool shouldBeEnabled) +{ + printf ("Executing %s with shouldBeEnabled=%d\n", __func__, shouldBeEnabled); + + // Arrange + KeySet * contract = ksNew (1, KS_END); + if (shouldBeEnabled) + { + ksAppendKey (contract, keyNew ("system:/elektra/contract/mountglobal/gopts", KEY_END)); + } + + // Act + bool result = isGoptsEnabledByContract (contract); + + // Assert + succeed_if_fmt (result == shouldBeEnabled, "result is %d but should be %d", result, shouldBeEnabled); + + ksDel (contract); +} + +static void test_initHooks_shouldInitAllHooksWithoutFailure (void) +{ + printf ("Executing %s\n", __func__); + + // Arrange + KDB * kdb = elektraCalloc (sizeof (struct _KDB)); + KeySet * config = ksNew (0, KS_END); + KeySet * modules = ksNew (0, KS_END); + KeySet * contract = ksNew (1, keyNew ("system:/elektra/contract/mountglobal/gopts", KEY_END), KS_END); + + kdb->global = ksNew (0, KS_END); + kdb->modules = modules; + + Key * errorKey = keyNew ("system:/elektra", KS_END); + + // Act + int result = initHooks (kdb, config, modules, contract, errorKey); + + // Assert + KeySet * meta = keyMeta (errorKey); + + succeed_if (result == 0, "result should be 0"); + succeed_if (ksGetSize (meta) == 0, "error key should not have meta data"); + succeed_if (kdb->hooks.gopts.plugin != NULL, "gopts plugin should be loaded"); + succeed_if (kdb->hooks.gopts.kdbHookGoptsGet != NULL, "gopts.kdbHookGoptsGet should be found"); + + ksDel (config); + ksDel (modules); + ksDel (contract); + + elektraFree (kdb); +} + +int main (int argc, char ** argv) +{ + printf ("HOOKS TESTS\n"); + printf ("=================\n\n"); + + init (argc, argv); + test_getPluginConfigFromContract_withConfigInContract (); + test_getPluginConfigFromContract_withoutConfigInContract (); + test_loadPlugin (); + test_loadPlugin_inexistent (); + test_isGoptsEnabledByContract (true); + test_isGoptsEnabledByContract (false); + test_initHooks_shouldInitAllHooksWithoutFailure (); + + printf ("\ntest_hooks RESULTS: %d test(s) done. %d error(s).\n", nbTest, nbError); + + return nbError; +}