test: introduce (mini) unit test framework
What changed, and why it matters
This commit is a pure test-infrastructure change. It introduces a small internal unit-test framework for the secp256k1 cryptographic library, replacing a long hand-written main() function with a registry of test cases and adding optional parallel test execution. It does not change any cryptographic code, public API, or production behavior, and it does not fix or introduce any security vulnerability.
No security action required. Treat as normal test-framework refactoring.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors src/tests.c to use a new src/unit_test.c / src/unit_test.h framework. It adds command-line parsing for –jobs, –iterations, –seed, preserves legacy positional arguments, and implements fork-based parallel test workers on POSIX systems. The build system is updated to detect sys/types.h, sys/wait.h, and unistd.h and define SUPPORTS_CONCURRENCY when available. No library code is modified.
Changed components
src/tests.csrc/unit_test.csrc/unit_test.hMakefile.amconfigure.acsrc/CMakeLists.txtInspect captured patch +674 / −143
diff --git a/Makefile.am b/Makefile.am
index 4cc97ba..dc79857 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 2f156dd..6028ee2 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 fa3b290..ecbbbbe 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 28bec59..efcdbdc 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 0000000..914594b
--- /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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#if defined(SUPPORTS_CONCURRENCY)
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#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 0000000..f1374a0
--- /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 */
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.