summaryrefslogtreecommitdiff
path: root/tests/api
diff options
context:
space:
mode:
Diffstat (limited to 'tests/api')
-rw-r--r--tests/api/test-api.h186
-rw-r--r--tests/api/test-audit.c171
-rw-r--r--tests/api/test-buffer.c417
-rw-r--r--tests/api/test-bytecode.c643
-rw-r--r--tests/api/test-client.c334
-rw-r--r--tests/api/test-dependency.c551
-rw-r--r--tests/api/test-fileio.c236
-rw-r--r--tests/api/test-fragment.c376
-rw-r--r--tests/api/test-license.c311
-rw-r--r--tests/api/test-path-utils.c119
-rw-r--r--tests/api/test-tuple.c258
-rw-r--r--tests/api/test-variable.c247
12 files changed, 3849 insertions, 0 deletions
diff --git a/tests/api/test-api.h b/tests/api/test-api.h
new file mode 100644
index 000000000000..cbe6598c846e
--- /dev/null
+++ b/tests/api/test-api.h
@@ -0,0 +1,186 @@
+/*
+ * test-api.h
+ * test API macros and functions.
+ *
+ * SPDX-License-Identifier: pkgconf
+ *
+ * Copyright (c) 2026 pkgconf authors (see AUTHORS).
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * This software is provided 'as is' and without any warranty, express or
+ * implied. In no event shall the authors be liable for any damages arising
+ * from the use of this software.
+ */
+
+#ifndef TEST_API_H
+#define TEST_API_H
+
+#include <libpkgconf/stdinc.h>
+#include <libpkgconf/libpkgconf.h>
+#include <libpkgconf/path.h>
+#include <tests/win-shim.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#if defined(__GNUC__) && !defined(__clang__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wpragmas" // older gcc may not know the next one
+ // Stop false-positive warnings for test cases
+# pragma GCC diagnostic ignored "-Wanalyzer-tainted-assertion"
+#endif
+
+#define TEST_FAIL_(fmt, ...) \
+ do { \
+ fprintf(stderr, "FAIL: %s:%d: " fmt "\n", \
+ __FILE__, __LINE__, __VA_ARGS__); \
+ exit(EXIT_FAILURE); \
+ } while (0)
+
+#define TEST_ASSERT_TRUE(expr) \
+ do { \
+ if (!(expr)) \
+ TEST_FAIL_("TEST_ASSERT_TRUE(%s) was false", #expr); \
+ } while (0)
+
+#define TEST_ASSERT_FALSE(expr) \
+ do { \
+ if ((expr)) \
+ TEST_FAIL_("TEST_ASSERT_FALSE(%s) was true", #expr); \
+ } while (0)
+
+#define TEST_ASSERT_NULL(expr) \
+ do { \
+ const void *_v = (const void *)(expr); \
+ if (_v != NULL) \
+ TEST_FAIL_("TEST_ASSERT_NULL(%s) was %p", #expr, _v); \
+ } while (0)
+
+#define TEST_ASSERT_NONNULL(expr) \
+ do { \
+ if ((expr) == NULL) \
+ TEST_FAIL_("TEST_ASSERT_NONNULL(%s) was NULL", #expr); \
+ } while (0)
+
+#define TEST_ASSERT_EQ(a, b) \
+ do { \
+ long long _a = (long long)(a); \
+ long long _b = (long long)(b); \
+ if (_a != _b) \
+ TEST_FAIL_("TEST_ASSERT_EQ(%s, %s): %lld != %lld", \
+ #a, #b, _a, _b); \
+ } while (0)
+
+#define TEST_ASSERT_LT(a, b) \
+ do { \
+ long long _a = (long long)(a); \
+ long long _b = (long long)(b); \
+ if (_a >= _b) \
+ TEST_FAIL_("TEST_ASSERT_LT(%s, %s): %lld >= %lld", \
+ #a, #b, _a, _b); \
+ } while (0)
+
+#define TEST_ASSERT_LE(a, b) \
+ do { \
+ long long _a = (long long)(a); \
+ long long _b = (long long)(b); \
+ if (_a > _b) \
+ TEST_FAIL_("TEST_ASSERT_LE(%s, %s): %lld > %lld", \
+ #a, #b, _a, _b); \
+ } while (0)
+
+#define TEST_ASSERT_GT(a, b) \
+ do { \
+ long long _a = (long long)(a); \
+ long long _b = (long long)(b); \
+ if (_a <= _b) \
+ TEST_FAIL_("TEST_ASSERT_GT(%s, %s): %lld <= %lld", \
+ #a, #b, _a, _b); \
+ } while (0)
+
+#define TEST_ASSERT_GE(a, b) \
+ do { \
+ long long _a = (long long)(a); \
+ long long _b = (long long)(b); \
+ if (_a < _b) \
+ TEST_FAIL_("TEST_ASSERT_GE(%s, %s): %lld < %lld", \
+ #a, #b, _a, _b); \
+ } while (0)
+
+#define TEST_ASSERT_NE(a, b) \
+ do { \
+ long long _a = (long long)(a); \
+ long long _b = (long long)(b); \
+ if (_a == _b) \
+ TEST_FAIL_("TEST_ASSERT_NE(%s, %s): both %lld", \
+ #a, #b, _a); \
+ } while (0)
+
+#define TEST_ASSERT_STRCMP_EQ(actual, expected) \
+ do { \
+ const char *_a = (actual); \
+ const char *_e = (expected); \
+ if (_a == NULL || _e == NULL || strcmp(_a, _e) != 0) \
+ TEST_FAIL_("TEST_ASSERT_STRCMP_EQ(%s, %s): [%s] != [%s]", \
+ #actual, #expected, \
+ _a ? _a : "(null)", _e ? _e : "(null)"); \
+ } while (0)
+
+#define TEST_ASSERT_STRCASECMP_EQ(actual, expected) \
+ do { \
+ const char *_a = (actual); \
+ const char *_e = (expected); \
+ if (_a == NULL || _e == NULL || strcasecmp(_a, _e) != 0) \
+ TEST_FAIL_("TEST_ASSERT_STRCASECMP_EQ(%s, %s): [%s] !~ [%s]", \
+ #actual, #expected, \
+ _a ? _a : "(null)", _e ? _e : "(null)"); \
+ } while (0)
+
+#define TEST_ASSERT_STRNCMP_EQ(actual, expected, n) \
+ do { \
+ const char *_a = (actual); \
+ const char *_e = (expected); \
+ size_t _n = (size_t)(n); \
+ if (_a == NULL || _e == NULL || strncmp(_a, _e, _n) != 0) \
+ TEST_FAIL_("TEST_ASSERT_STRNCMP_EQ(%s, %s, %zu): [%s] != [%s]", \
+ #actual, #expected, _n, \
+ _a ? _a : "(null)", _e ? _e : "(null)"); \
+ } while (0)
+
+#define TEST_ASSERT_STRSTR(haystack, needle) \
+ do { \
+ const char *_h = (haystack); \
+ const char *_n = (needle); \
+ if (_h == NULL || _n == NULL || strstr(_h, _n) == NULL) \
+ TEST_FAIL_("TEST_ASSERT_STRSTR(%s, %s): [%s] does not contain [%s]", \
+ #haystack, #needle, \
+ _h ? _h : "(null)", _n ? _n : "(null)"); \
+ } while (0)
+
+#define TEST_ASSERT_EMPTY_STRING(expr) \
+ do { \
+ const char *_s = (expr); \
+ if (_s == NULL) \
+ TEST_FAIL_("TEST_ASSERT_EMPTY_STRING(%s) was NULL", #expr); \
+ if (_s[0] != '\0') \
+ TEST_FAIL_("TEST_ASSERT_EMPTY_STRING(%s) was [%s]", #expr, _s); \
+ } while (0)
+
+#define TEST_RUN(name, fn) \
+ do { \
+ fprintf(stderr, "%s -> %s:", name, #fn); \
+ fn(); \
+ fprintf(stderr, " PASS\n"); \
+ } while (0)
+
+static inline pkgconf_client_t *
+test_client_new(void)
+{
+ pkgconf_cross_personality_t *pers = pkgconf_cross_personality_default();
+ return pkgconf_client_new(NULL, NULL, pers, NULL, NULL);
+}
+
+#endif // TEST_API_H
diff --git a/tests/api/test-audit.c b/tests/api/test-audit.c
new file mode 100644
index 000000000000..12f9f31e0dda
--- /dev/null
+++ b/tests/api/test-audit.c
@@ -0,0 +1,171 @@
+/*
+ * test-audit.c
+ * Tests for the public libpkgconf audit API.
+ *
+ * SPDX-License-Identifier: pkgconf
+ *
+ * Copyright (c) 2026 pkgconf authors (see AUTHORS).
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * This software is provided 'as is' and without any warranty, express or
+ * implied. In no event shall the authors be liable for any damages arising
+ * from the use of this software.
+ */
+
+#include "test-api.h"
+
+/*
+ * Read the entire contents of f (from the start) into buf.
+ * buf must be large enough; we keep test inputs small.
+ */
+static void
+slurp(FILE *f, char *buf, size_t bufsz)
+{
+ rewind(f);
+ size_t n = fread(buf, 1, bufsz - 1, f);
+ buf[n] = '\0';
+}
+
+static void
+test_audit_log_no_logfile_is_noop(void)
+{
+ pkgconf_client_t *client = test_client_new();
+
+ // With no audit log set, logging must be a silent no-op and must not crash.
+ pkgconf_audit_log(client, "should go nowhere\n");
+
+ pkgconf_client_free(client);
+}
+
+static void
+test_audit_set_log_and_write(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ FILE *logf = tmpfile();
+ TEST_ASSERT_NONNULL(logf);
+
+ pkgconf_audit_set_log(client, logf);
+ pkgconf_audit_log(client, "hello %s\n", "world");
+
+ char buf[256];
+ slurp(logf, buf, sizeof(buf));
+ TEST_ASSERT_STRCMP_EQ(buf, "hello world\n");
+
+ fclose(logf);
+ pkgconf_client_free(client);
+}
+
+static void
+test_audit_log_multiple_writes(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ FILE *logf = tmpfile();
+ TEST_ASSERT_NONNULL(logf);
+
+ pkgconf_audit_set_log(client, logf);
+ pkgconf_audit_log(client, "first\n");
+ pkgconf_audit_log(client, "second %d\n", 2);
+
+ char buf[256];
+ slurp(logf, buf, sizeof(buf));
+ TEST_ASSERT_STRCMP_EQ(buf, "first\nsecond 2\n");
+
+ fclose(logf);
+ pkgconf_client_free(client);
+}
+
+static void
+test_audit_log_dependency_versionless(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ FILE *logf = tmpfile();
+ TEST_ASSERT_NONNULL(logf);
+
+ pkgconf_audit_set_log(client, logf);
+
+ // A minimal package: only the fields the logger reads
+ pkgconf_pkg_t pkg = { 0 };
+ pkg.id = (char *) "foo";
+ pkg.version = (char *) "1.2.3";
+
+ /* A dependency with PKGCONF_CMP_ANY and no version: the logger
+ * should emit only "id [version]", skipping the comparator */
+ pkgconf_dependency_t dep = { 0 };
+ dep.compare = PKGCONF_CMP_ANY;
+ dep.version = NULL;
+
+ pkgconf_audit_log_dependency(client, &pkg, &dep);
+
+ char buf[256];
+ slurp(logf, buf, sizeof(buf));
+ TEST_ASSERT_STRCMP_EQ(buf, "foo [1.2.3]\n");
+
+ fclose(logf);
+ pkgconf_client_free(client);
+}
+
+static void
+test_audit_log_dependency_versioned(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ FILE *logf = tmpfile();
+ TEST_ASSERT_NONNULL(logf);
+
+ pkgconf_audit_set_log(client, logf);
+
+ pkgconf_pkg_t pkg = { 0 };
+ pkg.id = (char *) "bar";
+ pkg.version = (char *) "2.0";
+
+ /* version set AND compare != ANY: the logger emits the
+ * comparator and required version between id and [version] */
+ pkgconf_dependency_t dep = { 0 };
+ dep.compare = PKGCONF_CMP_GREATER_THAN_EQUAL;
+ dep.version = (char *) "1.5";
+
+ pkgconf_audit_log_dependency(client, &pkg, &dep);
+
+ char buf[256];
+ slurp(logf, buf, sizeof(buf));
+ TEST_ASSERT_STRCMP_EQ(buf, "bar >= 1.5 [2.0]\n");
+
+ fclose(logf);
+ pkgconf_client_free(client);
+}
+
+static void
+test_audit_log_dependency_no_logfile_is_noop(void)
+{
+ pkgconf_client_t *client = test_client_new();
+
+ pkgconf_pkg_t pkg = { 0 };
+ pkg.id = (char *) "foo";
+ pkg.version = (char *) "1.0";
+
+ pkgconf_dependency_t dep = { 0 };
+ dep.compare = PKGCONF_CMP_ANY;
+
+ // No log set: must be a silent no-op
+ pkgconf_audit_log_dependency(client, &pkg, &dep);
+
+ pkgconf_client_free(client);
+}
+
+int
+main(int argc, char *argv[])
+{
+ (void) argc;
+ const char *basename = pkgconf_path_find_basename(argv[0]);
+
+ TEST_RUN(basename, test_audit_log_no_logfile_is_noop);
+ TEST_RUN(basename, test_audit_set_log_and_write);
+ TEST_RUN(basename, test_audit_log_multiple_writes);
+ TEST_RUN(basename, test_audit_log_dependency_versionless);
+ TEST_RUN(basename, test_audit_log_dependency_versioned);
+ TEST_RUN(basename, test_audit_log_dependency_no_logfile_is_noop);
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/api/test-buffer.c b/tests/api/test-buffer.c
new file mode 100644
index 000000000000..c17818ba3b4b
--- /dev/null
+++ b/tests/api/test-buffer.c
@@ -0,0 +1,417 @@
+/*
+ * test-buffer.c
+ * Tests for libpkgconf buffer API.
+ *
+ * SPDX-License-Identifier: pkgconf
+ *
+ * Copyright (c) 2025 pkgconf authors (see AUTHORS).
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * This software is provided 'as is' and without any warranty, express or
+ * implied. In no event shall the authors be liable for any damages arising
+ * from the use of this software.
+ */
+
+#include <libpkgconf/stdinc.h>
+#include <libpkgconf/libpkgconf.h>
+#include "test-api.h"
+
+static void
+test_buffer_empty(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+
+ TEST_ASSERT_EQ(pkgconf_buffer_len(&buf), 0);
+ TEST_ASSERT_NULL(pkgconf_buffer_str(&buf));
+ TEST_ASSERT_EMPTY_STRING(pkgconf_buffer_str_or_empty(&buf));
+ TEST_ASSERT_EQ(pkgconf_buffer_lastc(&buf), '\0');
+
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_buffer_append(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+
+ TEST_ASSERT_TRUE(pkgconf_buffer_append(&buf, "hello"));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "hello");
+ TEST_ASSERT_EQ(pkgconf_buffer_len(&buf), 5);
+
+ TEST_ASSERT_TRUE(pkgconf_buffer_append(&buf, " world"));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "hello world");
+ TEST_ASSERT_EQ(pkgconf_buffer_len(&buf), 11);
+
+ TEST_ASSERT_EQ(pkgconf_buffer_lastc(&buf), 'd');
+
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_buffer_append_slice(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+
+ TEST_ASSERT_TRUE(pkgconf_buffer_append_slice(&buf, "abcdefgh", 3));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "abc");
+
+ TEST_ASSERT_TRUE(pkgconf_buffer_append_slice(&buf, "xyz", 0));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "abc");
+
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_buffer_append_fmt(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+
+ TEST_ASSERT_TRUE(pkgconf_buffer_append_fmt(&buf, "%s=%d", "x", 42));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "x=42");
+
+ TEST_ASSERT_TRUE(pkgconf_buffer_append_fmt(&buf, " %s", "ok"));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "x=42 ok");
+
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_buffer_prepend(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_append(&buf, "world");
+ TEST_ASSERT_TRUE(pkgconf_buffer_prepend(&buf, "hello "));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "hello world");
+
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_buffer_push_byte(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+
+ TEST_ASSERT_TRUE(pkgconf_buffer_push_byte(&buf, 'a'));
+ TEST_ASSERT_TRUE(pkgconf_buffer_push_byte(&buf, 'b'));
+ TEST_ASSERT_TRUE(pkgconf_buffer_push_byte(&buf, 'c'));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "abc");
+ TEST_ASSERT_EQ(pkgconf_buffer_len(&buf), 3);
+
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_buffer_trim_byte(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_append(&buf, "hello\n");
+ TEST_ASSERT_TRUE(pkgconf_buffer_trim_byte(&buf));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "hello");
+
+ pkgconf_buffer_reset(&buf);
+ pkgconf_buffer_push_byte(&buf, 'x');
+ TEST_ASSERT_TRUE(pkgconf_buffer_trim_byte(&buf));
+ TEST_ASSERT_EQ(pkgconf_buffer_len(&buf), 0);
+ TEST_ASSERT_FALSE(pkgconf_buffer_trim_byte(&buf));
+
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_buffer_reset(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_append(&buf, "some content");
+ TEST_ASSERT_NE(pkgconf_buffer_len(&buf), 0);
+
+ pkgconf_buffer_reset(&buf);
+ TEST_ASSERT_EQ(pkgconf_buffer_len(&buf), 0);
+ TEST_ASSERT_NULL(pkgconf_buffer_str(&buf));
+
+ TEST_ASSERT_TRUE(pkgconf_buffer_append(&buf, "fresh"));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "fresh");
+
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_buffer_freeze(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_append(&buf, "frozen");
+ char *out = pkgconf_buffer_freeze(&buf);
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "frozen");
+ TEST_ASSERT_EQ(pkgconf_buffer_len(&buf), 0);
+
+ TEST_ASSERT_NULL(pkgconf_buffer_freeze(&buf));
+
+ free(out);
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_buffer_copy(void)
+{
+ pkgconf_buffer_t src = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_buffer_t dst = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_append(&src, "original");
+ pkgconf_buffer_append(&dst, "to be replaced");
+ TEST_ASSERT_TRUE(pkgconf_buffer_copy(&src, &dst));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&dst), "original");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&src), "original"); // src is unchanged
+
+ pkgconf_buffer_finalize(&src);
+ pkgconf_buffer_finalize(&dst);
+}
+
+static void
+test_buffer_join(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+
+ TEST_ASSERT_TRUE(pkgconf_buffer_join(&buf, '/', "usr", "local", "lib", NULL));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "usr/local/lib");
+
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_buffer_contains(void)
+{
+ pkgconf_buffer_t hay = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_buffer_t needle = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_append(&hay, "the quick brown fox");
+
+ pkgconf_buffer_append(&needle, "quick");
+ TEST_ASSERT_TRUE(pkgconf_buffer_contains(&hay, &needle));
+
+ pkgconf_buffer_reset(&needle);
+ pkgconf_buffer_append(&needle, "slow");
+ TEST_ASSERT_FALSE(pkgconf_buffer_contains(&hay, &needle));
+
+ TEST_ASSERT_TRUE(pkgconf_buffer_contains_byte(&hay, 'q'));
+ TEST_ASSERT_FALSE(pkgconf_buffer_contains_byte(&hay, 'z'));
+
+ pkgconf_buffer_finalize(&hay);
+ pkgconf_buffer_finalize(&needle);
+}
+
+static void
+test_buffer_match(void)
+{
+ pkgconf_buffer_t a = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_buffer_t b = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_append(&a, "identical");
+ pkgconf_buffer_append(&b, "identical");
+ TEST_ASSERT_TRUE(pkgconf_buffer_match(&a, &b));
+
+ // Identical length, but different contents
+ pkgconf_buffer_reset(&b);
+ pkgconf_buffer_append(&b, "different");
+ TEST_ASSERT_FALSE(pkgconf_buffer_match(&a, &b));
+
+ // Different length and contents
+ pkgconf_buffer_reset(&b);
+ pkgconf_buffer_append(&b, "different!");
+ TEST_ASSERT_FALSE(pkgconf_buffer_match(&a, &b));
+
+ pkgconf_buffer_finalize(&a);
+ pkgconf_buffer_finalize(&b);
+}
+
+static void
+test_buffer_has_prefix(void)
+{
+ pkgconf_buffer_t hay = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_buffer_t pre = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_append(&hay, "/usr/local/lib");
+
+ pkgconf_buffer_append(&pre, "/usr");
+ TEST_ASSERT_TRUE(pkgconf_buffer_has_prefix(&hay, &pre));
+
+ pkgconf_buffer_reset(&pre);
+ pkgconf_buffer_append(&pre, "/opt");
+ TEST_ASSERT_FALSE(pkgconf_buffer_has_prefix(&hay, &pre));
+
+ pkgconf_buffer_finalize(&hay);
+ pkgconf_buffer_finalize(&pre);
+}
+
+static void
+test_buffer_subst(void)
+{
+ pkgconf_buffer_t src = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_buffer_t dst = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_append(&src, "prefix=${PREFIX}/share");
+ TEST_ASSERT_TRUE(pkgconf_buffer_subst(&dst, &src, "${PREFIX}", "/opt/foo"));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&dst), "prefix=/opt/foo/share");
+
+ pkgconf_buffer_finalize(&src);
+ pkgconf_buffer_finalize(&dst);
+}
+
+static void
+test_buffer_subst_empty_pattern(void)
+{
+ pkgconf_buffer_t src = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_buffer_t dst0 = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_buffer_t dst1 = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_buffer_t dst2 = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_buffer_t dst3 = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_buffer_t dst4 = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_buffer_t dst5 = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_append(&src, "prefix=${PREFIX}/share");
+
+ TEST_ASSERT_TRUE(pkgconf_buffer_subst(&dst0, &src, "", "foo"));
+ TEST_ASSERT_TRUE(pkgconf_buffer_subst(&dst1, &src, "", ""));
+ TEST_ASSERT_TRUE(pkgconf_buffer_subst(&dst2, &src, "", NULL));
+ TEST_ASSERT_TRUE(pkgconf_buffer_subst(&dst3, &src, NULL, "foo"));
+ TEST_ASSERT_TRUE(pkgconf_buffer_subst(&dst4, &src, NULL, ""));
+ TEST_ASSERT_TRUE(pkgconf_buffer_subst(&dst5, &src, NULL, NULL));
+
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&dst0), "prefix=${PREFIX}/share");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&dst1), "prefix=${PREFIX}/share");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&dst2), "prefix=${PREFIX}/share");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&dst3), "prefix=${PREFIX}/share");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&dst4), "prefix=${PREFIX}/share");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&dst5), "prefix=${PREFIX}/share");
+
+ pkgconf_buffer_finalize(&src);
+ pkgconf_buffer_finalize(&dst0);
+ pkgconf_buffer_finalize(&dst1);
+ pkgconf_buffer_finalize(&dst2);
+ pkgconf_buffer_finalize(&dst3);
+ pkgconf_buffer_finalize(&dst4);
+ pkgconf_buffer_finalize(&dst5);
+}
+
+static void
+test_buffer_escape(void)
+{
+ pkgconf_buffer_t src = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_buffer_t dst = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_span_t spans[] = {
+ { ' ', ' ' },
+ { '\t', '\t' },
+ };
+
+ pkgconf_buffer_append(&src, "a b\tc");
+ TEST_ASSERT_TRUE(pkgconf_buffer_escape(&dst, &src, spans, PKGCONF_ARRAY_SIZE(spans)));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&dst), "a\\ b\\\tc");
+
+ pkgconf_buffer_finalize(&src);
+ pkgconf_buffer_finalize(&dst);
+}
+
+static void
+test_str_eq_slice(void)
+{
+ TEST_ASSERT_TRUE(pkgconf_str_eq_slice("hello", "hello", 5));
+ TEST_ASSERT_FALSE(pkgconf_str_eq_slice("hello!", "hello", 5));
+ TEST_ASSERT_FALSE(pkgconf_str_eq_slice("hello", "world", 5));
+ TEST_ASSERT_FALSE(pkgconf_str_eq_slice(NULL, "hello", 5));
+}
+
+static void
+test_span_contains(void)
+{
+ pkgconf_span_t spans[] = {
+ { '0', '9' },
+ { 'a', 'z' },
+ };
+
+ TEST_ASSERT_TRUE(pkgconf_span_contains('5', spans, PKGCONF_ARRAY_SIZE(spans)));
+ TEST_ASSERT_TRUE(pkgconf_span_contains('m', spans, PKGCONF_ARRAY_SIZE(spans)));
+ TEST_ASSERT_FALSE(pkgconf_span_contains('A', spans, PKGCONF_ARRAY_SIZE(spans)));
+ TEST_ASSERT_FALSE(pkgconf_span_contains('!', spans, PKGCONF_ARRAY_SIZE(spans)));
+}
+
+static void
+test_buffer_fputs_nonempty(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ FILE *f = tmpfile();
+ TEST_ASSERT_NONNULL(f);
+
+ pkgconf_buffer_append(&buf, "hello world");
+ TEST_ASSERT_TRUE(pkgconf_buffer_fputs(&buf, f));
+
+ char out[64];
+ rewind(f);
+ size_t n = fread(out, 1, sizeof(out) - 1, f);
+ out[n] = '\0';
+
+ // Buffer contents followed by a newline.
+ TEST_ASSERT_STRCMP_EQ(out, "hello world\n");
+
+ fclose(f);
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_buffer_fputs_empty(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ FILE *f = tmpfile();
+ TEST_ASSERT_NONNULL(f);
+
+ // An empty buffer writes only a newline.
+ TEST_ASSERT_TRUE(pkgconf_buffer_fputs(&buf, f));
+
+ char out[64];
+ rewind(f);
+ size_t n = fread(out, 1, sizeof(out) - 1, f);
+ out[n] = '\0';
+
+ TEST_ASSERT_STRCMP_EQ(out, "\n");
+
+ fclose(f);
+ pkgconf_buffer_finalize(&buf);
+}
+
+int
+main(int argc, const char **argv)
+{
+ (void) argc;
+
+ const char *basename = pkgconf_path_find_basename(argv[0]);
+
+ TEST_RUN(basename, test_buffer_empty);
+ TEST_RUN(basename, test_buffer_append);
+ TEST_RUN(basename, test_buffer_append_slice);
+ TEST_RUN(basename, test_buffer_append_fmt);
+ TEST_RUN(basename, test_buffer_prepend);
+ TEST_RUN(basename, test_buffer_push_byte);
+ TEST_RUN(basename, test_buffer_trim_byte);
+ TEST_RUN(basename, test_buffer_reset);
+ TEST_RUN(basename, test_buffer_freeze);
+ TEST_RUN(basename, test_buffer_copy);
+ TEST_RUN(basename, test_buffer_join);
+ TEST_RUN(basename, test_buffer_contains);
+ TEST_RUN(basename, test_buffer_match);
+ TEST_RUN(basename, test_buffer_has_prefix);
+ TEST_RUN(basename, test_buffer_subst);
+ TEST_RUN(basename, test_buffer_subst_empty_pattern);
+ TEST_RUN(basename, test_buffer_escape);
+ TEST_RUN(basename, test_str_eq_slice);
+ TEST_RUN(basename, test_span_contains);
+ TEST_RUN(basename, test_buffer_fputs_nonempty);
+ TEST_RUN(basename, test_buffer_fputs_empty);
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/api/test-bytecode.c b/tests/api/test-bytecode.c
new file mode 100644
index 000000000000..7be48941eed9
--- /dev/null
+++ b/tests/api/test-bytecode.c
@@ -0,0 +1,643 @@
+/*
+ * test-bytecode.c
+ * Tests for the public libpkgconf bytecode API:
+ * pkgconf_bytecode_compile and pkgconf_bytecode_eval_str.
+ *
+ * SPDX-License-Identifier: pkgconf
+ *
+ * Copyright (c) 2025 pkgconf authors (see AUTHORS).
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * This software is provided 'as is' and without any warranty, express or
+ * implied. In no event shall the authors be liable for any damages arising
+ * from the use of this software.
+ */
+
+#include <libpkgconf/stdinc.h>
+#include <libpkgconf/libpkgconf.h>
+#include "test-api.h"
+
+/*
+ * Build a variable list with the given key/value pairs. Caller frees
+ * with pkgconf_variable_list_free().
+ *
+ * The key/value pairs are stored by compiling `value` as bytecode and
+ * stashing the result on a pkgconf_variable_t inside the list, which
+ * is how the parser builds variable scopes for real .pc files.
+ */
+static void
+seed_variable(pkgconf_list_t *vars, const char *key, const char *value)
+{
+ pkgconf_variable_t *v = pkgconf_variable_get_or_create(vars, key);
+ TEST_ASSERT_NONNULL(v);
+
+ pkgconf_buffer_reset(&v->bcbuf);
+ pkgconf_bytecode_compile(&v->bcbuf, value);
+ pkgconf_bytecode_from_buffer(&v->bc, &v->bcbuf);
+}
+
+static void
+test_emit_text_and_eval(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ pkgconf_buffer_t bcbuf = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_bytecode_t bc;
+ bool saw_sysroot = false;
+
+ TEST_ASSERT_TRUE(pkgconf_bytecode_emit_text(&bcbuf, "plain", 5));
+ pkgconf_bytecode_from_buffer(&bc, &bcbuf);
+
+ pkgconf_buffer_t out = PKGCONF_BUFFER_INITIALIZER;
+ TEST_ASSERT_TRUE(pkgconf_bytecode_eval(client, &vars, &bc, &out, &saw_sysroot));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str_or_empty(&out), "plain");
+
+ pkgconf_buffer_finalize(&out);
+ pkgconf_buffer_finalize(&bcbuf);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_emit_guards(void)
+{
+ pkgconf_buffer_t bcbuf = PKGCONF_BUFFER_INITIALIZER;
+
+ TEST_ASSERT_TRUE(pkgconf_bytecode_emit_text(&bcbuf, NULL, 5));
+ TEST_ASSERT_TRUE(pkgconf_bytecode_emit_text(&bcbuf, "x", 0));
+ TEST_ASSERT_TRUE(pkgconf_bytecode_emit_var(&bcbuf, NULL, 3));
+ TEST_ASSERT_TRUE(pkgconf_bytecode_emit_var(&bcbuf, "x", 0));
+
+ TEST_ASSERT_EQ(pkgconf_buffer_len(&bcbuf), 0);
+
+ pkgconf_buffer_finalize(&bcbuf);
+}
+
+static void
+test_emit_var_and_eval(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ pkgconf_buffer_t bcbuf = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_bytecode_t bc;
+ bool saw_sysroot = false;
+
+ seed_variable(&vars, "name", "world");
+
+ TEST_ASSERT_TRUE(pkgconf_bytecode_emit_text(&bcbuf, "hello ", 6));
+ TEST_ASSERT_TRUE(pkgconf_bytecode_emit_var(&bcbuf, "name", 4));
+ pkgconf_bytecode_from_buffer(&bc, &bcbuf);
+
+ pkgconf_buffer_t out = PKGCONF_BUFFER_INITIALIZER;
+ TEST_ASSERT_TRUE(pkgconf_bytecode_eval(client, &vars, &bc, &out, &saw_sysroot));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str_or_empty(&out), "hello world");
+
+ pkgconf_buffer_finalize(&out);
+ pkgconf_buffer_finalize(&bcbuf);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_emit_sysroot_and_eval(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ pkgconf_buffer_t bcbuf = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_bytecode_t bc;
+ bool saw_sysroot = false;
+
+ pkgconf_client_set_sysroot_dir(client, "/sysroot");
+
+ TEST_ASSERT_TRUE(pkgconf_bytecode_emit_sysroot(&bcbuf));
+ TEST_ASSERT_TRUE(pkgconf_bytecode_emit_text(&bcbuf, "/usr/include", 12));
+ pkgconf_bytecode_from_buffer(&bc, &bcbuf);
+
+ pkgconf_buffer_t out = PKGCONF_BUFFER_INITIALIZER;
+ TEST_ASSERT_TRUE(pkgconf_bytecode_eval(client, &vars, &bc, &out, &saw_sysroot));
+ TEST_ASSERT_TRUE(saw_sysroot);
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str_or_empty(&out), "/sysroot/usr/include");
+
+ pkgconf_buffer_finalize(&out);
+ pkgconf_buffer_finalize(&bcbuf);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_eval_null_args(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ pkgconf_buffer_t out = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_bytecode_t bc = { NULL, 0 };
+
+ // NULL client, bc, or out all return false.
+ TEST_ASSERT_FALSE(pkgconf_bytecode_eval(NULL, &vars, &bc, &out, NULL));
+ TEST_ASSERT_FALSE(pkgconf_bytecode_eval(client, &vars, NULL, &out, NULL));
+ TEST_ASSERT_FALSE(pkgconf_bytecode_eval(client, &vars, &bc, NULL, NULL));
+
+ pkgconf_buffer_finalize(&out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_compile_null_args(void)
+{
+ pkgconf_buffer_t out = PKGCONF_BUFFER_INITIALIZER;
+
+ TEST_ASSERT_FALSE(pkgconf_bytecode_compile(NULL, "x"));
+ TEST_ASSERT_FALSE(pkgconf_bytecode_compile(&out, NULL));
+
+ pkgconf_buffer_finalize(&out);
+}
+
+static void
+test_dollar_escape(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ // $$ collapses to a literal $; the following text is NOT treated as a variable reference
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "price: $$5 and $${notavar}", &saw_sysroot);
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "price: $5 and ${notavar}");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_malformed_unclosed(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ // A ${ with no closing } is emitted as literal text
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "broken ${unclosed", &saw_sysroot);
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "broken ${unclosed");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_empty_braces(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ // ${} has a zero-length name; it's emitted as literal text rather than treated as a variable
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "empty ${} here", &saw_sysroot);
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "empty ${} here");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_compile_time_sysroot(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ pkgconf_client_set_sysroot_dir(client, "/sysroot");
+
+ // ${pc_sysrootdir} is special-cased at compile time into an OP_SYSROOT op rather than a variable lookup
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "${pc_sysrootdir}/usr/lib", &saw_sysroot);
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_TRUE(saw_sysroot);
+ TEST_ASSERT_STRCMP_EQ(out, "/sysroot/usr/lib");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_sysroot_dot_disables(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ // A sysroot of "." disables sysroot rewriting: ${pc_sysrootdir} expands to empty
+ pkgconf_client_set_sysroot_dir(client, ".");
+
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "${pc_sysrootdir}/usr/lib", &saw_sysroot);
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "/usr/lib");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_sysroot_root_disables(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ // A sysroot of "/" (the root dir) disables rewriting.
+ pkgconf_client_set_sysroot_dir(client, "/");
+
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "${pc_sysrootdir}/usr/lib", &saw_sysroot);
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "/usr/lib");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_sysroot_trailing_slash_trimmed(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ // Trailing slashes on the sysroot are normalized away, so the result doesn't get a doubled slash
+ pkgconf_client_set_sysroot_dir(client, "/sysroot///");
+
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "${pc_sysrootdir}/usr/lib", &saw_sysroot);
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_TRUE(saw_sysroot);
+ TEST_ASSERT_STRCMP_EQ(out, "/sysroot/usr/lib");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_sysroot_normalizes_to_root_disables(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ /* A sysroot of "//" survives the bare-"/" early check but normalizes down to "/" after
+ * trailing-slash trimming, which then disables rewriting (empty sysroot) */
+ pkgconf_client_set_sysroot_dir(client, "//");
+
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "${pc_sysrootdir}/usr/lib", &saw_sysroot);
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "/usr/lib");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_circular_reference(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ // A variable that references itself must not infinite-loop: the `expanding` guard breaks the cycle
+ seed_variable(&vars, "loop", "${loop}");
+
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "${loop}", &saw_sysroot);
+
+ /* The self-reference is broken; behavior is "expands to nothing further"
+ * We mostly care that it returns rather than hanging or crashing */
+ if (out != NULL)
+ free(out);
+
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_references_var(void)
+{
+ pkgconf_buffer_t bcbuf = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_bytecode_compile(&bcbuf, "-I${includedir} -L${libdir}");
+
+ TEST_ASSERT_TRUE(pkgconf_bytecode_references_var(&bcbuf, "includedir"));
+ TEST_ASSERT_TRUE(pkgconf_bytecode_references_var(&bcbuf, "libdir"));
+ TEST_ASSERT_FALSE(pkgconf_bytecode_references_var(&bcbuf, "prefix"));
+
+ TEST_ASSERT_FALSE(pkgconf_bytecode_references_var(NULL, "x"));
+ TEST_ASSERT_FALSE(pkgconf_bytecode_references_var(&bcbuf, NULL));
+
+ pkgconf_buffer_finalize(&bcbuf);
+}
+
+static void
+test_references_var_text_only(void)
+{
+ pkgconf_buffer_t bcbuf = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_bytecode_compile(&bcbuf, "just plain text");
+
+ TEST_ASSERT_FALSE(pkgconf_bytecode_references_var(&bcbuf, "anything"));
+
+ pkgconf_buffer_finalize(&bcbuf);
+}
+
+static void
+test_rewrite_selfrefs(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ pkgconf_buffer_t prev = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_bytecode_compile(&prev, "old");
+
+ pkgconf_buffer_t rhs = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_bytecode_compile(&rhs, "${foo} new");
+
+ pkgconf_buffer_t rewritten = PKGCONF_BUFFER_INITIALIZER;
+ TEST_ASSERT_TRUE(pkgconf_bytecode_rewrite_selfrefs(&rewritten, &rhs, "foo", &prev));
+
+ pkgconf_bytecode_t bc;
+ pkgconf_bytecode_from_buffer(&bc, &rewritten);
+
+ pkgconf_buffer_t out = PKGCONF_BUFFER_INITIALIZER;
+ TEST_ASSERT_TRUE(pkgconf_bytecode_eval(client, &vars, &bc, &out, &saw_sysroot));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str_or_empty(&out), "old new");
+
+ pkgconf_buffer_finalize(&out);
+ pkgconf_buffer_finalize(&rewritten);
+ pkgconf_buffer_finalize(&rhs);
+ pkgconf_buffer_finalize(&prev);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_rewrite_selfrefs_no_match(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ pkgconf_buffer_t prev = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_bytecode_compile(&prev, "PREV");
+
+ pkgconf_buffer_t rhs = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_bytecode_compile(&rhs, "${other} tail");
+
+ pkgconf_buffer_t rewritten = PKGCONF_BUFFER_INITIALIZER;
+ TEST_ASSERT_TRUE(pkgconf_bytecode_rewrite_selfrefs(&rewritten, &rhs, "foo", &prev));
+
+ // ${other} survives; seed it so eval can resolve it
+ seed_variable(&vars, "other", "OTHER");
+
+ pkgconf_bytecode_t bc;
+ pkgconf_bytecode_from_buffer(&bc, &rewritten);
+
+ pkgconf_buffer_t out = PKGCONF_BUFFER_INITIALIZER;
+ TEST_ASSERT_TRUE(pkgconf_bytecode_eval(client, &vars, &bc, &out, &saw_sysroot));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str_or_empty(&out), "OTHER tail");
+
+ pkgconf_buffer_finalize(&out);
+ pkgconf_buffer_finalize(&rewritten);
+ pkgconf_buffer_finalize(&rhs);
+ pkgconf_buffer_finalize(&prev);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_eval_plain_text(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ // A string with no variable references should round-trip unchanged.
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "plain text value", &saw_sysroot);
+
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "plain text value");
+ TEST_ASSERT_FALSE(saw_sysroot);
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_eval_variable_substitution(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ seed_variable(&vars, "prefix", "/opt/foo");
+
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "${prefix}/lib", &saw_sysroot);
+
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "/opt/foo/lib");
+ TEST_ASSERT_FALSE(saw_sysroot);
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_eval_nested_variables(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ // Recursive expansion: libdir references prefix.
+ seed_variable(&vars, "prefix", "/usr/local");
+ seed_variable(&vars, "libdir", "${prefix}/lib");
+
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "-L${libdir}", &saw_sysroot);
+
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "-L/usr/local/lib");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_eval_undefined_variable(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ // Referencing an undefined variable should produce empty substitution, not failure
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "prefix=${nonexistent}/end", &saw_sysroot);
+
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "prefix=/end");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_eval_multiple_variables(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ seed_variable(&vars, "prefix", "/usr");
+ seed_variable(&vars, "exec_prefix", "/usr/local");
+
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "-I${prefix}/include -L${exec_prefix}/lib", &saw_sysroot);
+
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "-I/usr/include -L/usr/local/lib");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_eval_empty_input(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "", &saw_sysroot);
+
+ // An empty input may evaluate to either NULL or an empty string
+ if (out != NULL)
+ {
+ TEST_ASSERT_STRCMP_EQ(out, "");
+ free(out);
+ }
+
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_eval_sysroot_detection(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ pkgconf_client_set_sysroot_dir(client, "/sysroot");
+
+ seed_variable(&vars, "pc_sysrootdir", "/sysroot");
+ seed_variable(&vars, "includedir", "${pc_sysrootdir}/usr/include");
+
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "${includedir}", &saw_sysroot);
+
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_TRUE(saw_sysroot);
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_compile_produces_nonempty_buffer(void)
+{
+ pkgconf_buffer_t bc = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_bytecode_compile(&bc, "hello ${world}");
+
+ // The compiled bytecode buffer should be non-empty
+ TEST_ASSERT_NE(pkgconf_buffer_len(&bc), 0);
+
+ pkgconf_buffer_finalize(&bc);
+}
+
+static void
+test_compile_eval_roundtrip(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ // Compile directly, then evaluate via the bc field on a variable
+ seed_variable(&vars, "name", "world");
+
+ pkgconf_variable_t *v = pkgconf_variable_get_or_create(&vars, "greeting");
+ TEST_ASSERT_NONNULL(v);
+ pkgconf_buffer_reset(&v->bcbuf);
+ pkgconf_bytecode_compile(&v->bcbuf, "hello ${name}");
+ pkgconf_bytecode_from_buffer(&v->bc, &v->bcbuf);
+
+ char *out = pkgconf_variable_eval_str(client, &vars, v, &saw_sysroot);
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "hello world");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+int
+main(int argc, char *argv[])
+{
+ (void) argc;
+ const char *basename = pkgconf_path_find_basename(argv[0]);
+
+ TEST_RUN(basename, test_eval_plain_text);
+ TEST_RUN(basename, test_eval_empty_input);
+ TEST_RUN(basename, test_eval_variable_substitution);
+ TEST_RUN(basename, test_eval_nested_variables);
+ TEST_RUN(basename, test_eval_multiple_variables);
+ TEST_RUN(basename, test_eval_undefined_variable);
+ TEST_RUN(basename, test_eval_sysroot_detection);
+ TEST_RUN(basename, test_eval_null_args);
+
+ TEST_RUN(basename, test_emit_guards);
+ TEST_RUN(basename, test_emit_text_and_eval);
+ TEST_RUN(basename, test_emit_var_and_eval);
+ TEST_RUN(basename, test_emit_sysroot_and_eval);
+
+ TEST_RUN(basename, test_dollar_escape);
+ TEST_RUN(basename, test_malformed_unclosed);
+ TEST_RUN(basename, test_empty_braces);
+
+ TEST_RUN(basename, test_circular_reference);
+ TEST_RUN(basename, test_references_var);
+ TEST_RUN(basename, test_references_var_text_only);
+
+ TEST_RUN(basename, test_compile_time_sysroot);
+ TEST_RUN(basename, test_sysroot_dot_disables);
+ TEST_RUN(basename, test_sysroot_root_disables);
+ TEST_RUN(basename, test_sysroot_trailing_slash_trimmed);
+ TEST_RUN(basename, test_sysroot_normalizes_to_root_disables);
+
+ TEST_RUN(basename, test_rewrite_selfrefs);
+ TEST_RUN(basename, test_rewrite_selfrefs_no_match);
+
+ TEST_RUN(basename, test_compile_eval_roundtrip);
+ TEST_RUN(basename, test_compile_produces_nonempty_buffer);
+ TEST_RUN(basename, test_compile_null_args);
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/api/test-client.c b/tests/api/test-client.c
new file mode 100644
index 000000000000..ae8889b658c8
--- /dev/null
+++ b/tests/api/test-client.c
@@ -0,0 +1,334 @@
+/*
+ * test-client.c
+ * Tests for the public libpkgconf client API.
+ *
+ * SPDX-License-Identifier: pkgconf
+ *
+ * Copyright (c) 2026 pkgconf authors (see AUTHORS).
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * This software is provided 'as is' and without any warranty, express or
+ * implied. In no event shall the authors be liable for any damages arising
+ * from the use of this software.
+ */
+
+#include "test-api.h"
+
+static void
+test_client_new_and_free(void)
+{
+ pkgconf_cross_personality_t *pers = pkgconf_cross_personality_default();
+ pkgconf_client_t *client = pkgconf_client_new(NULL, NULL, pers, NULL, NULL);
+ TEST_ASSERT_NONNULL(client);
+
+ pkgconf_client_free(client);
+}
+
+static void
+test_client_init_and_deinit_stack(void)
+{
+ /* Caller-allocated client, the path the CLI itself uses
+ * (pkgconf_cli_state_t embeds a pkgconf_client_t by value) */
+ pkgconf_client_t client = { 0 };
+ pkgconf_cross_personality_t *pers = pkgconf_cross_personality_default();
+
+ pkgconf_client_init(&client, NULL, NULL, pers, NULL, NULL);
+ pkgconf_client_deinit(&client);
+}
+
+static void
+test_client_sysroot_dir(void)
+{
+ pkgconf_client_t *client = test_client_new();
+
+ pkgconf_client_set_sysroot_dir(client, "/tmp/sysroot");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_client_get_sysroot_dir(client), "/tmp/sysroot");
+
+ pkgconf_client_set_sysroot_dir(client, "/opt/sysroot");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_client_get_sysroot_dir(client), "/opt/sysroot");
+
+ pkgconf_client_free(client);
+}
+
+static void
+test_client_buildroot_dir(void)
+{
+ pkgconf_client_t *client = test_client_new();
+
+ pkgconf_client_set_buildroot_dir(client, "/tmp/buildroot");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_client_get_buildroot_dir(client), "/tmp/buildroot");
+
+ pkgconf_client_free(client);
+}
+
+static void
+test_client_flags(void)
+{
+ pkgconf_client_t *client = test_client_new();
+
+ pkgconf_client_set_flags(client, PKGCONF_PKG_PKGF_NO_CACHE | PKGCONF_PKG_PKGF_SKIP_CONFLICTS);
+
+ unsigned int flags = pkgconf_client_get_flags(client);
+ TEST_ASSERT_NE(flags & PKGCONF_PKG_PKGF_NO_CACHE, 0);
+ TEST_ASSERT_NE(flags & PKGCONF_PKG_PKGF_SKIP_CONFLICTS, 0);
+ TEST_ASSERT_EQ(flags & PKGCONF_PKG_PKGF_ENV_ONLY, 0);
+
+ pkgconf_client_free(client);
+}
+
+static void
+test_client_prefix_varname(void)
+{
+ pkgconf_client_t *client = test_client_new();
+
+ pkgconf_client_set_prefix_varname(client, "prefix");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_client_get_prefix_varname(client), "prefix");
+
+ pkgconf_client_set_prefix_varname(client, "custom_prefix");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_client_get_prefix_varname(client), "custom_prefix");
+
+ pkgconf_client_free(client);
+}
+
+/*
+ * Capture buffer for handler tests. Each handler writes the message
+ * into this buffer so the test can verify it fired with the expected
+ * content. Reset before each test that uses it.
+ */
+static char capture_buf[256];
+
+static bool
+capture_handler(const char *msg, const pkgconf_client_t *client, void *data)
+{
+ (void) client;
+ (void) data;
+
+ strncpy(capture_buf, msg, sizeof(capture_buf) - 1);
+ capture_buf[sizeof(capture_buf) - 1] = '\0';
+ return true;
+}
+
+static void
+capture_reset(void)
+{
+ memset(capture_buf, 0, sizeof(capture_buf));
+}
+
+static void
+test_client_error_handler_fires(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ capture_reset();
+
+ pkgconf_client_set_error_handler(client, capture_handler, NULL);
+
+ TEST_ASSERT_EQ(pkgconf_client_get_error_handler(client), capture_handler);
+
+ pkgconf_error(client, "test error: %d", 42);
+
+ TEST_ASSERT_NE(capture_buf[0], '\0');
+ TEST_ASSERT_STRSTR(capture_buf, "test error: 42");
+
+ pkgconf_client_free(client);
+}
+
+static void
+test_client_warn_handler_fires(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ capture_reset();
+
+ pkgconf_client_set_warn_handler(client, capture_handler, NULL);
+
+ TEST_ASSERT_EQ(pkgconf_client_get_warn_handler(client), capture_handler);
+
+ pkgconf_warn(client, "test warning: %s", "hello");
+
+ TEST_ASSERT_NE(capture_buf[0], '\0');
+ TEST_ASSERT_STRSTR(capture_buf, "test warning: hello");
+
+ pkgconf_client_free(client);
+}
+
+#ifndef PKGCONF_LITE
+static void
+test_client_trace_handler_fires(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ capture_reset();
+
+ pkgconf_client_set_trace_handler(client, capture_handler, NULL);
+
+ TEST_ASSERT_EQ(pkgconf_client_get_trace_handler(client), capture_handler);
+
+ PKGCONF_TRACE(client, "trace message: %d", 7);
+
+ TEST_ASSERT_NE(capture_buf[0], '\0');
+ TEST_ASSERT_STRSTR(capture_buf, "trace message: 7");
+
+ pkgconf_client_free(client);
+}
+#endif
+
+static void
+unveil_capture_handler(const pkgconf_client_t *client, const char *path, const char *permissions)
+{
+ (void) client;
+ (void) permissions;
+
+ if (path != NULL)
+ {
+ strncpy(capture_buf, path, sizeof(capture_buf) - 1);
+ capture_buf[sizeof(capture_buf) - 1] = '\0';
+ }
+}
+
+static void
+test_client_unveil_handler_installation(void)
+{
+ pkgconf_client_t *client = test_client_new();
+
+ pkgconf_client_set_unveil_handler(client, unveil_capture_handler);
+ TEST_ASSERT_EQ(pkgconf_client_get_unveil_handler(client), unveil_capture_handler);
+
+ pkgconf_client_free(client);
+}
+
+/*
+ * Custom environ handler that returns canned values for known keys
+ * and NULL otherwise.
+ */
+static const char *
+canned_environ_handler(const pkgconf_client_t *client, const char *key)
+{
+ (void) client;
+
+ if (!strcmp(key, "PKG_TEST_VAR"))
+ return "the_value";
+ if (!strcmp(key, "EMPTY_VAR"))
+ return "";
+ if (!strcmp(key, "PKG_CONFIG_SYSTEM_INCLUDE_PATH"))
+ return "/custom/include";
+ if (!strcmp(key, "PKG_CONFIG_SYSTEM_LIBRARY_PATH"))
+ return "/custom/lib";
+
+ return NULL;
+}
+
+static void
+test_client_getenv_via_handler(void)
+{
+ pkgconf_cross_personality_t *pers = pkgconf_cross_personality_default();
+ pkgconf_client_t *client = pkgconf_client_new(NULL, NULL, pers, NULL, canned_environ_handler);
+ TEST_ASSERT_NONNULL(client);
+
+ const char *v = pkgconf_client_getenv(client, "PKG_TEST_VAR");
+ TEST_ASSERT_NONNULL(v);
+ TEST_ASSERT_STRCMP_EQ(v, "the_value");
+
+ v = pkgconf_client_getenv(client, "EMPTY_VAR");
+ TEST_ASSERT_NONNULL(v);
+ TEST_ASSERT_STRCMP_EQ(v, "");
+
+ v = pkgconf_client_getenv(client, "UNDEFINED_VAR");
+ TEST_ASSERT_NULL(v);
+
+ pkgconf_client_free(client);
+}
+
+static void
+test_client_dir_list_build_smoke(void)
+{
+ pkgconf_cross_personality_t *pers = pkgconf_cross_personality_default();
+ pkgconf_client_t *client = pkgconf_client_new(NULL, NULL, pers, NULL, NULL);
+ TEST_ASSERT_NONNULL(client);
+
+ pkgconf_client_dir_list_build(client, pers);
+
+ /* The personality's default dir list comes from PKG_DEFAULT_PATH
+ * (set at compile time). After build, the client should have
+ * SOME directories registered; we don't assert specific paths
+ * since they vary by build configuration. */
+ TEST_ASSERT_NONNULL(client->dir_list.head);
+
+ pkgconf_client_free(client);
+}
+
+static void
+test_client_init_system_paths_from_environ(void)
+{
+ pkgconf_cross_personality_t *pers = pkgconf_cross_personality_default();
+ pkgconf_client_t *client = pkgconf_client_new(NULL, NULL, pers, NULL, canned_environ_handler);
+ TEST_ASSERT_NONNULL(client);
+
+ /* With PKG_CONFIG_SYSTEM_{INCLUDE,LIBRARY}_PATH set, init builds the filter dirs from the
+ * environment instead of copying the personality's defaults. */
+ TEST_ASSERT_TRUE(pkgconf_path_match_list("/custom/include", &client->filter_includedirs));
+ TEST_ASSERT_TRUE(pkgconf_path_match_list("/custom/lib", &client->filter_libdirs));
+
+ pkgconf_client_free(client);
+}
+
+static void
+test_client_preload_from_environ(void)
+{
+ pkgconf_cross_personality_t *pers = pkgconf_cross_personality_default();
+ pkgconf_client_t *client = pkgconf_client_new(NULL, NULL, pers, NULL, NULL);
+ TEST_ASSERT_NONNULL(client);
+
+ /* preload_from_environ reads the named var via getenv directly.
+ * Point it at a dir; preload_path will try to load .pc files from there.
+ * An empty/nonexistent dir is fine; we're exercising the split-and-iterate path, not asserting
+ * loads. */
+ setenv("PKG_TEST_PRELOAD", "/nonexistent/dir", 1);
+
+ pkgconf_client_preload_from_environ(client, "PKG_TEST_PRELOAD");
+
+ unsetenv("PKG_TEST_PRELOAD");
+ pkgconf_client_free(client);
+}
+
+#ifndef PKGCONF_LITE
+static void
+test_client_trace_null_client(void)
+{
+ TEST_ASSERT_FALSE(pkgconf_trace(NULL, "test.c", 42, "func", "msg %d", 42));
+}
+#endif
+
+int
+main(int argc, char *argv[])
+{
+ (void) argc;
+ const char *basename = pkgconf_path_find_basename(argv[0]);
+
+ TEST_RUN(basename, test_client_new_and_free);
+ TEST_RUN(basename, test_client_init_and_deinit_stack);
+ TEST_RUN(basename, test_client_init_system_paths_from_environ);
+ TEST_RUN(basename, test_client_preload_from_environ);
+
+ TEST_RUN(basename, test_client_sysroot_dir);
+ TEST_RUN(basename, test_client_buildroot_dir);
+ TEST_RUN(basename, test_client_flags);
+ TEST_RUN(basename, test_client_prefix_varname);
+
+ TEST_RUN(basename, test_client_error_handler_fires);
+ TEST_RUN(basename, test_client_warn_handler_fires);
+#ifndef PKGCONF_LITE
+ TEST_RUN(basename, test_client_trace_handler_fires);
+#endif
+ TEST_RUN(basename, test_client_unveil_handler_installation);
+
+ TEST_RUN(basename, test_client_getenv_via_handler);
+
+ TEST_RUN(basename, test_client_dir_list_build_smoke);
+
+#ifndef PKGCONF_LITE
+ TEST_RUN(basename, test_client_trace_null_client);
+#endif
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/api/test-dependency.c b/tests/api/test-dependency.c
new file mode 100644
index 000000000000..edd00b620f18
--- /dev/null
+++ b/tests/api/test-dependency.c
@@ -0,0 +1,551 @@
+/*
+ * test-dependency.c
+ * Tests for the public libpkgconf dependency API.
+ *
+ * SPDX-License-Identifier: pkgconf
+ *
+ * Copyright (c) 2026 pkgconf authors (see AUTHORS).
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * This software is provided 'as is' and without any warranty, express or
+ * implied. In no event shall the authors be liable for any damages arising
+ * from the use of this software.
+ */
+
+#include "test-api.h"
+
+static size_t
+dependency_count(const pkgconf_list_t *list)
+{
+ size_t n = 0;
+ const pkgconf_node_t *iter;
+
+ PKGCONF_FOREACH_LIST_ENTRY(list->head, iter)
+ {
+ n++;
+ }
+
+ return n;
+}
+
+static const pkgconf_dependency_t *
+dependency_at(const pkgconf_list_t *list, size_t index)
+{
+ const pkgconf_node_t *iter;
+ size_t i = 0;
+
+ PKGCONF_FOREACH_LIST_ENTRY(list->head, iter)
+ {
+ if (i++ == index)
+ return iter->data;
+ }
+
+ return NULL;
+}
+
+static void
+test_comparator_lookup_known(void)
+{
+ TEST_ASSERT_EQ(pkgconf_pkg_comparator_lookup_by_name("="), PKGCONF_CMP_EQUAL);
+ TEST_ASSERT_EQ(pkgconf_pkg_comparator_lookup_by_name("!="), PKGCONF_CMP_NOT_EQUAL);
+ TEST_ASSERT_EQ(pkgconf_pkg_comparator_lookup_by_name("<"), PKGCONF_CMP_LESS_THAN);
+ TEST_ASSERT_EQ(pkgconf_pkg_comparator_lookup_by_name("<="), PKGCONF_CMP_LESS_THAN_EQUAL);
+ TEST_ASSERT_EQ(pkgconf_pkg_comparator_lookup_by_name(">"), PKGCONF_CMP_GREATER_THAN);
+ TEST_ASSERT_EQ(pkgconf_pkg_comparator_lookup_by_name(">="), PKGCONF_CMP_GREATER_THAN_EQUAL);
+}
+
+static void
+test_comparator_lookup_unknown(void)
+{
+ TEST_ASSERT_EQ(pkgconf_pkg_comparator_lookup_by_name("~~"), PKGCONF_CMP_ANY);
+ TEST_ASSERT_EQ(pkgconf_pkg_comparator_lookup_by_name(""), PKGCONF_CMP_ANY);
+}
+
+static void
+test_comparator_roundtrip(void)
+{
+ pkgconf_pkg_comparator_t values[] =
+ {
+ PKGCONF_CMP_NOT_EQUAL,
+ PKGCONF_CMP_ANY,
+ PKGCONF_CMP_LESS_THAN,
+ PKGCONF_CMP_LESS_THAN_EQUAL,
+ PKGCONF_CMP_EQUAL,
+ PKGCONF_CMP_GREATER_THAN,
+ PKGCONF_CMP_GREATER_THAN_EQUAL,
+ };
+
+ for (size_t i = 0; i < PKGCONF_ARRAY_SIZE(values); i++)
+ {
+ pkgconf_dependency_t dep = { 0 };
+ dep.compare = values[i];
+
+ const char *str = pkgconf_pkg_get_comparator(&dep);
+ TEST_ASSERT_NONNULL(str);
+
+ // ANY's value does not round-trip so do not check
+ if (values[i] == PKGCONF_CMP_ANY)
+ continue;
+
+ pkgconf_pkg_comparator_t back = pkgconf_pkg_comparator_lookup_by_name(str);
+ TEST_ASSERT_EQ(back, values[i]);
+ }
+}
+
+static void
+test_parse_str_single(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t deps = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_dependency_parse_str(client, &deps, "foo", 0);
+
+ TEST_ASSERT_EQ(dependency_count(&deps), 1);
+ const pkgconf_dependency_t *d = dependency_at(&deps, 0);
+ TEST_ASSERT_NONNULL(d);
+ TEST_ASSERT_STRCMP_EQ(d->package, "foo");
+ TEST_ASSERT_EQ(d->compare, PKGCONF_CMP_ANY);
+ TEST_ASSERT_NULL(d->version);
+
+ pkgconf_dependency_free(&deps);
+ pkgconf_client_free(client);
+}
+
+static void
+test_parse_str_versioned(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t deps = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_dependency_parse_str(client, &deps, "foo >= 1.2.3", 0);
+
+ TEST_ASSERT_EQ(dependency_count(&deps), 1);
+ const pkgconf_dependency_t *d = dependency_at(&deps, 0);
+ TEST_ASSERT_NONNULL(d);
+ TEST_ASSERT_STRCMP_EQ(d->package, "foo");
+ TEST_ASSERT_EQ(d->compare, PKGCONF_CMP_GREATER_THAN_EQUAL);
+ TEST_ASSERT_STRCMP_EQ(d->version, "1.2.3");
+
+ pkgconf_dependency_free(&deps);
+ pkgconf_client_free(client);
+}
+
+static void
+test_parse_str_multiple_space_separated(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t deps = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_dependency_parse_str(client, &deps, "foo bar baz", 0);
+
+ TEST_ASSERT_EQ(dependency_count(&deps), 3);
+
+ const pkgconf_dependency_t *d0 = dependency_at(&deps, 0);
+ const pkgconf_dependency_t *d1 = dependency_at(&deps, 1);
+ const pkgconf_dependency_t *d2 = dependency_at(&deps, 2);
+ TEST_ASSERT_NONNULL(d0);
+ TEST_ASSERT_NONNULL(d1);
+ TEST_ASSERT_NONNULL(d2);
+ TEST_ASSERT_STRCMP_EQ(d0->package, "foo");
+ TEST_ASSERT_STRCMP_EQ(d1->package, "bar");
+ TEST_ASSERT_STRCMP_EQ(d2->package, "baz");
+
+ pkgconf_dependency_free(&deps);
+ pkgconf_client_free(client);
+}
+
+static void
+test_parse_str_multiple_comma_separated(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t deps = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_dependency_parse_str(client, &deps, "foo, bar, baz", 0);
+
+ TEST_ASSERT_EQ(dependency_count(&deps), 3);
+
+ const pkgconf_dependency_t *d0 = dependency_at(&deps, 0);
+ const pkgconf_dependency_t *d1 = dependency_at(&deps, 1);
+ const pkgconf_dependency_t *d2 = dependency_at(&deps, 2);
+ TEST_ASSERT_NONNULL(d0);
+ TEST_ASSERT_NONNULL(d1);
+ TEST_ASSERT_NONNULL(d2);
+ TEST_ASSERT_STRCMP_EQ(d0->package, "foo");
+ TEST_ASSERT_STRCMP_EQ(d1->package, "bar");
+ TEST_ASSERT_STRCMP_EQ(d2->package, "baz");
+
+ pkgconf_dependency_free(&deps);
+ pkgconf_client_free(client);
+}
+
+static void
+test_parse_str_mixed_versioned_and_bare(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t deps = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_dependency_parse_str(client, &deps, "foo >= 1.0, bar, baz < 2.0", 0);
+
+ TEST_ASSERT_EQ(dependency_count(&deps), 3);
+
+ const pkgconf_dependency_t *d0 = dependency_at(&deps, 0);
+ const pkgconf_dependency_t *d1 = dependency_at(&deps, 1);
+ const pkgconf_dependency_t *d2 = dependency_at(&deps, 2);
+
+ TEST_ASSERT_NONNULL(d0);
+ TEST_ASSERT_NONNULL(d1);
+ TEST_ASSERT_NONNULL(d2);
+
+ TEST_ASSERT_STRCMP_EQ(d0->package, "foo");
+ TEST_ASSERT_EQ(d0->compare, PKGCONF_CMP_GREATER_THAN_EQUAL);
+ TEST_ASSERT_STRCMP_EQ(d0->version, "1.0");
+
+ TEST_ASSERT_STRCMP_EQ(d1->package, "bar");
+ TEST_ASSERT_EQ(d1->compare, PKGCONF_CMP_ANY);
+
+ TEST_ASSERT_STRCMP_EQ(d2->package, "baz");
+ TEST_ASSERT_EQ(d2->compare, PKGCONF_CMP_LESS_THAN);
+ TEST_ASSERT_STRCMP_EQ(d2->version, "2.0");
+
+ pkgconf_dependency_free(&deps);
+ pkgconf_client_free(client);
+}
+
+static void
+test_parse_str_empty(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t deps = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_dependency_parse_str(client, &deps, "", 0);
+ TEST_ASSERT_EQ(dependency_count(&deps), 0);
+
+ pkgconf_dependency_free(&deps);
+ pkgconf_client_free(client);
+}
+
+static void
+test_parse_str_all_comparators(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t deps = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_dependency_parse_str(client, &deps, "a = 1, b != 2, c < 3, d <= 4, e > 5, f >= 6", 0);
+
+ TEST_ASSERT_EQ(dependency_count(&deps), 6);
+
+ const pkgconf_dependency_t *d0 = dependency_at(&deps, 0);
+ const pkgconf_dependency_t *d1 = dependency_at(&deps, 1);
+ const pkgconf_dependency_t *d2 = dependency_at(&deps, 2);
+ const pkgconf_dependency_t *d3 = dependency_at(&deps, 3);
+ const pkgconf_dependency_t *d4 = dependency_at(&deps, 4);
+ const pkgconf_dependency_t *d5 = dependency_at(&deps, 5);
+
+ TEST_ASSERT_NONNULL(d0);
+ TEST_ASSERT_NONNULL(d1);
+ TEST_ASSERT_NONNULL(d2);
+ TEST_ASSERT_NONNULL(d3);
+ TEST_ASSERT_NONNULL(d4);
+ TEST_ASSERT_NONNULL(d5);
+
+ TEST_ASSERT_EQ(d0->compare, PKGCONF_CMP_EQUAL);
+ TEST_ASSERT_EQ(d1->compare, PKGCONF_CMP_NOT_EQUAL);
+ TEST_ASSERT_EQ(d2->compare, PKGCONF_CMP_LESS_THAN);
+ TEST_ASSERT_EQ(d3->compare, PKGCONF_CMP_LESS_THAN_EQUAL);
+ TEST_ASSERT_EQ(d4->compare, PKGCONF_CMP_GREATER_THAN);
+ TEST_ASSERT_EQ(d5->compare, PKGCONF_CMP_GREATER_THAN_EQUAL);
+
+ pkgconf_dependency_free(&deps);
+ pkgconf_client_free(client);
+}
+
+static void
+test_dependency_add(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t deps = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_dependency_t *d = pkgconf_dependency_add(client, &deps, "foo", "1.0", PKGCONF_CMP_GREATER_THAN_EQUAL, 0);
+ TEST_ASSERT_NONNULL(d);
+ TEST_ASSERT_STRCMP_EQ(d->package, "foo");
+ TEST_ASSERT_STRCMP_EQ(d->version, "1.0");
+ TEST_ASSERT_EQ(d->compare, PKGCONF_CMP_GREATER_THAN_EQUAL);
+
+ TEST_ASSERT_EQ(dependency_count(&deps), 1);
+
+ pkgconf_dependency_unref(client, d);
+ pkgconf_dependency_free(&deps);
+ pkgconf_client_free(client);
+}
+
+static void
+test_dependency_add_no_version(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t deps = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_dependency_t *d = pkgconf_dependency_add(client, &deps, "foo", NULL, PKGCONF_CMP_ANY, 0);
+ TEST_ASSERT_NONNULL(d);
+ TEST_ASSERT_STRCMP_EQ(d->package, "foo");
+ TEST_ASSERT_EQ(d->compare, PKGCONF_CMP_ANY);
+
+ pkgconf_dependency_unref(client, d);
+ pkgconf_dependency_free(&deps);
+ pkgconf_client_free(client);
+}
+
+static void
+test_dependency_add_multiple(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t deps = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_dependency_t *d0 = pkgconf_dependency_add(client, &deps, "foo", NULL, PKGCONF_CMP_ANY, 0);
+ pkgconf_dependency_t *d1 = pkgconf_dependency_add(client, &deps, "bar", "2.0", PKGCONF_CMP_EQUAL, 0);
+ pkgconf_dependency_t *d2 = pkgconf_dependency_add(client, &deps, "baz", "3.0", PKGCONF_CMP_LESS_THAN, 0);
+
+ TEST_ASSERT_NONNULL(d0);
+ TEST_ASSERT_NONNULL(d1);
+ TEST_ASSERT_NONNULL(d2);
+
+ TEST_ASSERT_EQ(dependency_count(&deps), 3);
+
+ pkgconf_dependency_unref(client, d0);
+ pkgconf_dependency_unref(client, d1);
+ pkgconf_dependency_unref(client, d2);
+ pkgconf_dependency_free(&deps);
+ pkgconf_client_free(client);
+}
+
+static void
+test_dependency_collision_drops_flagged_newcomer(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t deps = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_dependency_t *first = pkgconf_dependency_add(client, &deps, "foo", "1.0", PKGCONF_CMP_EQUAL, 0);
+ TEST_ASSERT_NONNULL(first);
+ TEST_ASSERT_EQ(dependency_count(&deps), 1);
+
+ /* Adding the same dep WITH flags collides; the flagged newcomer
+ * is dropped in favour of the existing unflagged node, so _add
+ * returns NULL and the count stays at 1 */
+ pkgconf_dependency_t *second = pkgconf_dependency_add(client, &deps, "foo", "1.0", PKGCONF_CMP_EQUAL, PKGCONF_PKG_DEPF_INTERNAL);
+ TEST_ASSERT_NULL(second);
+ TEST_ASSERT_EQ(dependency_count(&deps), 1);
+
+ pkgconf_dependency_unref(client, first);
+ pkgconf_dependency_free(&deps);
+ pkgconf_client_free(client);
+}
+
+static void
+test_dependency_collision_drops_flagged_existing(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t deps = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_dependency_t *first = pkgconf_dependency_add(client, &deps, "foo", "1.0", PKGCONF_CMP_EQUAL, PKGCONF_PKG_DEPF_INTERNAL);
+ TEST_ASSERT_NONNULL(first);
+ TEST_ASSERT_EQ(dependency_count(&deps), 1);
+
+ /* Adding the same dep UNFLAGGED collides; the existing flagged
+ * node is deleted and unref'd, the unflagged newcomer takes its
+ * place; count stays at 1, but the node is now the new one */
+ pkgconf_dependency_t *second = pkgconf_dependency_add(client, &deps, "foo", "1.0", PKGCONF_CMP_EQUAL, 0);
+ TEST_ASSERT_NONNULL(second);
+ TEST_ASSERT_EQ(dependency_count(&deps), 1);
+ TEST_ASSERT_EQ(second->flags, 0);
+
+ pkgconf_dependency_unref(client, first);
+ pkgconf_dependency_unref(client, second);
+ pkgconf_dependency_free(&deps);
+ pkgconf_client_free(client);
+}
+
+static void
+test_version_equal(void)
+{
+ TEST_ASSERT_EQ(pkgconf_compare_version("1.0", "1.0"), 0);
+ TEST_ASSERT_EQ(pkgconf_compare_version("1.2.3", "1.2.3"), 0);
+ TEST_ASSERT_EQ(pkgconf_compare_version("", ""), 0);
+}
+
+static void
+test_version_simple_numeric(void)
+{
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0", "1.1"), 0);
+ TEST_ASSERT_GT(pkgconf_compare_version("1.1", "1.0"), 0);
+
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0", "2.0"), 0);
+ TEST_ASSERT_GT(pkgconf_compare_version("2.0", "1.0"), 0);
+
+ TEST_ASSERT_LT(pkgconf_compare_version("1.2.3", "1.2.4"), 0);
+ TEST_ASSERT_LT(pkgconf_compare_version("1.2.3", "1.3.0"), 0);
+ TEST_ASSERT_LT(pkgconf_compare_version("1.2.3", "2.0.0"), 0);
+}
+
+static void
+test_version_numeric_segments_not_lexical(void)
+{
+ TEST_ASSERT_GT(pkgconf_compare_version("1.10", "1.9"), 0);
+ TEST_ASSERT_GT(pkgconf_compare_version("1.100", "1.99"), 0);
+}
+
+static void
+test_version_different_lengths(void)
+{
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0", "1.0.1"), 0);
+ TEST_ASSERT_GT(pkgconf_compare_version("1.0.1", "1.0"), 0);
+}
+
+static void
+test_version_alpha_suffix(void)
+{
+ TEST_ASSERT_GT(pkgconf_compare_version("1.0a", "1.0"), 0);
+ TEST_ASSERT_GT(pkgconf_compare_version("1.0alpha", "1.0"), 0);
+ TEST_ASSERT_GT(pkgconf_compare_version("1.0rc1", "1.0"), 0);
+
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0alpha", "1.0beta"), 0);
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0beta", "1.0rc"), 0);
+}
+
+static void
+test_version_tilde_prerelease(void)
+{
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0~rc1", "1.0"), 0);
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0~alpha", "1.0"), 0);
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0~beta", "1.0~rc"), 0);
+
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0~rc1", "1.0rc1"), 0);
+}
+
+static void
+test_version_numeric_beats_alpha(void)
+{
+ TEST_ASSERT_GT(pkgconf_compare_version("1.0.1", "1.0a"), 0);
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0a", "1.0.1"), 0);
+}
+
+static void
+test_version_alpha_ordering(void)
+{
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0a", "1.0b"), 0);
+ TEST_ASSERT_GT(pkgconf_compare_version("1.0b", "1.0a"), 0);
+}
+
+static void
+test_version_dotted_vs_hyphenated(void)
+{
+ TEST_ASSERT_EQ(pkgconf_compare_version("1.0-1", "1.0.1"), 0);
+}
+
+static void
+test_version_leading_zeros(void)
+{
+ TEST_ASSERT_EQ(pkgconf_compare_version("1.01", "1.1"), 0);
+ TEST_ASSERT_EQ(pkgconf_compare_version("01.0", "1.0"), 0);
+}
+
+static void
+test_version_trailing_zero_segments(void)
+{
+ /* Pkgconf does NOT treat "1.0" and "1.0.0" as equivalent.
+ * Even a zero trailing numeric segment is additional content that makes the version greater.
+ */
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0", "1.0.0"), 0);
+ TEST_ASSERT_GT(pkgconf_compare_version("1.0.0", "1.0"), 0);
+ TEST_ASSERT_LT(pkgconf_compare_version("1", "1.0"), 0);
+ TEST_ASSERT_LT(pkgconf_compare_version("1", "1.0.0.0"), 0);
+}
+
+static void
+test_version_null_handling(void)
+{
+ TEST_ASSERT_LT(pkgconf_compare_version(NULL, "1.0"), 0);
+ TEST_ASSERT_GT(pkgconf_compare_version("1.0", NULL), 0);
+ TEST_ASSERT_LT(pkgconf_compare_version(NULL, NULL), 0);
+}
+
+static void
+test_version_tilde_both_sides(void)
+{
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0~", "1.0a"), 0);
+ TEST_ASSERT_GT(pkgconf_compare_version("1.0a", "1.0~"), 0);
+
+ TEST_ASSERT_EQ(pkgconf_compare_version("1.0~rc", "1.0~rc"), 0);
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0~a", "1.0~b"), 0);
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0~1", "1.0~2"), 0);
+}
+
+static void
+test_version_separator_equivalence(void)
+{
+ TEST_ASSERT_EQ(pkgconf_compare_version("1.0", "1-0"), 0);
+ TEST_ASSERT_EQ(pkgconf_compare_version("1.2.3", "1_2_3"), 0);
+ TEST_ASSERT_EQ(pkgconf_compare_version("1..2", "1.2"), 0);
+}
+
+static void
+test_version_case_insensitive(void)
+{
+ TEST_ASSERT_EQ(pkgconf_compare_version("1.0RC1", "1.0rc1"), 0);
+ TEST_ASSERT_EQ(pkgconf_compare_version("1.0A", "1.0a"), 0);
+}
+
+static void
+test_version_alpha_prefix(void)
+{
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0alpha", "1.0alphabeta"), 0);
+ TEST_ASSERT_GT(pkgconf_compare_version("1.0alphabeta", "1.0alpha"), 0);
+
+ TEST_ASSERT_LT(pkgconf_compare_version("1.0a", "1.0ab"), 0);
+ TEST_ASSERT_GT(pkgconf_compare_version("1.0ab", "1.0a"), 0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ (void) argc;
+ const char *basename = pkgconf_path_find_basename(argv[0]);
+
+ TEST_RUN(basename, test_comparator_lookup_known);
+ TEST_RUN(basename, test_comparator_lookup_unknown);
+ TEST_RUN(basename, test_comparator_roundtrip);
+
+ TEST_RUN(basename, test_parse_str_empty);
+ TEST_RUN(basename, test_parse_str_single);
+ TEST_RUN(basename, test_parse_str_versioned);
+ TEST_RUN(basename, test_parse_str_multiple_space_separated);
+ TEST_RUN(basename, test_parse_str_multiple_comma_separated);
+ TEST_RUN(basename, test_parse_str_mixed_versioned_and_bare);
+ TEST_RUN(basename, test_parse_str_all_comparators);
+
+ TEST_RUN(basename, test_dependency_add);
+ TEST_RUN(basename, test_dependency_add_no_version);
+ TEST_RUN(basename, test_dependency_add_multiple);
+ TEST_RUN(basename, test_dependency_collision_drops_flagged_newcomer);
+ TEST_RUN(basename, test_dependency_collision_drops_flagged_existing);
+
+ TEST_RUN(basename, test_version_equal);
+ TEST_RUN(basename, test_version_simple_numeric);
+ TEST_RUN(basename, test_version_numeric_segments_not_lexical);
+ TEST_RUN(basename, test_version_different_lengths);
+ TEST_RUN(basename, test_version_alpha_suffix);
+ TEST_RUN(basename, test_version_tilde_prerelease);
+ TEST_RUN(basename, test_version_numeric_beats_alpha);
+ TEST_RUN(basename, test_version_alpha_ordering);
+ TEST_RUN(basename, test_version_dotted_vs_hyphenated);
+ TEST_RUN(basename, test_version_leading_zeros);
+ TEST_RUN(basename, test_version_trailing_zero_segments);
+ TEST_RUN(basename, test_version_null_handling);
+ TEST_RUN(basename, test_version_tilde_both_sides);
+ TEST_RUN(basename, test_version_separator_equivalence);
+ TEST_RUN(basename, test_version_case_insensitive);
+ TEST_RUN(basename, test_version_alpha_prefix);
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/api/test-fileio.c b/tests/api/test-fileio.c
new file mode 100644
index 000000000000..a4d5f81679a6
--- /dev/null
+++ b/tests/api/test-fileio.c
@@ -0,0 +1,236 @@
+/*
+ * test-fileio.c
+ * Tests for the public libpkgconf file i/o API.
+ *
+ * SPDX-License-Identifier: pkgconf
+ *
+ * Copyright (c) 2026 pkgconf authors (see AUTHORS).
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * This software is provided 'as is' and without any warranty, express or
+ * implied. In no event shall the authors be liable for any damages arising
+ * from the use of this software.
+ */
+
+#include "test-api.h"
+
+static FILE *
+fmemstream(const char *contents)
+{
+ FILE *f = tmpfile();
+
+ TEST_ASSERT_NONNULL(f);
+
+ fwrite(contents, 1, strlen(contents), f);
+ rewind(f);
+
+ return f;
+}
+
+static void
+test_fgetline_no_trailing_newline(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ FILE *f = fmemstream("hello");
+
+ TEST_ASSERT_TRUE(pkgconf_fgetline(&buf, f));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "hello");
+
+ pkgconf_buffer_reset(&buf);
+ TEST_ASSERT_FALSE(pkgconf_fgetline(&buf, f));
+
+ fclose(f);
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_fgetline_empty_stream(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ FILE *f = fmemstream("");
+
+ TEST_ASSERT_FALSE(pkgconf_fgetline(&buf, f));
+
+ fclose(f);
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_fgetline_lf(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ FILE *f = fmemstream("hello\nworld\n");
+
+ TEST_ASSERT_TRUE(pkgconf_fgetline(&buf, f));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "hello");
+
+ pkgconf_buffer_reset(&buf);
+ TEST_ASSERT_TRUE(pkgconf_fgetline(&buf, f));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "world");
+
+ pkgconf_buffer_reset(&buf);
+ TEST_ASSERT_FALSE(pkgconf_fgetline(&buf, f));
+
+ fclose(f);
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_fgetline_crlf(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ FILE *f = fmemstream("hello\r\nworld\r\n");
+
+ TEST_ASSERT_TRUE(pkgconf_fgetline(&buf, f));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "hello");
+
+ pkgconf_buffer_reset(&buf);
+ TEST_ASSERT_TRUE(pkgconf_fgetline(&buf, f));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "world");
+
+ pkgconf_buffer_reset(&buf);
+ TEST_ASSERT_FALSE(pkgconf_fgetline(&buf, f));
+
+ fclose(f);
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_fgetline_lone_cr(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ FILE *f = fmemstream("hello\rworld\r");
+
+ TEST_ASSERT_TRUE(pkgconf_fgetline(&buf, f));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "hello");
+
+ pkgconf_buffer_reset(&buf);
+ TEST_ASSERT_TRUE(pkgconf_fgetline(&buf, f));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "world");
+
+ pkgconf_buffer_reset(&buf);
+ TEST_ASSERT_FALSE(pkgconf_fgetline(&buf, f));
+
+ fclose(f);
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_fgetline_backslash_continuation_lf(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ FILE *f = fmemstream("foo\\\nbar\n");
+
+ TEST_ASSERT_TRUE(pkgconf_fgetline(&buf, f));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "foobar");
+
+ fclose(f);
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_fgetline_backslash_continuation_crlf(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ FILE *f = fmemstream("foo\\\r\nbar\r\n");
+
+ TEST_ASSERT_TRUE(pkgconf_fgetline(&buf, f));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "foobar");
+
+ pkgconf_buffer_reset(&buf);
+ TEST_ASSERT_FALSE(pkgconf_fgetline(&buf, f));
+
+ fclose(f);
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_fgetline_backslash_continuation_lone_cr(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ FILE *f = fmemstream("foo\\\rbar\r");
+
+ TEST_ASSERT_TRUE(pkgconf_fgetline(&buf, f));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "foobar");
+
+ pkgconf_buffer_reset(&buf);
+ TEST_ASSERT_FALSE(pkgconf_fgetline(&buf, f));
+
+ fclose(f);
+ pkgconf_buffer_finalize(&buf);
+}
+
+// A backslash not immediately followed by a newline is not a continuation,
+// so it must be preserved literally in the output.
+static void
+test_fgetline_backslash_not_continuation(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ FILE *f = fmemstream("foo\\bar\n");
+
+ TEST_ASSERT_TRUE(pkgconf_fgetline(&buf, f));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "foo\\bar");
+
+ pkgconf_buffer_reset(&buf);
+ TEST_ASSERT_FALSE(pkgconf_fgetline(&buf, f));
+
+ fclose(f);
+ pkgconf_buffer_finalize(&buf);
+}
+
+// fgets() only stops on '\n', a full read buffer, or EOF. NOT on on a lone '\r'.
+// If a '\r' happens to be the very last byte fgets() manages to read
+// before the buffer fills up, the matching '\n' is still unread in the
+// stream and isn't visible to pkgconf_fgetline()'s lookahead, so the CRLF
+// pair gets split across two fgets() calls and is misparsed as two lines.
+static void
+test_fgetline_crlf_split_across_fgets_buffer(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ size_t prefix_len = PKGCONF_ITEM_SIZE - 2;
+ char *content = malloc(prefix_len + strlen("\r\nworld\n") + 1);
+ FILE *f;
+
+ TEST_ASSERT_NONNULL(content);
+ memset(content, 'a', prefix_len);
+ strcpy(content + prefix_len, "\r\nworld\n");
+
+ f = fmemstream(content);
+ free(content);
+
+ TEST_ASSERT_TRUE(pkgconf_fgetline(&buf, f));
+ TEST_ASSERT_EQ(strlen(pkgconf_buffer_str(&buf)), prefix_len);
+
+ pkgconf_buffer_reset(&buf);
+ TEST_ASSERT_TRUE(pkgconf_fgetline(&buf, f));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "world");
+
+ pkgconf_buffer_reset(&buf);
+ TEST_ASSERT_FALSE(pkgconf_fgetline(&buf, f));
+
+ fclose(f);
+ pkgconf_buffer_finalize(&buf);
+}
+
+int
+main(int argc, const char **argv)
+{
+ (void) argc;
+ const char *basename = pkgconf_path_find_basename(argv[0]);
+
+ TEST_RUN(basename, test_fgetline_no_trailing_newline);
+ TEST_RUN(basename, test_fgetline_empty_stream);
+ TEST_RUN(basename, test_fgetline_lf);
+ TEST_RUN(basename, test_fgetline_crlf);
+ TEST_RUN(basename, test_fgetline_lone_cr);
+ TEST_RUN(basename, test_fgetline_backslash_continuation_lf);
+ TEST_RUN(basename, test_fgetline_backslash_continuation_crlf);
+ TEST_RUN(basename, test_fgetline_backslash_continuation_lone_cr);
+ TEST_RUN(basename, test_fgetline_backslash_not_continuation);
+ TEST_RUN(basename, test_fgetline_crlf_split_across_fgets_buffer);
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/api/test-fragment.c b/tests/api/test-fragment.c
new file mode 100644
index 000000000000..3d49145ae518
--- /dev/null
+++ b/tests/api/test-fragment.c
@@ -0,0 +1,376 @@
+/*
+ * test-fragment.c
+ * Tests for the public libpkgconf fragment API.
+ *
+ * SPDX-License-Identifier: pkgconf
+ *
+ * Copyright (c) 2026 pkgconf authors (see AUTHORS).
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * This software is provided 'as is' and without any warranty, express or
+ * implied. In no event shall the authors be liable for any damages arising
+ * from the use of this software.
+ */
+
+#include "test-api.h"
+
+static size_t
+fragment_count(const pkgconf_list_t *list)
+{
+ size_t n = 0;
+ const pkgconf_node_t *iter;
+
+ PKGCONF_FOREACH_LIST_ENTRY(list->head, iter)
+ {
+ n++;
+ }
+
+ return n;
+}
+
+static const pkgconf_fragment_t *
+fragment_at(const pkgconf_list_t *list, size_t index)
+{
+ const pkgconf_node_t *iter;
+ size_t i = 0;
+
+ PKGCONF_FOREACH_LIST_ENTRY(list->head, iter)
+ {
+ if (i++ == index)
+ return iter->data;
+ }
+
+ return NULL;
+}
+
+/*
+ * Render a fragment list to a newly-allocated C string for assertions.
+ * Caller frees.
+ */
+static char *
+render_to_string(const pkgconf_list_t *list)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_fragment_render_buf(list, &buf, false, NULL, ' ');
+
+ if (pkgconf_buffer_str(&buf) == NULL)
+ return strdup("");
+
+ char *out = strdup(pkgconf_buffer_str(&buf));
+ pkgconf_buffer_finalize(&buf);
+ return out;
+}
+
+static void
+test_fragment_parse_cflags(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t frags = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ TEST_ASSERT_TRUE(pkgconf_fragment_parse(client, &frags, &vars, "-I/usr/include -DFOO=1", 0));
+ TEST_ASSERT_EQ(fragment_count(&frags), 2);
+
+ const pkgconf_fragment_t *first = fragment_at(&frags, 0);
+ TEST_ASSERT_NONNULL(first);
+ TEST_ASSERT_EQ(first->type, 'I');
+ TEST_ASSERT_STRCMP_EQ(first->data, "/usr/include");
+
+ pkgconf_fragment_free(&frags);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_fragment_parse_libs(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t frags = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ TEST_ASSERT_TRUE(pkgconf_fragment_parse(client, &frags, &vars, "-L/usr/lib -lfoo -lbar", 0));
+ TEST_ASSERT_EQ(fragment_count(&frags), 3);
+
+ const pkgconf_fragment_t *f0 = fragment_at(&frags, 0);
+ const pkgconf_fragment_t *f1 = fragment_at(&frags, 1);
+ const pkgconf_fragment_t *f2 = fragment_at(&frags, 2);
+
+ TEST_ASSERT_NONNULL(f0);
+ TEST_ASSERT_NONNULL(f1);
+ TEST_ASSERT_NONNULL(f2);
+
+ TEST_ASSERT_EQ(f0->type, 'L');
+ TEST_ASSERT_STRCMP_EQ(f0->data, "/usr/lib");
+ TEST_ASSERT_EQ(f1->type, 'l');
+ TEST_ASSERT_STRCMP_EQ(f1->data, "foo");
+ TEST_ASSERT_EQ(f2->type, 'l');
+ TEST_ASSERT_STRCMP_EQ(f2->data, "bar");
+
+ pkgconf_fragment_free(&frags);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_fragment_parse_empty(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t frags = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ TEST_ASSERT_TRUE(pkgconf_fragment_parse(client, &frags, &vars, "", 0));
+ TEST_ASSERT_EQ(fragment_count(&frags), 0);
+
+ pkgconf_fragment_free(&frags);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_fragment_add_single(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t frags = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_fragment_add(client, &frags, &vars, "-I/opt/include", 0);
+
+ TEST_ASSERT_EQ(fragment_count(&frags), 1);
+
+ const pkgconf_fragment_t *f = fragment_at(&frags, 0);
+ TEST_ASSERT_NONNULL(f);
+ TEST_ASSERT_EQ(f->type, 'I');
+ TEST_ASSERT_STRCMP_EQ(f->data, "/opt/include");
+
+ pkgconf_fragment_free(&frags);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_fragment_render_cflags(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t frags = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_fragment_parse(client, &frags, &vars, "-I/usr/include -I/opt/include", 0);
+
+ char *rendered = render_to_string(&frags);
+ TEST_ASSERT_NONNULL(rendered);
+ TEST_ASSERT_STRCMP_EQ(rendered, "-I/usr/include -I/opt/include");
+
+ free(rendered);
+ pkgconf_fragment_free(&frags);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_fragment_render_libs(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t frags = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_fragment_parse(client, &frags, &vars, "-L/usr/lib -lfoo", 0);
+
+ char *rendered = render_to_string(&frags);
+ TEST_ASSERT_NONNULL(rendered);
+ TEST_ASSERT_STRCMP_EQ(rendered, "-L/usr/lib -lfoo");
+
+ free(rendered);
+ pkgconf_fragment_free(&frags);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_fragment_render_empty(void)
+{
+ pkgconf_list_t frags = PKGCONF_LIST_INITIALIZER;
+
+ char *rendered = render_to_string(&frags);
+ TEST_ASSERT_NONNULL(rendered);
+ TEST_ASSERT_EMPTY_STRING(rendered);
+
+ free(rendered);
+}
+
+// Filter predicate: keep only -I (include) fragments.
+static bool
+filter_only_includes(const pkgconf_client_t *client, const pkgconf_fragment_t *frag, void *data)
+{
+ (void) client;
+ (void) data;
+ return frag->type == 'I';
+}
+
+// Filter predicate: keep only -l (library name) fragments.
+static bool
+filter_only_libnames(const pkgconf_client_t *client, const pkgconf_fragment_t *frag, void *data)
+{
+ (void) client;
+ (void) data;
+ return frag->type == 'l';
+}
+
+// Filter predicate: keep nothing.
+static bool
+filter_nothing(const pkgconf_client_t *client, const pkgconf_fragment_t *frag, void *data)
+{
+ (void) client;
+ (void) frag;
+ (void) data;
+ return false;
+}
+
+static void
+test_fragment_filter_only_includes(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t src = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t dst = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_fragment_parse(client, &src, &vars, "-I/usr/include -L/usr/lib -lfoo -I/opt/include", 0);
+
+ pkgconf_fragment_filter(client, &dst, &src, filter_only_includes, NULL);
+ TEST_ASSERT_EQ(fragment_count(&dst), 2);
+
+ const pkgconf_fragment_t *f0 = fragment_at(&dst, 0);
+ const pkgconf_fragment_t *f1 = fragment_at(&dst, 1);
+ TEST_ASSERT_NONNULL(f0);
+ TEST_ASSERT_NONNULL(f1);
+ TEST_ASSERT_EQ(f0->type, 'I');
+ TEST_ASSERT_STRCMP_EQ(f0->data, "/usr/include");
+ TEST_ASSERT_EQ(f1->type, 'I');
+ TEST_ASSERT_STRCMP_EQ(f1->data, "/opt/include");
+
+ pkgconf_fragment_free(&dst);
+ pkgconf_fragment_free(&src);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_fragment_filter_only_libnames(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t src = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t dst = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_fragment_parse(client, &src, &vars, "-L/usr/lib -lfoo -lbar -I/usr/include", 0);
+
+ pkgconf_fragment_filter(client, &dst, &src, filter_only_libnames, NULL);
+
+ TEST_ASSERT_EQ(fragment_count(&dst), 2);
+
+ const pkgconf_fragment_t *f0 = fragment_at(&dst, 0);
+ const pkgconf_fragment_t *f1 = fragment_at(&dst, 1);
+ TEST_ASSERT_NONNULL(f0);
+ TEST_ASSERT_NONNULL(f1);
+ TEST_ASSERT_EQ(f0->type, 'l');
+ TEST_ASSERT_STRCMP_EQ(f0->data, "foo");
+ TEST_ASSERT_EQ(f1->type, 'l');
+ TEST_ASSERT_STRCMP_EQ(f1->data, "bar");
+
+ pkgconf_fragment_free(&dst);
+ pkgconf_fragment_free(&src);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_fragment_filter_keeps_nothing(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t src = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t dst = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_fragment_parse(client, &src, &vars, "-I/usr/include -lfoo", 0);
+
+ pkgconf_fragment_filter(client, &dst, &src, filter_nothing, NULL);
+
+ TEST_ASSERT_EQ(fragment_count(&dst), 0);
+
+ pkgconf_fragment_free(&dst);
+ pkgconf_fragment_free(&src);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_fragment_has_system_dir_matches(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t frags = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_path_add("/usr/include", &client->filter_includedirs, false);
+
+ pkgconf_fragment_parse(client, &frags, &vars, "-I/usr/include -I/opt/include", 0);
+
+ const pkgconf_fragment_t *system = fragment_at(&frags, 0);
+ const pkgconf_fragment_t *other = fragment_at(&frags, 1);
+ TEST_ASSERT_NONNULL(system);
+ TEST_ASSERT_NONNULL(other);
+
+ TEST_ASSERT_TRUE(pkgconf_fragment_has_system_dir(client, system));
+ TEST_ASSERT_FALSE(pkgconf_fragment_has_system_dir(client, other));
+
+ pkgconf_fragment_free(&frags);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_fragment_has_system_dir_libs(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t frags = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_path_add("/usr/lib", &client->filter_libdirs, false);
+
+ pkgconf_fragment_parse(client, &frags, &vars, "-L/usr/lib -L/opt/lib", 0);
+
+ const pkgconf_fragment_t *system = fragment_at(&frags, 0);
+ const pkgconf_fragment_t *other = fragment_at(&frags, 1);
+ TEST_ASSERT_NONNULL(system);
+ TEST_ASSERT_NONNULL(other);
+
+ TEST_ASSERT_TRUE(pkgconf_fragment_has_system_dir(client, system));
+ TEST_ASSERT_FALSE(pkgconf_fragment_has_system_dir(client, other));
+
+ pkgconf_fragment_free(&frags);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+int
+main(int argc, char *argv[])
+{
+ (void) argc;
+ const char *basename = pkgconf_path_find_basename(argv[0]);
+
+ TEST_RUN(basename, test_fragment_parse_empty);
+ TEST_RUN(basename, test_fragment_parse_cflags);
+ TEST_RUN(basename, test_fragment_parse_libs);
+ TEST_RUN(basename, test_fragment_add_single);
+ TEST_RUN(basename, test_fragment_render_empty);
+ TEST_RUN(basename, test_fragment_render_cflags);
+ TEST_RUN(basename, test_fragment_render_libs);
+ TEST_RUN(basename, test_fragment_filter_only_includes);
+ TEST_RUN(basename, test_fragment_filter_only_libnames);
+ TEST_RUN(basename, test_fragment_filter_keeps_nothing);
+ TEST_RUN(basename, test_fragment_has_system_dir_matches);
+ TEST_RUN(basename, test_fragment_has_system_dir_libs);
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/api/test-license.c b/tests/api/test-license.c
new file mode 100644
index 000000000000..d77ed21a4c86
--- /dev/null
+++ b/tests/api/test-license.c
@@ -0,0 +1,311 @@
+/*
+ * test-license.c
+ * Tests for the public libpkgconf license API.
+ *
+ * SPDX-License-Identifier: pkgconf
+ *
+ * Copyright (c) 2026 pkgconf authors (see AUTHORS).
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * This software is provided 'as is' and without any warranty, express or
+ * implied. In no event shall the authors be liable for any damages arising
+ * from the use of this software.
+ */
+
+#include "test-api.h"
+
+static size_t
+license_count(const pkgconf_list_t *list)
+{
+ size_t n = 0;
+ const pkgconf_node_t *iter;
+
+ PKGCONF_FOREACH_LIST_ENTRY(list->head, iter)
+ {
+ n++;
+ }
+
+ return n;
+}
+
+static char *
+render_to_string(pkgconf_client_t *client, const pkgconf_list_t *list)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+ pkgconf_license_render(client, list, &buf);
+
+ char *out = strdup(pkgconf_buffer_str_or_empty(&buf));
+ pkgconf_buffer_finalize(&buf);
+ return out;
+}
+
+static void
+test_license_insert_and_free(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t licenses = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_license_insert(client, &licenses, PKGCONF_LICENSE_EXPRESSION, "BSD-3-Clause");
+ TEST_ASSERT_EQ(license_count(&licenses), 1);
+
+ const pkgconf_license_t *l = licenses.head->data;
+ TEST_ASSERT_NONNULL(l);
+ TEST_ASSERT_EQ(l->type, PKGCONF_LICENSE_EXPRESSION);
+ TEST_ASSERT_STRCMP_EQ(l->data, "BSD-3-Clause");
+
+ pkgconf_license_free(&licenses);
+ pkgconf_client_free(client);
+}
+
+static void
+test_license_free_empty(void)
+{
+ pkgconf_list_t licenses = PKGCONF_LIST_INITIALIZER;
+
+ // Freeing an empty list is a no-op. Smoke test.
+ pkgconf_license_free(&licenses);
+
+ // Smoke test
+ pkgconf_license_free(NULL);
+}
+
+static void
+test_license_evaluate_single(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t licenses = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_license_evaluate_str(client, &licenses, "BSD-3-Clause", 0);
+
+ TEST_ASSERT_EQ(license_count(&licenses), 1);
+ const pkgconf_license_t *l = licenses.head->data;
+ TEST_ASSERT_EQ(l->type, PKGCONF_LICENSE_EXPRESSION);
+ TEST_ASSERT_STRCMP_EQ(l->data, "BSD-3-Clause");
+
+ pkgconf_license_free(&licenses);
+ pkgconf_client_free(client);
+}
+
+static void
+test_license_evaluate_or(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t licenses = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_license_evaluate_str(client, &licenses, "MIT OR ISC", 0);
+
+ TEST_ASSERT_EQ(license_count(&licenses), 3);
+
+ char *rendered = render_to_string(client, &licenses);
+ TEST_ASSERT_STRCMP_EQ(rendered, "MIT OR ISC");
+ free(rendered);
+
+ pkgconf_license_free(&licenses);
+ pkgconf_client_free(client);
+}
+
+static void
+test_license_evaluate_and(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t licenses = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_license_evaluate_str(client, &licenses, "LGPL-2.1-only AND MIT", 0);
+
+ char *rendered = render_to_string(client, &licenses);
+ TEST_ASSERT_STRCMP_EQ(rendered, "LGPL-2.1-only AND MIT");
+ free(rendered);
+
+ pkgconf_license_free(&licenses);
+ pkgconf_client_free(client);
+}
+
+static void
+test_license_evaluate_multiple_keys_implicit_and(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t licenses = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_license_evaluate_str(client, &licenses, "BSD-3-Clause", 0);
+ pkgconf_license_evaluate_str(client, &licenses, "BSD-2-Clause", 0);
+
+ char *rendered = render_to_string(client, &licenses);
+ TEST_ASSERT_STRCMP_EQ(rendered, "BSD-3-Clause AND BSD-2-Clause");
+ free(rendered);
+
+ pkgconf_license_free(&licenses);
+ pkgconf_client_free(client);
+}
+
+static void
+test_license_evaluate_brackets(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t licenses = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_license_evaluate_str(client, &licenses, "ISC AND (BSD-3-Clause AND BSD-2-Clause)", 0);
+
+ char *rendered = render_to_string(client, &licenses);
+ TEST_ASSERT_STRCMP_EQ(rendered, "ISC AND (BSD-3-Clause AND BSD-2-Clause)");
+ free(rendered);
+
+ pkgconf_license_free(&licenses);
+ pkgconf_client_free(client);
+}
+
+static void
+test_license_evaluate_empty(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t licenses = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_license_evaluate_str(client, &licenses, "", 0);
+ TEST_ASSERT_EQ(license_count(&licenses), 0);
+
+ pkgconf_license_free(&licenses);
+ pkgconf_client_free(client);
+}
+
+static void
+test_license_evaluate_sanitizes(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t licenses = PKGCONF_LIST_INITIALIZER;
+
+ /* The sanitiser strips characters outside the allowed set
+ * (alnum, '-', '+', '(', ')', '.', ':'). A token of pure
+ * junk should sanitise to empty and be skipped. */
+ pkgconf_license_evaluate_str(client, &licenses, "BSD-3-Clause", 0);
+
+ const pkgconf_license_t *l = licenses.head->data;
+ TEST_ASSERT_STRCMP_EQ(l->data, "BSD-3-Clause");
+
+ pkgconf_license_free(&licenses);
+ pkgconf_client_free(client);
+}
+
+static void
+test_license_render_empty(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t licenses = PKGCONF_LIST_INITIALIZER;
+
+ char *rendered = render_to_string(client, &licenses);
+ TEST_ASSERT_EMPTY_STRING(rendered);
+ free(rendered);
+
+ pkgconf_license_free(&licenses);
+ pkgconf_client_free(client);
+}
+
+static void
+test_license_render_single(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t licenses = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_license_insert(client, &licenses, PKGCONF_LICENSE_EXPRESSION, "MIT");
+
+ char *rendered = render_to_string(client, &licenses);
+ TEST_ASSERT_STRCMP_EQ(rendered, "MIT");
+ free(rendered);
+
+ pkgconf_license_free(&licenses);
+ pkgconf_client_free(client);
+}
+
+static void
+test_license_copy_list(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t src = PKGCONF_LIST_INITIALIZER;
+ pkgconf_list_t dst = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_license_evaluate_str(client, &src, "MIT OR ISC", 0);
+ size_t src_count = license_count(&src);
+
+ pkgconf_license_copy_list(client, &dst, &src);
+ TEST_ASSERT_EQ(license_count(&dst), src_count);
+
+ /* The copy is independent: freeing the source must not affect
+ * the destination's rendered output. */
+ pkgconf_license_free(&src);
+
+ char *rendered = render_to_string(client, &dst);
+ TEST_ASSERT_STRCMP_EQ(rendered, "MIT OR ISC");
+ free(rendered);
+
+ pkgconf_license_free(&dst);
+ pkgconf_client_free(client);
+}
+
+static void
+test_license_evaluate_long_sanitized_token(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t licenses = PKGCONF_LIST_INITIALIZER;
+
+ char token[512];
+ token[0] = 'A';
+ memset(token + 1, '_', 400);
+ token[401] = '\0';
+
+ pkgconf_license_evaluate_str(client, &licenses, token, 0);
+
+ TEST_ASSERT_EQ(license_count(&licenses), 1);
+ const pkgconf_license_t *l = licenses.head->data;
+ TEST_ASSERT_EQ(l->type, PKGCONF_LICENSE_EXPRESSION);
+ TEST_ASSERT_STRCMP_EQ(l->data, "A");
+
+ pkgconf_license_free(&licenses);
+ pkgconf_client_free(client);
+}
+
+static void
+test_license_evaluate_unterminated_quote(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t licenses = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_license_evaluate_str(client, &licenses, "\"", 0);
+ TEST_ASSERT_EQ(license_count(&licenses), 0);
+
+ pkgconf_license_evaluate_str(client, &licenses, "MIT \"unterminated", 0);
+ TEST_ASSERT_EQ(license_count(&licenses), 0);
+
+ pkgconf_license_evaluate_str(client, &licenses, "\\", 0);
+ TEST_ASSERT_EQ(license_count(&licenses), 0);
+
+ pkgconf_license_free(&licenses);
+ pkgconf_client_free(client);
+}
+
+int
+main(int argc, char *argv[])
+{
+ (void) argc;
+ const char *basename = pkgconf_path_find_basename(argv[0]);
+
+ TEST_RUN(basename, test_license_insert_and_free);
+ TEST_RUN(basename, test_license_free_empty);
+
+ TEST_RUN(basename, test_license_evaluate_single);
+ TEST_RUN(basename, test_license_evaluate_or);
+ TEST_RUN(basename, test_license_evaluate_and);
+ TEST_RUN(basename, test_license_evaluate_multiple_keys_implicit_and);
+ TEST_RUN(basename, test_license_evaluate_brackets);
+ TEST_RUN(basename, test_license_evaluate_empty);
+ TEST_RUN(basename, test_license_evaluate_sanitizes);
+ TEST_RUN(basename, test_license_evaluate_long_sanitized_token);
+ TEST_RUN(basename, test_license_evaluate_unterminated_quote);
+
+ TEST_RUN(basename, test_license_render_empty);
+ TEST_RUN(basename, test_license_render_single);
+
+ TEST_RUN(basename, test_license_copy_list);
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/api/test-path-utils.c b/tests/api/test-path-utils.c
new file mode 100644
index 000000000000..519cadb6f639
--- /dev/null
+++ b/tests/api/test-path-utils.c
@@ -0,0 +1,119 @@
+/*
+ * test-path-utils.c
+ * Tests for libpkgconf internal path utility functions.
+ *
+ * SPDX-License-Identifier: pkgconf
+ *
+ * Copyright (c) 2025-2026 pkgconf authors (see AUTHORS).
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * This software is provided 'as is' and without any warranty, express or
+ * implied. In no event shall the authors be liable for any damages arising
+ * from the use of this software.
+ */
+
+#include <libpkgconf/stdinc.h>
+#include <libpkgconf/libpkgconf.h>
+#include <libpkgconf/path.h>
+#include "test-api.h"
+
+static void
+test_path_find_basename(void)
+{
+ TEST_ASSERT_STRCMP_EQ(pkgconf_path_find_basename("/usr/lib/pkgconfig/foo.pc"), "foo.pc");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_path_find_basename("/usr/lib/pkgconfig"), "pkgconfig");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_path_find_basename("foo.pc"), "foo.pc");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_path_find_basename("/foo.pc"), "foo.pc");
+ TEST_ASSERT_EMPTY_STRING(pkgconf_path_find_basename("/"));
+ TEST_ASSERT_EMPTY_STRING(pkgconf_path_find_basename(""));
+ TEST_ASSERT_EMPTY_STRING(pkgconf_path_find_basename("/usr/"));
+ TEST_ASSERT_EMPTY_STRING(pkgconf_path_find_basename("usr/"));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_path_find_basename("///usr/lib///pkgconfig///foo.pc"), "foo.pc");
+#ifdef _WIN32
+ TEST_ASSERT_STRCMP_EQ(pkgconf_path_find_basename("C:\\lib\\pkgconfig\\foo.pc"), "foo.pc");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_path_find_basename("C:/lib/pkgconfig/foo.pc"), "foo.pc");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_path_find_basename("C:/lib\\pkgconfig/foo.pc"), "foo.pc");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_path_find_basename("C:\\lib/pkgconfig\\foo.pc"), "foo.pc");
+#endif
+}
+
+static void
+test_path_trim_basename(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+
+ pkgconf_buffer_append(&buf, "/usr/lib/pkgconfig/foo.pc");
+ TEST_ASSERT_TRUE(pkgconf_path_trim_basename(&buf));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "/usr/lib/pkgconfig");
+ TEST_ASSERT_TRUE(pkgconf_path_trim_basename(&buf));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "/usr/lib");
+ TEST_ASSERT_TRUE(pkgconf_path_trim_basename(&buf));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "/usr");
+ TEST_ASSERT_TRUE(pkgconf_path_trim_basename(&buf));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "");
+ TEST_ASSERT_FALSE(pkgconf_path_trim_basename(&buf));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "");
+
+ pkgconf_buffer_reset(&buf);
+ pkgconf_buffer_append(&buf, "foo.pc");
+ TEST_ASSERT_FALSE(pkgconf_path_trim_basename(&buf));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "foo.pc");
+
+ pkgconf_buffer_finalize(&buf);
+}
+
+static void
+test_determine_prefix_logic(void)
+{
+ pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
+
+ // Normal case
+ pkgconf_buffer_append(&buf, "/opt/foo/lib/pkgconfig/bar.pc");
+ TEST_ASSERT_TRUE(pkgconf_path_trim_basename(&buf));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_path_find_basename(pkgconf_buffer_str(&buf)), "pkgconfig");
+ TEST_ASSERT_TRUE(pkgconf_path_trim_basename(&buf));
+ TEST_ASSERT_TRUE(pkgconf_path_trim_basename(&buf));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "/opt/foo");
+
+ // Short path: /pkgconfig/foo.pc
+ pkgconf_buffer_reset(&buf);
+ pkgconf_buffer_append(&buf, "/pkgconfig/foo.pc");
+ TEST_ASSERT_TRUE(pkgconf_path_trim_basename(&buf));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_path_find_basename(pkgconf_buffer_str(&buf)), "pkgconfig");
+ TEST_ASSERT_TRUE(pkgconf_path_trim_basename(&buf)); // trims pkgconfig, returns true because of /
+ TEST_ASSERT_FALSE(pkgconf_path_trim_basename(&buf)); // fails to trim further
+
+ // Another short path: lib/pkgconfig/foo.pc
+ pkgconf_buffer_reset(&buf);
+ pkgconf_buffer_append(&buf, "lib/pkgconfig/foo.pc");
+ TEST_ASSERT_TRUE(pkgconf_path_trim_basename(&buf));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_path_find_basename(pkgconf_buffer_str(&buf)), "pkgconfig");
+ TEST_ASSERT_TRUE(pkgconf_path_trim_basename(&buf));
+ TEST_ASSERT_FALSE(pkgconf_path_trim_basename(&buf));
+
+ // Trailing slash
+ pkgconf_buffer_reset(&buf);
+ pkgconf_buffer_append(&buf, "/usr/lib/pkgconfig/");
+ TEST_ASSERT_TRUE(pkgconf_path_trim_basename(&buf));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "/usr/lib/pkgconfig");
+ TEST_ASSERT_TRUE(pkgconf_path_trim_basename(&buf));
+ TEST_ASSERT_STRCMP_EQ(pkgconf_buffer_str(&buf), "/usr/lib");
+
+ pkgconf_buffer_finalize(&buf);
+}
+
+int
+main(int argc, char *argv[])
+{
+ (void) argc;
+ const char *basename = pkgconf_path_find_basename(argv[0]);
+
+ TEST_RUN(basename, test_path_find_basename);
+ TEST_RUN(basename, test_path_trim_basename);
+ TEST_RUN(basename, test_determine_prefix_logic);
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/api/test-tuple.c b/tests/api/test-tuple.c
new file mode 100644
index 000000000000..b648abffe3f3
--- /dev/null
+++ b/tests/api/test-tuple.c
@@ -0,0 +1,258 @@
+/*
+ * test-tuple.c
+ * Tests for the public libpkgconf tuple API.
+ *
+ * SPDX-License-Identifier: pkgconf
+ *
+ * Copyright (c) 2025 pkgconf authors (see AUTHORS).
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * This software is provided 'as is' and without any warranty, express or
+ * implied. In no event shall the authors be liable for any damages arising
+ * from the use of this software.
+ */
+
+#include <libpkgconf/stdinc.h>
+#include <libpkgconf/libpkgconf.h>
+#include "test-api.h"
+
+static void
+test_tuple_add_and_find(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t tuples = PKGCONF_LIST_INITIALIZER;
+
+ /* parse=false means store the value verbatim, no bytecode compile.
+ * That's the right mode for storing already-evaluated values. */
+ pkgconf_tuple_t *t = pkgconf_tuple_add(client, &tuples, "prefix", "/opt/foo", false, 0);
+ TEST_ASSERT_NONNULL(t);
+
+ const char *found = pkgconf_tuple_find(client, &tuples, "prefix");
+ TEST_ASSERT_NONNULL(found);
+ TEST_ASSERT_STRCMP_EQ(found, "/opt/foo");
+
+ pkgconf_tuple_free(&tuples);
+ pkgconf_client_free(client);
+}
+
+static void
+test_tuple_find_absent(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t tuples = PKGCONF_LIST_INITIALIZER;
+
+ const char *found = pkgconf_tuple_find(client, &tuples, "nonexistent");
+ TEST_ASSERT_EMPTY_STRING(found);
+
+ pkgconf_tuple_free(&tuples);
+ pkgconf_client_free(client);
+}
+
+static void
+test_tuple_add_multiple(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t tuples = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_tuple_add(client, &tuples, "prefix", "/usr", false, 0);
+ pkgconf_tuple_add(client, &tuples, "exec_prefix", "/usr/local", false, 0);
+ pkgconf_tuple_add(client, &tuples, "libdir", "/usr/lib", false, 0);
+
+ TEST_ASSERT_STRCMP_EQ(pkgconf_tuple_find(client, &tuples, "prefix"), "/usr");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_tuple_find(client, &tuples, "exec_prefix"), "/usr/local");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_tuple_find(client, &tuples, "libdir"), "/usr/lib");
+ TEST_ASSERT_EMPTY_STRING(pkgconf_tuple_find(client, &tuples, "datadir"));
+
+ pkgconf_tuple_free(&tuples);
+ pkgconf_client_free(client);
+}
+
+static void
+test_tuple_global_add_and_find(void)
+{
+ pkgconf_client_t *client = test_client_new();
+
+ pkgconf_tuple_add_global(client, "PKG_TEST_KEY", "test_value");
+
+ const char *found = pkgconf_tuple_find_global(client, "PKG_TEST_KEY");
+ TEST_ASSERT_NONNULL(found);
+ TEST_ASSERT_STRCMP_EQ(found, "test_value");
+
+ pkgconf_tuple_free_global(client);
+ pkgconf_client_free(client);
+}
+
+static void
+test_tuple_global_find_absent(void)
+{
+ pkgconf_client_t *client = test_client_new();
+
+ const char *found = pkgconf_tuple_find_global(client, "DOES_NOT_EXIST");
+ TEST_ASSERT_EMPTY_STRING(found);
+
+ pkgconf_client_free(client);
+}
+
+static void
+test_tuple_define_global_kv_form(void)
+{
+ pkgconf_client_t *client = test_client_new();
+
+ /* This is the exact path --define-variable=KEY=VALUE takes
+ * pkgconf_tuple_define_global parses the "key=value" form and inserts it as a global tuple. */
+ pkgconf_tuple_define_global(client, "myvar=myvalue");
+
+ const char *found = pkgconf_tuple_find_global(client, "myvar");
+ TEST_ASSERT_NONNULL(found);
+ TEST_ASSERT_STRCMP_EQ(found, "myvalue");
+
+ pkgconf_tuple_free_global(client);
+ pkgconf_client_free(client);
+}
+
+static void
+test_tuple_define_global_with_equals_in_value(void)
+{
+ pkgconf_client_t *client = test_client_new();
+
+ // Only the first '=' should split key from value; subsequent '=' characters belong to the value.
+ pkgconf_tuple_define_global(client, "CFLAGS=-DFOO=bar");
+
+ const char *found = pkgconf_tuple_find_global(client, "CFLAGS");
+ TEST_ASSERT_NONNULL(found);
+ TEST_ASSERT_STRCMP_EQ(found, "-DFOO=bar");
+
+ pkgconf_tuple_free_global(client);
+ pkgconf_client_free(client);
+}
+
+static void
+test_tuple_global_multiple(void)
+{
+ pkgconf_client_t *client = test_client_new();
+
+ pkgconf_tuple_define_global(client, "a=1");
+ pkgconf_tuple_define_global(client, "b=2");
+ pkgconf_tuple_define_global(client, "c=3");
+
+ TEST_ASSERT_STRCMP_EQ(pkgconf_tuple_find_global(client, "a"), "1");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_tuple_find_global(client, "b"), "2");
+ TEST_ASSERT_STRCMP_EQ(pkgconf_tuple_find_global(client, "c"), "3");
+
+ pkgconf_tuple_free_global(client);
+ pkgconf_client_free(client);
+}
+
+static void
+test_tuple_global_free_resets(void)
+{
+ pkgconf_client_t *client = test_client_new();
+
+ pkgconf_tuple_define_global(client, "temp=value");
+ TEST_ASSERT_NONNULL(pkgconf_tuple_find_global(client, "temp"));
+
+ pkgconf_tuple_free_global(client);
+ TEST_ASSERT_EMPTY_STRING(pkgconf_tuple_find_global(client, "temp"));
+
+ pkgconf_client_free(client);
+}
+
+static void
+test_tuple_free_empty(void)
+{
+ pkgconf_list_t tuples = PKGCONF_LIST_INITIALIZER;
+
+ // Freeing an empty list should be a no-op. Mostly an ASan/leak check smoke test.
+ pkgconf_tuple_free(&tuples);
+}
+
+static void
+test_tuple_define_variable_end_to_end(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ pkgconf_tuple_define_global(client, "myprefix=/opt/custom");
+
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "-I${myprefix}/include", &saw_sysroot);
+
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "-I/opt/custom/include");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_tuple_free_global(client);
+ pkgconf_client_free(client);
+}
+
+static void
+test_tuple_define_variable_overrides_local(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ pkgconf_variable_t *v = pkgconf_variable_get_or_create(&vars, "prefix");
+ TEST_ASSERT_NONNULL(v);
+ pkgconf_buffer_reset(&v->bcbuf);
+ pkgconf_bytecode_compile(&v->bcbuf, "/usr");
+ pkgconf_bytecode_from_buffer(&v->bc, &v->bcbuf);
+
+ // Simulate user passing --define-variable=prefix=/custom.
+ pkgconf_tuple_define_global(client, "prefix=/custom");
+
+ char *out = pkgconf_bytecode_eval_str(client, &vars, "${prefix}/lib", &saw_sysroot);
+
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "/custom/lib");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_tuple_free_global(client);
+ pkgconf_client_free(client);
+}
+
+static void
+test_tuple_escaped_quote(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t tuples = PKGCONF_LIST_INITIALIZER;
+
+ // A double-quoted value containing an escaped double-quote
+ pkgconf_tuple_t *t = pkgconf_tuple_add(client, &tuples, "key", "\"a\\\"b\"", true, 0);
+ TEST_ASSERT_NONNULL(t);
+
+ const char *found = pkgconf_tuple_find(client, &tuples, "key");
+ TEST_ASSERT_NONNULL(found);
+ TEST_ASSERT_STRCMP_EQ(found, "a\"b");
+
+ pkgconf_tuple_free(&tuples);
+ pkgconf_client_free(client);
+}
+
+int
+main(int argc, char *argv[])
+{
+ (void) argc;
+ const char *basename = pkgconf_path_find_basename(argv[0]);
+
+ TEST_RUN(basename, test_tuple_add_and_find);
+ TEST_RUN(basename, test_tuple_find_absent);
+ TEST_RUN(basename, test_tuple_add_multiple);
+ TEST_RUN(basename, test_tuple_free_empty);
+ TEST_RUN(basename, test_tuple_global_add_and_find);
+ TEST_RUN(basename, test_tuple_global_find_absent);
+ TEST_RUN(basename, test_tuple_define_global_kv_form);
+ TEST_RUN(basename, test_tuple_define_global_with_equals_in_value);
+ TEST_RUN(basename, test_tuple_global_multiple);
+ TEST_RUN(basename, test_tuple_global_free_resets);
+ TEST_RUN(basename, test_tuple_define_variable_end_to_end);
+ TEST_RUN(basename, test_tuple_define_variable_overrides_local);
+ TEST_RUN(basename, test_tuple_escaped_quote);
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/api/test-variable.c b/tests/api/test-variable.c
new file mode 100644
index 000000000000..ec65df3ae5a8
--- /dev/null
+++ b/tests/api/test-variable.c
@@ -0,0 +1,247 @@
+/*
+ * test-variable.c
+ * Tests for the public libpkgconf variable API.
+ *
+ * SPDX-License-Identifier: pkgconf
+ *
+ * Copyright (c) 2025 pkgconf authors (see AUTHORS).
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * This software is provided 'as is' and without any warranty, express or
+ * implied. In no event shall the authors be liable for any damages arising
+ * from the use of this software.
+ */
+
+#include <libpkgconf/stdinc.h>
+#include <libpkgconf/libpkgconf.h>
+#include "test-api.h"
+
+/*
+ * Install a value on a variable by compiling it as bytecode.
+ * Mirrors what the parser does when it reads a .pc field.
+ */
+static void
+seed_variable(pkgconf_list_t *vars, const char *key, const char *value)
+{
+ pkgconf_variable_t *v = pkgconf_variable_get_or_create(vars, key);
+ TEST_ASSERT_NONNULL(v);
+
+ pkgconf_buffer_reset(&v->bcbuf);
+ pkgconf_bytecode_compile(&v->bcbuf, value);
+ pkgconf_bytecode_from_buffer(&v->bc, &v->bcbuf);
+}
+
+static void
+test_variable_new_and_free(void)
+{
+ pkgconf_variable_t *v = pkgconf_variable_new("prefix");
+
+ TEST_ASSERT_NONNULL(v);
+ TEST_ASSERT_STRCMP_EQ(v->key, "prefix");
+
+ pkgconf_variable_free(v);
+}
+
+static void
+test_variable_get_or_create_creates(void)
+{
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_variable_t *v = pkgconf_variable_get_or_create(&vars, "prefix");
+ TEST_ASSERT_NONNULL(v);
+ TEST_ASSERT_STRCMP_EQ(v->key, "prefix");
+
+ pkgconf_variable_list_free(&vars);
+}
+
+static void
+test_variable_get_or_create_returns_existing(void)
+{
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_variable_t *first = pkgconf_variable_get_or_create(&vars, "libdir");
+ TEST_ASSERT_NONNULL(first);
+
+ // A second call with the same key should return the same object, not create a duplicate.
+ pkgconf_variable_t *second = pkgconf_variable_get_or_create(&vars, "libdir");
+ TEST_ASSERT_NONNULL(second);
+ TEST_ASSERT_EQ(first, second);
+
+ pkgconf_variable_list_free(&vars);
+}
+
+static void
+test_variable_find_present(void)
+{
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_variable_t *created = pkgconf_variable_get_or_create(&vars, "prefix");
+ TEST_ASSERT_NONNULL(created);
+
+ pkgconf_variable_t *found = pkgconf_variable_find(&vars, "prefix");
+ TEST_ASSERT_NONNULL(found);
+ TEST_ASSERT_EQ(created, found);
+
+ pkgconf_variable_list_free(&vars);
+}
+
+static void
+test_variable_find_absent(void)
+{
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_variable_t *found = pkgconf_variable_find(&vars, "nonexistent");
+ TEST_ASSERT_NULL(found);
+
+ pkgconf_variable_list_free(&vars);
+}
+
+static void
+test_variable_find_among_many(void)
+{
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_variable_get_or_create(&vars, "prefix");
+ pkgconf_variable_get_or_create(&vars, "exec_prefix");
+ pkgconf_variable_get_or_create(&vars, "libdir");
+ pkgconf_variable_get_or_create(&vars, "includedir");
+
+ TEST_ASSERT_NONNULL(pkgconf_variable_find(&vars, "prefix"));
+ TEST_ASSERT_NONNULL(pkgconf_variable_find(&vars, "exec_prefix"));
+ TEST_ASSERT_NONNULL(pkgconf_variable_find(&vars, "libdir"));
+ TEST_ASSERT_NONNULL(pkgconf_variable_find(&vars, "includedir"));
+ TEST_ASSERT_NULL(pkgconf_variable_find(&vars, "notpresent"));
+
+ pkgconf_variable_list_free(&vars);
+}
+
+static void
+test_variable_delete(void)
+{
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ pkgconf_variable_t *v = pkgconf_variable_get_or_create(&vars, "prefix");
+ TEST_ASSERT_NONNULL(v);
+ TEST_ASSERT_NONNULL(pkgconf_variable_find(&vars, "prefix"));
+
+ pkgconf_variable_delete(&vars, v);
+ TEST_ASSERT_NULL(pkgconf_variable_find(&vars, "prefix"));
+
+ pkgconf_variable_list_free(&vars);
+}
+
+static void
+test_variable_eval_str_plain(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ seed_variable(&vars, "version", "1.2.3");
+
+ pkgconf_variable_t *v = pkgconf_variable_find(&vars, "version");
+ TEST_ASSERT_NONNULL(v);
+
+ char *out = pkgconf_variable_eval_str(client, &vars, v, &saw_sysroot);
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "1.2.3");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_variable_eval_str_with_reference(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ // Standard pkg-config layout: libdir is derived from prefix.
+ seed_variable(&vars, "prefix", "/opt/foo");
+ seed_variable(&vars, "libdir", "${prefix}/lib");
+
+ pkgconf_variable_t *libdir = pkgconf_variable_find(&vars, "libdir");
+ TEST_ASSERT_NONNULL(libdir);
+
+ char *out = pkgconf_variable_eval_str(client, &vars, libdir, &saw_sysroot);
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "/opt/foo/lib");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_variable_eval_str_chained(void)
+{
+ pkgconf_client_t *client = test_client_new();
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+ bool saw_sysroot = false;
+
+ seed_variable(&vars, "prefix", "/usr/local");
+ seed_variable(&vars, "exec_prefix", "${prefix}");
+ seed_variable(&vars, "includedir", "${exec_prefix}/include");
+
+ pkgconf_variable_t *includedir = pkgconf_variable_find(&vars, "includedir");
+ TEST_ASSERT_NONNULL(includedir);
+
+ char *out = pkgconf_variable_eval_str(client, &vars, includedir, &saw_sysroot);
+ TEST_ASSERT_NONNULL(out);
+ TEST_ASSERT_STRCMP_EQ(out, "/usr/local/include");
+
+ free(out);
+ pkgconf_variable_list_free(&vars);
+ pkgconf_client_free(client);
+}
+
+static void
+test_variable_list_free_handles_empty(void)
+{
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ // Freeing an empty list should be a no-op and not crash. Mostly an ASan/leak smoke test.
+ pkgconf_variable_list_free(&vars);
+}
+
+static void
+test_variable_list_free_handles_many(void)
+{
+ pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
+
+ /* Add a mix of bare and value-carrying variables, then free the whole list at once.
+ * Mostly an ASan/leak smoke test. */
+ pkgconf_variable_get_or_create(&vars, "a");
+ seed_variable(&vars, "b", "/path/b");
+ pkgconf_variable_get_or_create(&vars, "c");
+ seed_variable(&vars, "d", "${b}/sub");
+
+ pkgconf_variable_list_free(&vars);
+}
+
+int
+main(int argc, char *argv[])
+{
+ (void) argc;
+ const char *basename = pkgconf_path_find_basename(argv[0]);
+
+ TEST_RUN(basename, test_variable_new_and_free);
+ TEST_RUN(basename, test_variable_get_or_create_creates);
+ TEST_RUN(basename, test_variable_get_or_create_returns_existing);
+ TEST_RUN(basename, test_variable_find_present);
+ TEST_RUN(basename, test_variable_find_absent);
+ TEST_RUN(basename, test_variable_find_among_many);
+ TEST_RUN(basename, test_variable_delete);
+ TEST_RUN(basename, test_variable_eval_str_plain);
+ TEST_RUN(basename, test_variable_eval_str_with_reference);
+ TEST_RUN(basename, test_variable_eval_str_chained);
+ TEST_RUN(basename, test_variable_list_free_handles_empty);
+ TEST_RUN(basename, test_variable_list_free_handles_many);
+
+ return EXIT_SUCCESS;
+}