From 48789dafc2a866bbc639184f0387637c0decb8c5 Mon Sep 17 00:00:00 2001 From: furszy Date: Wed, 3 Sep 2025 10:59:37 -0400 Subject: [PATCH] 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 | 347 +++++++++++++++++++++++++-------------------- src/unit_test.c | 342 ++++++++++++++++++++++++++++++++++++++++++++ src/unit_test.h | 120 ++++++++++++++++ 6 files changed, 683 insertions(+), 152 deletions(-) create mode 100644 src/unit_test.c create mode 100644 src/unit_test.h diff --git a/Makefile.am b/Makefile.am index 4cc97bab..dc798575 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 2f156ddc..6028ee28 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 fa3b2903..ecbbbbe8 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 28bec590..efcdbdc8 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,38 +7679,173 @@ 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); +/* --------------------------------------------------------- */ +/* Test Registry */ +/* --------------------------------------------------------- */ - /* 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); +/* --- 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); - /* run test RNG tests (must run before we really initialize the test RNG) */ - run_xoshiro256pp_tests(); +/* --- 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), +}; - /* find random seed */ - testrand_init(argc > 2 ? argv[2] : NULL); +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), +}; - /*** Setup test environment ***/ +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), +}; +static const struct tf_test_entry tests_scalar[] = { + CASE(scalar_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), +}; + +static const struct tf_test_entry tests_group[] = { + CASE(ge), + CASE(gej), + CASE(group_decompress), +}; + +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), +}; + +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 +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), + 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 */ + 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), + CASE(secp256k1_is_zero_array_test), + CASE(secp256k1_byteorder_tests), + CASE(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), +}; + +/* 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 @@ -7716,129 +7864,28 @@ int main(int argc, char **argv) { CHECK(STATIC_CTX != NULL); memcpy(STATIC_CTX, secp256k1_context_static, sizeof(secp256k1_context)); CHECK(!secp256k1_context_is_proper(STATIC_CTX)); + return 0; +} - /*** Run actual tests ***/ - - /* selftest tests */ - run_selftest_tests(); - - /* 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(); - - /* scratch tests */ - run_scratch_tests(); - - /* 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(); - -#ifdef ENABLE_MODULE_ECDH - /* ecdh tests */ - run_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(); - -#ifdef ENABLE_MODULE_RECOVERY - /* ECDSA pubkey recovery tests */ - run_recovery_tests(); -#endif - -#ifdef ENABLE_MODULE_EXTRAKEYS - run_extrakeys_tests(); -#endif - -#ifdef ENABLE_MODULE_SCHNORRSIG - run_schnorrsig_tests(); -#endif - -#ifdef ENABLE_MODULE_MUSIG - run_musig_tests(); -#endif - -#ifdef ENABLE_MODULE_ELLSWIFT - run_ellswift_tests(); -#endif - - /* util tests */ - run_secp256k1_memczero_test(); - run_secp256k1_is_zero_array_test(); - run_secp256k1_byteorder_tests(); - - run_cmov_tests(); - - /*** Tear down test environment ***/ +/* Shutdown test environment */ +static int teardown(void) { free(STATIC_CTX); secp256k1_context_destroy(CTX); - - testrand_finish(); - - printf("no problems found\n"); - return EXIT_SUCCESS; + return 0; } + +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; + + /* 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 00000000..914594bc --- /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 00000000..f1374a0d --- /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 */