From 9cce7038635afa7e94c434f1571a8e435350cb51 Mon Sep 17 00:00:00 2001 From: furszy Date: Tue, 9 Sep 2025 15:35:47 -0400 Subject: [PATCH 1/7] refactor: move 'gettime_i64()' to tests_common.h Relocate the clock time getter to tests_common.h to make it easily reusable across test programs. This will be useful for the upcoming unit test framework. Context - why not placing it inside testutil.h?: The bench program links against the production-compiled library, not its own compiled version. Therefore, `gettime_i64()` cannot be moved to testutil.h, because testutil.h calls `secp256k1_pubkey_save()`, which exists only in the internal secp256k1.c and not in the public API. --- Makefile.am | 1 + src/bench.h | 22 +--------------------- src/tests_common.h | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 21 deletions(-) create mode 100644 src/tests_common.h diff --git a/Makefile.am b/Makefile.am index d511853b05..4cc97babc9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -45,6 +45,7 @@ noinst_HEADERS += src/precomputed_ecmult.h noinst_HEADERS += src/precomputed_ecmult_gen.h noinst_HEADERS += src/assumptions.h noinst_HEADERS += src/checkmem.h +noinst_HEADERS += src/tests_common.h noinst_HEADERS += src/testutil.h noinst_HEADERS += src/util.h noinst_HEADERS += src/util_local_visibility.h diff --git a/src/bench.h b/src/bench.h index 232fb35fc0..4e8e961b67 100644 --- a/src/bench.h +++ b/src/bench.h @@ -12,27 +12,7 @@ #include #include -#if (defined(_MSC_VER) && _MSC_VER >= 1900) -# include -#else -# include -#endif - -static int64_t gettime_i64(void) { -#if (defined(_MSC_VER) && _MSC_VER >= 1900) - /* C11 way to get wallclock time */ - struct timespec tv; - if (!timespec_get(&tv, TIME_UTC)) { - fputs("timespec_get failed!", stderr); - exit(EXIT_FAILURE); - } - return (int64_t)tv.tv_nsec / 1000 + (int64_t)tv.tv_sec * 1000000LL; -#else - struct timeval tv; - gettimeofday(&tv, NULL); - return (int64_t)tv.tv_usec + (int64_t)tv.tv_sec * 1000000LL; -#endif -} +#include "tests_common.h" #define FP_EXP (6) #define FP_MULT (1000000LL) diff --git a/src/tests_common.h b/src/tests_common.h new file mode 100644 index 0000000000..a341633bbc --- /dev/null +++ b/src/tests_common.h @@ -0,0 +1,42 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_TESTS_COMMON_H +#define SECP256K1_TESTS_COMMON_H + +/*********************************************************************** + * Test Support Utilities + * + * This file provides general-purpose functions for tests and benchmark + * programs. Unlike testutil.h, this file is not linked to the library, + * allowing each program to choose whether to run against the production + * API or access library internals directly. + ***********************************************************************/ + +#include + +#if (defined(_MSC_VER) && _MSC_VER >= 1900) +# include +#else +# include +#endif + +static int64_t gettime_i64(void) { +#if (defined(_MSC_VER) && _MSC_VER >= 1900) + /* C11 way to get wallclock time */ + struct timespec tv; + if (!timespec_get(&tv, TIME_UTC)) { + fputs("timespec_get failed!", stderr); + exit(EXIT_FAILURE); + } + return (int64_t)tv.tv_nsec / 1000 + (int64_t)tv.tv_sec * 1000000LL; +#else + struct timeval tv; + gettimeofday(&tv, NULL); + return (int64_t)tv.tv_usec + (int64_t)tv.tv_sec * 1000000LL; +#endif +} + +#endif /* SECP256K1_TESTS_COMMON_H */ From 48789dafc2a866bbc639184f0387637c0decb8c5 Mon Sep 17 00:00:00 2001 From: furszy Date: Wed, 3 Sep 2025 10:59:37 -0400 Subject: [PATCH 2/7] test: introduce (mini) unit test framework Lightweight unit testing framework, providing a structured way to define, execute, and report tests. It includes a central test registry, a flexible command-line argument parser of the form "--key=value" / "-k=value" / "-key=value" (facilitating future framework extensions), ability to run tests in parallel and accumulated test time logging reports. So far the supported command-line args are: - "--jobs=" or "-j=" to specify the number of parallel workers. - "--seed=" to specify the RNG seed (random if not set). - "--iterations=" or "-i=" to specify the number of iterations. Compatibility Note: To stay compatible with previous versions, the framework also supports the two original positional arguments: the iterations count and the RNG seed (in that order). --- Makefile.am | 4 +- configure.ac | 8 ++ src/CMakeLists.txt | 14 +- src/tests.c | 329 ++++++++++++++++++++++++------------------- src/unit_test.c | 342 +++++++++++++++++++++++++++++++++++++++++++++ src/unit_test.h | 120 ++++++++++++++++ 6 files changed, 674 insertions(+), 143 deletions(-) create mode 100644 src/unit_test.c create mode 100644 src/unit_test.h diff --git a/Makefile.am b/Makefile.am index 4cc97babc9..dc798575e3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -47,6 +47,8 @@ noinst_HEADERS += src/assumptions.h noinst_HEADERS += src/checkmem.h noinst_HEADERS += src/tests_common.h noinst_HEADERS += src/testutil.h +noinst_HEADERS += src/unit_test.h +noinst_HEADERS += src/unit_test.c noinst_HEADERS += src/util.h noinst_HEADERS += src/util_local_visibility.h noinst_HEADERS += src/int128.h @@ -121,7 +123,7 @@ if USE_TESTS TESTS += noverify_tests noinst_PROGRAMS += noverify_tests noverify_tests_SOURCES = src/tests.c -noverify_tests_CPPFLAGS = $(SECP_CONFIG_DEFINES) +noverify_tests_CPPFLAGS = $(SECP_CONFIG_DEFINES) $(TEST_DEFINES) noverify_tests_LDADD = $(COMMON_LIB) $(PRECOMPUTED_LIB) noverify_tests_LDFLAGS = -static if !ENABLE_COVERAGE diff --git a/configure.ac b/configure.ac index 2f156ddc25..6028ee288d 100644 --- a/configure.ac +++ b/configure.ac @@ -443,6 +443,14 @@ if test x"$enable_experimental" = x"no"; then fi fi +# Check for concurrency support (tests only) +if test "x$enable_tests" != x"no"; then + AC_CHECK_HEADERS([sys/types.h sys/wait.h unistd.h]) + AS_IF([test "x$ac_cv_header_sys_types_h" = xyes && test "x$ac_cv_header_sys_wait_h" = xyes && + test "x$ac_cv_header_unistd_h" = xyes], [TEST_DEFINES="-DSUPPORTS_CONCURRENCY=1"], TEST_DEFINES="") + AC_SUBST(TEST_DEFINES) +fi + ### ### Generate output ### diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fa3b2903eb..ecbbbbe8e9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -134,15 +134,27 @@ if(SECP256K1_BUILD_BENCHMARK) endif() if(SECP256K1_BUILD_TESTS) + include(CheckIncludeFile) + check_include_file(sys/types.h HAVE_SYS_TYPES_H) + check_include_file(sys/wait.h HAVE_SYS_WAIT_H) + check_include_file(unistd.h HAVE_UNISTD_H) + + set(TEST_DEFINITIONS "") + if(HAVE_SYS_TYPES_H AND HAVE_SYS_WAIT_H AND HAVE_UNISTD_H) + list(APPEND TEST_DEFINITIONS SUPPORTS_CONCURRENCY=1) + endif() + add_executable(noverify_tests tests.c) target_link_libraries(noverify_tests secp256k1_precomputed secp256k1_asm) + target_compile_definitions(noverify_tests PRIVATE ${TEST_DEFINITIONS}) add_test(NAME secp256k1_noverify_tests COMMAND noverify_tests) if(NOT CMAKE_BUILD_TYPE STREQUAL "Coverage") add_executable(tests tests.c) - target_compile_definitions(tests PRIVATE VERIFY) + target_compile_definitions(tests PRIVATE VERIFY ${TEST_DEFINITIONS}) target_link_libraries(tests secp256k1_precomputed secp256k1_asm) add_test(NAME secp256k1_tests COMMAND tests) endif() + unset(TEST_DEFINITIONS) endif() if(SECP256K1_BUILD_EXHAUSTIVE_TESTS) diff --git a/src/tests.c b/src/tests.c index 28bec5904c..efcdbdc8b8 100644 --- a/src/tests.c +++ b/src/tests.c @@ -25,6 +25,8 @@ #include "checkmem.h" #include "testutil.h" #include "util.h" +#include "unit_test.h" +#include "unit_test.c" #include "../contrib/lax_der_parsing.c" #include "../contrib/lax_der_privatekey_parsing.c" @@ -37,7 +39,6 @@ #define CONDITIONAL_TEST(cnt, nam) if (COUNT < (cnt)) { printf("Skipping %s (iteration count too low)\n", nam); } else -static int COUNT = 16; static secp256k1_context *CTX = NULL; static secp256k1_context *STATIC_CTX = NULL; @@ -227,6 +228,12 @@ static void run_static_context_tests(int use_prealloc) { } } +static void run_all_static_context_tests(void) +{ + run_static_context_tests(0); + run_static_context_tests(1); +} + static void run_proper_context_tests(int use_prealloc) { int32_t dummy = 0; secp256k1_context *my_ctx, *my_ctx_fresh; @@ -349,6 +356,12 @@ static void run_proper_context_tests(int use_prealloc) { secp256k1_context_preallocated_destroy(NULL); } +static void run_all_proper_context_tests(void) +{ + run_proper_context_tests(0); + run_proper_context_tests(1); +} + static void run_scratch_tests(void) { const size_t adj_alloc = ((500 + ALIGNMENT - 1) / ALIGNMENT) * ALIGNMENT; @@ -7666,179 +7679,213 @@ static void run_cmov_tests(void) { ge_storage_cmov_test(); } -int main(int argc, char **argv) { - /* Disable buffering for stdout to improve reliability of getting - * diagnostic information. Happens right at the start of main because - * setbuf must be used before any other operation on the stream. */ - setbuf(stdout, NULL); - /* Also disable buffering for stderr because it's not guaranteed that it's - * unbuffered on all systems. */ - setbuf(stderr, NULL); - - /* find iteration count */ - if (argc > 1) { - COUNT = strtol(argv[1], NULL, 0); - } else { - const char* env = getenv("SECP256K1_TEST_ITERS"); - if (env && strlen(env) > 0) { - COUNT = strtol(env, NULL, 0); - } - } - if (COUNT <= 0) { - fputs("An iteration count of 0 or less is not allowed.\n", stderr); - return EXIT_FAILURE; - } - printf("test count = %i\n", COUNT); - - /* run test RNG tests (must run before we really initialize the test RNG) */ - run_xoshiro256pp_tests(); +/* --------------------------------------------------------- */ +/* Test Registry */ +/* --------------------------------------------------------- */ - /* find random seed */ - testrand_init(argc > 2 ? argv[2] : NULL); +/* --- Special test cases that must run before RNG initialization --- */ +static const struct tf_test_entry tests_no_rng[] = { + CASE(xoshiro256pp_tests), +}; +static const struct tf_test_module registry_modules_no_rng = MAKE_TEST_MODULE(no_rng); + +/* --- Standard test cases start here --- */ +static const struct tf_test_entry tests_general[] = { + CASE(selftest_tests), + CASE(all_proper_context_tests), + CASE(all_static_context_tests), + CASE(deprecated_context_flags_test), + CASE(scratch_tests), +}; - /*** Setup test environment ***/ +static const struct tf_test_entry tests_integer[] = { +#ifdef SECP256K1_WIDEMUL_INT128 + CASE(int128_tests), +#endif + CASE(ctz_tests), + CASE(modinv_tests), + CASE(inverse_tests), +}; - /* Create a global context available to all tests */ - CTX = secp256k1_context_create(SECP256K1_CONTEXT_NONE); - /* Randomize the context only with probability 15/16 - to make sure we test without context randomization from time to time. - TODO Reconsider this when recalibrating the tests. */ - if (testrand_bits(4)) { - unsigned char rand32[32]; - testrand256(rand32); - CHECK(secp256k1_context_randomize(CTX, rand32)); - } - /* Make a writable copy of secp256k1_context_static in order to test the effect of API functions - that write to the context. The API does not support cloning the static context, so we use - memcpy instead. The user is not supposed to copy a context but we should still ensure that - the API functions handle copies of the static context gracefully. */ - STATIC_CTX = malloc(sizeof(*secp256k1_context_static)); - CHECK(STATIC_CTX != NULL); - memcpy(STATIC_CTX, secp256k1_context_static, sizeof(secp256k1_context)); - CHECK(!secp256k1_context_is_proper(STATIC_CTX)); +static const struct tf_test_entry tests_hash[] = { + CASE(sha256_known_output_tests), + CASE(sha256_counter_tests), + CASE(hmac_sha256_tests), + CASE(rfc6979_hmac_sha256_tests), + CASE(tagged_sha256_tests), +}; - /*** Run actual tests ***/ +static const struct tf_test_entry tests_scalar[] = { + CASE(scalar_tests), +}; - /* selftest tests */ - run_selftest_tests(); +static const struct tf_test_entry tests_field[] = { + CASE(field_half), + CASE(field_misc), + CASE(field_convert), + CASE(field_be32_overflow), + CASE(fe_mul), + CASE(sqr), + CASE(sqrt), +}; - /* context tests */ - run_proper_context_tests(0); run_proper_context_tests(1); - run_static_context_tests(0); run_static_context_tests(1); - run_deprecated_context_flags_test(); +static const struct tf_test_entry tests_group[] = { + CASE(ge), + CASE(gej), + CASE(group_decompress), +}; - /* scratch tests */ - run_scratch_tests(); +static const struct tf_test_entry tests_ecmult[] = { + CASE(ecmult_pre_g), + CASE(wnaf), + CASE(point_times_order), + CASE(ecmult_near_split_bound), + CASE(ecmult_chain), + CASE(ecmult_constants), + CASE(ecmult_gen_blind), + CASE(ecmult_const_tests), + CASE(ecmult_multi_tests), + CASE(ec_combine), +}; - /* integer arithmetic tests */ -#ifdef SECP256K1_WIDEMUL_INT128 - run_int128_tests(); -#endif - run_ctz_tests(); - run_modinv_tests(); - run_inverse_tests(); - - /* sorting tests */ - run_hsort_tests(); - - /* hash tests */ - run_sha256_known_output_tests(); - run_sha256_counter_tests(); - run_hmac_sha256_tests(); - run_rfc6979_hmac_sha256_tests(); - run_tagged_sha256_tests(); - - /* scalar tests */ - run_scalar_tests(); - - /* field tests */ - run_field_half(); - run_field_misc(); - run_field_convert(); - run_field_be32_overflow(); - run_fe_mul(); - run_sqr(); - run_sqrt(); - - /* group tests */ - run_ge(); - run_gej(); - run_group_decompress(); - - /* ecmult tests */ - run_ecmult_pre_g(); - run_wnaf(); - run_point_times_order(); - run_ecmult_near_split_bound(); - run_ecmult_chain(); - run_ecmult_constants(); - run_ecmult_gen_blind(); - run_ecmult_const_tests(); - run_ecmult_multi_tests(); - run_ec_combine(); - - /* endomorphism tests */ - run_endomorphism_tests(); - - /* EC point parser test */ - run_ec_pubkey_parse_test(); - - /* EC key edge cases */ - run_eckey_edge_case_test(); - - /* EC key arithmetic test */ - run_eckey_negate_test(); +static const struct tf_test_entry tests_ec[] = { + CASE(endomorphism_tests), + CASE(ec_pubkey_parse_test), + CASE(eckey_edge_case_test), + CASE(eckey_negate_test), +}; #ifdef ENABLE_MODULE_ECDH - /* ecdh tests */ - run_ecdh_tests(); +static const struct tf_test_entry tests_ecdh[] = { + CASE(ecdh_tests), +}; #endif - /* ecdsa tests */ - run_ec_illegal_argument_tests(); - run_pubkey_comparison(); - run_pubkey_sort(); - run_random_pubkeys(); - run_ecdsa_der_parse(); - run_ecdsa_sign_verify(); - run_ecdsa_end_to_end(); - run_ecdsa_edge_cases(); - run_ecdsa_wycheproof(); +static const struct tf_test_entry tests_ecdsa[] = { + CASE(ec_illegal_argument_tests), + CASE(pubkey_comparison), + CASE(pubkey_sort), + CASE(random_pubkeys), + CASE(ecdsa_der_parse), + CASE(ecdsa_sign_verify), + CASE(ecdsa_end_to_end), + CASE(ecdsa_edge_cases), + CASE(ecdsa_wycheproof), +}; #ifdef ENABLE_MODULE_RECOVERY +static const struct tf_test_entry tests_recovery[] = { /* ECDSA pubkey recovery tests */ - run_recovery_tests(); + CASE(recovery_tests), +}; #endif #ifdef ENABLE_MODULE_EXTRAKEYS - run_extrakeys_tests(); +static const struct tf_test_entry tests_extrakeys[] = { + CASE(extrakeys_tests), +}; #endif #ifdef ENABLE_MODULE_SCHNORRSIG - run_schnorrsig_tests(); +static const struct tf_test_entry tests_schnorrsig[] = { + CASE(schnorrsig_tests), +}; #endif #ifdef ENABLE_MODULE_MUSIG - run_musig_tests(); +static const struct tf_test_entry tests_musig[] = { + CASE(musig_tests), +}; #endif #ifdef ENABLE_MODULE_ELLSWIFT - run_ellswift_tests(); +static const struct tf_test_entry tests_ellswift[] = { + CASE(ellswift_tests), +}; #endif - /* util tests */ - run_secp256k1_memczero_test(); - run_secp256k1_is_zero_array_test(); - run_secp256k1_byteorder_tests(); +static const struct tf_test_entry tests_utils[] = { + CASE(hsort_tests), + CASE(secp256k1_memczero_test), + CASE(secp256k1_is_zero_array_test), + CASE(secp256k1_byteorder_tests), + CASE(cmov_tests), +}; - run_cmov_tests(); +/* Register test modules */ +static const struct tf_test_module registry_modules[] = { + MAKE_TEST_MODULE(general), + MAKE_TEST_MODULE(integer), + MAKE_TEST_MODULE(hash), + MAKE_TEST_MODULE(scalar), + MAKE_TEST_MODULE(field), + MAKE_TEST_MODULE(group), + MAKE_TEST_MODULE(ecmult), + MAKE_TEST_MODULE(ec), +#ifdef ENABLE_MODULE_ECDH + MAKE_TEST_MODULE(ecdh), +#endif + MAKE_TEST_MODULE(ecdsa), +#ifdef ENABLE_MODULE_RECOVERY + MAKE_TEST_MODULE(recovery), +#endif +#ifdef ENABLE_MODULE_EXTRAKEYS + MAKE_TEST_MODULE(extrakeys), +#endif +#ifdef ENABLE_MODULE_SCHNORRSIG + MAKE_TEST_MODULE(schnorrsig), +#endif +#ifdef ENABLE_MODULE_MUSIG + MAKE_TEST_MODULE(musig), +#endif +#ifdef ENABLE_MODULE_ELLSWIFT + MAKE_TEST_MODULE(ellswift), +#endif + MAKE_TEST_MODULE(utils), +}; - /*** Tear down test environment ***/ +/* Setup test environment */ +static int setup(void) { + /* Create a global context available to all tests */ + CTX = secp256k1_context_create(SECP256K1_CONTEXT_NONE); + /* Randomize the context only with probability 15/16 + to make sure we test without context randomization from time to time. + TODO Reconsider this when recalibrating the tests. */ + if (testrand_bits(4)) { + unsigned char rand32[32]; + testrand256(rand32); + CHECK(secp256k1_context_randomize(CTX, rand32)); + } + /* Make a writable copy of secp256k1_context_static in order to test the effect of API functions + that write to the context. The API does not support cloning the static context, so we use + memcpy instead. The user is not supposed to copy a context but we should still ensure that + the API functions handle copies of the static context gracefully. */ + STATIC_CTX = malloc(sizeof(*secp256k1_context_static)); + CHECK(STATIC_CTX != NULL); + memcpy(STATIC_CTX, secp256k1_context_static, sizeof(secp256k1_context)); + CHECK(!secp256k1_context_is_proper(STATIC_CTX)); + return 0; +} + +/* Shutdown test environment */ +static int teardown(void) { free(STATIC_CTX); secp256k1_context_destroy(CTX); + return 0; +} - testrand_finish(); +int main(int argc, char **argv) { + struct tf_framework tf = {0}; + tf.registry_modules = registry_modules; + tf.num_modules = sizeof(registry_modules) / sizeof(registry_modules[0]); + tf.registry_no_rng = ®istry_modules_no_rng; - printf("no problems found\n"); - return EXIT_SUCCESS; + /* Add context creation/destruction functions */ + tf.fn_setup = setup; + tf.fn_teardown = teardown; + + /* Init and run framework */ + if (tf_init(&tf, argc, argv) != 0) return EXIT_FAILURE; + return tf_run(&tf); } + diff --git a/src/unit_test.c b/src/unit_test.c new file mode 100644 index 0000000000..914594bc13 --- /dev/null +++ b/src/unit_test.c @@ -0,0 +1,342 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#include +#include +#include + +#if defined(SUPPORTS_CONCURRENCY) +#include +#include +#include +#endif + +#include "unit_test.h" +#include "testrand.h" +#include "tests_common.h" + +#define UNUSED(x) (void)(x) + +/* Number of times certain tests will run */ +int COUNT = 16; + +static int parse_jobs_count(const char* key, const char* value, struct tf_framework* tf); +static int parse_iterations(const char* key, const char* value, struct tf_framework* tf); +static int parse_seed(const char* key, const char* value, struct tf_framework* tf); + +/* Mapping table: key -> handler */ +typedef int (*ArgHandler)(const char* key, const char* value, struct tf_framework* tf); +struct ArgMap { + const char* key; + ArgHandler handler; +}; + +/* + * Main entry point for handling command-line arguments. + * + * Developers should extend this map whenever new command-line + * options are introduced. Each new argument should be validated, + * converted to the appropriate type, and stored in 'tf->args' struct. + */ +static struct ArgMap arg_map[] = { + { "j", parse_jobs_count }, { "jobs", parse_jobs_count }, + { "i", parse_iterations }, { "iterations", parse_iterations }, + { "seed", parse_seed }, + { NULL, NULL } /* sentinel */ +}; + +/* Display options that are not printed elsewhere */ +static void print_args(const struct tf_args* args) { + printf("iterations = %d\n", COUNT); + printf("jobs = %d. %s execution.\n", args->num_processes, args->num_processes > 1 ? "Parallel" : "Sequential"); +} + +/* Main entry point for reading environment variables */ +static int read_env(struct tf_framework* tf) { + const char* env_iter = getenv("SECP256K1_TEST_ITERS"); + if (env_iter && strlen(env_iter) > 0) { + return parse_iterations("i", env_iter, tf); + } + return 0; +} + +static int parse_arg(const char* key, const char* value, struct tf_framework* tf) { + int i; + for (i = 0; arg_map[i].key != NULL; i++) { + if (strcmp(key, arg_map[i].key) == 0) { + return arg_map[i].handler(key, value, tf); + } + } + /* Unknown key: report just so typos don't silently pass. */ + fprintf(stderr, "Unknown argument '-%s=%s'\n", key, value); + return -1; +} + +static int parse_jobs_count(const char* key, const char* value, struct tf_framework* tf) { + char* ptr_val; + long val = strtol(value, &ptr_val, 10); /* base 10 */ + if (*ptr_val != '\0') { + fprintf(stderr, "Invalid number for -%s=%s\n", key, value); + return -1; + } + if (val < 0 || val > MAX_SUBPROCESSES) { + fprintf(stderr, "Arg '-%s' out of range: '%ld'. Range: 0..%d\n", key, val, MAX_SUBPROCESSES); + return -1; + } + tf->args.num_processes = (int) val; + return 0; +} + +static int parse_iterations(const char* key, const char* value, struct tf_framework* tf) { + UNUSED(key); UNUSED(tf); + if (!value) return 0; + COUNT = (int) strtol(value, NULL, 0); + if (COUNT <= 0) { + fputs("An iteration count of 0 or less is not allowed.\n", stderr); + return -1; + } + return 0; +} + +static int parse_seed(const char* key, const char* value, struct tf_framework* tf) { + UNUSED(key); + tf->args.custom_seed = (!value || strcmp(value, "NULL") == 0) ? NULL : value; + return 0; +} + +/* Strip up to two leading dashes */ +static const char* normalize_key(const char* arg, const char** err_msg) { + const char* key; + if (!arg || arg[0] != '-') { + *err_msg = "missing initial dash"; + return NULL; + } + /* single-dash short option */ + if (arg[1] != '-') return arg + 1; + + /* double-dash checks now */ + if (arg[2] == '\0') { + *err_msg = "missing option name after double dash"; + return NULL; + } + + if (arg[2] == '-') { + *err_msg = "too many leading dashes"; + return NULL; + } + + key = arg + 2; + if (key[1] == '\0') { + *err_msg = "short option cannot use double dash"; + return NULL; + } + return key; +} + +/* Read args: all must be in the form -key=value, --key=value or -key=value */ +static int read_args(int argc, char** argv, int start, struct tf_framework* tf) { + int i; + const char* key; + const char* value; + char* eq; + const char* err_msg = "unknown error"; + for (i = start; i < argc; i++) { + char* raw_arg = argv[i]; + if (!raw_arg || raw_arg[0] != '-') { + fprintf(stderr, "Invalid arg '%s': must start with '-'\n", raw_arg ? raw_arg : "(null)"); + return -1; + } + + key = normalize_key(raw_arg, &err_msg); + if (!key || *key == '\0') { + fprintf(stderr, "Invalid arg '%s': %s. Must be -k=value or --key=value\n", raw_arg, err_msg); + return -1; + } + + eq = strchr(raw_arg, '='); + if (!eq || eq == raw_arg + 1) { + fprintf(stderr, "Invalid arg '%s': must be -k=value or --key=value\n", raw_arg); + return -1; + } + + *eq = '\0'; /* split key and value */ + value = eq + 1; + if (!value || *value == '\0') { /* value is empty */ + fprintf(stderr, "Invalid arg '%s': value cannot be empty\n", raw_arg); + return -1; + } + + if (parse_arg(key, value, tf) != 0) return -1; + } + return 0; +} + +static void run_test(const struct tf_test_entry* t) { + printf("Running %s..\n", t->name); + t->func(); + printf("%s PASSED\n", t->name); +} + +/* Process tests in sequential order */ +static int run_sequential(struct tf_framework* tf) { + tf_test_ref ref; + const struct tf_test_module* mdl; + for (ref.group = 0; ref.group < tf->num_modules; ref.group++) { + mdl = &tf->registry_modules[ref.group]; + for (ref.idx = 0; ref.idx < mdl->size; ref.idx++) { + run_test(&mdl->data[ref.idx]); + } + } + return EXIT_SUCCESS; +} + +#if defined(SUPPORTS_CONCURRENCY) +/* Process tests in parallel */ +static int run_concurrent(struct tf_framework* tf) { + /* Sub-processes info */ + pid_t workers[MAX_SUBPROCESSES]; + int pipefd[2]; + int status = EXIT_SUCCESS; + int it; /* loop iterator */ + tf_test_ref ref; /* test index */ + + if (pipe(pipefd) != 0) { + perror("Error during pipe setup"); + return EXIT_FAILURE; + } + + /* Launch worker processes */ + for (it = 0; it < tf->args.num_processes; it++) { + pid_t pid = fork(); + if (pid < 0) { + perror("Error during process fork"); + return EXIT_FAILURE; + } + if (pid == 0) { + /* Child worker: read jobs from the shared pipe */ + close(pipefd[1]); /* children never write */ + while (read(pipefd[0], &ref, sizeof(ref)) == sizeof(ref)) { + run_test(&tf->registry_modules[ref.group].data[ref.idx]); + } + _exit(EXIT_SUCCESS); /* finish child process */ + } else { + /* Parent: save worker pid */ + workers[it] = pid; + } + } + + /* Parent: write all tasks into the pipe */ + close(pipefd[0]); /* close read end */ + for (ref.group = 0; ref.group < tf->num_modules; ref.group++) { + const struct tf_test_module* mdl = &tf->registry_modules[ref.group]; + for (ref.idx = 0; ref.idx < mdl->size; ref.idx++) { + if (write(pipefd[1], &ref, sizeof(ref)) == -1) { + perror("Error during workload distribution"); + close(pipefd[1]); + return EXIT_FAILURE; + } + } + } + /* Close write end to signal EOF */ + close(pipefd[1]); + /* Wait for all workers */ + for (it = 0; it < tf->args.num_processes; it++) { + int ret = 0; + if (waitpid(workers[it], &ret, 0) == -1 || ret != 0) { + status = EXIT_FAILURE; + } + } + + return status; +} +#endif + +static int tf_init(struct tf_framework* tf, int argc, char** argv) +{ + /* Caller must set the registry and its size before calling tf_init */ + if (tf->registry_modules == NULL || tf->num_modules <= 0) { + fprintf(stderr, "Error: tests registry not provided or empty\n"); + return EXIT_FAILURE; + } + + /* Initialize command-line options */ + tf->args.num_processes = 0; + tf->args.custom_seed = NULL; + + /* Disable buffering for stdout to improve reliability of getting + * diagnostic information. Happens right at the start of main because + * setbuf must be used before any other operation on the stream. */ + setbuf(stdout, NULL); + /* Also disable buffering for stderr because it's not guaranteed that it's + * unbuffered on all systems. */ + setbuf(stderr, NULL); + + /* Parse env args */ + if (read_env(tf) != 0) return EXIT_FAILURE; + + /* Parse command-line args */ + if (argc > 1) { + int named_arg_start = 1; /* index to begin processing named arguments */ + if (argc - 1 > MAX_ARGS) { /* first arg is always the binary path */ + fprintf(stderr, "Too many command-line arguments (max: %d)\n", MAX_ARGS); + return EXIT_FAILURE; + } + + /* Compatibility Note: The first two args were the number of iterations and the seed. */ + /* If provided, parse them and adjust the starting index for named arguments accordingly. */ + if (argv[1][0] != '-') { + int has_seed = argc > 2 && argv[2][0] != '-'; + if (parse_iterations("i", argv[1], tf) != 0) return EXIT_FAILURE; + if (has_seed) parse_seed("seed", argv[2], tf); + named_arg_start = has_seed ? 3 : 2; + } + if (read_args(argc, argv, named_arg_start, tf) != 0) { + return EXIT_FAILURE; + } + } + + return EXIT_SUCCESS; +} + +static int tf_run(struct tf_framework* tf) { + /* Process exit status */ + int status; + /* Loop iterator */ + int it; + /* Initial test time */ + int64_t start_time = gettime_i64(); + + /* Log configuration */ + print_args(&tf->args); + + /* Run test RNG tests (must run before we really initialize the test RNG) */ + /* Note: currently, these tests are executed sequentially because there */ + /* is really only one test. */ + for (it = 0; tf->registry_no_rng && it < tf->registry_no_rng->size; it++) { + run_test(&tf->registry_no_rng->data[it]); + } + + /* Initialize test RNG and library contexts */ + testrand_init(tf->args.custom_seed); + if (tf->fn_setup && tf->fn_setup() != 0) return EXIT_FAILURE; + + /* Check whether to process tests sequentially or concurrently */ + if (tf->args.num_processes <= 1) { + status = run_sequential(tf); + } else { +#if defined(SUPPORTS_CONCURRENCY) + status = run_concurrent(tf); +#else + fputs("Parallel execution not supported on your system. Running sequentially...\n", stderr); + status = run_sequential(tf); +#endif + } + + /* Print accumulated time */ + printf("Total execution time: %.3f seconds\n", (double)(gettime_i64() - start_time) / 1000000); + if (tf->fn_teardown && tf->fn_teardown() != 0) return EXIT_FAILURE; + + return status; +} diff --git a/src/unit_test.h b/src/unit_test.h new file mode 100644 index 0000000000..f1374a0d7c --- /dev/null +++ b/src/unit_test.h @@ -0,0 +1,120 @@ +/*********************************************************************** + * Distributed under the MIT software license, see the accompanying * + * file COPYING or https://www.opensource.org/licenses/mit-license.php.* + ***********************************************************************/ + +#ifndef SECP256K1_UNIT_TEST_H +#define SECP256K1_UNIT_TEST_H + +/* --------------------------------------------------------- */ +/* Configurable constants */ +/* --------------------------------------------------------- */ + +/* Maximum number of command-line arguments. + * Must be at least as large as the total number of tests + * to allow specifying all tests individually. */ +#define MAX_ARGS 150 +/* Maximum number of parallel jobs */ +#define MAX_SUBPROCESSES 16 + +/* --------------------------------------------------------- */ +/* Test Framework Registry Macros */ +/* --------------------------------------------------------- */ + +#define CASE(name) { #name, run_##name } + +#define MAKE_TEST_MODULE(name) { \ + #name, \ + tests_##name, \ + sizeof(tests_##name) / sizeof(tests_##name[0]) \ +} + +/* --------------------------------------------------------- */ +/* Test Framework API */ +/* --------------------------------------------------------- */ + +typedef void (*test_fn)(void); + +struct tf_test_entry { + const char* name; + test_fn func; +}; + +struct tf_test_module { + const char* name; + const struct tf_test_entry* data; + int size; +}; + +typedef int (*setup_ctx_fn)(void); +typedef int (*teardown_fn)(void); + +/* Reference to a test in the registry. Group index and test index */ +typedef struct { + int group; + int idx; +} tf_test_ref; + +/* --- Command-line args --- */ +struct tf_args { + /* 0 => sequential; 1..MAX_SUBPROCESSES => parallel workers */ + int num_processes; + /* Specific RNG seed */ + const char* custom_seed; +}; + +/* --------------------------------------------------------- */ +/* Public API */ +/* --------------------------------------------------------- */ + +struct tf_framework { + /* Command-line args */ + struct tf_args args; + /* Test modules registry */ + const struct tf_test_module* registry_modules; + /* Num of modules */ + int num_modules; + /* Registry for tests that require no RNG init */ + const struct tf_test_module* registry_no_rng; + /* Specific context setup and teardown functions */ + setup_ctx_fn fn_setup; + teardown_fn fn_teardown; +}; + +/* + * Initialize the test framework. + * + * Must be called before tf_run() and before any output is performed to + * stdout or stderr, because this function disables buffering on both + * streams to ensure reliable diagnostic output. + * + * Parses command-line arguments and configures the framework context. + * The caller must initialize the following members of 'tf' before calling: + * - tf->registry_modules + * - tf->num_modules + * + * Side effects: + * - stdout and stderr are set to unbuffered mode via setbuf(). + * This allows immediate flushing of diagnostic messages but may + * affect performance for other output operations. + * + * Returns: + * EXIT_SUCCESS (0) on success, + * EXIT_FAILURE (non-zero) on error. + */ +static int tf_init(struct tf_framework* tf, int argc, char** argv); + +/* + * Run tests based on the provided test framework context. + * + * This function uses the configuration stored in the tf_framework + * (targets, number of processes, iteration count, etc.) to determine + * which tests to execute and how to execute them. + * + * Returns: + * EXIT_SUCCESS (0) if all tests passed, + * EXIT_FAILURE (non-zero) otherwise. + */ +static int tf_run(struct tf_framework* tf); + +#endif /* SECP256K1_UNIT_TEST_H */ From 9ec3bfe22dc662875e6fca2419713eae105dabee Mon Sep 17 00:00:00 2001 From: furszy Date: Tue, 9 Sep 2025 11:45:17 -0400 Subject: [PATCH 3/7] test: adapt modules to the new test infrastructure This not only provides a structural improvement but also allows us to (1) specify individual tests to run and (2) execute each of them concurrently. --- src/modules/ecdh/tests_impl.h | 17 ++++++----- src/modules/ellswift/tests_impl.h | 7 +++++ src/modules/extrakeys/tests_impl.h | 21 +++++++------ src/modules/musig/tests_impl.h | 47 ++++++++++++++--------------- src/modules/recovery/tests_impl.h | 25 +++++++-------- src/modules/schnorrsig/tests_impl.h | 31 ++++++++++--------- src/tests.c | 38 +---------------------- src/unit_test.h | 13 ++++++++ 8 files changed, 93 insertions(+), 106 deletions(-) diff --git a/src/modules/ecdh/tests_impl.h b/src/modules/ecdh/tests_impl.h index 6888f18c64..c1a5e73c8a 100644 --- a/src/modules/ecdh/tests_impl.h +++ b/src/modules/ecdh/tests_impl.h @@ -7,6 +7,8 @@ #ifndef SECP256K1_MODULE_ECDH_TESTS_H #define SECP256K1_MODULE_ECDH_TESTS_H +#include "../../unit_test.h" + static int ecdh_hash_function_test_xpassthru(unsigned char *output, const unsigned char *x, const unsigned char *y, void *data) { (void)y; (void)data; @@ -182,12 +184,13 @@ static void test_ecdh_wycheproof(void) { } } -static void run_ecdh_tests(void) { - test_ecdh_api(); - test_ecdh_generator_basepoint(); - test_bad_scalar(); - test_result_basepoint(); - test_ecdh_wycheproof(); -} +/* --- Test registry --- */ +static const struct tf_test_entry tests_ecdh[] = { + CASE1(test_ecdh_api), + CASE1(test_ecdh_generator_basepoint), + CASE1(test_bad_scalar), + CASE1(test_result_basepoint), + CASE1(test_ecdh_wycheproof), +}; #endif /* SECP256K1_MODULE_ECDH_TESTS_H */ diff --git a/src/modules/ellswift/tests_impl.h b/src/modules/ellswift/tests_impl.h index b90fd0ab88..63c36e7ff1 100644 --- a/src/modules/ellswift/tests_impl.h +++ b/src/modules/ellswift/tests_impl.h @@ -7,6 +7,7 @@ #define SECP256K1_MODULE_ELLSWIFT_TESTS_H #include "../../../include/secp256k1_ellswift.h" +#include "../../unit_test.h" struct ellswift_xswiftec_inv_test { int enc_bitmap; @@ -433,4 +434,10 @@ void run_ellswift_tests(void) { } } +/* --- Test registry --- */ +/* TODO: subdivide test in cases */ +static const struct tf_test_entry tests_ellswift[] = { + CASE(ellswift_tests), +}; + #endif diff --git a/src/modules/extrakeys/tests_impl.h b/src/modules/extrakeys/tests_impl.h index ab4ef4a74b..abebd1106b 100644 --- a/src/modules/extrakeys/tests_impl.h +++ b/src/modules/extrakeys/tests_impl.h @@ -8,6 +8,7 @@ #define SECP256K1_MODULE_EXTRAKEYS_TESTS_H #include "../../../include/secp256k1_extrakeys.h" +#include "../../unit_test.h" static void test_xonly_pubkey(void) { secp256k1_pubkey pk; @@ -467,17 +468,17 @@ static void test_keypair_add(void) { } } -static void run_extrakeys_tests(void) { +/* --- Test registry --- */ +static const struct tf_test_entry tests_extrakeys[] = { /* xonly key test cases */ - test_xonly_pubkey(); - test_xonly_pubkey_tweak(); - test_xonly_pubkey_tweak_check(); - test_xonly_pubkey_tweak_recursive(); - test_xonly_pubkey_comparison(); - + CASE1(test_xonly_pubkey), + CASE1(test_xonly_pubkey_tweak), + CASE1(test_xonly_pubkey_tweak_check), + CASE1(test_xonly_pubkey_tweak_recursive), + CASE1(test_xonly_pubkey_comparison), /* keypair tests */ - test_keypair(); - test_keypair_add(); -} + CASE1(test_keypair), + CASE1(test_keypair_add), +}; #endif diff --git a/src/modules/musig/tests_impl.h b/src/modules/musig/tests_impl.h index b57b26264a..b4ba185494 100644 --- a/src/modules/musig/tests_impl.h +++ b/src/modules/musig/tests_impl.h @@ -20,6 +20,7 @@ #include "../../group.h" #include "../../hash.h" #include "../../util.h" +#include "../../unit_test.h" #include "vectors.h" @@ -36,7 +37,7 @@ static int create_keypair_and_pk(secp256k1_keypair *keypair, secp256k1_pubkey *p /* Just a simple (non-tweaked) 2-of-2 MuSig aggregate, sign, verify * test. */ -static void musig_simple_test(void) { +static void musig_simple_test_internal(void) { unsigned char sk[2][32]; secp256k1_keypair keypair[2]; secp256k1_musig_pubnonce pubnonce[2]; @@ -629,7 +630,7 @@ static void musig_tweak_test_helper(const secp256k1_xonly_pubkey* agg_pk, const /* Create aggregate public key P[0], tweak multiple times (using xonly and * plain tweaking) and test signing. */ -static void musig_tweak_test(void) { +static void musig_tweak_test_internal(void) { unsigned char sk[2][32]; secp256k1_pubkey pk[2]; const secp256k1_pubkey *pk_ptr[2]; @@ -1114,28 +1115,24 @@ static void musig_test_static_nonce_gen_counter(void) { CHECK(secp256k1_memcmp_var(pubnonce66, expected_pubnonce, sizeof(pubnonce66)) == 0); } -static void run_musig_tests(void) { - int i; - - for (i = 0; i < COUNT; i++) { - musig_simple_test(); - } - musig_api_tests(); - musig_nonce_test(); - for (i = 0; i < COUNT; i++) { - /* Run multiple times to ensure that pk and nonce have different y - * parities */ - musig_tweak_test(); - } - sha256_tag_test(); - musig_test_vectors_keyagg(); - musig_test_vectors_noncegen(); - musig_test_vectors_nonceagg(); - musig_test_vectors_signverify(); - musig_test_vectors_tweak(); - musig_test_vectors_sigagg(); - - musig_test_static_nonce_gen_counter(); -} +/* --- Test registry --- */ +REPEAT_TEST(musig_simple_test) +/* Run multiple times to ensure that pk and nonce have different y parities */ +REPEAT_TEST(musig_tweak_test) + +static const struct tf_test_entry tests_musig[] = { + CASE1(musig_simple_test), + CASE1(musig_api_tests), + CASE1(musig_nonce_test), + CASE1(musig_tweak_test), + CASE1(sha256_tag_test), + CASE1(musig_test_vectors_keyagg), + CASE1(musig_test_vectors_noncegen), + CASE1(musig_test_vectors_nonceagg), + CASE1(musig_test_vectors_signverify), + CASE1(musig_test_vectors_tweak), + CASE1(musig_test_vectors_sigagg), + CASE1(musig_test_static_nonce_gen_counter), +}; #endif diff --git a/src/modules/recovery/tests_impl.h b/src/modules/recovery/tests_impl.h index 7a28a3ce65..09554a242e 100644 --- a/src/modules/recovery/tests_impl.h +++ b/src/modules/recovery/tests_impl.h @@ -7,6 +7,8 @@ #ifndef SECP256K1_MODULE_RECOVERY_TESTS_H #define SECP256K1_MODULE_RECOVERY_TESTS_H +#include "../../unit_test.h" + static int recovery_test_nonce_function(unsigned char *nonce32, const unsigned char *msg32, const unsigned char *key32, const unsigned char *algo16, void *data, unsigned int counter) { (void) msg32; (void) key32; @@ -28,7 +30,7 @@ static int recovery_test_nonce_function(unsigned char *nonce32, const unsigned c return testrand_bits(1); } -static void test_ecdsa_recovery_api(void) { +static void test_ecdsa_recovery_api_internal(void) { /* Setup contexts that just count errors */ secp256k1_pubkey pubkey; secp256k1_pubkey recpubkey; @@ -92,7 +94,7 @@ static void test_ecdsa_recovery_api(void) { CHECK(secp256k1_ecdsa_recoverable_signature_parse_compact(CTX, &recsig, sig, recid) == 0); } -static void test_ecdsa_recovery_end_to_end(void) { +static void test_ecdsa_recovery_end_to_end_internal(void) { unsigned char extra[32] = {0x00}; unsigned char privkey[32]; unsigned char message[32]; @@ -324,15 +326,14 @@ static void test_ecdsa_recovery_edge_cases(void) { } } -static void run_recovery_tests(void) { - int i; - for (i = 0; i < COUNT; i++) { - test_ecdsa_recovery_api(); - } - for (i = 0; i < 64*COUNT; i++) { - test_ecdsa_recovery_end_to_end(); - } - test_ecdsa_recovery_edge_cases(); -} +/* --- Test registry --- */ +REPEAT_TEST(test_ecdsa_recovery_api) +REPEAT_TEST_MULT(test_ecdsa_recovery_end_to_end, 64) + +static const struct tf_test_entry tests_recovery[] = { + CASE1(test_ecdsa_recovery_api), + CASE1(test_ecdsa_recovery_end_to_end), + CASE1(test_ecdsa_recovery_edge_cases) +}; #endif /* SECP256K1_MODULE_RECOVERY_TESTS_H */ diff --git a/src/modules/schnorrsig/tests_impl.h b/src/modules/schnorrsig/tests_impl.h index 5abbeefe0b..9a1b15f0b2 100644 --- a/src/modules/schnorrsig/tests_impl.h +++ b/src/modules/schnorrsig/tests_impl.h @@ -8,6 +8,7 @@ #define SECP256K1_MODULE_SCHNORRSIG_TESTS_H #include "../../../include/secp256k1_schnorrsig.h" +#include "../../unit_test.h" /* Checks that a bit flip in the n_flip-th argument (that has n_bytes many * bytes) changes the hash function @@ -802,7 +803,7 @@ static int nonce_function_overflowing(unsigned char *nonce32, const unsigned cha return 1; } -static void test_schnorrsig_sign(void) { +static void test_schnorrsig_sign_internal(void) { unsigned char sk[32]; secp256k1_xonly_pubkey pk; secp256k1_keypair keypair; @@ -852,7 +853,7 @@ static void test_schnorrsig_sign(void) { /* Creates N_SIGS valid signatures and verifies them with verify and * verify_batch (TODO). Then flips some bits and checks that verification now * fails. */ -static void test_schnorrsig_sign_verify(void) { +static void test_schnorrsig_sign_verify_internal(void) { unsigned char sk[32]; unsigned char msg[N_SIGS][32]; unsigned char sig[N_SIGS][64]; @@ -965,18 +966,18 @@ static void test_schnorrsig_taproot(void) { CHECK(secp256k1_xonly_pubkey_tweak_add_check(CTX, output_pk_bytes, pk_parity, &internal_pk, tweak) == 1); } -static void run_schnorrsig_tests(void) { - int i; - run_nonce_function_bip340_tests(); - - test_schnorrsig_api(); - test_schnorrsig_sha256_tagged(); - test_schnorrsig_bip_vectors(); - for (i = 0; i < COUNT; i++) { - test_schnorrsig_sign(); - test_schnorrsig_sign_verify(); - } - test_schnorrsig_taproot(); -} +/* --- Test registry --- */ +REPEAT_TEST(test_schnorrsig_sign) +REPEAT_TEST(test_schnorrsig_sign_verify) + +static const struct tf_test_entry tests_schnorrsig[] = { + CASE(nonce_function_bip340_tests), + CASE1(test_schnorrsig_api), + CASE1(test_schnorrsig_sha256_tagged), + CASE1(test_schnorrsig_bip_vectors), + CASE1(test_schnorrsig_sign), + CASE1(test_schnorrsig_sign_verify), + CASE1(test_schnorrsig_taproot), +}; #endif diff --git a/src/tests.c b/src/tests.c index efcdbdc8b8..5f4f6b0f22 100644 --- a/src/tests.c +++ b/src/tests.c @@ -7755,12 +7755,6 @@ static const struct tf_test_entry tests_ec[] = { CASE(eckey_negate_test), }; -#ifdef ENABLE_MODULE_ECDH -static const struct tf_test_entry tests_ecdh[] = { - CASE(ecdh_tests), -}; -#endif - static const struct tf_test_entry tests_ecdsa[] = { CASE(ec_illegal_argument_tests), CASE(pubkey_comparison), @@ -7773,37 +7767,6 @@ static const struct tf_test_entry tests_ecdsa[] = { CASE(ecdsa_wycheproof), }; -#ifdef ENABLE_MODULE_RECOVERY -static const struct tf_test_entry tests_recovery[] = { - /* ECDSA pubkey recovery tests */ - CASE(recovery_tests), -}; -#endif - -#ifdef ENABLE_MODULE_EXTRAKEYS -static const struct tf_test_entry tests_extrakeys[] = { - CASE(extrakeys_tests), -}; -#endif - -#ifdef ENABLE_MODULE_SCHNORRSIG -static const struct tf_test_entry tests_schnorrsig[] = { - CASE(schnorrsig_tests), -}; -#endif - -#ifdef ENABLE_MODULE_MUSIG -static const struct tf_test_entry tests_musig[] = { - CASE(musig_tests), -}; -#endif - -#ifdef ENABLE_MODULE_ELLSWIFT -static const struct tf_test_entry tests_ellswift[] = { - CASE(ellswift_tests), -}; -#endif - static const struct tf_test_entry tests_utils[] = { CASE(hsort_tests), CASE(secp256k1_memczero_test), @@ -7827,6 +7790,7 @@ static const struct tf_test_module registry_modules[] = { #endif MAKE_TEST_MODULE(ecdsa), #ifdef ENABLE_MODULE_RECOVERY + /* ECDSA pubkey recovery tests */ MAKE_TEST_MODULE(recovery), #endif #ifdef ENABLE_MODULE_EXTRAKEYS diff --git a/src/unit_test.h b/src/unit_test.h index f1374a0d7c..7052fdc79e 100644 --- a/src/unit_test.h +++ b/src/unit_test.h @@ -22,6 +22,7 @@ /* --------------------------------------------------------- */ #define CASE(name) { #name, run_##name } +#define CASE1(name) { #name, name } #define MAKE_TEST_MODULE(name) { \ #name, \ @@ -29,6 +30,18 @@ sizeof(tests_##name) / sizeof(tests_##name[0]) \ } +/* Macro to wrap a test internal function with a COUNT loop (iterations number) */ +#define REPEAT_TEST(fn) REPEAT_TEST_MULT(fn, 1) +#define REPEAT_TEST_MULT(fn, multiplier) \ + static void fn(void) { \ + int i; \ + int repeat = COUNT * (multiplier); \ + for (i = 0; i < repeat; i++) \ + fn##_internal(); \ + } + + + /* --------------------------------------------------------- */ /* Test Framework API */ /* --------------------------------------------------------- */ From 0302c1a3d73137fcf75a7ca0f0f3cbcec0368ec8 Mon Sep 17 00:00:00 2001 From: furszy Date: Wed, 3 Sep 2025 15:27:23 -0400 Subject: [PATCH 4/7] test: add --help for command-line options Add a help message for the test suite, documenting available options, defaults, and backward-compatible positional arguments. --- src/unit_test.c | 28 ++++++++++++++++++++++++++++ src/unit_test.h | 2 ++ 2 files changed, 30 insertions(+) diff --git a/src/unit_test.c b/src/unit_test.c index 914594bc13..04878eecd0 100644 --- a/src/unit_test.c +++ b/src/unit_test.c @@ -74,6 +74,23 @@ static int parse_arg(const char* key, const char* value, struct tf_framework* tf return -1; } +static void help(void) { + printf("Usage: ./tests [options]\n\n"); + printf("Run the test suite for the project with optional configuration.\n\n"); + printf("Options:\n"); + printf(" --help, -h Show this help message\n"); + printf(" --jobs=, -j= Number of parallel worker processes (default: 0 = sequential)\n"); + printf(" --iterations=, -i= Number of iterations for each test (default: 16)\n"); + printf(" --seed= Set a specific RNG seed (default: random)\n"); + printf("\n"); + printf("Notes:\n"); + printf(" - All arguments must be provided in the form '--key=value', '-key=value' or '-k=value'.\n"); + printf(" - Single or double dashes are allowed for multi character options.\n"); + printf(" - Unknown arguments are reported but ignored.\n"); + printf(" - Sequential execution occurs if -jobs=0 or unspecified.\n"); + printf(" - Iterations and seed can also be passed as positional arguments before any other argument for backward compatibility.\n"); +} + static int parse_jobs_count(const char* key, const char* value, struct tf_framework* tf) { char* ptr_val; long val = strtol(value, &ptr_val, 10); /* base 10 */ @@ -157,6 +174,11 @@ static int read_args(int argc, char** argv, int start, struct tf_framework* tf) eq = strchr(raw_arg, '='); if (!eq || eq == raw_arg + 1) { + /* Allowed options without value */ + if (strcmp(key, "h") == 0 || strcmp(key, "help") == 0) { + tf->args.help = 1; + return 0; + } fprintf(stderr, "Invalid arg '%s': must be -k=value or --key=value\n", raw_arg); return -1; } @@ -264,6 +286,7 @@ static int tf_init(struct tf_framework* tf, int argc, char** argv) /* Initialize command-line options */ tf->args.num_processes = 0; tf->args.custom_seed = NULL; + tf->args.help = 0; /* Disable buffering for stdout to improve reliability of getting * diagnostic information. Happens right at the start of main because @@ -295,6 +318,11 @@ static int tf_init(struct tf_framework* tf, int argc, char** argv) if (read_args(argc, argv, named_arg_start, tf) != 0) { return EXIT_FAILURE; } + + if (tf->args.help) { + help(); + exit(EXIT_SUCCESS); + } } return EXIT_SUCCESS; diff --git a/src/unit_test.h b/src/unit_test.h index 7052fdc79e..1eefb016e1 100644 --- a/src/unit_test.h +++ b/src/unit_test.h @@ -74,6 +74,8 @@ struct tf_args { int num_processes; /* Specific RNG seed */ const char* custom_seed; + /* Whether to print the help msg */ + int help; }; /* --------------------------------------------------------- */ From 953f7b008865ed503f36b040c4af4db96c4d54d9 Mon Sep 17 00:00:00 2001 From: furszy Date: Wed, 3 Sep 2025 17:18:23 -0400 Subject: [PATCH 5/7] test: support running specific tests/modules targets Add support for specifying single tests or modules to run via the "--target" or "-t" command-line option. Multiple targets can be provided; only the specified tests or all tests in the specified module/s will run instead of the full suite. Examples: -t= runs an specific test. -t= runs all tests within the specified module. Both options can be provided multiple times. --- src/unit_test.c | 96 +++++++++++++++++++++++++++++++++++++++---------- src/unit_test.h | 13 ++++--- 2 files changed, 85 insertions(+), 24 deletions(-) diff --git a/src/unit_test.c b/src/unit_test.c index 04878eecd0..513bc2cc6b 100644 --- a/src/unit_test.c +++ b/src/unit_test.c @@ -25,6 +25,7 @@ int COUNT = 16; static int parse_jobs_count(const char* key, const char* value, struct tf_framework* tf); static int parse_iterations(const char* key, const char* value, struct tf_framework* tf); static int parse_seed(const char* key, const char* value, struct tf_framework* tf); +static int parse_target(const char* key, const char* value, struct tf_framework* tf); /* Mapping table: key -> handler */ typedef int (*ArgHandler)(const char* key, const char* value, struct tf_framework* tf); @@ -41,6 +42,7 @@ struct ArgMap { * converted to the appropriate type, and stored in 'tf->args' struct. */ static struct ArgMap arg_map[] = { + { "t", parse_target }, { "target", parse_target }, { "j", parse_jobs_count }, { "jobs", parse_jobs_count }, { "i", parse_iterations }, { "iterations", parse_iterations }, { "seed", parse_seed }, @@ -82,6 +84,8 @@ static void help(void) { printf(" --jobs=, -j= Number of parallel worker processes (default: 0 = sequential)\n"); printf(" --iterations=, -i= Number of iterations for each test (default: 16)\n"); printf(" --seed= Set a specific RNG seed (default: random)\n"); + printf(" --target=, -t= Run a specific test (can be provided multiple times)\n"); + printf(" --target=, -t= Run all tests within a specific module (can be provided multiple times)\n"); printf("\n"); printf("Notes:\n"); printf(" - All arguments must be provided in the form '--key=value', '-key=value' or '-k=value'.\n"); @@ -152,6 +156,34 @@ static const char* normalize_key(const char* arg, const char** err_msg) { return key; } +static int parse_target(const char* key, const char* value, struct tf_framework* tf) { + int group, idx; + const struct tf_test_entry* entry; + UNUSED(key); + /* Find test index in the registry */ + for (group = 0; group < tf->num_modules; group++) { + const struct tf_test_module* module = &tf->registry_modules[group]; + int add_all = strcmp(value, module->name) == 0; /* select all from module */ + for (idx = 0; idx < module->size; idx++) { + entry = &module->data[idx]; + if (add_all || strcmp(value, entry->name) == 0) { + if (tf->args.targets.size >= MAX_ARGS) { + fprintf(stderr, "Too many -target args (max: %d)\n", MAX_ARGS); + return -1; + } + tf->args.targets.slots[tf->args.targets.size++] = entry; + /* Matched a single test, we're done */ + if (!add_all) return 0; + } + } + /* If add_all was true, we added all tests in the module, so return */ + if (add_all) return 0; + } + fprintf(stderr, "Error: target '%s' not found (missing or module disabled).\n" + "Run program with -print_tests option to display available tests and modules.\n", value); + return -1; +} + /* Read args: all must be in the form -key=value, --key=value or -key=value */ static int read_args(int argc, char** argv, int start, struct tf_framework* tf) { int i; @@ -203,18 +235,16 @@ static void run_test(const struct tf_test_entry* t) { /* Process tests in sequential order */ static int run_sequential(struct tf_framework* tf) { - tf_test_ref ref; - const struct tf_test_module* mdl; - for (ref.group = 0; ref.group < tf->num_modules; ref.group++) { - mdl = &tf->registry_modules[ref.group]; - for (ref.idx = 0; ref.idx < mdl->size; ref.idx++) { - run_test(&mdl->data[ref.idx]); - } + int it; + for (it = 0; it < tf->args.targets.size; it++) { + run_test(tf->args.targets.slots[it]); } return EXIT_SUCCESS; } #if defined(SUPPORTS_CONCURRENCY) +static const int MAX_TARGETS = 255; + /* Process tests in parallel */ static int run_concurrent(struct tf_framework* tf) { /* Sub-processes info */ @@ -222,7 +252,15 @@ static int run_concurrent(struct tf_framework* tf) { int pipefd[2]; int status = EXIT_SUCCESS; int it; /* loop iterator */ - tf_test_ref ref; /* test index */ + unsigned char idx; /* test index */ + + if (tf->args.targets.size > MAX_TARGETS) { + fprintf(stderr, "Internal Error: the number of targets (%d) exceeds the maximum supported (%d). " + "If you need more, extend 'run_concurrent()' to handle additional targets.\n", + tf->args.targets.size, MAX_TARGETS); + exit(EXIT_FAILURE); + } + if (pipe(pipefd) != 0) { perror("Error during pipe setup"); @@ -239,8 +277,8 @@ static int run_concurrent(struct tf_framework* tf) { if (pid == 0) { /* Child worker: read jobs from the shared pipe */ close(pipefd[1]); /* children never write */ - while (read(pipefd[0], &ref, sizeof(ref)) == sizeof(ref)) { - run_test(&tf->registry_modules[ref.group].data[ref.idx]); + while (read(pipefd[0], &idx, sizeof(idx)) == sizeof(idx)) { + run_test(tf->args.targets.slots[(int)idx]); } _exit(EXIT_SUCCESS); /* finish child process */ } else { @@ -251,14 +289,12 @@ static int run_concurrent(struct tf_framework* tf) { /* Parent: write all tasks into the pipe */ close(pipefd[0]); /* close read end */ - for (ref.group = 0; ref.group < tf->num_modules; ref.group++) { - const struct tf_test_module* mdl = &tf->registry_modules[ref.group]; - for (ref.idx = 0; ref.idx < mdl->size; ref.idx++) { - if (write(pipefd[1], &ref, sizeof(ref)) == -1) { - perror("Error during workload distribution"); - close(pipefd[1]); - return EXIT_FAILURE; - } + for (it = 0; it < tf->args.targets.size; it++) { + idx = (unsigned char)it; + if (write(pipefd[1], &idx, sizeof(idx)) == -1) { + perror("Error during workload distribution"); + close(pipefd[1]); + return EXIT_FAILURE; } } /* Close write end to signal EOF */ @@ -287,6 +323,7 @@ static int tf_init(struct tf_framework* tf, int argc, char** argv) tf->args.num_processes = 0; tf->args.custom_seed = NULL; tf->args.help = 0; + tf->args.targets.size = 0; /* Disable buffering for stdout to improve reliability of getting * diagnostic information. Happens right at the start of main because @@ -331,11 +368,30 @@ static int tf_init(struct tf_framework* tf, int argc, char** argv) static int tf_run(struct tf_framework* tf) { /* Process exit status */ int status; + /* Whether to run all tests */ + int run_all; /* Loop iterator */ int it; /* Initial test time */ int64_t start_time = gettime_i64(); + /* Populate targets with all tests if none were explicitly specified */ + run_all = tf->args.targets.size == 0; + if (run_all) { + int group, idx; + for (group = 0; group < tf->num_modules; group++) { + const struct tf_test_module* module = &tf->registry_modules[group]; + for (idx = 0; idx < module->size; idx++) { + if (tf->args.targets.size >= MAX_ARGS) { + fprintf(stderr, "Internal Error: Number of tests (%d) exceeds MAX_ARGS (%d). " + "Increase MAX_ARGS to accommodate all tests.\n", tf->args.targets.size, MAX_ARGS); + return EXIT_FAILURE; + } + tf->args.targets.slots[tf->args.targets.size++] = &module->data[idx]; + } + } + } + /* Log configuration */ print_args(&tf->args); @@ -343,7 +399,9 @@ static int tf_run(struct tf_framework* tf) { /* Note: currently, these tests are executed sequentially because there */ /* is really only one test. */ for (it = 0; tf->registry_no_rng && it < tf->registry_no_rng->size; it++) { - run_test(&tf->registry_no_rng->data[it]); + if (run_all) { /* future: support filtering */ + run_test(&tf->registry_no_rng->data[it]); + } } /* Initialize test RNG and library contexts */ diff --git a/src/unit_test.h b/src/unit_test.h index 1eefb016e1..e76b8c0118 100644 --- a/src/unit_test.h +++ b/src/unit_test.h @@ -62,11 +62,12 @@ struct tf_test_module { typedef int (*setup_ctx_fn)(void); typedef int (*teardown_fn)(void); -/* Reference to a test in the registry. Group index and test index */ -typedef struct { - int group; - int idx; -} tf_test_ref; +struct tf_targets { + /* Target tests indexes */ + const struct tf_test_entry* slots[MAX_ARGS]; + /* Next available slot */ + int size; +}; /* --- Command-line args --- */ struct tf_args { @@ -76,6 +77,8 @@ struct tf_args { const char* custom_seed; /* Whether to print the help msg */ int help; + /* Target tests indexes */ + struct tf_targets targets; }; /* --------------------------------------------------------- */ From 95b9953ea44ad6273dd8ddd2040844a54936ed71 Mon Sep 17 00:00:00 2001 From: furszy Date: Mon, 8 Sep 2025 14:17:12 -0400 Subject: [PATCH 6/7] test: Add option to display all available tests Useful option to avoid opening the large tests.c file just to find the test case you want to run. --- src/unit_test.c | 31 ++++++++++++++++++++++++++++++- src/unit_test.h | 2 ++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/unit_test.c b/src/unit_test.c index 513bc2cc6b..4eace5266a 100644 --- a/src/unit_test.c +++ b/src/unit_test.c @@ -81,6 +81,7 @@ static void help(void) { printf("Run the test suite for the project with optional configuration.\n\n"); printf("Options:\n"); printf(" --help, -h Show this help message\n"); + printf(" --list_tests, -l Display list of all available tests and modules\n"); printf(" --jobs=, -j= Number of parallel worker processes (default: 0 = sequential)\n"); printf(" --iterations=, -i= Number of iterations for each test (default: 16)\n"); printf(" --seed= Set a specific RNG seed (default: random)\n"); @@ -95,6 +96,24 @@ static void help(void) { printf(" - Iterations and seed can also be passed as positional arguments before any other argument for backward compatibility.\n"); } +/* Print all tests in registry */ +static void print_test_list(struct tf_framework* tf) { + int m, t, total = 0; + printf("\nAvailable tests (%d modules):\n", tf->num_modules); + printf("========================================\n"); + for (m = 0; m < tf->num_modules; m++) { + const struct tf_test_module* mod = &tf->registry_modules[m]; + printf("Module: %s (%d tests)\n", mod->name, mod->size); + for (t = 0; t < mod->size; t++) { + printf("\t[%3d] %s\n", total + 1, mod->data[t].name); + total++; + } + printf("----------------------------------------\n"); + } + printf("\nRun specific module: ./tests -t=\n"); + printf("Run specific test: ./tests -t=\n\n"); +} + static int parse_jobs_count(const char* key, const char* value, struct tf_framework* tf) { char* ptr_val; long val = strtol(value, &ptr_val, 10); /* base 10 */ @@ -180,7 +199,7 @@ static int parse_target(const char* key, const char* value, struct tf_framework* if (add_all) return 0; } fprintf(stderr, "Error: target '%s' not found (missing or module disabled).\n" - "Run program with -print_tests option to display available tests and modules.\n", value); + "Run program with -list_tests option to display available tests and modules.\n", value); return -1; } @@ -211,6 +230,10 @@ static int read_args(int argc, char** argv, int start, struct tf_framework* tf) tf->args.help = 1; return 0; } + if (strcmp(key, "l") == 0 || strcmp(key, "list_tests") == 0) { + tf->args.list_tests = 1; + return 0; + } fprintf(stderr, "Invalid arg '%s': must be -k=value or --key=value\n", raw_arg); return -1; } @@ -324,6 +347,7 @@ static int tf_init(struct tf_framework* tf, int argc, char** argv) tf->args.custom_seed = NULL; tf->args.help = 0; tf->args.targets.size = 0; + tf->args.list_tests = 0; /* Disable buffering for stdout to improve reliability of getting * diagnostic information. Happens right at the start of main because @@ -360,6 +384,11 @@ static int tf_init(struct tf_framework* tf, int argc, char** argv) help(); exit(EXIT_SUCCESS); } + + if (tf->args.list_tests) { + print_test_list(tf); + exit(EXIT_SUCCESS); + } } return EXIT_SUCCESS; diff --git a/src/unit_test.h b/src/unit_test.h index e76b8c0118..b41e9f7514 100644 --- a/src/unit_test.h +++ b/src/unit_test.h @@ -77,6 +77,8 @@ struct tf_args { const char* custom_seed; /* Whether to print the help msg */ int help; + /* Whether to print the tests list msg */ + int list_tests; /* Target tests indexes */ struct tf_targets targets; }; From 2f4546ce5610f4f7841fc2dc2eef68dbfcdcc761 Mon Sep 17 00:00:00 2001 From: furszy Date: Tue, 9 Sep 2025 11:02:05 -0400 Subject: [PATCH 7/7] test: add --log option to display tests execution When enabled (--log=1), shows test start, completion, and execution time. --- src/unit_test.c | 32 +++++++++++++++++++++++++++----- src/unit_test.h | 5 +++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/unit_test.c b/src/unit_test.c index 4eace5266a..a1858a117a 100644 --- a/src/unit_test.c +++ b/src/unit_test.c @@ -26,6 +26,7 @@ static int parse_jobs_count(const char* key, const char* value, struct tf_framew static int parse_iterations(const char* key, const char* value, struct tf_framework* tf); static int parse_seed(const char* key, const char* value, struct tf_framework* tf); static int parse_target(const char* key, const char* value, struct tf_framework* tf); +static int parse_logging(const char* key, const char* value, struct tf_framework* tf); /* Mapping table: key -> handler */ typedef int (*ArgHandler)(const char* key, const char* value, struct tf_framework* tf); @@ -46,6 +47,7 @@ static struct ArgMap arg_map[] = { { "j", parse_jobs_count }, { "jobs", parse_jobs_count }, { "i", parse_iterations }, { "iterations", parse_iterations }, { "seed", parse_seed }, + { "log", parse_logging }, { NULL, NULL } /* sentinel */ }; @@ -87,6 +89,7 @@ static void help(void) { printf(" --seed= Set a specific RNG seed (default: random)\n"); printf(" --target=, -t= Run a specific test (can be provided multiple times)\n"); printf(" --target=, -t= Run all tests within a specific module (can be provided multiple times)\n"); + printf(" --log=<0|1> Enable or disable test execution logging (default: 0 = disabled)\n"); printf("\n"); printf("Notes:\n"); printf(" - All arguments must be provided in the form '--key=value', '-key=value' or '-k=value'.\n"); @@ -146,6 +149,12 @@ static int parse_seed(const char* key, const char* value, struct tf_framework* t return 0; } +static int parse_logging(const char* key, const char* value, struct tf_framework* tf) { + UNUSED(key); + tf->args.logging = value && strcmp(value, "1") == 0; + return 0; +} + /* Strip up to two leading dashes */ static const char* normalize_key(const char* arg, const char** err_msg) { const char* key; @@ -250,17 +259,20 @@ static int read_args(int argc, char** argv, int start, struct tf_framework* tf) return 0; } -static void run_test(const struct tf_test_entry* t) { +static void run_test_log(const struct tf_test_entry* t) { + int64_t start_time = gettime_i64(); printf("Running %s..\n", t->name); t->func(); - printf("%s PASSED\n", t->name); + printf("Test %s PASSED (%.3f sec)\n", t->name, (double)(gettime_i64() - start_time) / 1000000); } +static void run_test(const struct tf_test_entry* t) { t->func(); } + /* Process tests in sequential order */ static int run_sequential(struct tf_framework* tf) { int it; for (it = 0; it < tf->args.targets.size; it++) { - run_test(tf->args.targets.slots[it]); + tf->fn_run_test(tf->args.targets.slots[it]); } return EXIT_SUCCESS; } @@ -301,7 +313,7 @@ static int run_concurrent(struct tf_framework* tf) { /* Child worker: read jobs from the shared pipe */ close(pipefd[1]); /* children never write */ while (read(pipefd[0], &idx, sizeof(idx)) == sizeof(idx)) { - run_test(tf->args.targets.slots[(int)idx]); + tf->fn_run_test(tf->args.targets.slots[(int)idx]); } _exit(EXIT_SUCCESS); /* finish child process */ } else { @@ -348,6 +360,7 @@ static int tf_init(struct tf_framework* tf, int argc, char** argv) tf->args.help = 0; tf->args.targets.size = 0; tf->args.list_tests = 0; + tf->args.logging = 0; /* Disable buffering for stdout to improve reliability of getting * diagnostic information. Happens right at the start of main because @@ -391,6 +404,7 @@ static int tf_init(struct tf_framework* tf, int argc, char** argv) } } + tf->fn_run_test = tf->args.logging ? run_test_log : run_test; return EXIT_SUCCESS; } @@ -403,6 +417,12 @@ static int tf_run(struct tf_framework* tf) { int it; /* Initial test time */ int64_t start_time = gettime_i64(); + /* Verify 'tf_init' has been called */ + if (!tf->fn_run_test) { + fprintf(stderr, "Error: No test runner set. You must call 'tf_init' first to initialize the framework " + "or manually assign 'fn_run_test' before calling 'tf_run'.\n"); + return EXIT_FAILURE; + } /* Populate targets with all tests if none were explicitly specified */ run_all = tf->args.targets.size == 0; @@ -421,6 +441,8 @@ static int tf_run(struct tf_framework* tf) { } } + if (!tf->args.logging) printf("Tests running silently. Use '-log=1' to enable detailed logging\n"); + /* Log configuration */ print_args(&tf->args); @@ -429,7 +451,7 @@ static int tf_run(struct tf_framework* tf) { /* is really only one test. */ for (it = 0; tf->registry_no_rng && it < tf->registry_no_rng->size; it++) { if (run_all) { /* future: support filtering */ - run_test(&tf->registry_no_rng->data[it]); + tf->fn_run_test(&tf->registry_no_rng->data[it]); } } diff --git a/src/unit_test.h b/src/unit_test.h index b41e9f7514..bf301e5392 100644 --- a/src/unit_test.h +++ b/src/unit_test.h @@ -61,6 +61,7 @@ struct tf_test_module { typedef int (*setup_ctx_fn)(void); typedef int (*teardown_fn)(void); +typedef void (*run_test_fn)(const struct tf_test_entry*); struct tf_targets { /* Target tests indexes */ @@ -81,6 +82,8 @@ struct tf_args { int list_tests; /* Target tests indexes */ struct tf_targets targets; + /* Enable test execution logging */ + int logging; }; /* --------------------------------------------------------- */ @@ -99,6 +102,8 @@ struct tf_framework { /* Specific context setup and teardown functions */ setup_ctx_fn fn_setup; teardown_fn fn_teardown; + /* Test runner function (can be customized) */ + run_test_fn fn_run_test; }; /*