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=<num>" or "-j=<num>" to specify the number of parallel workers.
- "--seed=<hex>" to specify the RNG seed (random if not set).
- "--iterations=<num>" or "-i=<num>" 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).
This commit is contained in:
furszy
2025-09-03 10:59:37 -04:00
parent 9cce703863
commit 48789dafc2
6 changed files with 683 additions and 152 deletions

View File

@@ -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

View File

@@ -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
###

View File

@@ -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)

View File

@@ -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 = &registry_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);
}

342
src/unit_test.c Normal file
View File

@@ -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;
}

120
src/unit_test.h Normal file
View File

@@ -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 */