LCOV - code coverage report
Current view: top level - testing - testing_api_cmd_reserve_history.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 60.7 % 229 139
Test Date: 2026-04-14 15:39:31 Functions: 100.0 % 7 7

            Line data    Source code
       1              : /*
       2              :   This file is part of TALER
       3              :   Copyright (C) 2014-2024 Taler Systems SA
       4              : 
       5              :   TALER is free software; you can redistribute it and/or modify
       6              :   it under the terms of the GNU General Public License as
       7              :   published by the Free Software Foundation; either version 3, or
       8              :   (at your option) any later version.
       9              : 
      10              :   TALER is distributed in the hope that it will be useful, but
      11              :   WITHOUT ANY WARRANTY; without even the implied warranty of
      12              :   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      13              :   GNU General Public License for more details.
      14              : 
      15              :   You should have received a copy of the GNU General Public
      16              :   License along with TALER; see the file COPYING.  If not, see
      17              :   <http://www.gnu.org/licenses/>
      18              : */
      19              : /**
      20              :  * @file testing/testing_api_cmd_reserve_history.c
      21              :  * @brief Implement the /reserve/history test command.
      22              :  * @author Marcello Stanisci
      23              :  */
      24              : #include "taler/taler_json_lib.h"
      25              : #include <gnunet/gnunet_curl_lib.h>
      26              : #include "taler/taler_testing_lib.h"
      27              : 
      28              : 
      29              : /**
      30              :  * State for a "history" CMD.
      31              :  */
      32              : struct HistoryState
      33              : {
      34              : 
      35              :   /**
      36              :    * Public key of the reserve being analyzed.
      37              :    */
      38              :   struct TALER_ReservePublicKeyP reserve_pub;
      39              : 
      40              :   /**
      41              :    * Label to the command which created the reserve to check,
      42              :    * needed to resort the reserve key.
      43              :    */
      44              :   const char *reserve_reference;
      45              : 
      46              :   /**
      47              :    * Handle to the "reserve history" operation.
      48              :    */
      49              :   struct TALER_EXCHANGE_GetReservesHistoryHandle *rsh;
      50              : 
      51              :   /**
      52              :    * Expected reserve balance.
      53              :    */
      54              :   const char *expected_balance;
      55              : 
      56              :   /**
      57              :    * Private key of the reserve being analyzed.
      58              :    */
      59              :   const struct TALER_ReservePrivateKeyP *reserve_priv;
      60              : 
      61              :   /**
      62              :    * Interpreter state.
      63              :    */
      64              :   struct TALER_TESTING_Interpreter *is;
      65              : 
      66              :   /**
      67              :    * Expected HTTP response code.
      68              :    */
      69              :   unsigned int expected_response_code;
      70              : 
      71              : };
      72              : 
      73              : 
      74              : /**
      75              :  * Closure for analysis_cb().
      76              :  */
      77              : struct AnalysisContext
      78              : {
      79              :   /**
      80              :    * Reserve public key we are looking at.
      81              :    */
      82              :   const struct TALER_ReservePublicKeyP *reserve_pub;
      83              : 
      84              :   /**
      85              :    * Length of the @e history array.
      86              :    */
      87              :   unsigned int history_length;
      88              : 
      89              :   /**
      90              :    * Array of history items to match.
      91              :    */
      92              :   const struct TALER_EXCHANGE_ReserveHistoryEntry *history;
      93              : 
      94              :   /**
      95              :    * Array of @e history_length of matched entries.
      96              :    */
      97              :   bool *found;
      98              : 
      99              :   /**
     100              :    * Set to true if an entry could not be found.
     101              :    */
     102              :   bool failure;
     103              : };
     104              : 
     105              : 
     106              : /**
     107              :  * Compare @a h1 and @a h2.
     108              :  *
     109              :  * @param h1 a history entry
     110              :  * @param h2 a history entry
     111              :  * @return 0 if @a h1 and @a h2 are equal
     112              :  */
     113              : static int
     114           23 : history_entry_cmp (
     115              :   const struct TALER_EXCHANGE_ReserveHistoryEntry *h1,
     116              :   const struct TALER_EXCHANGE_ReserveHistoryEntry *h2)
     117              : {
     118           23 :   if (h1->type != h2->type)
     119            0 :     return 1;
     120           23 :   switch (h1->type)
     121              :   {
     122            8 :   case TALER_EXCHANGE_RTT_CREDIT:
     123            8 :     if ( (0 ==
     124            8 :           TALER_amount_cmp (&h1->amount,
     125            8 :                             &h2->amount)) &&
     126              :          (0 ==
     127            8 :           TALER_full_payto_cmp (h1->details.in_details.sender_url,
     128            8 :                                 h2->details.in_details.sender_url)) &&
     129            8 :          (h1->details.in_details.wire_reference ==
     130            8 :           h2->details.in_details.wire_reference) &&
     131            8 :          (GNUNET_TIME_timestamp_cmp (h1->details.in_details.timestamp,
     132              :                                      ==,
     133              :                                      h2->details.in_details.timestamp)) )
     134            8 :       return 0;
     135            0 :     return 1;
     136            7 :   case TALER_EXCHANGE_RTT_WITHDRAWAL:
     137            7 :     if ( (0 ==
     138            7 :           TALER_amount_cmp (&h1->amount,
     139            7 :                             &h2->amount)) &&
     140              :          (0 ==
     141            7 :           TALER_amount_cmp (&h1->details.withdraw.fee,
     142            7 :                             &h2->details.withdraw.fee)) &&
     143            7 :          (h1->details.withdraw.age_restricted ==
     144            7 :           h2->details.withdraw.age_restricted) &&
     145            7 :          ((! h1->details.withdraw.age_restricted) ||
     146            0 :           (h1->details.withdraw.max_age == h2->details.withdraw.max_age) ))
     147            7 :       return 0;
     148            0 :     return 1;
     149            0 :   case TALER_EXCHANGE_RTT_RECOUP:
     150              :     /* exchange_sig, exchange_pub and timestamp are NOT available
     151              :        from the original recoup response, hence here NOT check(able/ed) */
     152            0 :     if ( (0 ==
     153            0 :           TALER_amount_cmp (&h1->amount,
     154            0 :                             &h2->amount)) &&
     155              :          (0 ==
     156            0 :           GNUNET_memcmp (&h1->details.recoup_details.coin_pub,
     157              :                          &h2->details.recoup_details.coin_pub)) )
     158            0 :       return 0;
     159            0 :     return 1;
     160            0 :   case TALER_EXCHANGE_RTT_CLOSING:
     161              :     /* testing_api_cmd_exec_closer doesn't set the
     162              :        receiver_account_details, exchange_sig, exchange_pub or wtid or timestamp
     163              :        so we cannot test for it here. but if the amount matches,
     164              :        that should be good enough. */
     165            0 :     if ( (0 ==
     166            0 :           TALER_amount_cmp (&h1->amount,
     167            0 :                             &h2->amount)) &&
     168              :          (0 ==
     169            0 :           TALER_amount_cmp (&h1->details.close_details.fee,
     170              :                             &h2->details.close_details.fee)) )
     171            0 :       return 0;
     172            0 :     return 1;
     173            8 :   case TALER_EXCHANGE_RTT_MERGE:
     174            8 :     if ( (0 ==
     175            8 :           TALER_amount_cmp (&h1->amount,
     176            8 :                             &h2->amount)) &&
     177              :          (0 ==
     178            8 :           TALER_amount_cmp (&h1->details.merge_details.purse_fee,
     179            8 :                             &h2->details.merge_details.purse_fee)) &&
     180            8 :          (GNUNET_TIME_timestamp_cmp (h1->details.merge_details.merge_timestamp,
     181              :                                      ==,
     182              :                                      h2->details.merge_details.merge_timestamp))
     183            8 :          &&
     184            8 :          (GNUNET_TIME_timestamp_cmp (h1->details.merge_details.purse_expiration,
     185              :                                      ==,
     186              :                                      h2->details.merge_details.purse_expiration)
     187              :          )
     188            8 :          &&
     189              :          (0 ==
     190            8 :           GNUNET_memcmp (&h1->details.merge_details.merge_pub,
     191            8 :                          &h2->details.merge_details.merge_pub)) &&
     192              :          (0 ==
     193            8 :           GNUNET_memcmp (&h1->details.merge_details.h_contract_terms,
     194            8 :                          &h2->details.merge_details.h_contract_terms)) &&
     195              :          (0 ==
     196            8 :           GNUNET_memcmp (&h1->details.merge_details.purse_pub,
     197            8 :                          &h2->details.merge_details.purse_pub)) &&
     198              :          (0 ==
     199            8 :           GNUNET_memcmp (&h1->details.merge_details.reserve_sig,
     200            8 :                          &h2->details.merge_details.reserve_sig)) &&
     201            8 :          (h1->details.merge_details.min_age ==
     202            8 :           h2->details.merge_details.min_age) &&
     203            8 :          (h1->details.merge_details.flags ==
     204            8 :           h2->details.merge_details.flags) )
     205            8 :       return 0;
     206            0 :     return 1;
     207            0 :   case TALER_EXCHANGE_RTT_OPEN:
     208            0 :     if ( (0 ==
     209            0 :           TALER_amount_cmp (&h1->amount,
     210            0 :                             &h2->amount)) &&
     211            0 :          (GNUNET_TIME_timestamp_cmp (
     212              :             h1->details.open_request.request_timestamp,
     213              :             ==,
     214            0 :             h2->details.open_request.request_timestamp)) &&
     215            0 :          (GNUNET_TIME_timestamp_cmp (
     216              :             h1->details.open_request.reserve_expiration,
     217              :             ==,
     218            0 :             h2->details.open_request.reserve_expiration)) &&
     219            0 :          (h1->details.open_request.purse_limit ==
     220            0 :           h2->details.open_request.purse_limit) &&
     221              :          (0 ==
     222            0 :           TALER_amount_cmp (&h1->details.open_request.reserve_payment,
     223            0 :                             &h2->details.open_request.reserve_payment)) &&
     224              :          (0 ==
     225            0 :           GNUNET_memcmp (&h1->details.open_request.reserve_sig,
     226              :                          &h2->details.open_request.reserve_sig)) )
     227            0 :       return 0;
     228            0 :     return 1;
     229            0 :   case TALER_EXCHANGE_RTT_CLOSE:
     230            0 :     if ( (0 ==
     231            0 :           TALER_amount_cmp (&h1->amount,
     232            0 :                             &h2->amount)) &&
     233            0 :          (GNUNET_TIME_timestamp_cmp (
     234              :             h1->details.close_request.request_timestamp,
     235              :             ==,
     236            0 :             h2->details.close_request.request_timestamp)) &&
     237              :          (0 ==
     238            0 :           GNUNET_memcmp (&h1->details.close_request.target_account_h_payto,
     239            0 :                          &h2->details.close_request.target_account_h_payto)) &&
     240              :          (0 ==
     241            0 :           GNUNET_memcmp (&h1->details.close_request.reserve_sig,
     242              :                          &h2->details.close_request.reserve_sig)) )
     243            0 :       return 0;
     244            0 :     return 1;
     245              :   }
     246            0 :   GNUNET_assert (0);
     247              :   return 1;
     248              : }
     249              : 
     250              : 
     251              : /**
     252              :  * Check if @a cmd changed the reserve, if so, find the
     253              :  * entry in our history and set the respective index in found
     254              :  * to true. If the entry is not found, set failure.
     255              :  *
     256              :  * @param cls our `struct AnalysisContext *`
     257              :  * @param cmd command to analyze for impact on history
     258              :  */
     259              : static void
     260          584 : analyze_command (void *cls,
     261              :                  const struct TALER_TESTING_Command *cmd)
     262              : {
     263          584 :   struct AnalysisContext *ac = cls;
     264          584 :   const struct TALER_ReservePublicKeyP *reserve_pub = ac->reserve_pub;
     265          584 :   const struct TALER_EXCHANGE_ReserveHistoryEntry *history = ac->history;
     266          584 :   unsigned int history_length = ac->history_length;
     267          584 :   bool *found = ac->found;
     268              : 
     269          584 :   if (TALER_TESTING_cmd_is_batch (cmd))
     270              :   {
     271              :     struct TALER_TESTING_Command *cur;
     272              :     struct TALER_TESTING_Command *bcmd;
     273              : 
     274           47 :     cur = TALER_TESTING_cmd_batch_get_current (cmd);
     275           47 :     if (GNUNET_OK !=
     276           47 :         TALER_TESTING_get_trait_batch_cmds (cmd,
     277              :                                             &bcmd))
     278              :     {
     279            0 :       GNUNET_break (0);
     280            0 :       ac->failure = true;
     281            0 :       return;
     282              :     }
     283          552 :     for (unsigned int i = 0; NULL != bcmd[i].label; i++)
     284              :     {
     285          513 :       struct TALER_TESTING_Command *step = &bcmd[i];
     286              : 
     287          513 :       analyze_command (ac,
     288              :                        step);
     289          513 :       if (ac->failure)
     290              :       {
     291            0 :         GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     292              :                     "Entry for batch step `%s' missing in reserve history\n",
     293              :                     step->label);
     294            0 :         return;
     295              :       }
     296          513 :       if (step == cur)
     297            8 :         break; /* if *we* are in a batch, make sure not to analyze commands past 'now' */
     298              :     }
     299           47 :     return;
     300              :   }
     301              : 
     302              :   {
     303              :     const struct TALER_ReservePublicKeyP *rp;
     304          537 :     bool matched = false;
     305              : 
     306          537 :     if (GNUNET_OK !=
     307          537 :         TALER_TESTING_get_trait_reserve_pub (cmd,
     308              :                                              &rp))
     309          514 :       return; /* command does nothing for reserves */
     310          104 :     if (0 !=
     311          104 :         GNUNET_memcmp (rp,
     312              :                        reserve_pub))
     313           66 :       return; /* command affects some _other_ reserve */
     314           38 :     for (unsigned int j = 0; true; j++)
     315            0 :     {
     316              :       const struct TALER_EXCHANGE_ReserveHistoryEntry *he;
     317              : 
     318           38 :       if (GNUNET_OK !=
     319           38 :           TALER_TESTING_get_trait_reserve_history (cmd,
     320              :                                                    j,
     321              :                                                    &he))
     322              :       {
     323              :         /* NOTE: only for debugging... */
     324           15 :         GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
     325              :                     "Command `%s' has the reserve_pub, but lacks reserve history trait for index #%u\n",
     326              :                     cmd->label,
     327              :                     j);
     328           15 :         return; /* command does nothing for reserves */
     329              :       }
     330           47 :       for (unsigned int i = 0; i<history_length; i++)
     331              :       {
     332           47 :         if (found[i])
     333           24 :           continue; /* already found, skip */
     334           23 :         if (0 ==
     335           23 :             history_entry_cmp (he,
     336           23 :                                &history[i]))
     337              :         {
     338           23 :           found[i] = true;
     339           23 :           matched = true;
     340           23 :           ac->failure = false;
     341           23 :           break;
     342              :         }
     343              :       }
     344           23 :       if (matched)
     345           23 :         break;
     346              :     }
     347           23 :     if (! matched)
     348              :     {
     349            0 :       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
     350              :                   "Command `%s' no relevant reserve history entry not found\n",
     351              :                   cmd->label);
     352            0 :       ac->failure = true;
     353              :       ;
     354              :     }
     355              :   }
     356              : }
     357              : 
     358              : 
     359              : /**
     360              :  * Check that the reserve balance and HTTP response code are
     361              :  * both acceptable.
     362              :  *
     363              :  * @param cls closure.
     364              :  * @param rs HTTP response details
     365              :  */
     366              : static void
     367            8 : reserve_history_cb (void *cls,
     368              :                     const struct TALER_EXCHANGE_GetReservesHistoryResponse *rs)
     369              : {
     370            8 :   struct HistoryState *ss = cls;
     371            8 :   struct TALER_TESTING_Interpreter *is = ss->is;
     372              :   struct TALER_Amount eb;
     373              : 
     374            8 :   ss->rsh = NULL;
     375            8 :   if (ss->expected_response_code != rs->hr.http_status)
     376              :   {
     377            0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     378              :                 "Unexpected HTTP response code: %d in %s:%u\n",
     379              :                 rs->hr.http_status,
     380              :                 __FILE__,
     381              :                 __LINE__);
     382            0 :     json_dumpf (rs->hr.reply,
     383              :                 stderr,
     384              :                 0);
     385            0 :     TALER_TESTING_interpreter_fail (ss->is);
     386            0 :     return;
     387              :   }
     388            8 :   if (MHD_HTTP_OK != rs->hr.http_status)
     389              :   {
     390            0 :     TALER_TESTING_interpreter_next (is);
     391            0 :     return;
     392              :   }
     393            8 :   GNUNET_assert (GNUNET_OK ==
     394              :                  TALER_string_to_amount (ss->expected_balance,
     395              :                                          &eb));
     396              : 
     397            8 :   if (0 != TALER_amount_cmp (&eb,
     398              :                              &rs->details.ok.balance))
     399              :   {
     400            0 :     GNUNET_break (0);
     401            0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     402              :                 "Unexpected amount in reserve: %s\n",
     403              :                 TALER_amount_to_string (&rs->details.ok.balance));
     404            0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     405              :                 "Expected balance of: %s\n",
     406              :                 TALER_amount_to_string (&eb));
     407            0 :     TALER_TESTING_interpreter_fail (ss->is);
     408            0 :     return;
     409              :   }
     410            8 :   {
     411            8 :     bool found[rs->details.ok.history_len];
     412            8 :     struct AnalysisContext ac = {
     413            8 :       .reserve_pub = &ss->reserve_pub,
     414            8 :       .history = rs->details.ok.history,
     415            8 :       .history_length = rs->details.ok.history_len,
     416              :       .found = found
     417              :     };
     418              : 
     419            8 :     memset (found,
     420              :             0,
     421              :             sizeof (found));
     422            8 :     TALER_TESTING_iterate (is,
     423              :                            true,
     424              :                            &analyze_command,
     425              :                            &ac);
     426            8 :     if (ac.failure)
     427              :     {
     428            0 :       json_dumpf (rs->hr.reply,
     429              :                   stderr,
     430              :                   JSON_INDENT (2));
     431            0 :       TALER_TESTING_interpreter_fail (ss->is);
     432            0 :       return;
     433              :     }
     434           31 :     for (unsigned int i = 0; i<rs->details.ok.history_len; i++)
     435              :     {
     436           23 :       if (found[i])
     437           23 :         continue;
     438            0 :       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     439              :                   "History entry at index %u of type %d not justified by command history\n",
     440              :                   i,
     441              :                   rs->details.ok.history[i].type);
     442            0 :       json_dumpf (rs->hr.reply,
     443              :                   stderr,
     444              :                   JSON_INDENT (2));
     445            0 :       TALER_TESTING_interpreter_fail (ss->is);
     446            0 :       return;
     447              :     }
     448              :   }
     449            8 :   TALER_TESTING_interpreter_next (is);
     450              : }
     451              : 
     452              : 
     453              : /**
     454              :  * Run the command.
     455              :  *
     456              :  * @param cls closure.
     457              :  * @param cmd the command being executed.
     458              :  * @param is the interpreter state.
     459              :  */
     460              : static void
     461            8 : history_run (void *cls,
     462              :              const struct TALER_TESTING_Command *cmd,
     463              :              struct TALER_TESTING_Interpreter *is)
     464              : {
     465            8 :   struct HistoryState *ss = cls;
     466              :   const struct TALER_TESTING_Command *create_reserve;
     467              : 
     468            8 :   ss->is = is;
     469              :   create_reserve
     470            8 :     = TALER_TESTING_interpreter_lookup_command (is,
     471              :                                                 ss->reserve_reference);
     472            8 :   if (NULL == create_reserve)
     473              :   {
     474            0 :     GNUNET_break (0);
     475            0 :     TALER_TESTING_interpreter_fail (is);
     476            0 :     return;
     477              :   }
     478            8 :   if (GNUNET_OK !=
     479            8 :       TALER_TESTING_get_trait_reserve_priv (create_reserve,
     480              :                                             &ss->reserve_priv))
     481              :   {
     482            0 :     GNUNET_break (0);
     483            0 :     TALER_LOG_ERROR ("Failed to find reserve_priv for history query\n");
     484            0 :     TALER_TESTING_interpreter_fail (is);
     485            0 :     return;
     486              :   }
     487            8 :   GNUNET_CRYPTO_eddsa_key_get_public (&ss->reserve_priv->eddsa_priv,
     488              :                                       &ss->reserve_pub.eddsa_pub);
     489            8 :   ss->rsh = TALER_EXCHANGE_get_reserves_history_create (
     490              :     TALER_TESTING_interpreter_get_context (is),
     491              :     TALER_TESTING_get_exchange_url (is),
     492              :     TALER_TESTING_get_keys (is),
     493              :     ss->reserve_priv);
     494            8 :   if (NULL == ss->rsh)
     495              :   {
     496            0 :     GNUNET_break (0);
     497            0 :     TALER_TESTING_interpreter_fail (is);
     498            0 :     return;
     499              :   }
     500            8 :   if (TALER_EC_NONE !=
     501            8 :       TALER_EXCHANGE_get_reserves_history_start (ss->rsh,
     502              :                                                  &reserve_history_cb,
     503              :                                                  ss))
     504              :   {
     505            0 :     GNUNET_break (0);
     506            0 :     TALER_EXCHANGE_get_reserves_history_cancel (ss->rsh);
     507            0 :     ss->rsh = NULL;
     508            0 :     TALER_TESTING_interpreter_fail (is);
     509            0 :     return;
     510              :   }
     511              : }
     512              : 
     513              : 
     514              : /**
     515              :  * Offer internal data from a "history" CMD, to other commands.
     516              :  *
     517              :  * @param cls closure.
     518              :  * @param[out] ret result.
     519              :  * @param trait name of the trait.
     520              :  * @param index index number of the object to offer.
     521              :  * @return #GNUNET_OK on success.
     522              :  */
     523              : static enum GNUNET_GenericReturnValue
     524           24 : history_traits (void *cls,
     525              :                 const void **ret,
     526              :                 const char *trait,
     527              :                 unsigned int index)
     528              : {
     529           24 :   struct HistoryState *hs = cls;
     530              :   struct TALER_TESTING_Trait traits[] = {
     531           24 :     TALER_TESTING_make_trait_reserve_pub (&hs->reserve_pub),
     532           24 :     TALER_TESTING_trait_end ()
     533              :   };
     534              : 
     535           24 :   return TALER_TESTING_get_trait (traits,
     536              :                                   ret,
     537              :                                   trait,
     538              :                                   index);
     539              : }
     540              : 
     541              : 
     542              : /**
     543              :  * Cleanup the state from a "reserve history" CMD, and possibly
     544              :  * cancel a pending operation thereof.
     545              :  *
     546              :  * @param cls closure.
     547              :  * @param cmd the command which is being cleaned up.
     548              :  */
     549              : static void
     550            8 : history_cleanup (void *cls,
     551              :                  const struct TALER_TESTING_Command *cmd)
     552              : {
     553            8 :   struct HistoryState *ss = cls;
     554              : 
     555            8 :   if (NULL != ss->rsh)
     556              :   {
     557            0 :     TALER_TESTING_command_incomplete (ss->is,
     558              :                                       cmd->label);
     559            0 :     TALER_EXCHANGE_get_reserves_history_cancel (ss->rsh);
     560            0 :     ss->rsh = NULL;
     561              :   }
     562            8 :   GNUNET_free (ss);
     563            8 : }
     564              : 
     565              : 
     566              : struct TALER_TESTING_Command
     567            8 : TALER_TESTING_cmd_reserve_history (const char *label,
     568              :                                    const char *reserve_reference,
     569              :                                    const char *expected_balance,
     570              :                                    unsigned int expected_response_code)
     571              : {
     572              :   struct HistoryState *ss;
     573              : 
     574            8 :   GNUNET_assert (NULL != reserve_reference);
     575            8 :   ss = GNUNET_new (struct HistoryState);
     576            8 :   ss->reserve_reference = reserve_reference;
     577            8 :   ss->expected_balance = expected_balance;
     578            8 :   ss->expected_response_code = expected_response_code;
     579              :   {
     580            8 :     struct TALER_TESTING_Command cmd = {
     581              :       .cls = ss,
     582              :       .label = label,
     583              :       .run = &history_run,
     584              :       .cleanup = &history_cleanup,
     585              :       .traits = &history_traits
     586              :     };
     587              : 
     588            8 :     return cmd;
     589              :   }
     590              : }
        

Generated by: LCOV version 2.0-1