diff options
Diffstat (limited to 'tests/test-runner.c')
| -rw-r--r-- | tests/test-runner.c | 1576 |
1 files changed, 1576 insertions, 0 deletions
diff --git a/tests/test-runner.c b/tests/test-runner.c new file mode 100644 index 000000000000..5fbb426184c6 --- /dev/null +++ b/tests/test-runner.c @@ -0,0 +1,1576 @@ +/* + * test-runner.c + * test harness + * + * 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/config.h> +#include <libpkgconf/libpkgconf.h> +#include <libpkgconf/stdinc.h> +#include <tests/win-shim.h> +#include <sys/types.h> +#include <cli/core.h> +#include <cli/getopt_long.h> +#include <limits.h> +#include <assert.h> + +#ifndef PKGCONF_LITE +# if !defined(_WIN32) && !defined(__HAIKU__) +# define PKGCONF_TEST_PLATFORM "unix" +# elif !defined(_WIN32) +# define PKGCONF_TEST_PLATFORM "haiku" +# else +# define PKGCONF_TEST_PLATFORM "windows" +# endif +#else // PKGCONF_LITE +# define PKGCONF_TEST_PLATFORM "lite" +#endif // PKGCONF_LITE + +static void test_parser_warn(void *p, const char *fmt, ...) PRINTFLIKE(2, 3); +static void handle_substs(pkgconf_buffer_t *dest, const pkgconf_buffer_t *src, const char *pwd); + +static pkgconf_buffer_t test_fixtures_dir = PKGCONF_BUFFER_INITIALIZER; +static pkgconf_buffer_t test_tool_dir = PKGCONF_BUFFER_INITIALIZER; +static bool debug = false; + +typedef enum test_match_strategy_ +{ + MATCH_EXACT = 0, + MATCH_PARTIAL, + MATCH_EMPTY, +} pkgconf_test_match_strategy_t; + +typedef struct test_case_ +{ + char *name; + char *testfile_dir; + + pkgconf_list_t search_path; + pkgconf_buffer_t query; + + pkgconf_list_t expected_stdout; + pkgconf_test_match_strategy_t match_stdout; + + pkgconf_list_t expected_stderr; + pkgconf_test_match_strategy_t match_stderr; + + pkgconf_buffer_t expected_stdout_file; + + int exitcode; + uint64_t wanted_flags; + + pkgconf_list_t env_vars; + + pkgconf_buffer_t want_env_prefix; + pkgconf_buffer_t want_variable; + pkgconf_buffer_t fragment_filter; + + pkgconf_buffer_t skip_platforms; + bool require_utf8_locale; + + pkgconf_list_t define_variables; + + int verbosity; + + pkgconf_buffer_t atleast_version; + pkgconf_buffer_t exact_version; + pkgconf_buffer_t max_version; + + pkgconf_buffer_t tool; + pkgconf_buffer_t tool_args; // TODO: tool-specific flags + + pkgconf_list_t mkdirs; +#ifndef _WIN32 + pkgconf_list_t symlinks; +#endif + pkgconf_list_t copies; + +#ifndef PKGCONF_LITE + pkgconf_buffer_t want_personality; +#endif +} pkgconf_test_case_t; + +typedef struct test_state_ +{ + pkgconf_cli_state_t cli_state; + const pkgconf_test_case_t *testcase; +} pkgconf_test_state_t; + +typedef struct test_environ_ +{ + pkgconf_node_t node; + char *key; + char *value; +} pkgconf_test_environ_t; + +typedef struct test_output_ +{ + pkgconf_output_t output; + + pkgconf_buffer_t o_stdout; + pkgconf_buffer_t o_stderr; +} pkgconf_test_output_t; + +typedef struct test_flag_pair_ +{ + const char *name; + uint64_t flag; +} pkgconf_test_flag_pair_t; + +typedef void (*test_keyword_func_t)(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value); + +typedef struct test_keyword_pair_ +{ + const char *keyword; + const test_keyword_func_t func; + const ptrdiff_t offset; +} pkgconf_test_keyword_pair_t; + +static void +test_environment_push(pkgconf_test_case_t *testcase, const char *key, const char *value) +{ + pkgconf_test_environ_t *env = calloc(1, sizeof(*env)); + if (env == NULL) + return; + + env->key = strdup(key); + env->value = strdup(value); + pkgconf_node_insert_tail(&env->node, env, &testcase->env_vars); +} + +static void +test_environment_free(pkgconf_list_t *env_list) +{ + pkgconf_node_t *iter, *iter_next; + + PKGCONF_FOREACH_LIST_ENTRY_SAFE(env_list->head, iter_next, iter) + { + pkgconf_test_environ_t *env = iter->data; + + pkgconf_node_delete(&env->node, env_list); + + free(env->key); + free(env->value); + free(env); + } +} + +static const char * +environ_lookup_handler(const pkgconf_client_t *client, const char *key) +{ + pkgconf_test_state_t *state = client->client_data; + pkgconf_node_t *iter; + + PKGCONF_FOREACH_LIST_ENTRY(state->testcase->env_vars.head, iter) + { + pkgconf_test_environ_t *env = iter->data; + + if (!strcmp(key, env->key)) + { + char cwd[PATH_MAX] = {0}; + const char *pwd = getcwd(cwd, sizeof(cwd)); + + pkgconf_buffer_t expanded = PKGCONF_BUFFER_INITIALIZER; + handle_substs(&expanded, PKGCONF_BUFFER_FROM_STR(env->value), pwd); + + free(env->value); + env->value = strdup(pkgconf_buffer_str_or_empty(&expanded)); + pkgconf_buffer_finalize(&expanded); + + return env->value; + } + } + + return NULL; +} + +#ifndef PKGCONF_LITE +static bool +debug_handler(const char *msg, const pkgconf_client_t *client, void *data) +{ + (void) client; + (void) data; + fprintf(stderr, "%s", msg); + return true; +} +#endif // PKGCONF_LITE + +static bool +error_handler(const char *msg, const pkgconf_client_t *client, void *data) +{ + (void) data; + pkgconf_test_state_t *state = client->client_data; + pkgconf_output_fmt(client->output, state->testcase->wanted_flags & PKG_ERRORS_ON_STDOUT ? PKGCONF_OUTPUT_STDOUT : PKGCONF_OUTPUT_STDERR, "%s", msg); + return true; +} + +static bool +write_handler(pkgconf_output_t *output, pkgconf_output_stream_t stream, const pkgconf_buffer_t *buffer) +{ + pkgconf_test_output_t *out = (pkgconf_test_output_t *) output; + pkgconf_buffer_t *dest = stream == PKGCONF_OUTPUT_STDERR ? &out->o_stderr : &out->o_stdout; + + pkgconf_buffer_append(dest, pkgconf_buffer_str(buffer)); + return true; +} + +static pkgconf_output_t * +test_output(void) +{ + static pkgconf_test_output_t output = + { + .output.write = write_handler, + }; + + return &output.output; +} + +static void +test_output_reset(pkgconf_test_output_t *out) +{ + pkgconf_buffer_reset(&out->o_stdout); + pkgconf_buffer_reset(&out->o_stderr); +} + +/* + * handle_substs: expand %TEST_FIXTURES_DIR%, %DIR_SEP%, and %PWD% + * in src into dest. pwd may be NULL, in which case %PWD% is left as-is + * (it should only appear in fields that are re-expanded after tmp_dir creation). + */ +static void +handle_substs(pkgconf_buffer_t *dest, const pkgconf_buffer_t *src, const char *pwd) +{ + struct subst_pair + { + const char *key; + const char *value; + } subst_pairs[] = + { + {"%TEST_FIXTURES_DIR%", pkgconf_buffer_str(&test_fixtures_dir)}, + {"%DIR_SEP%", PKG_CONFIG_PATH_SEP_S}, + {"%PWD%", pwd != NULL ? pwd : "%PWD%"}, + }; + + pkgconf_buffer_t workbuf_src = PKGCONF_BUFFER_INITIALIZER; + pkgconf_buffer_t workbuf_dest = PKGCONF_BUFFER_INITIALIZER; + + if (!pkgconf_buffer_len(src)) + return; + + pkgconf_buffer_append(&workbuf_dest, pkgconf_buffer_str(src)); + + for (size_t i = 0; i < PKGCONF_ARRAY_SIZE(subst_pairs); i++) + { + pkgconf_buffer_reset(&workbuf_src); + pkgconf_buffer_append(&workbuf_src, pkgconf_buffer_str(&workbuf_dest)); + + pkgconf_buffer_reset(&workbuf_dest); + pkgconf_buffer_subst(&workbuf_dest, &workbuf_src, subst_pairs[i].key, subst_pairs[i].value); + } + + pkgconf_buffer_append(dest, pkgconf_buffer_str(&workbuf_dest)); + + pkgconf_buffer_finalize(&workbuf_src); + pkgconf_buffer_finalize(&workbuf_dest); +} + +static int +test_keyword_pair_cmp(const void *key, const void *ptr) +{ + const pkgconf_test_keyword_pair_t *pair = ptr; + return strcasecmp(key, pair->keyword); +} + +static void +test_keyword_set_int(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value) +{ + (void) keyword; + (void) warnprefix; + + int *dest = (int *)((char *) testcase + offset); + *dest = atoi(value); +} + +static void +test_keyword_set_bool(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value) +{ + (void) keyword; + (void) warnprefix; + + bool *dest = (bool *)((char *) testcase + offset); + *dest = !strcasecmp(value, "true"); +} + +static void +test_keyword_set_buffer(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value) +{ + (void) keyword; + (void) warnprefix; + + pkgconf_buffer_t *dest = (pkgconf_buffer_t *)((char *) testcase + offset); + handle_substs(dest, PKGCONF_BUFFER_FROM_STR((char *) value), NULL); +} + +static void +test_keyword_extend_bufferset(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value) +{ + (void) keyword; + (void) warnprefix; + + pkgconf_list_t *dest = (pkgconf_list_t *)((char *) testcase + offset); + pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER; + + handle_substs(&buf, PKGCONF_BUFFER_FROM_STR((char *) value), NULL); + pkgconf_bufferset_extend(dest, &buf); + pkgconf_buffer_finalize(&buf); +} + +static int +test_flag_pair_cmp(const void *key, const void *ptr) +{ + const pkgconf_test_flag_pair_t *pair = ptr; + return strcasecmp(key, pair->name); +} + +static const pkgconf_test_flag_pair_t test_flag_pairs[] = +{ + {"cflags", PKG_CFLAGS}, + {"cflags-only-i", PKG_CFLAGS_ONLY_I}, + {"cflags-only-other", PKG_CFLAGS_ONLY_OTHER}, + {"debug", PKG_DEBUG}, + {"define-prefix", PKG_DEFINE_PREFIX}, + {"digraph", PKG_DIGRAPH}, + {"dont-define-prefix", PKG_DONT_DEFINE_PREFIX}, + {"dont-relocate-paths", PKG_DONT_RELOCATE_PATHS}, + {"dump-license", PKG_DUMP_LICENSE}, + {"dump-license-file", PKG_DUMP_LICENSE_FILE}, + {"dump-personality", PKG_DUMP_PERSONALITY}, + {"dump-source", PKG_DUMP_SOURCE}, + {"env-only", PKG_ENV_ONLY}, + {"errors-on-stdout", PKG_ERRORS_ON_STDOUT}, + {"exists", PKG_EXISTS}, + {"exists-cflags", PKG_EXISTS_CFLAGS}, + {"fragment-tree", PKG_FRAGMENT_TREE}, + {"ignore-conflicts", PKG_IGNORE_CONFLICTS}, + {"internal-cflags", PKG_INTERNAL_CFLAGS}, + {"keep-system-cflags", PKG_KEEP_SYSTEM_CFLAGS}, + {"keep-system-libs", PKG_KEEP_SYSTEM_LIBS}, + {"libs", PKG_LIBS}, + {"libs-only-ldpath", PKG_LIBS_ONLY_LDPATH}, + {"libs-only-libname", PKG_LIBS_ONLY_LIBNAME}, + {"libs-only-other", PKG_LIBS_ONLY_OTHER}, + {"link-abi", PKG_LINK_ABI}, + {"list", PKG_LIST}, + {"list-package-names", PKG_LIST_PACKAGE_NAMES}, + {"modversion", PKG_MODVERSION}, + {"msvc-syntax", PKG_MSVC_SYNTAX}, + {"newlines", PKG_NEWLINES}, + {"no-cache", PKG_NO_CACHE}, + {"no-provides", PKG_NO_PROVIDES}, + {"no-uninstalled", PKG_NO_UNINSTALLED}, + {"path", PKG_PATH}, + {"print-digraph-query-nodes", PKG_PRINT_DIGRAPH_QUERY_NODES}, + {"print-errors", PKG_PRINT_ERRORS}, + {"print-provides", PKG_PROVIDES}, + {"print-requires", PKG_REQUIRES}, + {"print-requires-private", PKG_REQUIRES_PRIVATE}, + {"print-variables", PKG_VARIABLES}, + {"pure", PKG_PURE}, + {"shared", PKG_SHARED}, + {"short-errors", PKG_SHORT_ERRORS}, + {"silence-errors", PKG_SILENCE_ERRORS}, + {"simulate", PKG_SIMULATE}, + {"solution", PKG_SOLUTION}, + {"static", PKG_STATIC}, + {"uninstalled", PKG_UNINSTALLED}, + {"validate", PKG_VALIDATE}, +}; + +static void +test_keyword_set_wanted_flags(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value) +{ + int i; + int flagcount; + char **flags = NULL; + + (void) keyword; + (void) warnprefix; + (void) offset; + + pkgconf_argv_split(value, &flagcount, &flags); + + for (i = 0; i < flagcount; i++) + { + const char *flag = flags[i]; + const pkgconf_test_flag_pair_t *pair = bsearch(flag, + test_flag_pairs, PKGCONF_ARRAY_SIZE(test_flag_pairs), + sizeof(*pair), test_flag_pair_cmp); + + if (pair == NULL) + continue; + + testcase->wanted_flags |= pair->flag; + } + + pkgconf_argv_free(flags); +} + +static size_t +prefixed_path_split(const char *text, pkgconf_list_t *dirlist, const char *prefix) +{ + size_t count = 0; + char *workbuf, *p, *iter; + + if (text == NULL) + return 0; + + iter = workbuf = strdup(text); + while ((p = strtok(iter, PKG_CONFIG_PATH_SEP_S)) != NULL) + { + pkgconf_buffer_t pathbuf = PKGCONF_BUFFER_INITIALIZER; + + pkgconf_buffer_append(&pathbuf, prefix); + pkgconf_buffer_push_byte(&pathbuf, '/'); + pkgconf_buffer_append(&pathbuf, p); + pkgconf_path_add(pkgconf_buffer_str(&pathbuf), dirlist, false); + pkgconf_buffer_finalize(&pathbuf); + + count++, iter = NULL; + } + free(workbuf); + + return count; +} + +static void +test_keyword_set_path_list(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value) +{ + (void) keyword; + (void) warnprefix; + + pkgconf_list_t *dest = (pkgconf_list_t *)((char *) testcase + offset); + prefixed_path_split(value, dest, pkgconf_buffer_str(&test_fixtures_dir)); +} + +static void +test_keyword_set_match_strategy(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value) +{ + (void) keyword; + (void) warnprefix; + + pkgconf_test_match_strategy_t *dest = (pkgconf_test_match_strategy_t *)((char *) testcase + offset); + + if (!strcasecmp(value, "partial")) + *dest = MATCH_PARTIAL; + + if (!strcasecmp(value, "empty")) + *dest = MATCH_EMPTY; +} + +static void +test_keyword_set_environment(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value) +{ + (void) keyword; + (void) offset; + + char *eq = strchr(value, '='); + if (eq == NULL) + { + fprintf(stderr, "%s: malformed Environment entry: %s\n", warnprefix, value); + return; + } + + *eq++ = '\0'; + + // store raw, vars are expanded at run time + test_environment_push(testcase, value, eq); +} + +#ifdef _WIN32 +static void +test_keyword_disabled(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value) +{ + (void) testcase; + (void) keyword; + (void) warnprefix; + (void) offset; + (void) value; +} +#endif + +static const pkgconf_test_keyword_pair_t test_keyword_pairs[] = +{ + {"AtLeastVersion", test_keyword_set_buffer, offsetof(pkgconf_test_case_t, atleast_version)}, + {"DefineVariable", test_keyword_extend_bufferset, offsetof(pkgconf_test_case_t, define_variables)}, + {"Environment", test_keyword_set_environment, offsetof(pkgconf_test_case_t, env_vars)}, + {"ExactVersion", test_keyword_set_buffer, offsetof(pkgconf_test_case_t, exact_version)}, + {"ExpectedExitCode", test_keyword_set_int, offsetof(pkgconf_test_case_t, exitcode)}, + {"ExpectedStderr", test_keyword_extend_bufferset, offsetof(pkgconf_test_case_t, expected_stderr)}, + {"ExpectedStdout", test_keyword_extend_bufferset, offsetof(pkgconf_test_case_t, expected_stdout)}, + {"ExpectedStdoutFile", test_keyword_set_buffer, offsetof(pkgconf_test_case_t, expected_stdout_file)}, + {"FragmentFilter", test_keyword_set_buffer, offsetof(pkgconf_test_case_t, fragment_filter)}, + {"MatchStderr", test_keyword_set_match_strategy, offsetof(pkgconf_test_case_t, match_stderr)}, + {"MatchStdout", test_keyword_set_match_strategy, offsetof(pkgconf_test_case_t, match_stdout)}, + {"MaxVersion", test_keyword_set_buffer, offsetof(pkgconf_test_case_t, max_version)}, + {"PackageSearchPath", test_keyword_set_path_list, offsetof(pkgconf_test_case_t, search_path)}, + {"Query", test_keyword_set_buffer, offsetof(pkgconf_test_case_t, query)}, + {"RequireUtf8Locale", test_keyword_set_bool, offsetof(pkgconf_test_case_t, require_utf8_locale)}, + {"SetupCopy", test_keyword_extend_bufferset, offsetof(pkgconf_test_case_t, copies)}, + {"SetupMkdir", test_keyword_extend_bufferset, offsetof(pkgconf_test_case_t, mkdirs)}, +#ifdef _WIN32 + {"SetupSymlink", test_keyword_disabled, 0}, +#else + {"SetupSymlink", test_keyword_extend_bufferset, offsetof(pkgconf_test_case_t, symlinks)}, +#endif + {"SkipPlatforms", test_keyword_set_buffer, offsetof(pkgconf_test_case_t, skip_platforms)}, + {"Tool", test_keyword_set_buffer, offsetof(pkgconf_test_case_t, tool)}, + {"ToolArgs", test_keyword_set_buffer, offsetof(pkgconf_test_case_t, tool_args)}, + {"VerbosityLevel", test_keyword_set_int, offsetof(pkgconf_test_case_t, verbosity)}, + {"WantedFlags", test_keyword_set_wanted_flags, offsetof(pkgconf_test_case_t, wanted_flags)}, + {"WantEnvPrefix", test_keyword_set_buffer, offsetof(pkgconf_test_case_t, want_env_prefix)}, +#ifndef PKGCONF_LITE + {"WantPersonality", test_keyword_set_buffer, offsetof(pkgconf_test_case_t, want_personality)}, +#endif + {"WantVariable", test_keyword_set_buffer, offsetof(pkgconf_test_case_t, want_variable)}, +}; + +static void +test_keyword_set(void *data, const char *warnprefix, const char *keyword, const char *value) +{ + pkgconf_test_case_t *testcase = data; + const pkgconf_test_keyword_pair_t *pair = bsearch(keyword, + test_keyword_pairs, PKGCONF_ARRAY_SIZE(test_keyword_pairs), + sizeof(*pair), test_keyword_pair_cmp); + + if (pair == NULL || pair->func == NULL) + return; + + pair->func(testcase, warnprefix, keyword, pair->offset, value); +} + +static const pkgconf_parser_operand_func_t test_parser_ops[256] = +{ + [':'] = (pkgconf_parser_operand_func_t) test_keyword_set, +}; + +static void +test_parser_warn(void *p, const char *fmt, ...) +{ + va_list va; + + (void) p; + + va_start(va, fmt); + vfprintf(stderr, fmt, va); + va_end(va); +} + +static pkgconf_test_case_t * +load_test_case(char *testfile) +{ + FILE *testf = fopen(testfile, "r"); + if (testf == NULL) + return NULL; + + pkgconf_test_case_t *out = calloc(1, sizeof(*out)); + if (out == NULL) + goto cleanup; + + char *nameptr; + if ((nameptr = strrchr(testfile, '/')) != NULL) + nameptr++; + else + nameptr = testfile; + + out->name = strdup(nameptr); + + // store directory containing the test file for ExpectedStdoutFile resolution + { + char *dirend = strrchr(testfile, '/'); + if (dirend != NULL) + { + size_t dirlen = (size_t)(dirend - testfile); + out->testfile_dir = calloc(1, dirlen + 1); + if (out->testfile_dir != NULL) + memcpy(out->testfile_dir, testfile, dirlen); + } + else + out->testfile_dir = strdup("."); + } + + pkgconf_parser_parse(testf, out, test_parser_ops, test_parser_warn, testfile); + +cleanup: + fclose(testf); + return out; +} + +// we use a custom personality to ensure the tests are fully hermetic +static pkgconf_cross_personality_t * +personality_for_test(const pkgconf_test_case_t *testcase) +{ +#ifndef PKGCONF_LITE + if (pkgconf_buffer_len(&testcase->want_personality)) + return pkgconf_cross_personality_find(pkgconf_buffer_str(&testcase->want_personality)); +#endif + + pkgconf_cross_personality_t *pers = calloc(1, sizeof(*pers)); + if (pers == NULL) + return NULL; + + pers->name = strdup("test"); + pkgconf_path_copy_list(&pers->dir_list, &testcase->search_path); + pkgconf_path_add("/test/sysroot/include", &pers->filter_includedirs, false); + pkgconf_path_add("/test/sysroot/lib", &pers->filter_libdirs, false); + + return pers; +} + +static bool +report_failure(pkgconf_test_match_strategy_t match, const pkgconf_buffer_t *expected, const pkgconf_buffer_t *actual, const char *buffername) +{ + fprintf(stderr, + "================================================================================\n" + "%s did not%s match:\n" + " expected: [%s]\n" + " actual: [%s]\n" + "================================================================================\n", + buffername, match == MATCH_PARTIAL ? " partially" : "", + pkgconf_buffer_str_or_empty(expected), + pkgconf_buffer_str_or_empty(actual)); + + return false; +} + +static bool +test_match_buffer(pkgconf_test_match_strategy_t match, const pkgconf_buffer_t *expected, const pkgconf_buffer_t *actual, const char *buffername) +{ + if (!pkgconf_buffer_len(expected) && match != MATCH_EMPTY) + return true; + + if (!pkgconf_buffer_len(actual)) + { + if (match == MATCH_EMPTY) + return true; + + return report_failure(match, expected, actual, buffername); + } + + if (match == MATCH_PARTIAL) + return pkgconf_buffer_contains(actual, expected) ? true : report_failure(match, expected, actual, buffername); + + return pkgconf_buffer_match(actual, expected) ? true : report_failure(match, expected, actual, buffername); +} + +static bool +read_file_into_buffer(FILE *f, pkgconf_buffer_t *buf) +{ + char tmp[4096] = {0}; + size_t n; + while ((n = fread(tmp, 1, sizeof(tmp), f)) > 0) + pkgconf_buffer_append_slice(buf, tmp, n); + + if (ferror(f)) + return false; + + return true; +} + +static bool +open_file_into_buffer(const char *path, pkgconf_buffer_t *buf) +{ + FILE *f = fopen(path, "r"); + if (f == NULL) + return false; + + bool ok = read_file_into_buffer(f, buf); + fclose(f); + return ok; +} + +static bool +copy_file(const char *dst, const char *src) +{ + FILE *fsrc = fopen(src, "rb"); + if (!fsrc) + return false; + + FILE *fdst = fopen(dst, "wb"); + if (!fdst) + { + int errno_save = errno; + fclose(fsrc); + errno = errno_save; + return false; + } + + bool ok = true; + char buf[4096] = {0}; + size_t nr; + while ((nr = fread(buf, 1, sizeof(buf), fsrc)) > 0) + { + if (fwrite(buf, 1, nr, fdst) != nr) + { + ok = false; + break; + } + } + + if (ferror(fsrc) || ferror(fdst)) + ok = false; + + int errno_save = errno; + fclose(fsrc); + fclose(fdst); + errno = errno_save; + + return ok; +} + +/* + * Recursively remove a directory tree. + */ +static void +rmdir_recursive(const char *path) +{ + DIR *dir = opendir(path); + if (dir == NULL) + return; + + struct dirent *ent; + while ((ent = readdir(dir)) != NULL) + { + if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) + continue; + + pkgconf_buffer_t child = PKGCONF_BUFFER_INITIALIZER; + pkgconf_buffer_append(&child, path); + pkgconf_buffer_push_byte(&child, '/'); + pkgconf_buffer_append(&child, ent->d_name); + +#ifdef _WIN32 + if (_access(pkgconf_buffer_str(&child), 0) == 0) + { + // Get required buffer size + int size = MultiByteToWideChar(CP_ACP, 0, pkgconf_buffer_str(&child), -1, NULL, 0); + + // Allocate and convert + wchar_t* wide_path = calloc(size, sizeof(wchar_t)); + assert(wide_path != NULL); + MultiByteToWideChar(CP_ACP, 0, pkgconf_buffer_str(&child), -1, wide_path, size); + + DWORD attrs = GetFileAttributesW(wide_path); + + free(wide_path); + + if (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY)) + rmdir_recursive(pkgconf_buffer_str(&child)); + else + unlink(pkgconf_buffer_str(&child)); + } +#else + struct stat st; + if (lstat(pkgconf_buffer_str(&child), &st) == 0) + { + if (S_ISDIR(st.st_mode)) + rmdir_recursive(pkgconf_buffer_str(&child)); + else + unlink(pkgconf_buffer_str(&child)); + } +#endif + + pkgconf_buffer_finalize(&child); + } + + closedir(dir); + rmdir(path); +} + +/* + * Recursively make a directory tree. + */ +static bool +mkdir_recursive(const char *path) +{ + if (!path) + return false; + + const char *tmpstr = NULL; + pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER; + size_t i = 0; + + while (path[i]) + { + // Append characters up to the next separator + size_t start = i; + while (path[i] && path[i] != '/') + i++; + + pkgconf_buffer_append_slice(&buf, path + start, i - start); + + // If we hit a separator, try to create this component + if (path[i] == '/') + { + pkgconf_buffer_push_byte(&buf, '/'); + tmpstr = pkgconf_buffer_str(&buf); + if (tmpstr && mkdir(tmpstr, 0755) != 0 && errno != EEXIST) + { + pkgconf_buffer_finalize(&buf); + return false; + } + i++; // skip the separator + } + } + + // Make the final directory (handles paths without trailing separator) + tmpstr = pkgconf_buffer_str(&buf); + bool ok = true; + if (tmpstr && strlen(tmpstr) > 0) + { + if (mkdir(tmpstr, 0755) != 0 && errno != EEXIST) + ok = false; + } + + pkgconf_buffer_finalize(&buf); + return ok; +} + +// Returns true if we need tmp_dir +static bool +needs_tmp_dir(const pkgconf_test_case_t *testcase) +{ +#ifdef _WIN32 + return testcase->mkdirs.head != NULL || testcase->copies.head != NULL; +#else // _WIN32 + return testcase->mkdirs.head != NULL || testcase->copies.head != NULL || testcase->symlinks.head != NULL; +#endif // _WIN32 +} + +static int +run_tool(const pkgconf_test_case_t *testcase, pkgconf_buffer_t *o_stdout, pkgconf_buffer_t *o_stderr) +{ + (void) o_stderr; // TODO: external tool stderr goes to real stderr for now + + pkgconf_buffer_t cmdbuf = PKGCONF_BUFFER_INITIALIZER; + + // build: <tool-dir>/<tool> <tool_args> + if (pkgconf_buffer_len(&test_tool_dir)) + { + pkgconf_buffer_append(&cmdbuf, pkgconf_buffer_str(&test_tool_dir)); + pkgconf_buffer_push_byte(&cmdbuf, '/'); + } + + pkgconf_buffer_append(&cmdbuf, pkgconf_buffer_str(&testcase->tool)); + + if (pkgconf_buffer_len(&testcase->tool_args)) + { + pkgconf_buffer_append(&cmdbuf, " "); + pkgconf_buffer_append(&cmdbuf, pkgconf_buffer_str(&testcase->tool_args)); + } + + // Inject Environment vars for the child process + char tool_cwd[PATH_MAX] = {0}; + const char *pwd = getcwd(tool_cwd, sizeof(tool_cwd)); + + pkgconf_node_t *iter; + PKGCONF_FOREACH_LIST_ENTRY(testcase->env_vars.head, iter) + { + pkgconf_test_environ_t *env = iter->data; + pkgconf_buffer_t expanded = PKGCONF_BUFFER_INITIALIZER; + handle_substs(&expanded, PKGCONF_BUFFER_FROM_STR(env->value), pwd); + setenv(env->key, pkgconf_buffer_str_or_empty(&expanded), 1); + pkgconf_buffer_finalize(&expanded); + } + + FILE *pipe = popen(pkgconf_buffer_str(&cmdbuf), "r"); + pkgconf_buffer_finalize(&cmdbuf); + + if (pipe == NULL) + { + fprintf(stderr, "popen failed for tool '%s': %s\n", + pkgconf_buffer_str(&testcase->tool), strerror(errno)); + return -1; + } + + bool ok = read_file_into_buffer(pipe, o_stdout); + int saved_errno = errno; // pclose() will clobber errno, save it + int status = pclose(pipe); + if (!ok) + { + fprintf(stderr, "read failed into buffer for command '%s': %s", + pkgconf_buffer_str(&testcase->tool), strerror(saved_errno)); + return -1; + } + + if (status == -1) + { + fprintf(stderr, "pclose failed for command '%s': %s\n", + pkgconf_buffer_str(&testcase->tool), strerror(errno)); + return -1; + } + +#if defined(WIFEXITED) && defined(WEXITSTATUS) + if (WIFEXITED(status)) + return WEXITSTATUS(status); + + fprintf(stderr, "command '%s' did not exit normally\n", + pkgconf_buffer_str(&testcase->tool)); + return -1; +#else + return status; +#endif +} + +/* + * Split a bufferset entry on the first space into left and right halves. + * Caller must free *left_out and *right_out. + */ +static bool +split_pair(const char *entry, char **left_out, char **right_out) +{ + if (entry == NULL) + return false; + + const char *sp = strchr(entry, ' '); + if (sp == NULL) + return false; + + *left_out = pkgconf_strndup(entry, (size_t)(sp - entry)); + *right_out = strdup(sp + 1); + return true; +} + +/* + * run_setup: execute mkdirs, copies, and symlinks in order. + * Must be called after chdir() into the tmp_dir. + */ +static bool +run_setup(const pkgconf_test_case_t *testcase, const char *pwd) +{ + pkgconf_node_t *iter; + + // mkdirs: each entry is a single path, relative to tmp_dir + PKGCONF_FOREACH_LIST_ENTRY(testcase->mkdirs.head, iter) + { + pkgconf_bufferset_t *set = iter->data; + + pkgconf_buffer_t path = PKGCONF_BUFFER_INITIALIZER; + handle_substs(&path, &set->buffer, pwd); + + bool ok = mkdir_recursive(pkgconf_buffer_str(&path)); + pkgconf_buffer_finalize(&path); + + if (!ok && errno != EEXIST) + { + fprintf(stderr, "SetupMkdir: mkdir '%s' failed: %s\n", + pkgconf_buffer_str_or_empty(&set->buffer), strerror(errno)); + return false; + } + } + + // copies: "src dst", src relative to TEST_FIXTURES_DIR, dst relative to tmp_dir + PKGCONF_FOREACH_LIST_ENTRY(testcase->copies.head, iter) + { + pkgconf_bufferset_t *set = iter->data; + + pkgconf_buffer_t expanded = PKGCONF_BUFFER_INITIALIZER; + handle_substs(&expanded, &set->buffer, pwd); + + char *left = NULL, *right = NULL; + if (!split_pair(pkgconf_buffer_str(&expanded), &left, &right)) + { + fprintf(stderr, "SetupCopy: malformed entry (expected 'src dst'): %s\n", + pkgconf_buffer_str_or_empty(&set->buffer)); + pkgconf_buffer_finalize(&expanded); + return false; + } + pkgconf_buffer_finalize(&expanded); + + pkgconf_buffer_t srcpath = PKGCONF_BUFFER_INITIALIZER; + pkgconf_buffer_append(&srcpath, pkgconf_buffer_str(&test_fixtures_dir)); + pkgconf_buffer_push_byte(&srcpath, '/'); + pkgconf_buffer_append(&srcpath, left); + + bool ok = copy_file(right, pkgconf_buffer_str(&srcpath)); + pkgconf_buffer_finalize(&srcpath); + if (!ok) + { + fprintf(stderr, "SetupCopy: failed to copy file '%s' to '%s': %s\n", left, right, strerror(errno)); + free(left); + free(right); + return false; + } + + free(left); + free(right); + } + +#ifndef _WIN32 + // symlinks: "target linkpath" — both may be relative to tmp_dir or absolute after %PWD% expansion + PKGCONF_FOREACH_LIST_ENTRY(testcase->symlinks.head, iter) + { + pkgconf_bufferset_t *set = iter->data; + + pkgconf_buffer_t expanded = PKGCONF_BUFFER_INITIALIZER; + handle_substs(&expanded, &set->buffer, pwd); + + char *target = NULL, *linkpath = NULL; + if (!split_pair(pkgconf_buffer_str(&expanded), &target, &linkpath)) + { + fprintf(stderr, "SetupSymlink: malformed entry (expected 'target linkpath'): %s\n", + pkgconf_buffer_str_or_empty(&set->buffer)); + pkgconf_buffer_finalize(&expanded); + return false; + } + pkgconf_buffer_finalize(&expanded); + + unlink(linkpath); + + if (symlink(target, linkpath) != 0) + { + fprintf(stderr, "SetupSymlink: symlink('%s', '%s') failed: %s\n", + target, linkpath, strerror(errno)); + free(target); + free(linkpath); + return false; + } + + free(target); + free(linkpath); + } +#endif // _WIN32 + + return true; +} + +static void +annotate_result(const pkgconf_test_case_t *testcase, int ret, const pkgconf_test_output_t *out) +{ + pkgconf_buffer_t search_path_buf = PKGCONF_BUFFER_INITIALIZER; + const pkgconf_node_t *iter; + + PKGCONF_FOREACH_LIST_ENTRY(testcase->search_path.head, iter) + { + const pkgconf_path_t *path = iter->data; + + if (pkgconf_buffer_len(&search_path_buf)) + pkgconf_buffer_push_byte(&search_path_buf, ' '); + + pkgconf_buffer_append(&search_path_buf, path->path); + } + + pkgconf_buffer_t wanted_flags_buf = PKGCONF_BUFFER_INITIALIZER; + + for (size_t i = 0; i < PKGCONF_ARRAY_SIZE(test_flag_pairs); i++) + { + const pkgconf_test_flag_pair_t *pair = &test_flag_pairs[i]; + + if ((testcase->wanted_flags & pair->flag) == pair->flag) + { + if (pkgconf_buffer_len(&wanted_flags_buf)) + pkgconf_buffer_push_byte(&wanted_flags_buf, ' '); + + pkgconf_buffer_append(&wanted_flags_buf, pair->name); + } + } + + pkgconf_buffer_t env_buf = PKGCONF_BUFFER_INITIALIZER; + + PKGCONF_FOREACH_LIST_ENTRY(testcase->env_vars.head, iter) + { + const pkgconf_test_environ_t *env = iter->data; + + if (pkgconf_buffer_len(&env_buf)) + pkgconf_buffer_append(&env_buf, "\n "); + + pkgconf_buffer_append_fmt(&env_buf, "%s: %s", env->key, env->value); + } + + fprintf(stderr, + "--------------------------------------------------------------------------------\n" + "search-path: <%s>\n" + "wanted-flags: <%s>\n" + "environment:\n" + " %s\n" + "query: [%s]\n" + "exit-code: %d\n" + "verbosity: %d\n", + pkgconf_buffer_str_or_empty(&search_path_buf), + pkgconf_buffer_str_or_empty(&wanted_flags_buf), + pkgconf_buffer_str_or_empty(&env_buf), + pkgconf_buffer_str_or_empty(&testcase->query), + ret, + testcase->verbosity); + + if (pkgconf_buffer_len(&testcase->tool)) + fprintf(stderr, "tool: [%s] tool-args: [%s]\n", + pkgconf_buffer_str_or_empty(&testcase->tool), + pkgconf_buffer_str_or_empty(&testcase->tool_args)); + + fprintf(stderr, "stdout: [%s]\n", + pkgconf_buffer_str_or_empty(&out->o_stdout)); + + PKGCONF_FOREACH_LIST_ENTRY(testcase->expected_stdout.head, iter) + { + pkgconf_bufferset_t *set = iter->data; + + fprintf(stderr, + "expected-stdout: [%s] (%s)\n", + pkgconf_buffer_str_or_empty(&set->buffer), + testcase->match_stdout == MATCH_PARTIAL ? "partial" : "exact"); + } + + if (pkgconf_buffer_len(&testcase->expected_stdout_file)) + fprintf(stderr, "expected-stdout-file: [%s]\n", + pkgconf_buffer_str_or_empty(&testcase->expected_stdout_file)); + + fprintf(stderr, "stderr: [%s]\n", + pkgconf_buffer_str_or_empty(&out->o_stderr)); + + PKGCONF_FOREACH_LIST_ENTRY(testcase->expected_stderr.head, iter) + { + pkgconf_bufferset_t *set = iter->data; + + fprintf(stderr, + "expected-stderr: [%s] (%s)\n", + pkgconf_buffer_str_or_empty(&set->buffer), + testcase->match_stderr == MATCH_PARTIAL ? "partial" : "exact"); + } + + PKGCONF_FOREACH_LIST_ENTRY(testcase->define_variables.head, iter) + { + pkgconf_bufferset_t *set = iter->data; + fprintf(stderr, "define-variable: [%s]\n", pkgconf_buffer_str_or_empty(&set->buffer)); + } + + fprintf(stderr, + "want-env-prefix: [%s]\n" + "fragment-filter: [%s]\n" + "--------------------------------------------------------------------------------\n", + pkgconf_buffer_str_or_empty(&testcase->want_env_prefix), + pkgconf_buffer_str_or_empty(&testcase->fragment_filter)); + + pkgconf_buffer_finalize(&search_path_buf); + pkgconf_buffer_finalize(&wanted_flags_buf); + pkgconf_buffer_finalize(&env_buf); +} + +static bool +run_test_case(const pkgconf_test_case_t *testcase) +{ + bool passed = true; + + const pkgconf_buffer_t *our_platform = PKGCONF_BUFFER_FROM_STR(PKGCONF_TEST_PLATFORM); + if (pkgconf_buffer_contains(&testcase->skip_platforms, our_platform)) + { + printf("# test skipped on %s\nSKIP: %s\n", + pkgconf_buffer_str(our_platform), testcase->name); + return true; + } + + if (testcase->require_utf8_locale && !pkgconf_is_locale_utf8()) + { + printf("# test skipped: requires a UTF-8 locale\nSKIP: %s\n", testcase->name); + return true; + } + + // If the test has setup steps, create a new tmp_dir and chdir into it. + char original_cwd[PATH_MAX] = {0}; + char *tmp_dir = NULL; + + if (getcwd(original_cwd, sizeof(original_cwd)) == NULL) + { + fprintf(stderr, "FAIL: getcwd failed: %s\n", strerror(errno)); + return false; + } + + if (needs_tmp_dir(testcase)) + { + pkgconf_buffer_t tmp_buf = PKGCONF_BUFFER_INITIALIZER; + pkgconf_buffer_append_fmt(&tmp_buf, "%s/pkgconf-test-XXXXXX", original_cwd); + tmp_dir = pkgconf_buffer_freeze(&tmp_buf); + + if (mkdtemp(tmp_dir) == NULL) + { + fprintf(stderr, "FAIL: mkdtemp failed: %s\n", strerror(errno)); + free(tmp_dir); + return false; + } + + if (chdir(tmp_dir) != 0) + { + fprintf(stderr, "FAIL: chdir('%s') failed: %s\n", tmp_dir, strerror(errno)); + rmdir(tmp_dir); + free(tmp_dir); + return false; + } + + if (!run_setup(testcase, tmp_dir)) + { + fprintf(stderr, "FAIL: %s (setup failed)\n", testcase->name); + chdir(original_cwd); + rmdir_recursive(tmp_dir); + free(tmp_dir); + return false; + } + } + + pkgconf_test_output_t *out = (pkgconf_test_output_t *) test_output(); + int ret; + + if (pkgconf_buffer_len(&testcase->tool)) + { + ret = run_tool(testcase, &out->o_stdout, &out->o_stderr); + } + else + { + pkgconf_cross_personality_t *personality = personality_for_test(testcase); + pkgconf_test_state_t state = + { + .cli_state.want_flags = testcase->wanted_flags, + .cli_state.want_env_prefix = pkgconf_buffer_str(&testcase->want_env_prefix), + .cli_state.want_variable = pkgconf_buffer_str(&testcase->want_variable), + .cli_state.want_fragment_filter = pkgconf_buffer_str(&testcase->fragment_filter), + .cli_state.required_module_version = pkgconf_buffer_str(&testcase->atleast_version), + .cli_state.required_exact_module_version = pkgconf_buffer_str(&testcase->exact_version), + .cli_state.required_max_module_version = pkgconf_buffer_str(&testcase->max_version), + .cli_state.verbosity = testcase->verbosity, + .testcase = testcase, + }; + + pkgconf_client_init(&state.cli_state.pkg_client, error_handler, NULL, personality, &state, environ_lookup_handler); + pkgconf_client_set_output(&state.cli_state.pkg_client, &out->output); + + pkgconf_node_t *iter; + PKGCONF_FOREACH_LIST_ENTRY(testcase->define_variables.head, iter) + { + pkgconf_bufferset_t *set = iter->data; + pkgconf_tuple_define_global(&state.cli_state.pkg_client, pkgconf_buffer_str_or_empty(&set->buffer)); + } + + /* + * Re-expand Query now that %PWD% is known (if we have a tmp_dir). + * For tests without a tmp_dir this is a no-op since %PWD% won't appear. + */ + char query_cwd[PATH_MAX] = {0}; + const char *query_pwd = getcwd(query_cwd, sizeof(query_cwd)); + + pkgconf_buffer_t query_expanded = PKGCONF_BUFFER_INITIALIZER; + handle_substs(&query_expanded, &testcase->query, query_pwd); + + pkgconf_buffer_t arg_buf = PKGCONF_BUFFER_INITIALIZER; + int test_argc = 0; + char **test_argv = NULL; + + if (pkgconf_buffer_len(&query_expanded)) + pkgconf_buffer_append_fmt(&arg_buf, "pkgconf %s", pkgconf_buffer_str(&query_expanded)); + else + pkgconf_buffer_append(&arg_buf, "pkgconf"); + + pkgconf_argv_split(pkgconf_buffer_str(&arg_buf), &test_argc, &test_argv); + pkgconf_buffer_finalize(&arg_buf); + pkgconf_buffer_finalize(&query_expanded); + + pkgconf_client_set_warn_handler(&state.cli_state.pkg_client, error_handler, NULL); + +#ifndef PKGCONF_LITE + if (debug) + pkgconf_client_set_trace_handler(&state.cli_state.pkg_client, debug_handler, NULL); +#endif // PKGCONF_LITE + + ret = pkgconf_cli_run(&state.cli_state, test_argc, test_argv, 1); + pkgconf_argv_free(test_argv); + } + + if (pkgconf_buffer_len(&out->o_stdout)) + pkgconf_buffer_trim_byte(&out->o_stdout); + + if (pkgconf_buffer_len(&out->o_stderr)) + pkgconf_buffer_trim_byte(&out->o_stderr); + + pkgconf_node_t *iter; + + PKGCONF_FOREACH_LIST_ENTRY(testcase->expected_stdout.head, iter) + { + pkgconf_bufferset_t *set = iter->data; + + char expected_cwd[PATH_MAX] = {0}; + const char *expected_pwd = getcwd(expected_cwd, sizeof(expected_cwd)); + + pkgconf_buffer_t expected_expanded = PKGCONF_BUFFER_INITIALIZER; + handle_substs(&expected_expanded, &set->buffer, expected_pwd); + + if (!test_match_buffer(testcase->match_stdout, &expected_expanded, &out->o_stdout, "stdout")) + passed = false; + + pkgconf_buffer_finalize(&expected_expanded); + } + + // ExpectedStdoutFile: load file relative to the .test file's directory + if (pkgconf_buffer_len(&testcase->expected_stdout_file)) + { + pkgconf_buffer_t filepath = PKGCONF_BUFFER_INITIALIZER; + pkgconf_buffer_append(&filepath, testcase->testfile_dir); + pkgconf_buffer_push_byte(&filepath, '/'); + pkgconf_buffer_append(&filepath, pkgconf_buffer_str(&testcase->expected_stdout_file)); + + pkgconf_buffer_t file_contents = PKGCONF_BUFFER_INITIALIZER; + if (!open_file_into_buffer(pkgconf_buffer_str(&filepath), &file_contents)) + { + fprintf(stderr, "ExpectedStdoutFile: failed to open '%s': %s\n", pkgconf_buffer_str(&filepath), strerror(errno)); + passed = false; + } + else + { + if (pkgconf_buffer_len(&file_contents)) + pkgconf_buffer_trim_byte(&file_contents); + + if (!test_match_buffer(testcase->match_stdout, &file_contents, &out->o_stdout, "stdout (file)")) + passed = false; + } + + pkgconf_buffer_finalize(&file_contents); + pkgconf_buffer_finalize(&filepath); + } + + PKGCONF_FOREACH_LIST_ENTRY(testcase->expected_stderr.head, iter) + { + pkgconf_bufferset_t *set = iter->data; + + if (!test_match_buffer(testcase->match_stderr, &set->buffer, &out->o_stderr, "stderr")) + passed = false; + } + + if (ret != testcase->exitcode) + { + fprintf(stderr, "exitcode %d does not match expected %d\n", ret, testcase->exitcode); + passed = false; + } + + printf("%s: %s\n", passed ? "PASS" : "FAIL", testcase->name); + + if (!passed) + annotate_result(testcase, ret, out); + + test_output_reset(out); + + // Restore cwd and clean up tmp_dir if we created one + if (tmp_dir && strcmp(tmp_dir, original_cwd) != 0) + { + chdir(original_cwd); + rmdir_recursive(tmp_dir); + } + + free(tmp_dir); + return passed; +} + +static void +free_test_case(pkgconf_test_case_t *testcase) +{ + pkgconf_bufferset_free(&testcase->define_variables); + pkgconf_bufferset_free(&testcase->expected_stderr); + pkgconf_bufferset_free(&testcase->expected_stdout); + pkgconf_bufferset_free(&testcase->mkdirs); +#ifndef _WIN32 + pkgconf_bufferset_free(&testcase->symlinks); +#endif // _WIN32 + pkgconf_bufferset_free(&testcase->copies); + + test_environment_free(&testcase->env_vars); + pkgconf_path_free(&testcase->search_path); + + pkgconf_buffer_finalize(&testcase->query); + pkgconf_buffer_finalize(&testcase->want_env_prefix); + pkgconf_buffer_finalize(&testcase->want_variable); + pkgconf_buffer_finalize(&testcase->fragment_filter); + pkgconf_buffer_finalize(&testcase->skip_platforms); + pkgconf_buffer_finalize(&testcase->atleast_version); + pkgconf_buffer_finalize(&testcase->exact_version); + pkgconf_buffer_finalize(&testcase->max_version); + pkgconf_buffer_finalize(&testcase->expected_stdout_file); + pkgconf_buffer_finalize(&testcase->tool); + pkgconf_buffer_finalize(&testcase->tool_args); + +#ifndef PKGCONF_LITE + pkgconf_buffer_finalize(&testcase->want_personality); +#endif + + free(testcase->name); + free(testcase->testfile_dir); + free(testcase); +} + +static bool +process_test_case(char *testcase_file) +{ + pkgconf_test_case_t *testcase = load_test_case(testcase_file); + bool ret; + + if (testcase == NULL) + { + fprintf(stderr, "test %s failed to load\n", testcase_file); + return false; + } + + ret = run_test_case(testcase); + free_test_case(testcase); + + return ret; +} + +static inline bool +str_has_suffix(const char *str, const char *suffix) +{ + size_t str_len = strlen(str); + size_t suf_len = strlen(suffix); + + if (str_len < suf_len) + return false; + + return !strncasecmp(str + str_len - suf_len, suffix, suf_len); +} + +static int +path_sort_cmp(const void *a, const void *b) +{ + return strcmp(*(const char **) a, *(const char **) b); +} + +static bool +process_test_directory(char *dirpath) +{ + bool ret = true; + DIR *dir = opendir(dirpath); + if (dir == NULL) + { + fprintf(stderr, "failed to open test directory %s\n", dirpath); + return false; + } + char **paths = NULL; + size_t numpaths = 0; + + struct dirent *dirent; + for (dirent = readdir(dir); dirent != NULL; dirent = readdir(dir)) + { + pkgconf_buffer_t pathbuf = PKGCONF_BUFFER_INITIALIZER; + + pkgconf_buffer_append(&pathbuf, dirpath); + pkgconf_buffer_push_byte(&pathbuf, '/'); + pkgconf_buffer_append(&pathbuf, dirent->d_name); + + char *pathstr = pkgconf_buffer_freeze(&pathbuf); + if (pathstr == NULL) + continue; + + if (!str_has_suffix(pathstr, ".test")) + { + free(pathstr); + continue; + } + + paths = pkgconf_reallocarray(paths, ++numpaths, sizeof(void *)); + paths[numpaths - 1] = pathstr; + } + + qsort(paths, numpaths, sizeof(void *), path_sort_cmp); + + for (size_t i = 0; i < numpaths; i++) + { + char *pathstr = paths[i]; + + ret = process_test_case(pathstr); + if (!ret) + break; + } + + for (size_t i = 0; i < numpaths; i++) + free(paths[i]); + + free(paths); + closedir(dir); + return ret; +} + +static void +usage(void) +{ + fprintf(stderr, "usage: test-runner --test-fixtures <path-to-fixtures> [--tool-dir <path>] <path-to-tests>\n"); + exit(EXIT_FAILURE); +} + +int +main(int argc, char *argv[]) +{ + int ret; + char *test_fixtures_dir_arg = NULL; + char *test_tool_dir_arg = NULL; + + struct pkg_option options[] = + { + {"test-fixtures", required_argument, NULL, 1}, + {"debug", no_argument, NULL, 2}, + {"test-case", required_argument, NULL, 3}, + {"tool-dir", required_argument, NULL, 4}, + {NULL, 0, NULL, 0}, + }; + char *testcase = NULL; + + while ((ret = pkg_getopt_long_only(argc, argv, "", options, NULL)) != -1) + { + switch (ret) + { + case 1: + test_fixtures_dir_arg = pkg_optarg; + break; + case 2: + debug = true; + break; + case 3: + testcase = pkg_optarg; + break; + case 4: + test_tool_dir_arg = pkg_optarg; + break; + } + } + + if (test_fixtures_dir_arg == NULL) + usage(); + + { + char test_fixtures_dir_abs[PATH_MAX] = {0}; + if (!realpath(test_fixtures_dir_arg, test_fixtures_dir_abs)) + { + fprintf(stderr, "realpath failed: %s\n", strerror(errno)); + return EXIT_FAILURE; + } + const pkgconf_buffer_t *test_fixtures_dir_arg_buf = PKGCONF_BUFFER_FROM_STR_NONNULL(test_fixtures_dir_abs); + pkgconf_buffer_subst(&test_fixtures_dir, test_fixtures_dir_arg_buf, "\\", "/"); + } + + if (test_tool_dir_arg != NULL) + { + const pkgconf_buffer_t *test_tool_dir_arg_buf = PKGCONF_BUFFER_FROM_STR(test_tool_dir_arg); + pkgconf_buffer_subst(&test_tool_dir, test_tool_dir_arg_buf, "\\", "/"); + } + + if (testcase != NULL) + return process_test_case(testcase) ? EXIT_SUCCESS : EXIT_FAILURE; + + if (argv[pkg_optind] == NULL) + usage(); + + return process_test_directory(argv[pkg_optind]) ? EXIT_SUCCESS : EXIT_FAILURE; +} |
