Introduce (mini) unit test framework #1734

pull furszy wants to merge 7 commits into bitcoin-core:master from furszy:2025_unit_test_framework changing 14 files +929 −239
  1. furszy commented at 5:12 pm on September 4, 2025: member

    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)
  2. furszy renamed this:
    Introduce (mini) unit test framework
    WIP: Introduce (mini) unit test framework
    on Sep 4, 2025
  3. furszy force-pushed on Sep 4, 2025
  4. furszy force-pushed on Sep 4, 2025
  5. furszy force-pushed on Sep 4, 2025
  6. furszy force-pushed on Sep 4, 2025
  7. furszy force-pushed on Sep 5, 2025
  8. real-or-random added the label assurance on Sep 5, 2025
  9. real-or-random added the label feature on Sep 5, 2025
  10. real-or-random commented at 6:36 am on September 5, 2025: contributor

    Nice!

    I looked at existing unit test frameworks in the past, but nothing seemed appropriate for us. There are not that many for C, and they were either overkill or too simple (just handful of ifdefs) so they didn’t add any functionality. I thought writing our own is too annoying (or I was just lazy). But the framework is ~300 lines, that seems fine to me.

  11. in src/util.h:1 in 8a3b9d3141 outdated


    real-or-random commented at 6:40 am on September 5, 2025:

    The commit message says that the function is moved to testutil.h but it’s actually moved to util.h, which is included in the library. But we don’t want to add a dependency on headers like <time.h> in the real library (we currently call only malloc, free, printf, and abort and even these can be avoided if the user insists).

    I think it’s perfectly fine to include testutil.h in the benchmarks if this is necessary to avoid code duplication.


    furszy commented at 5:24 pm on September 5, 2025:

    The commit message says that the function is moved to testutil.h but it’s actually moved to util.h, which is included in the library. But we don’t want to add a dependency on headers like <time.h> in the real library (we currently call only malloc, free, printf, and abort and even these can be avoided if the user insists).

    I think it’s perfectly fine to include testutil.h in the benchmarks if this is necessary to avoid code duplication.

    Yeah, I initially did that but ended up moving it to util.h (without updating the commit message…) because including testutil.h alone in the bench.h file was not enough. I also needed to include testrand_impl.h to get access to the testrand implementation (which was blocking me from doing it because I wasn’t expecting to have to include implementation files manually at the binary top level file - it just felt odd), as well as time.h, and change cmake for the benchmark target to get access to the precomputed tables files.

    But all good now, got some clarifications from Pieter. PR updated per feedback. Thanks!


    furszy commented at 8:19 pm on September 9, 2025:
    just in case; connecting this comment to #1734 (review) for the rationale behind the latest change.
  12. real-or-random commented at 8:34 am on September 5, 2025: contributor
    see also #1211
  13. furszy force-pushed on Sep 5, 2025
  14. furszy force-pushed on Sep 5, 2025
  15. furszy force-pushed on Sep 6, 2025
  16. furszy force-pushed on Sep 6, 2025
  17. furszy force-pushed on Sep 6, 2025
  18. jonasnick commented at 3:21 pm on September 7, 2025: contributor

    Thanks @furszy. I played around with this a little bit. It reduces the execution time on my machine from 26 seconds to 10 seconds (-jobs=16). Very nice! Some observations:

    • It would be helpful if a helptext would be output when tests is run with -h or --help.
    • I think showing all the tests that have passed is a bit overkill. I’m already assuming that the tests pass if they do not show up in the output. I only need to see tests that don’t pass.
    • Maybe a future PR can autodetect the number of cores and set -jobs automatically by default?
    • There’s an -iter command line flag, but the test output shows “test count”. It would be better if we were consistent.
  19. jonasnick commented at 3:29 pm on September 7, 2025: contributor
    • The available “targets” seem to be a bit arbitary. Maybe we can try to put them into groups (perhaps similar to the [grouping in #1211](https://github.com/bitcoin-core/secp256k1/pull/1211/files#diff-c2d5f1f7616875ab71cd41b053cfb428696988ff89642b931a0963d50f34f7e8R7495))?
       0./tests -print_tests
       1Available tests (58):
       2    --------------------------------------------------
       3    [  1] selftest_tests
       4    [  2] all_proper_context_tests
       5    [  3] all_static_context_tests
       6    [  4] deprecated_context_flags_test
       7    [  5] scratch_tests
       8    [  6] int128_tests
       9    [  7] ctz_tests
      10    [  8] modinv_tests
      11    [  9] inverse_tests
      12    [ 10] hsort_tests
      13    [ 11] sha256_known_output_tests
      14    [ 12] sha256_counter_tests
      15    [ 13] hmac_sha256_tests
      16    [ 14] rfc6979_hmac_sha256_tests
      17    [ 15] tagged_sha256_tests
      18    [ 16] scalar_tests
      19    [ 17] field_half
      20    [ 18] field_misc
      21    [ 19] field_convert
      22    [ 20] field_be32_overflow
      23    [ 21] fe_mul
      24    [ 22] sqr
      25    [ 23] sqrt
      26    [ 24] ge
      27    [ 25] gej
      28    [ 26] group_decompress
      29    [ 27] ecmult_pre_g
      30    [ 28] wnaf
      31    [ 29] point_times_order
      32    [ 30] ecmult_near_split_bound
      33    [ 31] ecmult_chain
      34    [ 32] ecmult_constants
      35    [ 33] ecmult_gen_blind
      36    [ 34] ecmult_const_tests
      37    [ 35] ecmult_multi_tests
      38    [ 36] ec_combine
      39    [ 37] endomorphism_tests
      40    [ 38] ec_pubkey_parse_test
      41    [ 39] eckey_edge_case_test
      42    [ 40] eckey_negate_test
      43    [ 41] ecdh_tests
      44    [ 42] ec_illegal_argument_tests
      45    [ 43] pubkey_comparison
      46    [ 44] pubkey_sort
      47    [ 45] random_pubkeys
      48    [ 46] ecdsa_der_parse
      49    [ 47] ecdsa_sign_verify
      50    [ 48] ecdsa_end_to_end
      51    [ 49] ecdsa_edge_cases
      52    [ 50] ecdsa_wycheproof
      53    [ 51] extrakeys_tests
      54    [ 52] schnorrsig_tests
      55    [ 53] musig_tests
      56    [ 54] ellswift_tests
      57    [ 55] secp256k1_memczero_test
      58    [ 56] secp256k1_is_zero_array_test
      59    [ 57] secp256k1_byteorder_tests
      60    [ 58] cmov_tests
      
  20. furszy force-pushed on Sep 8, 2025
  21. furszy force-pushed on Sep 8, 2025
  22. furszy force-pushed on Sep 8, 2025
  23. furszy commented at 2:10 pm on September 9, 2025: member

    Thanks for the review jonasnick!

    Thanks @furszy. I played around with this a little bit. It reduces the execution time on my machine from 26 seconds to 10 seconds (-jobs=16). Very nice!

    Awesome :). I think we can actually do even better, will do some changes.

    • It would be helpful if a helptext would be output when tests is run with -h or --help.

    The help message was actually already there, but for -help only (with a single - and I forgot to add it to the PR description). I just pushed support for -h as well.

    • I think showing all the tests that have passed is a bit overkill. I’m already assuming that the tests pass if they do not show up in the output. I only need to see tests that don’t pass.

    Sure. Will hide the logging behind a -log option (or -silent if we want the opposite behavior). From my experience, logging sometimes helps spot regressions or areas that can be improved (like when a test suddenly takes longer than usual), and it’s also reassuring to see them run and pass when you have a large number of them. I’ve also been thinking about adding per-test execution time loggings, might be a good opportunity to include it too.

    • Maybe a future PR can autodetect the number of cores and set -jobs automatically by default?

    I’m not sure we want that. Sequential execution is usually “standard” on any system because we don’t know what else the user might be running. Picking a number of parallel tasks automatically (even if it is a low number) could hang the CPU or even make it run slower than sequential if the system is overloaded.

    • There’s an -iter command line flag, but the test output shows “test count”. It would be better if we were consistent.

    Sure 👍🏼. That was carried over from the previous code; will improve it.

  24. furszy commented at 2:21 pm on September 9, 2025: member

    The available “targets” seem to be a bit arbitary. Maybe we can try to put them into groups (perhaps similar to the [grouping in #1211](https://github.com/bitcoin-core/secp256k1/pull/1211/files#diff-c2d5f1f7616875ab71cd41b053cfb428696988ff89642b931a0963d50f34f7e8R7495))?

    Yeah. Just reworked the framework to support registering and running groups of tests in a generic manner. This means we can now run specific tests and/or specific groups of tests via the -target/-t arg.

    On top of that, made the framework reusable across binaries and improved the overall API (we can now easily connect the tests_exhaustive binary to it too — probably something for a follow-up), along with improvements to the consumers’ structure, enforcing consistency and a specific pattern that all consumers will follow.

    Other than that, the -print_tests option was improved to display the available modules and tests.

    A simple usage example: ./tests -print_tests → pick any module (like field) and run: ./tests -t=field You can also combine this with -j=<num_workers>, plus specify multiple targets as needed.

  25. furszy force-pushed on Sep 9, 2025
  26. in src/bench.c:12 in 4900aee07d outdated
     6@@ -7,9 +7,13 @@
     7 #include <stdio.h>
     8 #include <stdlib.h>
     9 #include <string.h>
    10+#include <time.h>
    11 
    12+#include "secp256k1.c"
    


    sipa commented at 3:58 pm on September 9, 2025:

    Sorry, NACK this.

    The (external) benchmark should link against the production-compiled library, not compile its own version.


    furszy commented at 4:31 pm on September 9, 2025:
    np, that’s from the first move-only commit that is merely there to gain access to the existing gettime() function from the test framework. Worst-case scenario I can dedup the code (or create an util_time.h file). Will see how to adapt it. I remember that this had some extra complications because including testutil.h requires secp256k1_pubkey_save which only exists on secp256k1.c. But will check it again, I got more insights of the library now.

    furszy commented at 7:51 pm on September 9, 2025:

    done, solved. Introduced a new file tests_common.h, to hold general-purpose utility functions shared across test programs that do not depend on the internal library. Which helps avoid code duplication.

    Note: we should probably consider renaming testutil.h to testutil_internals.h (or similar) to make it clear that it has access to library internals.

  27. furszy force-pushed on Sep 9, 2025
  28. furszy renamed this:
    WIP: Introduce (mini) unit test framework
    Introduce (mini) unit test framework
    on Sep 9, 2025
  29. hebasto commented at 1:49 pm on September 10, 2025: member

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

    I agree with this assessment.

    For now, I slightly prefer #1211 for the following reasons:

    • smaller diff;
    • adding a new test does not seem convoluted in that branch;
    • it scales reasonably well (though not perfectly).

    In general, I believe parallelism management could (and I believe it should) be offloaded from the C code to the build system. Moreover, CTest provides a range of options to customize test execution and output. This functionality could of course be ported to the Autotools-based build system, but I am not sure it would be worth the effort.

  30. furszy commented at 2:33 pm on September 10, 2025: member

    smaller diff;

    I wouldn’t consider the larger diff a complication. The size comes from reorganizing tests under a proper framework and introducing features like granular test/group selection and a named command-line options. In my view, it should be seen as a one-off restructuring that makes testing scalable and smoother for development, and it sets us up much better long term.

    • adding a new test does not seem convoluted in that branch;

    Adding new tests is not hard but granularly selecting them to run in isolation is not possible. You will continue be forced to comment code from a long main function if you just want to run a single test or a mix of single tests from different groups.

    Have you tested how fast this one gets if you parallelize it in the same way you did for #1211? This one should go faster because we can select and run each individual test on a different process (in #1211 you can only run groups of tests).

    In general, I believe parallelism management could (and I believe it should) be offloaded from the C code to the build system. Moreover, CTest provides a range of options to customize test execution and output. This functionality could of course be ported to the Autotools-based build system, but I am not sure it would be worth the effort.

    It could be added here for CI too (I would be very interested to see your results), but I don’t think ctest is very useful for daily development. It adds another layer of complexity; for example, you can’t easily pass command-line arguments to the binaries you’re running (as far as I have seen, it requires you to enable the “script mode” and even that is not enough). I think both approaches can coexist as long as the maintenance burden stays minimal.

  31. in src/unit_test.c:86 in aa5f041f2f outdated
    85+    printf("    -print_tests                        Display list of all available tests and modules\n");
    86+    printf("    -j=<num>, -jobs=<num>               Number of parallel worker processes (default: 0 = sequential)\n");
    87+    printf("    -iter=<num>, -iterations=<num>      Number of iterations for each test (default: 16)\n");
    88+    printf("    -seed=<hex>                         Set a specific RNG seed (default: random)\n");
    89+    printf("    -target=<test name>, -t=<name>      Run a specific test (can be provided multiple times)\n");
    90+    printf("    -target=<module name>, -t=<module>  Run all tests within a specific module (can be provided multiple times)\n");
    


    hebasto commented at 2:54 pm on September 10, 2025:
    Is there a specific reason to overload the -target option? If not, could a different one be used instead?

    furszy commented at 3:13 pm on September 10, 2025:

    Is there a specific reason to overload the -target option? If not, could a different one be used instead?

    Mainly because both are targets; the only diff is that one target runs more test cases than the other one. In general, it’s similar to how most unit test frameworks work. For example, on boost and gtest you can run groups of tests and single tests with the same command (--run_test and --gtest_filter respectively); the only diff with them is that they add the module name as a prefix when specifying single test case (which we could also do here).


    hebasto commented at 5:31 pm on September 10, 2025:
    Right. Let’s keep it this way. It works nicely with CMake code. See https://github.com/hebasto/secp256k1/commit/3ff3105edc5e78aaceae0dddba5f788ec83a163d.

    furszy commented at 2:21 pm on September 11, 2025:
    nice!
  32. hebasto commented at 3:13 pm on September 10, 2025: member

    In case of a disable module, could the output be made more explanatory instead of this:

    0$ ./build/bin/tests -target=recovery
    1Error: target not found: 'recovery'
    

    For example (from #1211): https://github.com/bitcoin-core/secp256k1/blob/3e404f2b8643493854e664389dbc7817763ad4ac/src/tests.c#L7475-L7482

  33. furszy commented at 3:19 pm on September 10, 2025: member

    In case of a disable module, could the output be made more explanatory instead of this:

    0$ ./build/bin/tests -target=recovery
    1Error: target not found: 'recovery'
    

    Yeah sure.

  34. hebasto commented at 4:09 pm on September 10, 2025: member

    Removed

    nm

  35. hebasto commented at 4:24 pm on September 10, 2025: member

    Have you tested how fast this one gets if you parallelize it in the same way you did for #1211? This one should go faster because we can select and run each individual test on a different process (in #1211 you can only run groups of tests).

    This branch with splitting by modules:

     0$ ctest --test-dir build -j 16 --output-on-failure
     1Test project /home/hebasto/dev/secp256k1/secp256k1/build
     2      Start  1: secp256k1_tests::general
     3      Start  2: secp256k1_tests::integer
     4      Start  3: secp256k1_tests::hash
     5      Start  4: secp256k1_tests::scalar
     6      Start  5: secp256k1_tests::field
     7      Start  6: secp256k1_tests::group
     8      Start  7: secp256k1_tests::ecmult
     9      Start  8: secp256k1_tests::ec
    10      Start  9: secp256k1_tests::ecdh
    11      Start 10: secp256k1_tests::ecdsa
    12      Start 11: secp256k1_tests::recovery
    13      Start 12: secp256k1_tests::extrakeys
    14      Start 13: secp256k1_tests::schnorrsig
    15      Start 14: secp256k1_tests::musig
    16      Start 15: secp256k1_tests::ellswift
    17      Start 16: secp256k1_tests::utils
    18 1/33 Test  [#1](/bitcoin-core-secp256k1/1/): secp256k1_tests::general ...............   Passed    0.01 sec
    19 2/33 Test [#11](/bitcoin-core-secp256k1/11/): secp256k1_tests::recovery ..............***Skipped   0.00 sec
    20      Start 17: secp256k1_noverify_tests::general
    21      Start 18: secp256k1_noverify_tests::integer
    22 3/33 Test [#16](/bitcoin-core-secp256k1/16/): secp256k1_tests::utils .................   Passed    0.00 sec
    23 4/33 Test [#17](/bitcoin-core-secp256k1/17/): secp256k1_noverify_tests::general ......   Passed    0.00 sec
    24      Start 19: secp256k1_noverify_tests::hash
    25      Start 20: secp256k1_noverify_tests::scalar
    26 5/33 Test  [#3](/bitcoin-core-secp256k1/3/): secp256k1_tests::hash ..................   Passed    0.02 sec
    27      Start 21: secp256k1_noverify_tests::field
    28 6/33 Test  [#8](/bitcoin-core-secp256k1/8/): secp256k1_tests::ec ....................   Passed    0.03 sec
    29      Start 22: secp256k1_noverify_tests::group
    30 7/33 Test [#12](/bitcoin-core-secp256k1/12/): secp256k1_tests::extrakeys .............   Passed    0.03 sec
    31 8/33 Test [#19](/bitcoin-core-secp256k1/19/): secp256k1_noverify_tests::hash .........   Passed    0.02 sec
    32      Start 23: secp256k1_noverify_tests::ecmult
    33      Start 24: secp256k1_noverify_tests::ec
    34 9/33 Test [#20](/bitcoin-core-secp256k1/20/): secp256k1_noverify_tests::scalar .......   Passed    0.03 sec
    35      Start 25: secp256k1_noverify_tests::ecdh
    3610/33 Test [#24](/bitcoin-core-secp256k1/24/): secp256k1_noverify_tests::ec ...........   Passed    0.02 sec
    37      Start 26: secp256k1_noverify_tests::ecdsa
    3811/33 Test  [#4](/bitcoin-core-secp256k1/4/): secp256k1_tests::scalar ................   Passed    0.05 sec
    39      Start 27: secp256k1_noverify_tests::recovery
    4012/33 Test [#27](/bitcoin-core-secp256k1/27/): secp256k1_noverify_tests::recovery .....***Skipped   0.00 sec
    41      Start 28: secp256k1_noverify_tests::extrakeys
    4213/33 Test [#28](/bitcoin-core-secp256k1/28/): secp256k1_noverify_tests::extrakeys ....   Passed    0.01 sec
    43      Start 29: secp256k1_noverify_tests::schnorrsig
    4414/33 Test [#13](/bitcoin-core-secp256k1/13/): secp256k1_tests::schnorrsig ............   Passed    0.07 sec
    45      Start 30: secp256k1_noverify_tests::musig
    4615/33 Test [#25](/bitcoin-core-secp256k1/25/): secp256k1_noverify_tests::ecdh .........   Passed    0.05 sec
    47      Start 31: secp256k1_noverify_tests::ellswift
    4816/33 Test [#29](/bitcoin-core-secp256k1/29/): secp256k1_noverify_tests::schnorrsig ...   Passed    0.03 sec
    49      Start 32: secp256k1_noverify_tests::utils
    5017/33 Test [#32](/bitcoin-core-secp256k1/32/): secp256k1_noverify_tests::utils ........   Passed    0.00 sec
    51      Start 33: secp256k1_exhaustive_tests
    5218/33 Test  [#9](/bitcoin-core-secp256k1/9/): secp256k1_tests::ecdh ..................   Passed    0.13 sec
    5319/33 Test [#30](/bitcoin-core-secp256k1/30/): secp256k1_noverify_tests::musig ........   Passed    0.11 sec
    5420/33 Test [#14](/bitcoin-core-secp256k1/14/): secp256k1_tests::musig .................   Passed    0.29 sec
    5521/33 Test [#26](/bitcoin-core-secp256k1/26/): secp256k1_noverify_tests::ecdsa ........   Passed    0.98 sec
    5622/33 Test  [#5](/bitcoin-core-secp256k1/5/): secp256k1_tests::field .................   Passed    1.24 sec
    5723/33 Test [#22](/bitcoin-core-secp256k1/22/): secp256k1_noverify_tests::group ........   Passed    1.71 sec
    5824/33 Test [#10](/bitcoin-core-secp256k1/10/): secp256k1_tests::ecdsa .................   Passed    1.95 sec
    5925/33 Test [#21](/bitcoin-core-secp256k1/21/): secp256k1_noverify_tests::field ........   Passed    1.94 sec
    6026/33 Test  [#2](/bitcoin-core-secp256k1/2/): secp256k1_tests::integer ...............   Passed    2.47 sec
    6127/33 Test [#18](/bitcoin-core-secp256k1/18/): secp256k1_noverify_tests::integer ......   Passed    2.47 sec
    6228/33 Test  [#6](/bitcoin-core-secp256k1/6/): secp256k1_tests::group .................   Passed    4.24 sec
    6329/33 Test [#31](/bitcoin-core-secp256k1/31/): secp256k1_noverify_tests::ellswift .....   Passed    4.32 sec
    6430/33 Test [#33](/bitcoin-core-secp256k1/33/): secp256k1_exhaustive_tests .............   Passed    7.87 sec
    6531/33 Test [#23](/bitcoin-core-secp256k1/23/): secp256k1_noverify_tests::ecmult .......   Passed    8.77 sec
    6632/33 Test [#15](/bitcoin-core-secp256k1/15/): secp256k1_tests::ellswift ..............   Passed   13.11 sec
    6733/33 Test  [#7](/bitcoin-core-secp256k1/7/): secp256k1_tests::ecmult ................   Passed   17.27 sec
    68
    69100% tests passed, 0 tests failed out of 33
    70
    71Total Test time (real) =  17.31 sec
    72
    73The following tests did not run:
    74	 11 - secp256k1_tests::recovery (Skipped)
    75	 27 - secp256k1_noverify_tests::recovery (Skipped)
    

    UPD. Even after splitting up the ecmult tests, the ellswift tests are still the longest.

  36. furszy commented at 4:34 pm on September 10, 2025: member

    This branch with splitting by modules

    Nice thanks, so this doesn’t introduce any overhead compared to #1211 when run by modules (same functionality in both places). That’s good to know.

    Now, here you can go faster by parallelizing by test cases instead of modules. Can run ./tests -print_tests to see all available individual cases.

    UPD. Even after splitting up the ecmult tests, the ellswift tests are still the longest.

    I left a todo comment about that. We can subdivide the ellswift tests (there is a single function atm) and they will be automatically parallelizable here too.

  37. hebasto commented at 5:04 pm on September 10, 2025: member

    Now, here you can go faster by parallelizing by test cases instead of modules. Can run ./tests -print_tests to see all available individual cases.

    Yes, but that output can be quite noisy, especially when running in downstream projects such as Bitcoin Core.

    That said, combining both approaches should work.

  38. hebasto commented at 5:28 pm on September 10, 2025: member

    Now, here you can go faster by parallelizing by test cases instead of modules. Can run ./tests -print_tests to see all available individual cases.

    Yes, but that output can be quite noisy, especially when running in downstream projects such as Bitcoin Core.

    That said, combining both approaches should work.

    For example, for this branch:

     0$ ctest --test-dir build -j 16 --output-on-failure
     1Test project /home/hebasto/dev/secp256k1/secp256k1/build
     2      Start  1: secp256k1_tests::ecmult_constants
     3      Start  2: secp256k1_tests::ecmult
     4      Start  3: secp256k1_tests::general
     5      Start  4: secp256k1_tests::integer
     6      Start  5: secp256k1_tests::hash
     7      Start  6: secp256k1_tests::scalar
     8      Start  7: secp256k1_tests::field
     9      Start  8: secp256k1_tests::group
    10      Start  9: secp256k1_tests::ec
    11      Start 10: secp256k1_tests::ecdh
    12      Start 11: secp256k1_tests::ecdsa
    13      Start 12: secp256k1_tests::recovery
    14      Start 13: secp256k1_tests::extrakeys
    15      Start 14: secp256k1_tests::schnorrsig
    16      Start 15: secp256k1_tests::musig
    17      Start 16: secp256k1_tests::ellswift
    18 1/35 Test  [#3](/bitcoin-core-secp256k1/3/): secp256k1_tests::general .....................   Passed    0.01 sec
    19 2/35 Test [#12](/bitcoin-core-secp256k1/12/): secp256k1_tests::recovery ....................***Skipped   0.00 sec
    20      Start 17: secp256k1_tests::utils
    21      Start 18: secp256k1_noverify_tests::ecmult_constants
    22 3/35 Test [#17](/bitcoin-core-secp256k1/17/): secp256k1_tests::utils .......................   Passed    0.00 sec
    23      Start 19: secp256k1_noverify_tests::ecmult
    24 4/35 Test  [#5](/bitcoin-core-secp256k1/5/): secp256k1_tests::hash ........................   Passed    0.02 sec
    25      Start 20: secp256k1_noverify_tests::general
    26 5/35 Test [#20](/bitcoin-core-secp256k1/20/): secp256k1_noverify_tests::general ............   Passed    0.00 sec
    27      Start 21: secp256k1_noverify_tests::integer
    28 6/35 Test [#13](/bitcoin-core-secp256k1/13/): secp256k1_tests::extrakeys ...................   Passed    0.03 sec
    29      Start 22: secp256k1_noverify_tests::hash
    30 7/35 Test  [#9](/bitcoin-core-secp256k1/9/): secp256k1_tests::ec ..........................   Passed    0.03 sec
    31      Start 23: secp256k1_noverify_tests::scalar
    32 8/35 Test  [#6](/bitcoin-core-secp256k1/6/): secp256k1_tests::scalar ......................   Passed    0.05 sec
    33      Start 24: secp256k1_noverify_tests::field
    34 9/35 Test [#22](/bitcoin-core-secp256k1/22/): secp256k1_noverify_tests::hash ...............   Passed    0.02 sec
    35      Start 25: secp256k1_noverify_tests::group
    3610/35 Test [#23](/bitcoin-core-secp256k1/23/): secp256k1_noverify_tests::scalar .............   Passed    0.03 sec
    37      Start 26: secp256k1_noverify_tests::ec
    3811/35 Test [#26](/bitcoin-core-secp256k1/26/): secp256k1_noverify_tests::ec .................   Passed    0.01 sec
    39      Start 27: secp256k1_noverify_tests::ecdh
    4012/35 Test [#14](/bitcoin-core-secp256k1/14/): secp256k1_tests::schnorrsig ..................   Passed    0.07 sec
    41      Start 28: secp256k1_noverify_tests::ecdsa
    4213/35 Test [#27](/bitcoin-core-secp256k1/27/): secp256k1_noverify_tests::ecdh ...............   Passed    0.05 sec
    43      Start 29: secp256k1_noverify_tests::recovery
    4414/35 Test [#29](/bitcoin-core-secp256k1/29/): secp256k1_noverify_tests::recovery ...........***Skipped   0.00 sec
    45      Start 30: secp256k1_noverify_tests::extrakeys
    4615/35 Test [#30](/bitcoin-core-secp256k1/30/): secp256k1_noverify_tests::extrakeys ..........   Passed    0.01 sec
    47      Start 31: secp256k1_noverify_tests::schnorrsig
    4816/35 Test [#10](/bitcoin-core-secp256k1/10/): secp256k1_tests::ecdh ........................   Passed    0.14 sec
    49      Start 32: secp256k1_noverify_tests::musig
    5017/35 Test [#31](/bitcoin-core-secp256k1/31/): secp256k1_noverify_tests::schnorrsig .........   Passed    0.03 sec
    51      Start 33: secp256k1_noverify_tests::ellswift
    5218/35 Test [#32](/bitcoin-core-secp256k1/32/): secp256k1_noverify_tests::musig ..............   Passed    0.12 sec
    53      Start 34: secp256k1_noverify_tests::utils
    5419/35 Test [#34](/bitcoin-core-secp256k1/34/): secp256k1_noverify_tests::utils ..............   Passed    0.00 sec
    55      Start 35: secp256k1_exhaustive_tests
    5620/35 Test [#15](/bitcoin-core-secp256k1/15/): secp256k1_tests::musig .......................   Passed    0.29 sec
    5721/35 Test [#28](/bitcoin-core-secp256k1/28/): secp256k1_noverify_tests::ecdsa ..............   Passed    0.97 sec
    5822/35 Test  [#7](/bitcoin-core-secp256k1/7/): secp256k1_tests::field .......................   Passed    1.62 sec
    5923/35 Test [#25](/bitcoin-core-secp256k1/25/): secp256k1_noverify_tests::group ..............   Passed    1.75 sec
    6024/35 Test [#24](/bitcoin-core-secp256k1/24/): secp256k1_noverify_tests::field ..............   Passed    1.97 sec
    6125/35 Test [#11](/bitcoin-core-secp256k1/11/): secp256k1_tests::ecdsa .......................   Passed    2.80 sec
    6226/35 Test [#21](/bitcoin-core-secp256k1/21/): secp256k1_noverify_tests::integer ............   Passed    3.79 sec
    6327/35 Test  [#4](/bitcoin-core-secp256k1/4/): secp256k1_tests::integer .....................   Passed    3.99 sec
    6428/35 Test [#33](/bitcoin-core-secp256k1/33/): secp256k1_noverify_tests::ellswift ...........   Passed    4.26 sec
    6529/35 Test  [#8](/bitcoin-core-secp256k1/8/): secp256k1_tests::group .......................   Passed    4.75 sec
    6630/35 Test [#18](/bitcoin-core-secp256k1/18/): secp256k1_noverify_tests::ecmult_constants ...   Passed    5.71 sec
    6731/35 Test [#19](/bitcoin-core-secp256k1/19/): secp256k1_noverify_tests::ecmult .............   Passed    8.26 sec
    6832/35 Test [#35](/bitcoin-core-secp256k1/35/): secp256k1_exhaustive_tests ...................   Passed    8.61 sec
    6933/35 Test  [#1](/bitcoin-core-secp256k1/1/): secp256k1_tests::ecmult_constants ............   Passed   10.30 sec
    7034/35 Test  [#2](/bitcoin-core-secp256k1/2/): secp256k1_tests::ecmult ......................   Passed   10.98 sec
    7135/35 Test [#16](/bitcoin-core-secp256k1/16/): secp256k1_tests::ellswift ....................   Passed   12.77 sec
    72
    73100% tests passed, 0 tests failed out of 35
    74
    75Total Test time (real) =  12.78 sec
    76
    77The following tests did not run:
    78	 12 - secp256k1_tests::recovery (Skipped)
    79	 29 - secp256k1_noverify_tests::recovery (Skipped)
    
  39. hebasto commented at 5:28 pm on September 10, 2025: member
    Concept ACK.
  40. furszy commented at 5:42 pm on September 10, 2025: member

    Now, here you can go faster by parallelizing by test cases instead of modules. Can run ./tests -print_tests to see all available individual cases.

    Yes, but that output can be quite noisy, especially when running in downstream projects such as Bitcoin Core. That said, combining both approaches should work.

    For example, for this branch:

    0$ ctest --test-dir build -j 16 --output-on-failure
    1...
    2
    3100% tests passed, 0 tests failed out of 35
    4
    5Total Test time (real) =  12.78 sec
    

    Cool, thanks for testing! That’s a good speedup. And we can do even better, because your branch only specifies a few single test cases, not all, to run in isolation; the rest are still running as a single process group (the library currently has 88 single test cases, and your output shows that only 35 were run - so you must have grouped most of them).

    Now, here you can go faster by parallelizing by test cases instead of modules. Can run ./tests -print_tests to see all available individual cases.

    Yes, but that output can be quite noisy, especially when running in downstream projects such as Bitcoin Core.

    It may worth to try using the binary -j (or -jobs) command-line arg from ctest in some way if excessive logging is a concern on the CI. Because we can control it internally; the output will be minimal (unless you specify -log=1).

  41. hebasto commented at 5:46 pm on September 10, 2025: member

    And we can do even better, because your branch only specifies a few single test cases, not all, to run in isolation; the rest are still running as a single process group (the library currently has 88 single test cases, and your output shows that only 35 were run - so you must have grouped most of them).

    I doubt this would improve performance, since the total runtime is now constrained by the longest-running test.

  42. furszy commented at 7:15 pm on September 10, 2025: member

    I doubt this would improve performance, since the total runtime is now constrained by the longest-running test.

    Ah, I see. We could split the test in a follow-up then. At first glance, it seems internal cases are well defined.

  43. furszy force-pushed on Sep 10, 2025
  44. in src/tests_common.h:24 in 785c34ea86 outdated
    17+
    18+#if (defined(_MSC_VER) && _MSC_VER >= 1900)
    19+#  include <time.h>
    20+#else
    21+#  include <sys/time.h>
    22+#endif
    


    hebasto commented at 10:58 am on September 11, 2025:

    a759cf5b9408ca630654e04faea56ff4f6944d45:

    Include <stdint.h> for int64_t?


    furszy commented at 6:40 pm on September 12, 2025:
    updated, thanks
  45. in src/tests.c:29 in 785c34ea86 outdated
    24@@ -25,6 +25,8 @@
    25 #include "checkmem.h"
    26 #include "testutil.h"
    27 #include "util.h"
    28+#include "unit_test.h"
    29+#include "unit_test.c"
    


    hebasto commented at 11:43 am on September 11, 2025:

    e262ba546a9d7b1e7a5977613efe36ff2c67b313:

    I believe there’s no reason to keep everything in a single translation unit. Tests are not such a performance-critical task. And this pattern really confuses static analysis tools.

    Let’s keep it as a separate module, for example, as done in this branch.


    furszy commented at 2:20 pm on September 11, 2025:

    hebasto commented at 3:26 pm on September 11, 2025:

    Cross-posting from https://github.com/hebasto/secp256k1/commit/082b928b7d16f83d8749a695f2a4b4e293e322a4#r165610965: @sipa wrote:

    The design is there because of performance, to allow inlining across the layers.

    That’s indeed unnecessary in tests, but that’s not a reason to deviate from the design; consistency is nice. @hebasto wrote: While consistency is indeed nice, I still see a reason for doing this, namely, more efficient use of static code analysis tools.


    hebasto commented at 4:50 pm on September 11, 2025:

    And this pattern really confuses static analysis tools. @furszy asked me offline to provide an example of such a tool in use.

    Let’s take IWYU as an example and run it as follows:

    0cmake -B build -DCMAKE_C_COMPILER=clang-19 -DCMAKE_C_INCLUDE_WHAT_YOU_USE=include-what-you-use
    1cmake --build build -t tests
    

    When run on the branch with a separate TU, it correctly identifies a missing header:

     0[ 60%] Building C object src/CMakeFiles/unit_test.dir/unit_test.c.o
     1Warning: include-what-you-use reported diagnostics:
     2
     3(/home/hebasto/dev/secp256k1/secp256k1/src/unit_test.h has correct #includes/fwd-decls)
     4
     5/home/hebasto/dev/secp256k1/secp256k1/src/unit_test.c should add these lines:
     6#include <stdint.h>         // for int64_t
     7
     8/home/hebasto/dev/secp256k1/secp256k1/src/unit_test.c should remove these lines:
     9
    10The full include-list for /home/hebasto/dev/secp256k1/secp256k1/src/unit_test.c:
    11#include "unit_test.h"
    12#include <stdint.h>         // for int64_t
    13#include <stdio.h>          // for fprintf, printf, NULL, stderr, perror
    14#include <stdlib.h>         // for EXIT_FAILURE, strtol, EXIT_SUCCESS, getenv
    15#include <string.h>         // for strcmp, strchr, strlen
    16#include <sys/types.h>      // for pid_t
    17#include <sys/wait.h>       // for waitpid
    18#include <unistd.h>         // for close, _exit, fork, pipe, read, write
    19#include "testrand_impl.h"  // for testrand_init
    20#include "tests_common.h"   // for gettime_i64
    21---
    

    On this branch, unit_test.c is skipped during analysis (and I assume the same would happen with other tools).

  46. in src/tests.c:7689 in 785c34ea86 outdated
    7709-    /* run test RNG tests (must run before we really initialize the test RNG) */
    7710-    run_xoshiro256pp_tests();
    7711+/* --- Context Independent --- */
    7712+static struct TestEntry tests_no_ctx[] = {
    7713+    CASE(xoshiro256pp_tests),
    7714+    {NULL, NULL}
    


    hebasto commented at 3:41 pm on September 11, 2025:

    e262ba546a9d7b1e7a5977613efe36ff2c67b313:

    Why is this necessary?


    furszy commented at 4:10 pm on September 11, 2025:
    The sentinel avoids specifying the array size for this special case, which is not included in the general modules list. I could include the size to match the structure of the others as well. This was just a small shortcut to avoid doing so.

    theStack commented at 3:01 pm on September 12, 2025:
    Was a bit confused as well to see these values only in a single TestEntry array, would suggest to either use sentinels for all of the arrays or for none of them for consistency.

    furszy commented at 6:42 pm on September 12, 2025:
    Done. Updated to use the size-based approach everywhere, mainly because the compiler may be able to optimize it.
  47. in src/tests_common.h:6 in a759cf5b94 outdated
    0@@ -0,0 +1,40 @@
    1+/***********************************************************************
    2+ * Distributed under the MIT software license, see the accompanying    *
    3+ * file COPYING or https://www.opensource.org/licenses/mit-license.php.*
    4+ ***********************************************************************/
    5+
    6+#ifndef LIBSECP256K1_TEST_COMMON_H
    


    theStack commented at 2:14 pm on September 12, 2025:

    nit, for consistency with other headers:

    0#ifndef SECP256K1_TEST_COMMON_H
    

    furszy commented at 6:40 pm on September 12, 2025:
    done, thanks.
  48. in src/unit_test.c:8 in e262ba546a outdated
    0@@ -0,0 +1,293 @@
    1+/***********************************************************************
    2+ * Copyright (c) 2025  Matias Furszyfer (furszy)                       *
    3+ * Distributed under the MIT software license, see the accompanying    *
    4+ * file COPYING or https://www.opensource.org/licenses/mit-license.php.*
    5+ ***********************************************************************/
    6+
    7+#ifndef LIBSECP256K1_UNIT_TEST_C
    8+#define LIBSECP256K1_UNIT_TEST_C
    


    theStack commented at 2:20 pm on September 12, 2025:
    seems unusual to have include guards for .c files and we currently don’t follow that practice (looking at e.g. secp256k1.c), I presume we prefer a compile error if someone ever tried to include a .c file (which itself is already very unusual as well :p) more than once per translation unit?

    furszy commented at 6:44 pm on September 12, 2025:
    yeah sure. Extra guards removed. Thanks
  49. theStack commented at 3:09 pm on September 12, 2025: contributor

    Concept ACK

    Tested this quickly on an arm64 machine and observed a nice >2x speedup (~24.4s j=1 vs. ~11.1s j=6), also played around running only tests of certain modules, which worked as expected. Left just a few first-look nits.

  50. furszy force-pushed on Sep 12, 2025
  51. furszy commented at 6:46 pm on September 12, 2025: member
    Updated per feedback, thanks theStack and hebasto!
  52. in src/tests_common.h:6 in 5a501061ea outdated
    0@@ -0,0 +1,42 @@
    1+/***********************************************************************
    2+ * Distributed under the MIT software license, see the accompanying    *
    3+ * file COPYING or https://www.opensource.org/licenses/mit-license.php.*
    4+ ***********************************************************************/
    5+
    6+#ifndef SECP256K1_TEST_COMMON_H
    


    hebasto commented at 11:57 am on September 13, 2025:

    91eac9c9411bf27607ffc1e114fec039a7fca00a: a typo:

    0#ifndef SECP256K1_TESTS_COMMON_H
    

    furszy commented at 1:39 pm on September 14, 2025:
    Done, thanks.
  53. in src/unit_test.c:22 in 5a501061ea outdated
    17+#if !defined(_PID_T) && !defined(pid_t)
    18+#   include <sys/types.h>
    19+#endif
    20+#    define SUPPORTS_CONCURRENCY 1
    21+#  endif
    22+#endif
    


    hebasto commented at 12:16 pm on September 13, 2025:

    I’m not sure about this implementation:

    1. __has_include works fine with modern compilers. However, it is a C23 feature. Should we rely on it?

    2. The availability of headers and symbols is usually a task for the build system, which defines SUPPORTS_CONCURRENCY based on the header / symbol checks.


    furszy commented at 3:10 pm on September 13, 2025:

    I’m not sure about this implementation:

    1. __has_include works fine with modern compilers. However, it is a C23 feature. Should we rely on it?
    2. The availability of headers and symbols is usually a task for the build system, which defines SUPPORTS_CONCURRENCY based on the header / symbol checks.

    Hmm, yeah. I was trying to avoid touching the build systems (since we have two now), but can move the code there too. The guards here are mainly to postpone Windows support for future work (if we ever want to add support for such platform).


    furszy commented at 1:39 pm on September 14, 2025:
    Done. Let me know if the build system changes can be shrunk even more. I tried to keep them minimal.
  54. in src/unit_test.c:302 in 5a501061ea outdated
    236+            perror("Error during pipe setup");
    237+            return EXIT_FAILURE;
    238+        }
    239+
    240+        pid = fork();
    241+        if (pid < 0) {
    


    hebasto commented at 12:48 pm on September 13, 2025:

    318460c39aec04aa3580df5eadc7a935355968de

    nit: It would be good to check return values of the pipe() and fork() calls consistently, for example ...==-1.


    furszy commented at 1:32 pm on September 14, 2025:
    I’m not sure these two functions are directly comparable. They have different return value ranges: pipe() just indicates success or failure (0 or -1), while fork() has three cases: -1 for an error, 0 in the child process, and any value greater than 0 is the child’s PID in the parent. So it seemed slightly more accurate to me to describe the entire range for fork().
  55. in src/unit_test.h:25 in 5a501061ea outdated
    21+/* --------------------------------------------------------- */
    22+/* Test Framework Registry Macros                            */
    23+/* --------------------------------------------------------- */
    24+
    25+#define CASE(name) { #name, run_##name }
    26+#define CASE1(name) { #name, name }
    


    hebasto commented at 1:13 pm on September 13, 2025:

    6162b7617d3731e08d2b59baf79ba602182cf85e:

    It’s unfortunate to have such a pair of confusing macros. Perhaps their naming could be improved, although I don’t have a concrete suggestion.


    furszy commented at 3:41 pm on September 13, 2025:

    6162b76:

    It’s unfortunate to have such a pair of confusing macros. Perhaps their naming could be improved, although I don’t have a concrete suggestion.

    A small scripted-diff would improve the situation and let us use only one of them. We just need to rename the test functions that start with “run_*” so that is not included in the name. I just tried to avoid expanding the scope of the PR further.


    real-or-random commented at 7:09 am on September 29, 2025:
    Agreed, having this in a follow-up PR is fine. (But also here is fine.)
  56. hebasto commented at 1:21 pm on September 13, 2025: member

    Approach ACK 5a501061ea082d66e193234023843738a328e4a4.

    I’ve completed my first round of reviewing. The code looks good.

  57. refactor: move 'gettime_i64()' to tests_common.h
    Relocate the clock time getter to tests_common.h to
    make it easily reusable across test programs. This
    will be useful for the upcoming unit test framework.
    
    Context - why not placing it inside testutil.h?:
    The bench program links against the production-compiled library,
    not its own compiled version. Therefore, `gettime_i64()` cannot
    be moved to testutil.h, because testutil.h calls
    `secp256k1_pubkey_save()`, which exists only in the internal
    secp256k1.c and not in the public API.
    9cce703863
  58. furszy force-pushed on Sep 14, 2025
  59. furszy commented at 1:38 pm on September 14, 2025: member
    Updated per feedback, thanks Hebasto!
    Moved the headers existence check to the build systems (both).
  60. in src/tests.c:7694 in 4e7bdc135a outdated
    7717+};
    7718+static struct TestModule registry_modules_no_ctx = MAKE_TEST_MODULE(no_ctx);
    7719+
    7720+/* --- Context-dependent tests start here --- */
    7721+static struct TestEntry tests_general[] = {
    7722+    CASE(selftest_tests),
    


    stratospher commented at 4:17 pm on September 15, 2025:
    0423532: nit: selftest_tests doesn’t require a context though unlike other tests inside tests_general.

    furszy commented at 2:39 pm on September 16, 2025:
    related to #1734 (review).
  61. in configure.ac:450 in 726e70b899 outdated
    442@@ -443,6 +443,16 @@ if test x"$enable_experimental" = x"no"; then
    443   fi
    444 fi
    445 
    446+# Check for concurrency support (tests only)
    447+if test "x$enable_tests" != x"no"; then
    448+  AC_CHECK_HEADERS([sys/types.h sys/wait.h unistd.h])
    449+  SUPPORTS_CONCURRENCY=0
    450+  if test "x$HAVE_SYS_TYPES_H" = xyes -a "x$HAVE_SYS_WAIT_H" = xyes -a "x$HAVE_UNISTD_H" = xyes; then
    


    john-moffett commented at 5:28 pm on September 15, 2025:

    This always fails for me as written, but works when cache variables are used instead:

    if test "x$ac_cv_header_sys_types_h" = xyes -a "x$ac_cv_header_sys_wait_h" = xyes -a "x$ac_cv_header_unistd_h" = xyes; then


    furszy commented at 7:27 pm on September 15, 2025:
    That’s interesting. Thanks!. Updated per feedback plus also modified it to use the AS_IF macro because that seems to be the autoconf-idiomatic way to write conditionals (which might avoid portability issues with the test … -a … option). Let me know how it goes on your end now.

    john-moffett commented at 1:06 pm on September 16, 2025:
    Successful 👍

  62. in src/unit_test.c:140 in 726e70b899 outdated
    135+    return 0;
    136+}
    137+
    138+static int parse_target(const char* value, struct TestFramework* tf) {
    139+    TestRef i = {/*idx_module=*/0, /*idx_test=*/0};
    140+    if (tf->args.targets.size >= MAX_ARGS) {
    


    john-moffett commented at 5:38 pm on September 15, 2025:
    I think this should be checked in the inner loop, too. Passing a bunch of modules , eg, ./tests -t=ecdsa -t=ecdsa -t=ecdsa -t=ecdsa -t=ecdsa -t=ecdsa -t=ecdsa -t=ecdsa -t=ecdsa -t=ecdsa -t=ecdsa -t=ecdsa -t=ecdsa -t=ecdsa -t=ecdsa -t=ecdsa -t=ecdsa and it will write past slots[149] and clobbers adjacent struct fields. On my machine, it doesn’t even crash but just corrupts the size and logging fields.

    furszy commented at 7:29 pm on September 15, 2025:
    Yeah, good catch. Fixed. I should have moved the check when introduced support for module selection.
  63. john-moffett commented at 5:40 pm on September 15, 2025: contributor
    I like this a lot. Approach ACK 726e70b
  64. furszy force-pushed on Sep 15, 2025
  65. furszy commented at 7:33 pm on September 15, 2025: member
    Updated per feedback. Thanks john-moffett! Adapted the autoconf code for better portability and fixed a misplaced targets size check.
  66. in src/unit_test.h:7 in 0423532ea9 outdated
    0@@ -0,0 +1,111 @@
    1+/***********************************************************************
    2+ * Copyright (c) 2025  Matias Furszyfer (furszy)                       *
    3+ * Distributed under the MIT software license, see the accompanying    *
    4+ * file COPYING or https://www.opensource.org/licenses/mit-license.php.*
    5+ ***********************************************************************/
    6+
    7+#ifndef LIBSECP256K1_UNIT_TEST_H
    


    stratospher commented at 4:48 am on September 16, 2025:
    0423532: SECP256K1_UNIT_TEST_H here as well

    furszy commented at 2:37 pm on September 16, 2025:
    Done
  67. in src/unit_test.c:77 in 0423532ea9 outdated
    72+static int parse_iterations(const char* arg) {
    73+    /* Find iteration count */
    74+    if (arg) {
    75+        COUNT = (int) strtol(arg, NULL, 0);
    76+    } else {
    77+        const char* env = getenv("SECP256K1_TEST_ITERS");
    


    stratospher commented at 7:16 am on September 16, 2025:

    0423532: weird, not able to use the environment variable option. I’m seeing:

    • time ./build/bin/tests -iter=64 in 1m10.047s
    • time SECP256K1_TEST_ITERS=64 ./build/bin/tests in 0m26.908s

    furszy commented at 2:28 pm on September 16, 2025:

    0423532: weird, not able to use the environment variable option. I’m seeing:

    • time ./build/bin/tests -iter=64 in 1m10.047s
    • time SECP256K1_TEST_ITERS=64 ./build/bin/tests in 0m26.908s

    Had it on my to do list and it seems I never enabled it (which kinda shows how much I like env vars..). Thanks!. Enabled now.

  68. in src/unit_test.c:138 in 0423532ea9 outdated
    81+    }
    82+    if (COUNT <= 0) {
    83+        fputs("An iteration count of 0 or less is not allowed.\n", stderr);
    84+        return -1;
    85+    }
    86+    printf("Iterations count = %i\n", COUNT);
    


    stratospher commented at 7:24 am on September 16, 2025:

    0423532: also don’t see the Iterations count = log in the CI. I guess this is related to the environment variable not being set.

    maybe useful to always print the log even if it’s the default value being used/-iters isn’t used ?


    furszy commented at 2:37 pm on September 16, 2025:

    0423532: also don’t see the Iterations count = log in the CI. I guess this is related to the environment variable not being set.

    Yeah. We only print when the arg is provided by the user.

    maybe useful to always print the log even if it’s the default value being used/-iters isn’t used ?

    We should probably unify all args prints within a single place too.


    furszy commented at 2:40 pm on September 24, 2025:
    Done.
  69. in src/unit_test.h:75 in 0423532ea9 outdated
    70+    /* Test modules registry */
    71+    struct TestModule* registry_modules;
    72+    /* Num of modules */
    73+    int num_modules;
    74+    /* Registry for tests that require no context setup */
    75+    struct TestModule* registry_no_ctx;
    


    stratospher commented at 12:52 pm on September 16, 2025:
    0423532: is there any particular reason for separating the tests which don’t require context setup? many other tests in the other registry_modules like integer, scalar, field, group don’t require a context either.

    furszy commented at 2:02 pm on September 16, 2025:

    0423532: is there any particular reason for separating the tests which don’t require context setup? many other tests in the other registry_modules like integer, scalar, field, group don’t require a context either.

    These are just tests that must run before the RNG initialization, which itself happens before everything else, hence my “no context” naming, though that phrase has a different connotation here, agree. Could call it registry_no_rng? unless you have a better suggestion.

  70. kmk142789 approved
  71. stratospher commented at 1:15 pm on September 16, 2025: contributor
    nice! useful to be able to run a subset of tests.
  72. in src/unit_test.c:149 in aaacb77019 outdated
    142+        struct TestModule* module = &tf->registry_modules[i.group];
    143+        int add_all = strcmp(value, module->name) == 0; /* select all from module */
    144+        for (i.idx = 0; i.idx < module->size; i.idx++) {
    145+            if (add_all || strcmp(value, module->data[i.idx].name) == 0) {
    146+                tf->args.targets.slots[tf->args.targets.size] = i;
    147+                if (++tf->args.targets.size >= MAX_ARGS) {
    


    john-moffett commented at 1:49 pm on September 16, 2025:
    Nit: this fails when the number of targets is exactly MAX_ARGS (or greater) rather than strictly > MAX_ARGS, which I’d think is more natural.

    furszy commented at 3:32 pm on September 16, 2025:

    Nit: this fails when the number of targets is exactly MAX_ARGS (or greater) rather than strictly > MAX_ARGS, which I’d think is more natural.

    The array has indices 0..MAX_ARGS-1, so when size == MAX_ARGS-1 we already have MAX_ARGS elements. Using only > would allow size == MAX_ARGS (which actually means having one more than MAX_ARGS), causing the next write to overflow the array before the error is thrown.


    john-moffett commented at 3:55 pm on September 16, 2025:

    Sorry, wasn’t suggesting simply replacing >= with > there, but rather just pointing out that, eg, doing exactly 5 tests would fail if MAX_ARGS == 5, which seems unnatural.

    Right now the code does the write before the guard. I’d switch to before. Something like:

    0if (tf->args.targets.size >= MAX_ARGS) {
    1      fprintf(stderr, ...);
    2      return -1;
    3}
    4tf->args.targets.slots[tf->args.targets.size++] = i;
    

    furszy commented at 5:50 pm on September 16, 2025:
    Ah ok, now we are sync. Updated per feedback. Thanks!
  73. in src/unit_test.c:380 in aaacb77019 outdated
    375+    /* Initialize test RNG and library contexts */
    376+    testrand_init(tf->args.custom_seed);
    377+    if (tf->fn_setup && tf->fn_setup() != 0) return EXIT_FAILURE;
    378+
    379+    /* Check whether to process tests sequentially or concurrently */
    380+    if (tf->args.num_processes == 0) {
    


    john-moffett commented at 2:16 pm on September 16, 2025:
    Nit: should it be sequential for 1 as well?

    furszy commented at 3:36 pm on September 16, 2025:

    Nit: should it be sequential for 1 as well?

    that was actually my testing trick to exercise the concurrent path with a single process. Removed now. Thanks.

  74. in src/unit_test.c:264 in aaacb77019 outdated
    259+    }
    260+
    261+    /* Close all pipes to signal workers to exit */
    262+    for (it = 0; it < tf->args.num_processes; it++) close(pipes[it][1]);
    263+    /* Wait for all workers */
    264+    for (it = 0; it < tf->args.num_processes; it++) waitpid(workers[it], &status, 0);
    


    john-moffett commented at 2:27 pm on September 16, 2025:

    There’s different exit code behavior between running concurrently and sequentially. Sequentially, the CHECK failure will call abort and kill the main process and return an error code. Concurrently, the abort and error code are printed but discarded and the parent returns EXIT_SUCCESS unconditionally. I think if jobs were ever auto-detected (or anyone ever relied on exit codes for the concurrent path), this would result in a test harness incorrectly reporting success even on failure.

    Maybe collect the exit codes in this loop and return failure if any fail?


    furszy commented at 4:03 pm on September 16, 2025:

    There’s different exit code behavior between running concurrently and sequentially. Sequentially, the CHECK failure will call abort and kill the main process and return an error code. Concurrently, the abort and error code are printed but discarded and the parent returns EXIT_SUCCESS unconditionally. I think if jobs were ever auto-detected (or anyone ever relied on exit codes for the concurrent path), this would result in a test harness incorrectly reporting success even on failure.

    Maybe collect the exit codes in this loop and return failure if any fail?

    Hmm yeah, good catch. This is something I introduced during the run_concurrent decoupling. The code actually collects the exit values in the status variable but misses to return it. Fixed now. Thanks!

  75. john-moffett commented at 2:34 pm on September 16, 2025: contributor
    Two more nits and one observation. Otherwise all looks good.
  76. furszy force-pushed on Sep 16, 2025
  77. in src/unit_test.c:364 in 906b45a73f outdated
    359+        int slot = 0;
    360+        for (ref.group = 0; ref.group < tf->num_modules; ref.group++) {
    361+            struct TestModule* group = &tf->registry_modules[ref.group];
    362+            for (ref.idx = 0; ref.idx < group->size; ref.idx++) {
    363+                tf->args.targets.slots[slot++] = ref;
    364+                if (slot >= MAX_ARGS) {
    


    john-moffett commented at 4:36 pm on September 16, 2025:
    Same issue here with the MAX_ARGS check. I’d also move the guard to before the write. Sorry for the piecemeal comments!

    furszy commented at 5:51 pm on September 16, 2025:

    Same issue here with the MAX_ARGS check. I’d also move the guard to before the write. Sorry for the piecemeal comments!

    Don’t worry, keep them coming :). Updated per feedback.

  78. furszy force-pushed on Sep 16, 2025
  79. john-moffett commented at 2:35 pm on September 17, 2025: contributor
    ACK 7b51e53
  80. in src/unit_test.c:80 in 7b51e53607
    75+
    76+static void help(void) {
    77+    printf("Usage: ./tests [options]\n\n");
    78+    printf("Run the test suite for the project with optional configuration.\n\n");
    79+    printf("Options:\n");
    80+    printf("    -help, -h                           Show this help message\n");
    


    jonasnick commented at 12:22 pm on September 23, 2025:
    Is there a reason for not using the GNU standard of using -- for multi character options?

    furszy commented at 2:23 pm on September 23, 2025:

    Is there a reason for not using the GNU standard of using -- for multi character options?

    Not a fully rational reason, just way too many years working on Core. Happy to switch to the double-dash style to follow the GNU convention.


    furszy commented at 2:50 pm on September 24, 2025:
    Done. Added support for the multi-char options.

    hebasto commented at 12:20 pm on September 30, 2025:
    Update the PR description accordingly?
  81. in src/unit_test.c:283 in 7b51e53607
    278+{
    279+    /* Caller must set tf->registry and tf->num_tests before calling tf_init. */
    280+    if (tf->registry_modules == NULL || tf->num_modules <= 0) {
    281+        fprintf(stderr, "Error: tests registry not provided or empty\n");
    282+        return EXIT_FAILURE;
    283+    }
    


    jonasnick commented at 12:24 pm on September 23, 2025:
    Comment appears to be outdated because it refers to non-existing struct members (same in the tf_init doc in unit_test.h).

    furszy commented at 9:02 pm on September 23, 2025:
    fixed. Thanks
  82. jonasnick commented at 12:25 pm on September 23, 2025: contributor

    Concept ACK

    I closed #1211.

  83. furszy force-pushed on Sep 23, 2025
  84. furszy commented at 9:08 pm on September 23, 2025: member
    Thanks for the feedback! Ended up redesigning part of the args-parsing structure to support double-dash multi-char options in a clean and extensible manner.
  85. in src/unit_test.c:78 in c39b3df84e outdated
    63+        if (strcmp(key, arg_map[i].key) == 0) {
    64+            return arg_map[i].handler(key, value, tf);
    65+        }
    66+    }
    67+    /* Unknown key: report just so typos don’t silently pass. */
    68+    printf("Unknown argument '-%s=%s'\n", key, value);
    


    theStack commented at 6:28 am on September 24, 2025:
    Is there any good reason for not just failing here? Allowing unknown options for command-line tools seems to be a very unusual practice in general (I think I’ve only seen that in the slightly different context of config file parsing for forward-compatibility).

    furszy commented at 2:03 pm on September 24, 2025:

    Is there any good reason for not just failing here? Allowing unknown options for command-line tools seems to be a very unusual practice in general (I think I’ve only seen that in the slightly different context of config file parsing for forward-compatibility).

    The idea is to support downgrades and cross-version testing without forcing changes to automation/people scripts. It gives us more flexibility to change options in the future without breaking the existing setups.

    That being said, if there’s a strong preference to fail here, could change it.


    theStack commented at 2:29 pm on September 24, 2025:
    I see. No strong objections from me, happy to ACK with or without. Personally I will very likely never have an use-case for running tests on past versions (this IMHO makes much more sense for benchmarks).

    furszy commented at 2:48 pm on September 24, 2025:

    Personally I will very likely never have an use-case for running tests on past versions (this IMHO makes much more sense for benchmarks).

    I usually compare testing times across versions too. Caught a few regressions in that way.

  86. in src/unit_test.c:95 in c39b3df84e outdated
    90+    COUNT = (int) strtol(value, NULL, 0);
    91+    if (COUNT <= 0) {
    92+        fputs("An iteration count of 0 or less is not allowed.\n", stderr);
    93+        return -1;
    94+    }
    95+    printf("Iterations count = %i\n", COUNT);
    


    theStack commented at 7:04 am on September 24, 2025:
    +1 on always printing the iteration count (also to retain the current behavior on master). (EDIT: this comment is referring to the previous discussion/suggestion #1734 (review))

    furszy commented at 2:40 pm on September 24, 2025:
    Done.
  87. in src/unit_test.c:11 in c39b3df84e outdated
     6+
     7+#include <stdio.h>
     8+#include <stdlib.h>
     9+#include <string.h>
    10+
    11+#if defined(SUPPORTS_CONCURRENCY) && SUPPORTS_CONCURRENCY
    


    theStack commented at 7:25 am on September 24, 2025:
    nit: is the defined(SUPPORTS_CONCURRENCY) part needed? it’s not used in similar pre-processor conditions below

    real-or-random commented at 7:03 pm on September 24, 2025:
    The standard mandates that undefined macros evaluate to 0 in an #if.

    furszy commented at 10:07 pm on September 24, 2025:
    Done, updated.

    hebasto commented at 0:26 am on September 26, 2025:

    If I’ve grepped the codebase correctly, this will be the only external definition used as #if DEFINITION.

    The standard mandates that undefined macros evaluate to 0 in an #if.

    Nevertheless, this approach has proven fragile. I would kindly ask to reconsider the design and use #if defined(DEFINITION) instead, which would also require slight adjustment in both build systems.


    furszy commented at 5:35 pm on September 26, 2025:

    The standard mandates that undefined macros evaluate to 0 in an #if.

    Nevertheless, this approach has proven fragile. I would kindly ask to reconsider the design and use #if defined(DEFINITION) instead, which would also require slight adjustment in both build systems.

    Happy to change it either way, but let’s try to reach a consensus first.


    real-or-random commented at 8:13 am on September 29, 2025:
    I agree, it’s better to use definedness than the actual value, simply for consistency with how we do boolean build flags in the rest of the code base.

    furszy commented at 6:37 pm on September 29, 2025:
    Based on the CI response, it seems Windows dislikes undefined values.. Fixed.
  88. theStack commented at 7:29 am on September 24, 2025: contributor

    Tested again with the new parsing logic [1], worked as expected :heavy_check_mark: . I haven’t had a chance yet to test on a system where SUPPORTS_CONCURRENCY doesn’t hold.

    [1] small general rant: it’s a bit depressing to see that in the year 2025 command-line parsing still has to be implemented manually. I was about to suggest using getopt(3) , but that is not part of the C standard library (in contrast to what the linked manpage under “LIBRARY” might suggest) and only available on POSIX systems :unamused:

  89. furszy commented at 2:15 pm on September 24, 2025: member

    [1] small general rant: it’s a bit depressing to see that in the year 2025 command-line parsing still has to be implemented manually.

    I don’t think we’re allowed to rant; we’re coding in a C89 project with no memory allocations.. in 2025.

  90. furszy force-pushed on Sep 24, 2025
  91. furszy force-pushed on Sep 24, 2025
  92. in src/unit_test.c:303 in 35b00d0fe6 outdated
    298@@ -282,6 +299,12 @@ static int tf_init(struct TestFramework* tf, int argc, char** argv)
    299             return EXIT_FAILURE;
    300         }
    301 
    302+        /* Check if we need to print help */
    303+        if (argv[1] && (strcmp(argv[1], "-help") == 0 || strcmp(argv[1], "-h") == 0)) {
    


    theStack commented at 1:17 am on September 25, 2025:
    Noticed that --help is currently not working, so could add that here, since the double dash variant is explicitly listed in the output. Same for --print_tests in the later commit.

    furszy commented at 5:15 pm on September 26, 2025:
    good catch. Fixed. Also refactored the code so this doesn’t happen again.
  93. in src/unit_test.h:90 in dc45bdf4a6 outdated
    85+/*
    86+ * Initialize the test framework.
    87+ *
    88+ * Must be called before tf_run() and as early as possible in the program.
    89+ * Parses command-line arguments and configures the framework context.
    90+ * The caller must set 'registry' and 'num_tests' before calling.
    


    theStack commented at 1:24 am on September 25, 2025:
    0 * The caller must set 'registry_modules' and 'num_modules' before calling.
    

    (guess these fields origin from an earlier iteration of the PR and were then renamed at some point)


    furszy commented at 5:15 pm on September 26, 2025:
    yeah, fixed.
  94. in src/unit_test.c:30 in dc45bdf4a6 outdated
    25+
    26+static int parse_jobs_count(const char* key, const char* value, struct TestFramework* tf);
    27+static int parse_iterations(const char* key, const char* value, struct TestFramework* tf);
    28+static int parse_seed(const char* key, const char* value, struct TestFramework* tf);
    29+
    30+/* Mapping table: key → handler */
    


    theStack commented at 1:38 am on September 25, 2025:

    meta-nit: seeing this fancy single-character arrow made me wonder if this file is UTF-8 encoded, and according to the file utility it indeed is:

    0$ file ./src/unit_test.c
    1./src/unit_test.c: C source, Unicode text, UTF-8 text
    

    Seems that other source files in the repo are not (see $ file ./src/*.h ./src/*.c, resulting all in “C source, ASCII text”). I could imagine that in theory really old compilers could struggle with that and using ASCII encoding would be the safe choice, but not sure if that concern is practically relevant at all.


    real-or-random commented at 7:35 am on September 26, 2025:

    It’s C89, but hey, at least we got emojis in the source code! :rofl:

    Serious: I don’t think it’s a problem in practice but it can’t be wrong to stick to ASCII for consistency.


    furszy commented at 1:58 pm on September 26, 2025:
    ok, will save my emojis for the blockchain only.
  95. hebasto commented at 11:15 pm on September 25, 2025: member

    Tested 0ddcd093def9ac371bff3d4665096c0a10a7d94d on Ubuntu 12.04.5 LTS using GCC 4.6.3. It works as expected, apart from the previously mentioned minor issue.

    UPD. (1) Tested on Windows 11.

    (2) Cross-compiled for Windows and tested on Windows 11.

    In both cases everything is OK. “Parallel execution not supported on your system. Running sequentially..”

  96. in src/unit_test.c:461 in 0ddcd093de outdated
    443+        status = run_sequential(tf);
    444+    } else {
    445+#if SUPPORTS_CONCURRENCY
    446+        status = run_concurrent(tf);
    447+#else
    448+        fputs("Parallel execution not supported on your system. Running sequentially..\n", stderr);
    


    hebasto commented at 0:14 am on September 26, 2025:

    nit:

    0        fputs("Parallel execution not supported on your system. Running sequentially...\n", stderr);
    

    real-or-random commented at 7:56 am on September 29, 2025:
    nit: not resolved? (and did you want “.” instead of “…”?)

    furszy commented at 3:40 pm on September 29, 2025:
    ups, solved now. Three-dots was the intention here.
  97. in src/CMakeLists.txt:140 in 0ddcd093de
    133@@ -134,12 +134,23 @@ if(SECP256K1_BUILD_BENCHMARK)
    134 endif()
    135 
    136 if(SECP256K1_BUILD_TESTS)
    137+  include(CheckIncludeFiles)
    138+  check_include_files("sys/types.h" HAVE_SYS_TYPES_H)
    139+  check_include_files("sys/wait.h" HAVE_SYS_WAIT_H)
    140+  check_include_files("unistd.h" HAVE_UNISTD_H)
    


    hebasto commented at 9:04 am on September 26, 2025:

    The CheckIncludeFile module (in singular) appears more suitable:

    0  include(CheckIncludeFile)
    1  check_include_file(sys/types.h HAVE_SYS_TYPES_H)
    2  check_include_file(sys/wait.h HAVE_SYS_WAIT_H)
    3  check_include_file(unistd.h HAVE_UNISTD_H)
    

    hebasto commented at 9:12 am on September 26, 2025:

    It may sound like overkill, but we’re actually interested in the symbols provided by headers, not the headers themselves. For example, when cross-compiling for Windows, unistd.h is available, but fork() is not.

    So something like the following would be more robust:

    0  include(CheckSymbolExists)
    1  check_symbol_exists(waitpid "sys/wait.h" HAVE_WAITPID)
    2  check_symbol_exists(fork "unistd.h" HAVE_FORK)
    3  ...
    

    real-or-random commented at 8:11 am on September 29, 2025:
    Did you mark this “resolved” unintentionally?

    furszy commented at 3:26 pm on September 29, 2025:

    Did you mark this “resolved” unintentionally?

    I pushed hebasto’s initial CheckIncludeFile suggestion after discussing the CheckSymbolExists one via DM, and forgot to update this thread (sorry for that). It just seemed to be an overkill for a feature we might introduce in a follow-up. Adding Windows support is not hard, it is just a bit more code.


    hebasto commented at 1:05 pm on September 30, 2025:

    Adding Windows support is not hard, it is just a bit more code.

    My apologies if I was misunderstood. I mentioned cross-compiling for Windows only as an example, not as the basis of my comment. I do believe the build system should check the actual functionality we intend to use, which is the functions, rather than just the headers.

    Here are a couple of examples for the fork() function from Bitcoin Core:

    0AC_CHECK_DECLS([fork])
    
    0check_cxx_symbol_exists(fork "unistd.h" HAVE_DECL_FORK)
    

    I’m fine with leaving this improvement for a follow-up PR.


    furszy commented at 1:44 pm on September 30, 2025:

    Following the overkill rationale; If we go with a more granular approach, we would have to check for every relevant symbol?. E.g. pid_t, write, read, and close too. The reason I included <sys/types.h> (in addition to <unistd.h>) is that historically, pid_t has sometimes been declared in one header vs. the other, depending on platform and era.

    In any case, I just unsolved this thread so we don’t forget it.


    real-or-random commented at 10:31 am on October 1, 2025:

    Following the overkill rationale; If we go with a more granular approach, we would have to check for every relevant symbol?. E.g. pid_t, write, read, and close too.

    I think a pragmatic middle ground is to check for symbols where we know that some common platforms don’t have them, e.g., fork on Windows. Something like pid_t never needs to be checked because it’s in the signature of fork.

  98. furszy force-pushed on Sep 26, 2025
  99. furszy commented at 5:29 pm on September 26, 2025: member
    Thanks for the feedback, hebasto and theStack. Merged the -help/print_tests options parsing with the general parsing procedure to fix the missing double-dash case and removed the UTF-8 arrow.
  100. in src/unit_test.c:311 in f55cf86615 outdated
    306+    /* Process exit status */
    307+    int status;
    308+    /* Loop iterator */
    309+    int it;
    310+    /* Initial test time */
    311+    int64_t start_time = gettime_i64(); /* maybe move this after the slots set */
    


    theStack commented at 3:15 pm on September 28, 2025:
    nit: this comment sounds like a TODO? seems reasonable to set start_time as late as possible, after all the internal setup work is done (OTOH, setting slots is probably so fast that it doesn’t really matter…)

    furszy commented at 7:52 pm on September 28, 2025:
    yeah, will just leave it here for now.
  101. in src/unit_test.c:232 in e6ab0b486f outdated
    234-        for (ref.idx = 0; ref.idx < mdl->size; ref.idx++) {
    235-            run_test(&mdl->data[ref.idx]);
    236-        }
    237+    int it;
    238+    for (it = 0; it < tf->args.targets.size; it++) {
    239+        TestRef* index = &tf->args.targets.slots[it];
    


    theStack commented at 3:20 pm on September 28, 2025:

    const-correctness nit

    0        const TestRef* index = &tf->args.targets.slots[it];
    

    furszy commented at 7:53 pm on September 28, 2025:
    done
  102. in src/unit_test.c:154 in e6ab0b486f outdated
    149@@ -146,6 +150,32 @@ static const char* normalize_key(const char* arg, const char** err_msg) {
    150     return key;
    151 }
    152 
    153+static int parse_target(const char* key, const char* value, struct TestFramework* tf) {
    154+    TestRef i = {/*idx_module=*/0, /*idx_test=*/0};
    


    theStack commented at 4:15 pm on September 28, 2025:
    nit: comments don’t match the actual field names

    furszy commented at 7:53 pm on September 28, 2025:
    thx, done.
  103. theStack approved
  104. theStack commented at 4:15 pm on September 28, 2025: contributor

    ACK fed7e3820031dce3b6af2ed36ec2a5e096c90d42

    Tested a bit more and verified that all existing tests have been adapted to the new framework. Didn’t review the autotools and CMake changes in-depth as I’m not too familiar with build systems. Left only a few non-blocking nits below. Also, dumping some nice-to-have follow-up ideas came to my mind (not sure if introducing additional complexity is worth it though):

    • show summary at the end (number of executed/passed/failed tests)
    • if a single test fails, still run the others (would need some deeper changes though, as right now we just crash on a failed condition)
    • allow to run tests specified by index numbers shown in --print_tests

    nit: unit_test.c is still in UTF-8 according to the file utility.

  105. furszy force-pushed on Sep 28, 2025
  106. furszy commented at 7:57 pm on September 28, 2025: member

    nit: unit_test.c is still in UTF-8 according to the file utility.

    Fixed now. Thanks.

  107. in src/unit_test.c:20 in 10eee5d1f7 outdated
    16+
    17+#include "unit_test.h"
    18+#include "testrand.h"
    19+#include "tests_common.h"
    20+
    21+#define UNUSED(x) (void)(x)
    


    real-or-random commented at 7:06 am on September 29, 2025:
    nit: We just use (void)x; throughout the entire code base. It’s a common C idiom, so I think the indirection through the UNUSED macro makes reading the code a tiny bit harder. (But no strong opinions!)

    furszy commented at 3:27 pm on September 29, 2025:
    Gotcha. Will leave it as is for now to avoid a large rebase. Happy to change it if more of us prefer the direct (void)x too.
  108. in src/unit_test.h:118 in 10eee5d1f7 outdated
    114+/* --------------------------------------------------------- */
    115+
    116+/*
    117+ * Initialize the test framework.
    118+ *
    119+ * Must be called before tf_run() and as early as possible in the program.
    


    real-or-random commented at 7:11 am on September 29, 2025:

    “as early as possible in the program.”

    That seems imprecise. What’s the reason for this requirement?


    furszy commented at 1:58 pm on September 29, 2025:

    “as early as possible in the program.”

    That seems imprecise. What’s the reason for this requirement?

    It disables stdout and stderr buffering (the setbuf() calls).


    real-or-random commented at 3:43 pm on September 29, 2025:
    This seems to be a side effect worth mentioning anyway, so I’d suggest adding this to the docs.

    furszy commented at 6:38 pm on September 29, 2025:
    Done.
  109. in src/unit_test.h:121 in 10eee5d1f7
    116+/*
    117+ * Initialize the test framework.
    118+ *
    119+ * Must be called before tf_run() and as early as possible in the program.
    120+ * Parses command-line arguments and configures the framework context.
    121+ * The caller must set 'registry_modules' and 'num_modules' before calling.
    


    real-or-random commented at 7:11 am on September 29, 2025:
    0 * The caller must set 'tf.registry_modules' and 'tf.num_modules' before calling.
    

    furszy commented at 3:28 pm on September 29, 2025:
    Done as suggested.
  110. in src/unit_test.h:72 in 10eee5d1f7 outdated
    68+typedef struct {
    69+    int group;
    70+    int idx;
    71+} TestRef;
    72+
    73+struct Targets {
    


    real-or-random commented at 7:13 am on September 29, 2025:
    style nit: These struct names should be lowercase and snake_case for consistency with the rest of the code base.

    furszy commented at 2:13 pm on September 29, 2025:

    style nit: These struct names should be lowercase and snake_case for consistency with the rest of the code base.

    My concern with that is that it’s easy for struct names to clash with variable names using that style. In the rest of the codebase, this isn’t a problem because we prefix structs with secp256k1, but here that would mean more boilerplate without much gain?. I could add the prefix + change them to lowercase if you feel strongly about it. I’m just thinking out loud before entering a large rebase.


    real-or-random commented at 4:06 pm on September 29, 2025:

    My concern with that is that it’s easy for struct names to clash with variable names using that style.

    Hehe, technically, the struct tags are in different namespaces than variables. :) But I see the point, of course.

    In the rest of the codebase, this isn’t a problem because we prefix structs with secp256k1, but here that would mean more boilerplate without much gain?.

    Perhaps we could just prefix it just with tf_ like the tf functions? I think this would actually add clarity to it. If you’re concerned about boilerplate, you could also typedef the struct away. I think we do this for (almost?) all struct defs in the code base, but it’s certainly not a requirement.

    I could add the prefix + change them to lowercase if you feel strongly about it. I’m just thinking out loud before entering a large rebase.

    I have to admit that I feel a bit strongly about the style convention, but I think it’s totally reasonable to add a commit on top of the current PR instead of needing to go through all the commits and edit them.


    furszy commented at 6:24 pm on September 29, 2025:
    Done as suggested. Ported all the structs to use the tf_ prefix with lowercase and snake_case. I wasn’t really in favor of adding an extra commit on top, it felt a bit messy.
  111. in src/unit_test.h:99 in 10eee5d1f7 outdated
    91+    struct Targets targets;
    92+    /* Enable test execution logging */
    93+    int logging;
    94+};
    95+
    96+struct TestFramework {
    


    real-or-random commented at 7:14 am on September 29, 2025:
    nit: This is above the “Public API” sectioning comment, but this struct seems to be part of the public API,

    furszy commented at 2:23 pm on September 29, 2025:

    nit: This is above the “Public API” sectioning comment, but this struct seems to be part of the public API,

    That’s because I was initially thinking of adding a factory function that returns an initialized TestFramework, but it ended up feeling like extra boilerplate with no real benefit. Will move it.

  112. in src/unit_test.h:28 in 10eee5d1f7
    23+/* --------------------------------------------------------- */
    24+
    25+#define CASE(name) { #name, run_##name }
    26+#define CASE1(name) { #name, name }
    27+
    28+#define MAKE_TEST_MODULE(name) {\
    


    real-or-random commented at 7:16 am on September 29, 2025:

    micronit:

    0#define MAKE_TEST_MODULE(name) { \
    

    Actually, below in REPEAT_TEST_MULT, there’s a different style. I think the “one space” style is more common in this code base.


    furszy commented at 3:28 pm on September 29, 2025:
    Done as suggested
  113. in src/unit_test.h:2 in 10eee5d1f7
    0@@ -0,0 +1,142 @@
    1+/***********************************************************************
    2+ * Copyright (c) 2025  Matias Furszyfer (furszy)                       *
    


    real-or-random commented at 7:24 am on September 29, 2025:

    nit: This is totally fine to have the year and your name here if you want to keep it, but we recently came to the conclusion that simply omitting the “Copyright (c)” line for new files makes maintenance easier (see the ellswift module for example).

    Rationale: Adding the year and name is legally not required to obtain the copyright. It’s merely a hint on who has the copyright and when it will expire, but we have git for this. Updating the date is annoying, so no one does it in practice. And it’s never entirely clear when to update the name list, so no one does it in practice. So after a few years, this line will probably be outdated.


    furszy commented at 3:29 pm on September 29, 2025:
    Gotcha. Removed.
  114. in src/unit_test.c:234 in 10eee5d1f7
    229+            /* Allowed options without value */
    230+            if (strcmp(key, "h") == 0 || strcmp(key, "help") == 0) {
    231+                tf->args.help = 1;
    232+                return 0;
    233+            }
    234+            if (strcmp(key, "print_tests") == 0) {
    


    real-or-random commented at 7:25 am on September 29, 2025:
    It would be nice to support the GNU style print-tests here as well now that we support --. (Not sure if it must be documented but I guess it won’t hurt.)

    real-or-random commented at 10:23 am on October 1, 2025:
    Perhaps you missed this one because it’s hidden by github. If you ignored it intentionally, nevermind, it’s totally not a blocker.

    furszy commented at 2:06 pm on October 1, 2025:

    Perhaps you missed this one because it’s hidden by github. If you ignored it intentionally, nevermind, it’s totally not a blocker.

    Yeah, I was going to reply you and never did. Thanks for the reminder.

    It would be nice to support the GNU style print-tests here as well now that we support --. (Not sure if it must be documented but I guess it won’t hurt.)

    The change is not really big but have thought about this for a while, and I’d rather keep - reserved only for introducing new command-line options. Mainly because allowing dashes inside names blurs that boundary and makes syntax less clear. Using underscores keeps it simple and consistent: “dashes always mean new option, underscores always separate words”. If you feel strongly about adding it, we could but it feels a bit messy to me.


    real-or-random commented at 7:40 am on October 2, 2025:

    The change is not really big but have thought about this for a while, and I’d rather keep - reserved only for introducing new command-line options. Mainly because allowing dashes inside names blurs that boundary and makes syntax less clear.

    Agreed, let’s avoid everything that makes parsing more complicated.


    theStack commented at 3:27 pm on October 2, 2025:

    feel-free-to-ignore yocto-nit: wouldn’t this currently only be a one-character change?

     0diff --git a/src/unit_test.c b/src/unit_test.c
     1index a1858a1..715fb60 100644
     2--- a/src/unit_test.c
     3+++ b/src/unit_test.c
     4@@ -239,7 +239,7 @@ static int read_args(int argc, char** argv, int start, struct tf_framework* tf)
     5                 tf->args.help = 1;
     6                 return 0;
     7             }
     8-            if (strcmp(key, "l") == 0 || strcmp(key, "list_tests") == 0) {
     9+            if (strcmp(key, "l") == 0 || strcmp(key, "list-tests") == 0) {
    10                 tf->args.list_tests = 1;
    11                 return 0;
    12             }
    

    seems to work just fine. Would in doubt slightly prefer to adhere to widely accepted standards (in Bitcoin Core we also use dashes in parameter names, fwiw). I guess it’s not worth it block the PR at all, but if maintainers and other reviewers feel strongly about dash-vs-underscores, could also sidestep the topic by simply renaming to “–list” :stuck_out_tongue:


    furszy commented at 3:48 pm on October 2, 2025:
    Yeah, it’s not about the size of the change (could just add another strcmp to support both too). My point is more general: it feels cleaner to standardize on “dashes start options, underscores separate words”, and not mix the two.
  115. in src/unit_test.c:87 in 10eee5d1f7
    82+static void help(void) {
    83+    printf("Usage: ./tests [options]\n\n");
    84+    printf("Run the test suite for the project with optional configuration.\n\n");
    85+    printf("Options:\n");
    86+    printf("    --help, -h                           Show this help message\n");
    87+    printf("    --print_tests                        Display list of all available tests and modules\n");
    


    real-or-random commented at 7:28 am on September 29, 2025:
    nit: I think list_tests would be more precise here (and a bit more in line with other unix programs). Perhaps we could have a -l shortcut?

    furszy commented at 3:29 pm on September 29, 2025:
    Done as suggested.
  116. in src/unit_test.c:100 in 10eee5d1f7
     95+    printf("Notes:\n");
     96+    printf("    - All arguments must be provided in the form '--key=value', '-key=value' or '-k=value'.\n");
     97+    printf("    - Single or double dashes are allowed for multi character options.\n");
     98+    printf("    - Unknown arguments are reported but ignored.\n");
     99+    printf("    - Sequential execution occurs if -jobs=0 or unspecified.\n");
    100+    printf("    - The first two positional arguments (iterations and seed) are also supported for backward compatibility.\n");
    


    real-or-random commented at 7:36 am on September 29, 2025:
    nit: I think the sentence structure is a bit weird here. Suggestion: “Iterations and seed can also be passed as positional arguments before any other argument for backward compatibility.”

    furszy commented at 3:29 pm on September 29, 2025:
    Thanks. Done as suggested.
  117. in src/unit_test.c:174 in 10eee5d1f7
    169+        *err_msg = "too many leading dashes";
    170+        return NULL;
    171+    }
    172+
    173+    key = arg + 2; /* double-dash long option */
    174+    if (key[1] == '\0') {
    


    real-or-random commented at 7:52 am on September 29, 2025:

    If I pass -- as arg to this function, then key[1] == arg[3] and this is beyond the end of the string, so this is UB.

    (On my machine, I get “unknown error” as the error message. I think having this as a fallback is not good. It simply hides coding errors, and it would be better to assert that err_msg is set when an error is indicated via the return value.)


    furszy commented at 2:57 pm on September 29, 2025:

    If I pass – as arg to this function, then key[1] == arg[3] and this is beyond the end of the string, so this is UB.

    Nice catch. Fixed.

    (On my machine, I get “unknown error” as the error message. I think having this as a fallback is not good. It simply hides coding errors, and it would be better to assert that err_msg is set when an error is indicated via the return value.)

    You know, I actually avoided using runtime assertions because I didn’t find any throughout the library.


    real-or-random commented at 3:53 pm on September 29, 2025:

    You know, I actually avoided using runtime assertions because I didn’t find any throughout the library.

    We don’t have it in the library itself, but we obviously have CHECK in the test-only code, so you could use that one. Even on current master, it’s used in tests.c in the setup code for purposes different from testing the library itself, e.g., this line: https://github.com/bitcoin-core/secp256k1/blob/baa265429fa8f1686138380e52a75c25b0344719/src/tests.c#L7699-L7700


    furszy commented at 6:34 pm on September 29, 2025:

    Been thinking about this. If we use CHECK, we lose the arg key-value error logging. Which will make debugging slightly harder (we will know where it failed but not under which values). We probably could extend CHECK to also print a message crafted at runtime when provided.

    Maybe we can leave this for a follow-up? I’m unsure we should keep expanding this PR further.


    real-or-random commented at 7:22 pm on September 29, 2025:
    Sure, it’s okay to keep it. I was just trying to say that CHECK is an option in general for test code.
  118. in src/unit_test.c:57 in 10eee5d1f7 outdated
    53+};
    54+
    55+/* Display options that are not printed elsewhere */
    56+static void print_args(const struct Args* args) {
    57+    printf("iterations = %d\n", COUNT);
    58+    printf("jobs = %d\n", args->num_processes);
    


    real-or-random commented at 8:03 am on September 29, 2025:
    Perhaps print “sequential execution” here additionally to jobs = 0 or instead.

    furszy commented at 6:20 pm on September 29, 2025:
    Done as suggested
  119. in src/unit_test.c:378 in 10eee5d1f7
    373+            return EXIT_FAILURE;
    374+        }
    375+
    376+        /* Compatibility Note: The first two args were the number of iterations and the seed. */
    377+        /* If provided, parse them and adjust the starting index for named arguments accordingly. */
    378+        if (argv[1] && argv[1][0] != '-') {
    


    real-or-random commented at 8:06 am on September 29, 2025:
    I think argv[1] must hold here because argc > 1. (Similar thing in the next line).

    furszy commented at 3:30 pm on September 29, 2025:
    Sure. Done as suggested.
  120. furszy force-pushed on Sep 29, 2025
  121. furszy force-pushed on Sep 29, 2025
  122. furszy force-pushed on Sep 29, 2025
  123. in src/CMakeLists.txt:142 in 9a0214bda0
    133@@ -134,12 +134,23 @@ if(SECP256K1_BUILD_BENCHMARK)
    134 endif()
    135 
    136 if(SECP256K1_BUILD_TESTS)
    137+  include(CheckIncludeFile)
    138+  check_include_file(sys/types.h HAVE_SYS_TYPES_H)
    139+  check_include_file(sys/wait.h HAVE_SYS_WAIT_H)
    140+  check_include_file(unistd.h HAVE_UNISTD_H)
    141+
    142+  set(TEST_DEFINITIONS)
    


    hebasto commented at 7:13 pm on September 29, 2025:
    0  set(TEST_DEFINITIONS "")
    

    hebasto commented at 7:17 pm on September 29, 2025:
    And it would be prudent to unset(TEST_DEFINITIONS) after its last usage for the sake of code hygiene.

    furszy commented at 8:01 pm on September 29, 2025:
    awesome, thanks!
  124. hebasto commented at 7:27 pm on September 29, 2025: member

    To fix Autotools:

     0diff --git a/Makefile.am b/Makefile.am
     1index d379c3f..dc79857 100644
     2--- a/Makefile.am
     3+++ b/Makefile.am
     4@@ -123,7 +123,7 @@ if USE_TESTS
     5 TESTS += noverify_tests
     6 noinst_PROGRAMS += noverify_tests
     7 noverify_tests_SOURCES = src/tests.c
     8-noverify_tests_CPPFLAGS = $(SECP_CONFIG_DEFINES) -DSUPPORTS_CONCURRENCY=$(SUPPORTS_CONCURRENCY)
     9+noverify_tests_CPPFLAGS = $(SECP_CONFIG_DEFINES) $(TEST_DEFINES)
    10 noverify_tests_LDADD = $(COMMON_LIB) $(PRECOMPUTED_LIB)
    11 noverify_tests_LDFLAGS = -static
    12 if !ENABLE_COVERAGE
    13diff --git a/configure.ac b/configure.ac
    14index 55eebdf..6028ee2 100644
    15--- a/configure.ac
    16+++ b/configure.ac
    17@@ -447,8 +447,8 @@ fi
    18 if test "x$enable_tests" != x"no"; then
    19   AC_CHECK_HEADERS([sys/types.h sys/wait.h unistd.h])
    20   AS_IF([test "x$ac_cv_header_sys_types_h" = xyes && test "x$ac_cv_header_sys_wait_h" = xyes &&
    21-         test "x$ac_cv_header_unistd_h" = xyes], [SUPPORTS_CONCURRENCY=1])
    22-  AC_SUBST(SUPPORTS_CONCURRENCY)
    23+         test "x$ac_cv_header_unistd_h" = xyes], [TEST_DEFINES="-DSUPPORTS_CONCURRENCY=1"], TEST_DEFINES="")
    24+  AC_SUBST(TEST_DEFINES)
    25 fi
    26 
    27 ###
    
  125. furszy force-pushed on Sep 29, 2025
  126. furszy commented at 8:01 pm on September 29, 2025: member
    Thanks @hebasto! Updated with autotools patch.
  127. in configure.ac:450 in 6aebdb4112
    442@@ -443,6 +443,14 @@ if test x"$enable_experimental" = x"no"; then
    443   fi
    444 fi
    445 
    446+# Check for concurrency support (tests only)
    447+if test "x$enable_tests" != x"no"; then
    448+  AC_CHECK_HEADERS([sys/types.h sys/wait.h unistd.h])
    449+  AS_IF([test "x$ac_cv_header_sys_types_h" = xyes && test "x$ac_cv_header_sys_wait_h" = xyes &&
    450+         test "x$ac_cv_header_unistd_h" = xyes], [TEST_DEFINES="-DSUPPORTS_CONCURRENCY=1"], TEST_DEFINES="")
    


    hebasto commented at 12:10 pm on September 30, 2025:
    0  AC_CHECK_HEADERS([sys/types.h sys/wait.h unistd.h], [TEST_DEFINES="-DSUPPORTS_CONCURRENCY=1"], [TEST_DEFINES=""])
    

    furszy commented at 1:45 pm on September 30, 2025:
    Neat. Applied. Thanks!

    hebasto commented at 3:01 pm on September 30, 2025:

    I’m terribly sorry. My suggestion was a blunder. The action-if-found is executed when one of the header files is found.

    Please revert it.

    My apologies.


    furszy commented at 3:53 pm on September 30, 2025:
    nothing to apologies, thanks for the support!

    furszy commented at 8:01 pm on September 30, 2025:
    Done. Reverted.
  128. hebasto approved
  129. hebasto commented at 1:21 pm on September 30, 2025: member

    ACK modulo #1734 (review) (sorry for overlooking that earlier).

    This discussion can be continued in #1724.

    This comment can be addressed in a follow-up PR.

  130. in src/unit_test.h:58 in 98350a7fad outdated
    40+    test_fn func;
    41+};
    42+
    43+struct tf_test_module {
    44+    const char* name;
    45+    struct tf_test_entry* data;
    


    sipa commented at 1:44 pm on September 30, 2025:

    In commit “test: introduce (mini) unit test framework”

    This could be const? (and then all the static struct tf_test_entry ...[] = { in tests.c can be const too).


    furszy commented at 8:02 pm on September 30, 2025:
    Sure. Done as suggested.
  131. furszy force-pushed on Sep 30, 2025
  132. hebasto approved
  133. hebasto commented at 1:46 pm on September 30, 2025: member

    ACK b209c65782c4b4fbfa337ecf4dfac2ee142db84b.

    UPD. See #1734 (review)

  134. in src/tests.c:7688 in b2e4749d8b outdated
    7711 
    7712-    /* find random seed */
    7713-    testrand_init(argc > 2 ? argv[2] : NULL);
    7714+/* --- Special test cases that must run before RNG initialization --- */
    7715+static struct tf_test_entry tests_no_rng[] = {
    7716+    CASE(xoshiro256pp_tests),
    


    sipa commented at 1:50 pm on September 30, 2025:
    Huh, I was not aware that trailing commas in array initializers were legal in C, but apparently they are - even in C89.

    furszy commented at 8:05 pm on September 30, 2025:
    hey, don’t take everything from me! these trailing commas are my emotional support right now.

    real-or-random commented at 7:05 am on October 1, 2025:

    Huh, I was not aware that trailing commas in array initializers were legal in C

    I beg to differ: #1169 (review) :smile:

  135. in src/unit_test.c:237 in b2e4749d8b outdated
    232+            close(pipes[it][0]); /* Close read end */
    233+            workers[it] = pid;
    234+        }
    235+    }
    236+
    237+    /* Now that we have all sub-processes, distribute workload in round-robin */
    


    sipa commented at 2:01 pm on September 30, 2025:

    In commit “test: introduce (mini) unit test framework”

    As long as there are no more than 256 tests (or some other metric to break them up by), you can use the make parallellism trick.

    Have a single pipe, which the master process writes to, and all child workers read from. The master just writers single-byte test identifiers to the pipe, and the workers read from it. Every byte will be read by exactly one worker process, which executes the test, and then reads another byte until done.

    This gives you a super cheap and portable way of actually distributing the jobs equitably (round robin risks giving all long-running tests to the same process for example).


    furszy commented at 8:06 pm on September 30, 2025:
    Super neat! Thanks for the suggestion. It’s pretty amazing that the kernel can handle the pipe synchronization for us.

    real-or-random commented at 7:20 am on October 1, 2025:

    I see you’ve implemented it already. It’s indeed neat. We currently have 89 tests [1], so there’s some room for the foreseeable future (unless we start splitting the cases up too granularly).

    [1] It is probably an attribution to the wonderful C89, which popped up here multiple times. :smile:


    theStack commented at 4:53 pm on October 2, 2025:

    We currently have 89 tests [1] …

    At the high risk of totally ruining the C joke, I’m only seeing 88 tests on the latest push (2f4546ce5610f4f7841fc2dc2eef68dbfcdcc761):

     0$ ./build/bin/tests -l | head -n 5
     1
     2Available tests (16 modules):
     3========================================
     4Module: general (5 tests)
     5        [  1] selftest_tests
     6$ ./build/bin/tests -l | tail -n 7
     7        [ 87] secp256k1_byteorder_tests
     8        [ 88] cmov_tests
     9----------------------------------------
    10
    11Run specific module: ./tests -t=<module_name>
    12Run specific test: ./tests -t=<test_name>
    

    Did you indeed have 89 earlier (or, my best guess, had the same output, but assumed the test list starts counting at zero?). Just checking that all tests are covered, though I’m pretty certain they are, based on manual verification from an earlier review round.


    real-or-random commented at 7:09 am on October 3, 2025:
    I probably just miscounted. I had counted manually using grep [-c] CASE( and grep [-c] CASE1(, trying to ignore false positives.
  136. in src/tests.c:7839 in b2e4749d8b outdated
    8007+/* Shutdown test environment */
    8008+static int teardown(void) {
    8009     free(STATIC_CTX);
    8010     secp256k1_context_destroy(CTX);
    8011 
    8012     testrand_finish();
    


    sipa commented at 2:25 pm on September 30, 2025:

    In commit “test: introduce (mini) unit test framework”

    The testrand_finish() call in here, when in parallel mode, isn’t really useful, because the actually used randomness is in the child processes, without communication to the parent where this runs.

    The point of this "random run =" output line is verifying if two test runs are actually identical (not just the seed was the same, but all output of the RNG was the same). I don’t think it’s all that important, so if it breaks with this, it might be worth just removing.


    real-or-random commented at 7:16 am on October 1, 2025:

    That’s a great catch. Concept ACK on removing the line.

    But this reminds me of the purpose of outputting the initial seed, namely reproducibility. With that in mind, the output should also contain the target parameter. I think seed, target, and jobs together should make it possible to reproduce the run exactly. Perhaps it’s nicer to simply output an entire command line like To reproduce, run ./tests --jobs ... --target ... --seed ... or something like that.


    furszy commented at 2:01 pm on October 10, 2025:

    But this reminds me of the purpose of outputting the initial seed, namely reproducibility. With that in mind, the output should also contain the target parameter. I think seed, target, and jobs together should make it possible to reproduce the run exactly. Perhaps it’s nicer to simply output an entire command line like To reproduce, run ./tests –jobs … –target … –seed … or something like that.

    Yeah sure. Will do if have to re-touch or in a tiny follow-up to not invalidate the current reviews.

  137. sipa commented at 2:37 pm on September 30, 2025: contributor

    Concept ACK

    Two overall comments:

    • I saw some discussion about it, but I’m unconvinced about ignoring unknown arguments. I think the risk of silently ignoring a misspelled option is worse than the complexity of dealing with testing across versions.
    • A big feature I’m missing is integration into the build framework, such that e.g. test invocations get automatically split by module, and have ctest --test_dir=build -j16 run the tests binary once for each module. This would automatically give well-balanced parallellism, that’s automatically integrated into CI, even for projects that include it (Bitcoin Core). Is something like that planned?
  138. hebasto commented at 2:54 pm on September 30, 2025: member
    • A big feature I’m missing is integration into the build framework, such that e.g. test invocations get automatically split by module, and have ctest --test_dir=build -j16 run the tests binary once for each module. This would automatically give well-balanced parallellism, that’s automatically integrated into CI, even for projects that include it (Bitcoin Core). Is something like that planned?

    See #1734 (comment).

  139. furszy force-pushed on Sep 30, 2025
  140. 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).
    48789dafc2
  141. test: adapt modules to the new test infrastructure
    This not only provides a structural improvement but also
    allows us to (1) specify individual tests to run and (2)
    execute each of them concurrently.
    9ec3bfe22d
  142. test: add --help for command-line options
    Add a help message for the test suite, documenting available options,
    defaults, and backward-compatible positional arguments.
    0302c1a3d7
  143. test: support running specific tests/modules targets
    Add support for specifying single tests or modules to run via the
    "--target" or "-t" command-line option. Multiple targets can be
    provided; only the specified tests or all tests in the specified
    module/s will run instead of the full suite.
    
    Examples:
    -t=<test name> runs an specific test.
    -t=<module name> runs all tests within the specified module.
    
    Both options can be provided multiple times.
    953f7b0088
  144. test: Add option to display all available tests
    Useful option to avoid opening the large tests.c file just to find
    the test case you want to run.
    95b9953ea4
  145. test: add --log option to display tests execution
    When enabled (--log=1), shows test start, completion, and execution time.
    2f4546ce56
  146. in src/unit_test.c:328 in eed1390205 outdated
    313+    }
    314+
    315+    /* Parent: write all tasks into the pipe */
    316+    close(pipefd[0]); /* close read end */
    317+    for (it = 0; it < tf->args.targets.size; it++) {
    318+        idx = (unsigned char)it;
    


    real-or-random commented at 7:21 am on October 1, 2025:
    It may be good to error out here if it > 255.

    furszy commented at 2:19 pm on October 1, 2025:

    It may be good to error out here if it > 255.

    This is already capped during the targets load but doesn’t hurt to add a more descriptive error message to it.

  147. furszy force-pushed on Oct 1, 2025
  148. furszy commented at 2:23 pm on October 1, 2025: member

    Updated per feedback. Thanks!

    1. Removed testrand_finish() call.
    2. Added a more descriptive error message for the 255 tests current cap.
  149. theStack approved
  150. theStack commented at 3:24 pm on October 2, 2025: contributor

    re-ACK 2f4546ce5610f4f7841fc2dc2eef68dbfcdcc761

    Sequential vs. parallel test runs on my arm64 workstation with 12 cores, seeing a nice ~2.7x speedup (I guess much more is possible in the futureif the longest-running tests are split up further):

     0$ uname -m -o -s -r -v
     1Linux 6.17.0-8-qcom-x1e [#8](/bitcoin-core-secp256k1/8/)-Ubuntu SMP PREEMPT_DYNAMIC Sun Aug 31 21:03:54 UTC 2025 aarch64 GNU/Linux
     2$ ./build/bin/tests --jobs=0
     3Tests running silently. Use '-log=1' to enable detailed logging
     4iterations = 16
     5jobs = 0. Sequential execution.
     6random seed = 09e628902f7e42bc5be35e96ebf5edee
     7Total execution time: 24.329 seconds
     8$ ./build/bin/tests --jobs=$(nproc)
     9Tests running silently. Use '-log=1' to enable detailed logging
    10iterations = 16
    11jobs = 12. Parallel execution.
    12random seed = 08636e4354f2ffa33260a6506827c00d
    13Total execution time: 9.002 seconds
    

    (EDIT: noted after posting that I didn’t compile in the recovery module, i.e. its 4 tests were not executed. But this doesn’t change the numbers significantly.)

  151. real-or-random approved
  152. real-or-random commented at 8:55 am on October 14, 2025: contributor

    ACK 2f4546ce5610f4f7841fc2dc2eef68dbfcdcc761

    The remaining comments can be addressed in a follow-up PR.

  153. hebasto approved
  154. hebasto commented at 11:14 pm on October 14, 2025: member

    ACK 2f4546ce5610f4f7841fc2dc2eef68dbfcdcc761.

    nit: 953f7b008865ed503f36b040c4af4db96c4d54d9 commit shouldn’t include this line:https://github.com/bitcoin-core/secp256k1/blob/953f7b008865ed503f36b040c4af4db96c4d54d9/src/unit_test.c#L183

  155. real-or-random merged this on Oct 15, 2025
  156. real-or-random closed this on Oct 15, 2025

  157. fanquake referenced this in commit 3cbf7cb3e6 on Oct 15, 2025
  158. furszy deleted the branch on Oct 15, 2025
  159. hebasto commented at 9:02 pm on October 25, 2025: member
    While reviewing #1732, I realized that the commit “refactor: move ‘gettime_i64()’ to tests_common.h” might have been a suboptimal approach. The tests do not really require nanosecond precision, and the standard clock() should suffice. Moreover, changing the gettime_i64 implementation would require unnecessary modifications to test source code and/or build targets.
  160. furszy commented at 6:03 pm on October 27, 2025: member

    While reviewing #1732, I realized that the commit “refactor: move ‘gettime_i64()’ to tests_common.h” might have been a suboptimal approach. The tests do not really require nanosecond precision, and the standard clock() should suffice. Moreover, changing the gettime_i64 implementation would require unnecessary modifications to test source code and/or build targets.

    After your CMake changes, the approach seems acceptable for the test framework as well. You are handling everything at the build-system level right now. If #1732 weren’t changing the function’s name, the test code wouldn’t require any changes.

    Apart from that, it seems we could have a separate function that retrieves the elapsed time rather than the CPU time. A small wrapper that internally calls to the other time function, providing CLOCK_REALTIME or CLOCK_MONOTONIC. But that seems like an easy change to #1732. I think we should first decide whether #1732 is the final way to go. This adaptation can be done at the end as it would be minimal.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin-core/secp256k1. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-11-03 20:15 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me