/ src / secp256k1 / src / unit_test.c
unit_test.c
  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  #include <stdio.h>
  7  #include <stdlib.h>
  8  #include <string.h>
  9  
 10  #if defined(SUPPORTS_CONCURRENCY)
 11  #include <sys/types.h>
 12  #include <sys/wait.h>
 13  #include <unistd.h>
 14  #endif
 15  
 16  #include "unit_test.h"
 17  #include "testrand.h"
 18  #include "tests_common.h"
 19  
 20  #define UNUSED(x) (void)(x)
 21  
 22  /* Number of times certain tests will run */
 23  int COUNT = 16;
 24  
 25  static int parse_jobs_count(const char* key, const char* value, struct tf_framework* tf);
 26  static int parse_iterations(const char* key, const char* value, struct tf_framework* tf);
 27  static int parse_seed(const char* key, const char* value, struct tf_framework* tf);
 28  static int parse_target(const char* key, const char* value, struct tf_framework* tf);
 29  static int parse_logging(const char* key, const char* value, struct tf_framework* tf);
 30  
 31  /* Mapping table: key -> handler */
 32  typedef int (*ArgHandler)(const char* key, const char* value, struct tf_framework* tf);
 33  struct ArgMap {
 34      const char* key;
 35      ArgHandler handler;
 36  };
 37  
 38  /*
 39   *   Main entry point for handling command-line arguments.
 40   *
 41   *   Developers should extend this map whenever new command-line
 42   *   options are introduced. Each new argument should be validated,
 43   *   converted to the appropriate type, and stored in 'tf->args' struct.
 44   */
 45  static struct ArgMap arg_map[] = {
 46      { "t", parse_target }, { "target", parse_target },
 47      { "j", parse_jobs_count }, { "jobs", parse_jobs_count },
 48      { "i", parse_iterations }, { "iterations", parse_iterations },
 49      { "seed", parse_seed },
 50      { "log", parse_logging },
 51      { NULL, NULL } /* sentinel */
 52  };
 53  
 54  /* Display options that are not printed elsewhere */
 55  static void print_args(const struct tf_args* args) {
 56      printf("iterations = %d\n", COUNT);
 57      printf("jobs = %d. %s execution.\n", args->num_processes, args->num_processes > 1 ? "Parallel" : "Sequential");
 58  }
 59  
 60  /* Main entry point for reading environment variables */
 61  static int read_env(struct tf_framework* tf) {
 62      const char* env_iter = getenv("SECP256K1_TEST_ITERS");
 63      if (env_iter && strlen(env_iter) > 0) {
 64          return parse_iterations("i", env_iter, tf);
 65      }
 66      return 0;
 67  }
 68  
 69  static int parse_arg(const char* key, const char* value, struct tf_framework* tf) {
 70      int i;
 71      for (i = 0; arg_map[i].key != NULL; i++) {
 72          if (strcmp(key, arg_map[i].key) == 0) {
 73              return arg_map[i].handler(key, value, tf);
 74          }
 75      }
 76      /* Unknown key: report just so typos don't silently pass. */
 77      fprintf(stderr, "Unknown argument '-%s=%s'\n", key, value);
 78      return -1;
 79  }
 80  
 81  static void help(void) {
 82      printf("Usage: ./tests [options]\n\n");
 83      printf("Run the test suite for the project with optional configuration.\n\n");
 84      printf("Options:\n");
 85      printf("    --help, -h                           Show this help message\n");
 86      printf("    --list_tests, -l                     Display list of all available tests and modules\n");
 87      printf("    --jobs=<num>, -j=<num>               Number of parallel worker processes (default: 0 = sequential)\n");
 88      printf("    --iterations=<num>, -i=<num>         Number of iterations for each test (default: 16)\n");
 89      printf("    --seed=<hex>                         Set a specific RNG seed (default: random)\n");
 90      printf("    --target=<test name>, -t=<name>      Run a specific test (can be provided multiple times)\n");
 91      printf("    --target=<module name>, -t=<module>  Run all tests within a specific module (can be provided multiple times)\n");
 92      printf("    --log=<0|1>                          Enable or disable test execution logging (default: 0 = disabled)\n");
 93      printf("\n");
 94      printf("Notes:\n");
 95      printf("    - All arguments must be provided in the form '--key=value', '-key=value' or '-k=value'.\n");
 96      printf("    - Single or double dashes are allowed for multi character options.\n");
 97      printf("    - Unknown arguments are reported but ignored.\n");
 98      printf("    - Sequential execution occurs if -jobs=0 or unspecified.\n");
 99      printf("    - Iterations and seed can also be passed as positional arguments before any other argument for backward compatibility.\n");
100  }
101  
102  /* Print all tests in registry */
103  static void print_test_list(struct tf_framework* tf) {
104      int m, t, total = 0;
105      printf("\nAvailable tests (%d modules):\n", tf->num_modules);
106      printf("========================================\n");
107      for (m = 0; m < tf->num_modules; m++) {
108          const struct tf_test_module* mod = &tf->registry_modules[m];
109          printf("Module: %s (%d tests)\n", mod->name, mod->size);
110          for (t = 0; t < mod->size; t++) {
111              printf("\t[%3d] %s\n", total + 1, mod->data[t].name);
112              total++;
113          }
114          printf("----------------------------------------\n");
115      }
116      printf("\nRun specific module: ./tests -t=<module_name>\n");
117      printf("Run specific test: ./tests -t=<test_name>\n\n");
118  }
119  
120  static int parse_jobs_count(const char* key, const char* value, struct tf_framework* tf) {
121      char* ptr_val;
122      long val = strtol(value, &ptr_val, 10); /* base 10 */
123      if (*ptr_val != '\0') {
124          fprintf(stderr, "Invalid number for -%s=%s\n", key, value);
125          return -1;
126      }
127      if (val < 0 || val > MAX_SUBPROCESSES) {
128          fprintf(stderr, "Arg '-%s' out of range: '%ld'. Range: 0..%d\n", key, val, MAX_SUBPROCESSES);
129          return -1;
130      }
131      tf->args.num_processes = (int) val;
132      return 0;
133  }
134  
135  static int parse_iterations(const char* key, const char* value, struct tf_framework* tf) {
136      UNUSED(key); UNUSED(tf);
137      if (!value) return 0;
138      COUNT = (int) strtol(value, NULL, 0);
139      if (COUNT <= 0) {
140          fputs("An iteration count of 0 or less is not allowed.\n", stderr);
141          return -1;
142      }
143      return 0;
144  }
145  
146  static int parse_seed(const char* key, const char* value, struct tf_framework* tf) {
147      UNUSED(key);
148      tf->args.custom_seed = (!value || strcmp(value, "NULL") == 0) ? NULL : value;
149      return 0;
150  }
151  
152  static int parse_logging(const char* key, const char* value, struct tf_framework* tf) {
153      UNUSED(key);
154      tf->args.logging = value && strcmp(value, "1") == 0;
155      return 0;
156  }
157  
158  /* Strip up to two leading dashes */
159  static const char* normalize_key(const char* arg, const char** err_msg) {
160      const char* key;
161      if (!arg || arg[0] != '-') {
162          *err_msg = "missing initial dash";
163          return NULL;
164      }
165      /* single-dash short option */
166      if (arg[1] != '-') return arg + 1;
167  
168      /* double-dash checks now */
169      if (arg[2] == '\0') {
170          *err_msg = "missing option name after double dash";
171          return NULL;
172      }
173  
174      if (arg[2] == '-') {
175          *err_msg = "too many leading dashes";
176          return NULL;
177      }
178  
179      key = arg + 2;
180      if (key[1] == '\0') {
181          *err_msg = "short option cannot use double dash";
182          return NULL;
183      }
184      return key;
185  }
186  
187  static int parse_target(const char* key, const char* value, struct tf_framework* tf) {
188      int group, idx;
189      const struct tf_test_entry* entry;
190      UNUSED(key);
191      /* Find test index in the registry */
192      for (group = 0; group < tf->num_modules; group++) {
193          const struct tf_test_module* module = &tf->registry_modules[group];
194          int add_all = strcmp(value, module->name) == 0; /* select all from module */
195          for (idx = 0; idx < module->size; idx++) {
196              entry = &module->data[idx];
197              if (add_all || strcmp(value, entry->name) == 0) {
198                  if (tf->args.targets.size >= MAX_ARGS) {
199                      fprintf(stderr, "Too many -target args (max: %d)\n", MAX_ARGS);
200                      return -1;
201                  }
202                  tf->args.targets.slots[tf->args.targets.size++] = entry;
203                  /* Matched a single test, we're done */
204                  if (!add_all) return 0;
205              }
206          }
207          /* If add_all was true, we added all tests in the module, so return */
208          if (add_all) return 0;
209      }
210      fprintf(stderr, "Error: target '%s' not found (missing or module disabled).\n"
211                      "Run program with -list_tests option to display available tests and modules.\n", value);
212      return -1;
213  }
214  
215  /* Read args: all must be in the form -key=value, --key=value or -key=value */
216  static int read_args(int argc, char** argv, int start, struct tf_framework* tf) {
217      int i;
218      const char* key;
219      const char* value;
220      char* eq;
221      const char* err_msg = "unknown error";
222      for (i = start; i < argc; i++) {
223          char* raw_arg = argv[i];
224          if (!raw_arg || raw_arg[0] != '-') {
225              fprintf(stderr, "Invalid arg '%s': must start with '-'\n", raw_arg ? raw_arg : "(null)");
226              return -1;
227          }
228  
229          key = normalize_key(raw_arg, &err_msg);
230          if (!key || *key == '\0') {
231              fprintf(stderr, "Invalid arg '%s': %s. Must be -k=value or --key=value\n", raw_arg, err_msg);
232              return -1;
233          }
234  
235          eq = strchr(raw_arg, '=');
236          if (!eq || eq == raw_arg + 1) {
237              /* Allowed options without value */
238              if (strcmp(key, "h") == 0 || strcmp(key, "help") == 0) {
239                  tf->args.help = 1;
240                  return 0;
241              }
242              if (strcmp(key, "l") == 0 || strcmp(key, "list_tests") == 0) {
243                  tf->args.list_tests = 1;
244                  return 0;
245              }
246              fprintf(stderr, "Invalid arg '%s': must be -k=value or --key=value\n", raw_arg);
247              return -1;
248          }
249  
250          *eq = '\0'; /* split key and value */
251          value = eq + 1;
252          if (!value || *value == '\0') { /* value is empty */
253              fprintf(stderr, "Invalid arg '%s': value cannot be empty\n", raw_arg);
254              return -1;
255          }
256  
257          if (parse_arg(key, value, tf) != 0) return -1;
258      }
259      return 0;
260  }
261  
262  static void run_test_log(const struct tf_test_entry* t) {
263      int64_t start_time = gettime_i64();
264      printf("Running %s..\n", t->name);
265      t->func();
266      printf("Test %s PASSED (%.3f sec)\n", t->name, (double)(gettime_i64() - start_time) / 1000000);
267  }
268  
269  static void run_test(const struct tf_test_entry* t) { t->func(); }
270  
271  /* Process tests in sequential order */
272  static int run_sequential(struct tf_framework* tf) {
273      int it;
274      for (it = 0; it < tf->args.targets.size; it++) {
275          tf->fn_run_test(tf->args.targets.slots[it]);
276      }
277      return EXIT_SUCCESS;
278  }
279  
280  #if defined(SUPPORTS_CONCURRENCY)
281  static const int MAX_TARGETS = 255;
282  
283  /* Process tests in parallel */
284  static int run_concurrent(struct tf_framework* tf) {
285      /* Sub-processes info */
286      pid_t workers[MAX_SUBPROCESSES];
287      int pipefd[2];
288      int status = EXIT_SUCCESS;
289      int it; /* loop iterator */
290      unsigned char idx; /* test index */
291  
292      if (tf->args.targets.size > MAX_TARGETS) {
293          fprintf(stderr, "Internal Error: the number of targets (%d) exceeds the maximum supported (%d). "
294                  "If you need more, extend 'run_concurrent()' to handle additional targets.\n",
295                  tf->args.targets.size, MAX_TARGETS);
296          exit(EXIT_FAILURE);
297      }
298  
299  
300      if (pipe(pipefd) != 0) {
301          perror("Error during pipe setup");
302          return EXIT_FAILURE;
303      }
304  
305      /* Launch worker processes */
306      for (it = 0; it < tf->args.num_processes; it++) {
307          pid_t pid = fork();
308          if (pid < 0) {
309              perror("Error during process fork");
310              return EXIT_FAILURE;
311          }
312          if (pid == 0) {
313              /* Child worker: read jobs from the shared pipe */
314              close(pipefd[1]); /* children never write */
315              while (read(pipefd[0], &idx, sizeof(idx)) == sizeof(idx)) {
316                  tf->fn_run_test(tf->args.targets.slots[(int)idx]);
317              }
318              _exit(EXIT_SUCCESS); /* finish child process */
319          } else {
320              /* Parent: save worker pid */
321              workers[it] = pid;
322          }
323      }
324  
325      /* Parent: write all tasks into the pipe */
326      close(pipefd[0]); /* close read end */
327      for (it = 0; it < tf->args.targets.size; it++) {
328          idx = (unsigned char)it;
329          if (write(pipefd[1], &idx, sizeof(idx)) == -1) {
330              perror("Error during workload distribution");
331              close(pipefd[1]);
332              return EXIT_FAILURE;
333          }
334      }
335      /* Close write end to signal EOF */
336      close(pipefd[1]);
337      /* Wait for all workers */
338      for (it = 0; it < tf->args.num_processes; it++) {
339          int ret = 0;
340          if (waitpid(workers[it], &ret, 0) == -1 || ret != 0) {
341              status = EXIT_FAILURE;
342          }
343      }
344  
345      return status;
346  }
347  #endif
348  
349  static int tf_init(struct tf_framework* tf, int argc, char** argv)
350  {
351      /* Caller must set the registry and its size before calling tf_init */
352      if (tf->registry_modules == NULL || tf->num_modules <= 0) {
353          fprintf(stderr, "Error: tests registry not provided or empty\n");
354          return EXIT_FAILURE;
355      }
356  
357      /* Initialize command-line options */
358      tf->args.num_processes = 0;
359      tf->args.custom_seed = NULL;
360      tf->args.help = 0;
361      tf->args.targets.size = 0;
362      tf->args.list_tests = 0;
363      tf->args.logging = 0;
364  
365      /* Disable buffering for stdout to improve reliability of getting
366       * diagnostic information. Happens right at the start of main because
367       * setbuf must be used before any other operation on the stream. */
368      setbuf(stdout, NULL);
369      /* Also disable buffering for stderr because it's not guaranteed that it's
370       * unbuffered on all systems. */
371      setbuf(stderr, NULL);
372  
373      /* Parse env args */
374      if (read_env(tf) != 0) return EXIT_FAILURE;
375  
376      /* Parse command-line args */
377      if (argc > 1) {
378          int named_arg_start = 1; /* index to begin processing named arguments */
379          if (argc - 1 > MAX_ARGS) { /* first arg is always the binary path */
380              fprintf(stderr, "Too many command-line arguments (max: %d)\n", MAX_ARGS);
381              return EXIT_FAILURE;
382          }
383  
384          /* Compatibility Note: The first two args were the number of iterations and the seed. */
385          /* If provided, parse them and adjust the starting index for named arguments accordingly. */
386          if (argv[1][0] != '-') {
387              int has_seed = argc > 2 && argv[2][0] != '-';
388              if (parse_iterations("i", argv[1], tf) != 0) return EXIT_FAILURE;
389              if (has_seed) parse_seed("seed", argv[2], tf);
390              named_arg_start = has_seed ? 3 : 2;
391          }
392          if (read_args(argc, argv, named_arg_start, tf) != 0) {
393              return EXIT_FAILURE;
394          }
395  
396          if (tf->args.help) {
397              help();
398              exit(EXIT_SUCCESS);
399          }
400  
401          if (tf->args.list_tests) {
402              print_test_list(tf);
403              exit(EXIT_SUCCESS);
404          }
405      }
406  
407      tf->fn_run_test = tf->args.logging ? run_test_log : run_test;
408      return EXIT_SUCCESS;
409  }
410  
411  static int tf_run(struct tf_framework* tf) {
412      /* Process exit status */
413      int status;
414      /* Whether to run all tests */
415      int run_all;
416      /* Loop iterator */
417      int it;
418      /* Initial test time */
419      int64_t start_time = gettime_i64();
420      /* Verify 'tf_init' has been called */
421      if (!tf->fn_run_test) {
422          fprintf(stderr, "Error: No test runner set. You must call 'tf_init' first to initialize the framework "
423                          "or manually assign 'fn_run_test' before calling 'tf_run'.\n");
424          return EXIT_FAILURE;
425      }
426  
427      /* Populate targets with all tests if none were explicitly specified */
428      run_all = tf->args.targets.size == 0;
429      if (run_all) {
430          int group, idx;
431          for (group = 0; group < tf->num_modules; group++) {
432              const struct tf_test_module* module = &tf->registry_modules[group];
433              for (idx = 0; idx < module->size; idx++) {
434                  if (tf->args.targets.size >= MAX_ARGS) {
435                      fprintf(stderr, "Internal Error: Number of tests (%d) exceeds MAX_ARGS (%d). "
436                                      "Increase MAX_ARGS to accommodate all tests.\n", tf->args.targets.size, MAX_ARGS);
437                      return EXIT_FAILURE;
438                  }
439                  tf->args.targets.slots[tf->args.targets.size++] = &module->data[idx];
440              }
441          }
442      }
443  
444      if (!tf->args.logging) printf("Tests running silently. Use '-log=1' to enable detailed logging\n");
445  
446      /* Log configuration */
447      print_args(&tf->args);
448  
449      /* Run test RNG tests (must run before we really initialize the test RNG) */
450      /* Note: currently, these tests are executed sequentially because there */
451      /* is really only one test. */
452      for (it = 0; tf->registry_no_rng && it < tf->registry_no_rng->size; it++) {
453          if (run_all) { /* future: support filtering */
454              tf->fn_run_test(&tf->registry_no_rng->data[it]);
455          }
456      }
457  
458      /* Initialize test RNG and library contexts */
459      testrand_init(tf->args.custom_seed);
460      if (tf->fn_setup && tf->fn_setup() != 0) return EXIT_FAILURE;
461  
462      /* Check whether to process tests sequentially or concurrently */
463      if (tf->args.num_processes <= 1) {
464          status = run_sequential(tf);
465      } else {
466  #if defined(SUPPORTS_CONCURRENCY)
467          status = run_concurrent(tf);
468  #else
469          fputs("Parallel execution not supported on your system. Running sequentially...\n", stderr);
470          status = run_sequential(tf);
471  #endif
472      }
473  
474      /* Print accumulated time */
475      printf("Total execution time: %.3f seconds\n", (double)(gettime_i64() - start_time) / 1000000);
476      if (tf->fn_teardown && tf->fn_teardown() != 0) return EXIT_FAILURE;
477  
478      return status;
479  }