dpm_require no longer takes a minimum version and applies no version criterion of its own. A handle now means the module is valid, not that it suits the caller. dpm_module_info_of is added alongside it, reporting the name, version, description, and minimum-libdpm-core version read at load, so a consuming module can judge a dependency's version for itself. The one rule still enforced is the minimum-version handshake, where libdpm-core is the host and refuses a module that demands a newer library than the one running. dpm_module_info_of joins the version script, so the exported surface is now fourteen symbols under DPM_CORE_1.0. Separately, the bare word "core" is gone from prose everywhere. It named both the command-line tool and the library, so every use forced the reader to guess which. Text now says "the dpm binary" or "libdpm-core". Identifiers keep their spelling: libdpm-core, core.h, core.conf, the "core" configuration namespace, dpm_core_version, core_min, DPM_CORE_1.0, the dpmcore namespace, test_core, core_api. Three user-visible strings changed with it: the load-refusal message now reads "requires libdpm-core >= X, running libdpm-core is Y — update libdpm-core", and the info module's description and help text name the library. The test asserting on the refusal text was updated to match. DESIGN.md's terminology line no longer defines "DPM Core" as the CLI, which was the source of the ambiguity. OVERVIEW.md is restructured around the three layers a reader meets DPM at — user, developer, filesystem — so a code-level symbol never appears without saying whose layer it is. MODULES.md describes the bundled info module as testing and demonstrating full DPM system functionality rather than as a reference implementation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
200 lines
7.3 KiB
C++
200 lines
7.3 KiB
C++
/**
|
|
* @file test_core.cpp
|
|
* @brief Test binary: drives libdpm-core through its public C API
|
|
*
|
|
* Exercises context lifecycle, configuration, the full load-time
|
|
* validation matrix against the fixture modules, versioned require,
|
|
* typed API access, generic dispatch, and enumeration. Exits nonzero
|
|
* on any failure.
|
|
*
|
|
* @copyright Copyright (c) 2026 SILO GROUP LLC
|
|
* @author Chris Punches <chris.punches@silogroup.org>
|
|
*
|
|
* Part of the Dark Horse Linux Package Manager (DPM)
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
#include <dpm/core.h>
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
#ifndef TEST_FIXTURE_MODULES
|
|
#error "TEST_FIXTURE_MODULES must be defined"
|
|
#endif
|
|
#ifndef TEST_FIXTURE_CONF
|
|
#error "TEST_FIXTURE_CONF must be defined"
|
|
#endif
|
|
|
|
static int g_failures = 0;
|
|
static int g_checks = 0;
|
|
|
|
#define CHECK(cond) \
|
|
do { \
|
|
g_checks++; \
|
|
if (!(cond)) { \
|
|
g_failures++; \
|
|
std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, \
|
|
#cond); \
|
|
} \
|
|
} while (0)
|
|
|
|
static bool error_contains(dpm_ctx* ctx, const char* needle)
|
|
{
|
|
const char* err = dpm_last_error(ctx);
|
|
return err != nullptr && std::strstr(err, needle) != nullptr;
|
|
}
|
|
|
|
/* Mirror of the good fixture's table layout (consumer-side spec decl). */
|
|
struct good_api_v1_s {
|
|
dpm_api_table_header hdr;
|
|
int (*ping)(void);
|
|
};
|
|
|
|
int main(void)
|
|
{
|
|
/* ---- dpm_open: invalid explicit override refuses ---- */
|
|
{
|
|
dpm_open_overrides bad = {"/does/not/exist/conf", nullptr, nullptr, -1};
|
|
CHECK(dpm_open(&bad) == nullptr);
|
|
|
|
dpm_open_overrides bad_level = {nullptr, nullptr, nullptr, 99};
|
|
CHECK(dpm_open(&bad_level) == nullptr);
|
|
}
|
|
|
|
/* ---- context with fixture config and fixture module path ---- */
|
|
dpm_open_overrides overrides = {TEST_FIXTURE_CONF, TEST_FIXTURE_MODULES,
|
|
nullptr, -1};
|
|
dpm_ctx* ctx = dpm_open(&overrides);
|
|
CHECK(ctx != nullptr);
|
|
if (!ctx) {
|
|
std::fprintf(stderr, "cannot continue without a context\n");
|
|
return 1;
|
|
}
|
|
|
|
/* ---- services ---- */
|
|
{
|
|
CHECK(std::strcmp(dpm_core_version(), DPM_CORE_VERSION_EXPECTED) == 0);
|
|
|
|
/* config: values resolve per-module, per-section */
|
|
const char* v = dpm_config_get(ctx, "testmod", "main", "key");
|
|
CHECK(v != nullptr && std::strcmp(v, "value") == 0);
|
|
v = dpm_config_get(ctx, "testmod", "section2", "other");
|
|
CHECK(v != nullptr && std::strcmp(v, "42") == 0);
|
|
CHECK(dpm_config_get(ctx, "testmod", "main", "absent") == nullptr);
|
|
CHECK(dpm_config_get(ctx, "nomodule", "main", "key") == nullptr);
|
|
|
|
/* module-path override wins over the config value */
|
|
const char* path = dpm_module_path(ctx);
|
|
CHECK(path != nullptr &&
|
|
std::strncmp(path, TEST_FIXTURE_MODULES,
|
|
std::strlen(TEST_FIXTURE_MODULES)) == 0);
|
|
}
|
|
|
|
/* ---- validation matrix: every broken fixture refused, precisely ---- */
|
|
{
|
|
CHECK(dpm_require(ctx, "missing_symbols") == nullptr);
|
|
CHECK(error_contains(ctx, "missing required contract symbols"));
|
|
CHECK(error_contains(ctx, "dpm_module_execute"));
|
|
|
|
CHECK(dpm_require(ctx, "core_too_new") == nullptr);
|
|
CHECK(error_contains(ctx, "update libdpm-core"));
|
|
|
|
CHECK(dpm_require(ctx, "bad_version") == nullptr);
|
|
CHECK(error_contains(ctx, "malformed version"));
|
|
|
|
CHECK(dpm_require(ctx, "lying_manifest") == nullptr);
|
|
CHECK(error_contains(ctx, "does not resolve"));
|
|
|
|
CHECK(dpm_require(ctx, "bad_magic") == nullptr);
|
|
CHECK(error_contains(ctx, "magic"));
|
|
|
|
CHECK(dpm_require(ctx, "nonexistent") == nullptr);
|
|
CHECK(error_contains(ctx, "not found"));
|
|
}
|
|
|
|
/* ---- known-good module: require, versions, api, execute ---- */
|
|
{
|
|
dpm_module* good = dpm_require(ctx, "good");
|
|
CHECK(good != nullptr);
|
|
|
|
/* loaded at most once per context */
|
|
CHECK(dpm_require(ctx, "good") == good);
|
|
|
|
/* libdpm-core reports what it saw; the caller judges compatibility */
|
|
dpm_module_info seen;
|
|
CHECK(dpm_module_info_of(ctx, good, &seen) == 0);
|
|
CHECK(std::strcmp(seen.name, "good") == 0);
|
|
CHECK(std::strcmp(seen.version, "1.2.3") == 0);
|
|
CHECK(std::strcmp(seen.core_min, "0.1.0") == 0);
|
|
CHECK(seen.description != nullptr && *seen.description);
|
|
|
|
CHECK(dpm_module_info_of(ctx, nullptr, &seen) != 0);
|
|
CHECK(dpm_module_info_of(ctx, good, nullptr) != 0);
|
|
|
|
/* typed access */
|
|
const void* table = dpm_get_api(ctx, good, "good", 1);
|
|
CHECK(table != nullptr);
|
|
if (table) {
|
|
const auto* api = static_cast<const good_api_v1_s*>(table);
|
|
CHECK(api->hdr.magic == DPM_API_TABLE_MAGIC);
|
|
CHECK(api->hdr.size == sizeof(good_api_v1_s));
|
|
CHECK(api->ping() == 42);
|
|
}
|
|
|
|
/* absent api/version pairs are known from the manifest */
|
|
CHECK(dpm_get_api(ctx, good, "good", 2) == nullptr);
|
|
CHECK(dpm_get_api(ctx, good, "ghost", 1) == nullptr);
|
|
|
|
/* generic dispatch */
|
|
CHECK(dpm_execute(ctx, good, nullptr, 0, nullptr) == 0);
|
|
}
|
|
|
|
/* ---- enumeration: only the valid module surfaces ---- */
|
|
{
|
|
dpm_cursor* cur = dpm_list_modules(ctx);
|
|
CHECK(cur != nullptr);
|
|
if (cur) {
|
|
int count = 0;
|
|
dpm_module_info info;
|
|
while (dpm_cursor_next(cur, &info) == 0) {
|
|
count++;
|
|
CHECK(std::strcmp(info.name, "good") == 0);
|
|
CHECK(std::strcmp(info.version, "1.2.3") == 0);
|
|
CHECK(std::strcmp(info.core_min, "0.1.0") == 0);
|
|
CHECK(info.description != nullptr && *info.description);
|
|
}
|
|
CHECK(count == 1);
|
|
dpm_cursor_free(cur);
|
|
}
|
|
|
|
/* unreadable module path refuses with a reason */
|
|
dpm_open_overrides bad_path = {TEST_FIXTURE_CONF, "/does/not/exist",
|
|
nullptr, -1};
|
|
dpm_ctx* ctx2 = dpm_open(&bad_path);
|
|
CHECK(ctx2 != nullptr);
|
|
if (ctx2) {
|
|
CHECK(dpm_list_modules(ctx2) == nullptr);
|
|
CHECK(error_contains(ctx2, "module path"));
|
|
dpm_close(ctx2);
|
|
}
|
|
}
|
|
|
|
dpm_close(ctx);
|
|
|
|
std::printf("%d checks, %d failures\n", g_checks, g_failures);
|
|
return g_failures == 0 ? 0 : 1;
|
|
}
|