Merge bitcoin-core/secp256k1#1734: Introduce (mini) unit test framework

2f4546ce56 test: add --log option to display tests execution (furszy)
95b9953ea4 test: Add option to display all available tests (furszy)
953f7b0088 test: support running specific tests/modules targets (furszy)
0302c1a3d7 test: add --help for command-line options (furszy)
9ec3bfe22d test: adapt modules to the new test infrastructure (furszy)
48789dafc2 test: introduce (mini) unit test framework (furszy)
9cce703863 refactor: move 'gettime_i64()' to tests_common.h (furszy)

Pull request description:

  Early Note:
  Don’t be scared by the PR’s line changes count — most of it’s just doc or part of the test framework API.

  Context:
  Currently, all tests run single-threaded sequentially and the library lacks the ability to specify which test (or group of tests) you would like to run. This is not only inconvenient as more tests are added but also time consuming during development and affects downstream projects that may want to parallelize the workload (such as Bitcoin-Core CI).

  PR Goal:
  Introduce a lightweight, extensible C89 unit test framework with no dynamic memory allocations, providing a structured way to register, execute, and report tests. The framework supports named command-line arguments in `-key=value` form, parallel test execution across multiple worker processes, granular test selection (selecting tests either by name or by module name), and time accumulation reports.

  The introduced framework supports:
  * `-help` or `-h`: display list of available commands along with their descriptions.
  * `-jobs=<num>`: distribute tests across multiple worker processes (default: sequential if 0).
  * `-target=<name>` or `-t=<name>`: run only specific tests by name; can be repeated to select multiple tests.
  *  `-target=<module name>`, `-t=<module>`  Run all tests within a specific module (can be provided multiple times)
  * `-seed=<hex>`: set a specific RNG seed (defaults to random if unspecified).
  * `-iterations=<n>`: specify the number of iterations.
  * `-list_tests`:   display list of available tests and modules you can run.
  * `-log=<0|1>`: enable or disable test execution logging (default: 0 = disabled).

  Beyond these features, the idea is to also make future developments smoother, as adding new tests require only a single entry in the central test registry, and new command-line options can be introduced easily by extending the framework’s `parse_arg()` function.

  Compatibility Note:
  The framework continues accepting the two positional arguments previously supported (iterations and seed), ensuring existing workflows remain intact.

  Testing Notes:
  Have fun. You can quickly try it through `./tests -j=<workers_num>` for parallel execution  or `./tests -t=<test_name>` to run a specific test (call `./tests -print_tests` to display all available tests and modules).

  Extra Note:
  I haven't checked the exhaustive tests file so far, but I will soon. For now, this only runs all tests declared in the `tests` binary.

  Testing Results: (Current master branch vs PR in seconds)

  * Raspberry Pi 5: master \~100 s → PR \~38 s (5 jobs)
  * MacBook Pro M1: master \~30 s → PR \~10 s (6 jobs)

ACKs for top commit:
  theStack:
    re-ACK 2f4546ce56
  real-or-random:
    ACK 2f4546ce56
  hebasto:
    ACK 2f4546ce56.

Tree-SHA512: 85ca2cbb620b84b35b353d5d4cf093c388fc3851ca405eeb0e458f8fa72b60534bccd357c7edabf8fc9aa93d9ad0a6fbac3dd5c4d5f9dfdf4d8701a9834755b9
This commit is contained in:
merge-script committed 2025-10-15 08:37:23 +02:00
commit d543c0d917
14 files changed
+930 -240

No files matched your search

+4 -1
View File
@@ -45,7 +45,10 @@ 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/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
@@ -120,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
+8
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
###
+13 -1
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)
+1 -21
View File
@@ -12,27 +12,7 @@
#include <stdio.h>
#include <string.h>
#if (defined(_MSC_VER) && _MSC_VER >= 1900)
# include <time.h>
#else
# include <sys/time.h>
#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)
+10 -7
View File
@@ -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;
@@ -178,12 +180,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 */
+7
View File
@@ -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
+11 -10
View File
@@ -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
+21 -24
View File
@@ -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;
/* --- 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)
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();
}
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
+13 -12
View File
@@ -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 */
+15 -14
View File
@@ -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 registry --- */
REPEAT_TEST(test_schnorrsig_sign)
REPEAT_TEST(test_schnorrsig_sign_verify)
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();
}
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
+161 -150
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;
@@ -7650,38 +7663,137 @@ 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),
};
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),
};
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
/* ECDSA pubkey recovery tests */
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
@@ -7700,129 +7812,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);
}
+42
View File
@@ -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 <stdint.h>
#if (defined(_MSC_VER) && _MSC_VER >= 1900)
# include <time.h>
#else
# include <sys/time.h>
#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 */
+479
View File
@@ -0,0 +1,479 @@
/***********************************************************************
* 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);
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);
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[] = {
{ "t", parse_target }, { "target", parse_target },
{ "j", parse_jobs_count }, { "jobs", parse_jobs_count },
{ "i", parse_iterations }, { "iterations", parse_iterations },
{ "seed", parse_seed },
{ "log", parse_logging },
{ 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 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(" --list_tests, -l Display list of all available tests and modules\n");
printf(" --jobs=<num>, -j=<num> Number of parallel worker processes (default: 0 = sequential)\n");
printf(" --iterations=<num>, -i=<num> Number of iterations for each test (default: 16)\n");
printf(" --seed=<hex> Set a specific RNG seed (default: random)\n");
printf(" --target=<test name>, -t=<name> Run a specific test (can be provided multiple times)\n");
printf(" --target=<module name>, -t=<module> 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");
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");
}
/* 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=<module_name>\n");
printf("Run specific test: ./tests -t=<test_name>\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 */
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;
}
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;
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;
}
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 -list_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;
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) {
/* Allowed options without value */
if (strcmp(key, "h") == 0 || strcmp(key, "help") == 0) {
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;
}
*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_log(const struct tf_test_entry* t) {
int64_t start_time = gettime_i64();
printf("Running %s..\n", t->name);
t->func();
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++) {
tf->fn_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 */
pid_t workers[MAX_SUBPROCESSES];
int pipefd[2];
int status = EXIT_SUCCESS;
int it; /* loop iterator */
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");
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], &idx, sizeof(idx)) == sizeof(idx)) {
tf->fn_run_test(tf->args.targets.slots[(int)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 (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 */
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;
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
* 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;
}
if (tf->args.help) {
help();
exit(EXIT_SUCCESS);
}
if (tf->args.list_tests) {
print_test_list(tf);
exit(EXIT_SUCCESS);
}
}
tf->fn_run_test = tf->args.logging ? run_test_log : run_test;
return EXIT_SUCCESS;
}
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();
/* 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;
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];
}
}
}
if (!tf->args.logging) printf("Tests running silently. Use '-log=1' to enable detailed logging\n");
/* 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++) {
if (run_all) { /* future: support filtering */
tf->fn_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;
}
+145
View File
@@ -0,0 +1,145 @@
/***********************************************************************
* 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 CASE1(name) { #name, name }
#define MAKE_TEST_MODULE(name) { \
#name, \
tests_##name, \
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 */
/* --------------------------------------------------------- */
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);
typedef void (*run_test_fn)(const struct tf_test_entry*);
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 {
/* 0 => sequential; 1..MAX_SUBPROCESSES => parallel workers */
int num_processes;
/* Specific RNG seed */
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;
/* Enable test execution logging */
int logging;
};
/* --------------------------------------------------------- */
/* 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;
/* Test runner function (can be customized) */
run_test_fn fn_run_test;
};
/*
* 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 */