summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/exchange/taler-exchange-expire.c41
-rw-r--r--src/exchange/taler-exchange-httpd.h4
-rw-r--r--src/exchange/taler-exchange-httpd_purses_get.c31
-rw-r--r--src/exchangedb/common-0001.sql8
-rw-r--r--src/exchangedb/exchange-0001-part.sql29
-rw-r--r--src/include/taler_testing_lib.h24
-rw-r--r--src/lib/exchange_api_purses_get.c5
-rw-r--r--src/testing/Makefile.am2
-rw-r--r--src/testing/test_exchange_api-cs.conf8
-rw-r--r--src/testing/test_exchange_api-rsa.conf9
-rw-r--r--src/testing/test_exchange_p2p.c58
-rw-r--r--src/testing/testing_api_cmd_exec_expire.c162
-rw-r--r--src/testing/testing_api_cmd_exec_router.c161
-rw-r--r--src/testing/testing_api_cmd_purse_get.c1
14 files changed, 517 insertions, 26 deletions
diff --git a/src/exchange/taler-exchange-expire.c b/src/exchange/taler-exchange-expire.c
index c7691930b..b5df64a77 100644
--- a/src/exchange/taler-exchange-expire.c
+++ b/src/exchange/taler-exchange-expire.c
@@ -95,6 +95,12 @@ static int global_ret;
*/
static int test_mode;
+/**
+ * If this is a first-time run, we immediately
+ * try to catch up with the present.
+ */
+static bool jump_mode;
+
/**
* Select a shard to work on.
@@ -188,6 +194,7 @@ static void
release_shard (struct Shard *s)
{
enum GNUNET_DB_QueryStatus qs;
+ unsigned long long wc = (unsigned long long) s->work_counter;
qs = db_plugin->complete_shard (
db_plugin->cls,
@@ -209,10 +216,14 @@ release_shard (struct Shard *s)
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Purse expiration shard completed with %llu purses\n",
- (unsigned long long) s->work_counter);
+ wc);
/* normal case */
break;
}
+ if ( (0 == wc) &&
+ (test_mode) &&
+ (! jump_mode) )
+ GNUNET_SCHEDULER_shutdown ();
}
@@ -262,13 +273,16 @@ run_expire (void *cls)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to obtain database connection!\n");
+ abort_shard (s);
global_ret = EXIT_FAILURE;
GNUNET_SCHEDULER_shutdown ();
return;
}
- if (db_plugin->start (db_plugin->cls,
+ if (GNUNET_OK !=
+ db_plugin->start (db_plugin->cls,
"expire-purse"))
{
+ GNUNET_break (0);
global_ret = EXIT_FAILURE;
db_plugin->rollback (db_plugin->cls);
abort_shard (s);
@@ -290,6 +304,7 @@ run_expire (void *cls)
case GNUNET_DB_STATUS_SOFT_ERROR:
db_plugin->rollback (db_plugin->cls);
abort_shard (s);
+ GNUNET_assert (NULL == task);
task = GNUNET_SCHEDULER_add_now (&run_shard,
NULL);
return;
@@ -303,6 +318,7 @@ run_expire (void *cls)
{
release_shard (s);
}
+ GNUNET_assert (NULL == task);
task = GNUNET_SCHEDULER_add_now (&run_shard,
NULL);
return;
@@ -310,6 +326,7 @@ run_expire (void *cls)
/* commit, and go again immediately */
s->work_counter++;
(void) commit_or_warn ();
+ GNUNET_assert (NULL == task);
task = GNUNET_SCHEDULER_add_now (&run_expire,
s);
}
@@ -343,9 +360,15 @@ run_shard (void *cls)
qs = db_plugin->begin_shard (db_plugin->cls,
"expire",
shard_size,
- shard_size.rel_value_us,
+ jump_mode
+ ? GNUNET_TIME_absolute_subtract (
+ GNUNET_TIME_absolute_get (),
+ shard_size).
+ abs_value_us
+ : shard_size.rel_value_us,
&s->shard_start.abs_value_us,
&s->shard_end.abs_value_us);
+ jump_mode = false;
if (0 >= qs)
{
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
@@ -355,6 +378,7 @@ run_shard (void *cls)
GNUNET_free (s);
delay = GNUNET_TIME_randomized_backoff (delay,
GNUNET_TIME_UNIT_SECONDS);
+ GNUNET_assert (NULL == task);
task = GNUNET_SCHEDULER_add_delayed (delay,
&run_shard,
NULL);
@@ -368,9 +392,10 @@ run_shard (void *cls)
GNUNET_SCHEDULER_shutdown ();
return;
}
- if (GNUNET_TIME_absolute_is_future (s->shard_end))
+ if (GNUNET_TIME_absolute_is_future (s->shard_start))
{
- task = GNUNET_SCHEDULER_add_at (s->shard_end,
+ GNUNET_assert (NULL == task);
+ task = GNUNET_SCHEDULER_add_at (s->shard_start,
&run_shard,
NULL);
abort_shard (s);
@@ -379,12 +404,12 @@ run_shard (void *cls)
/* If this is a first-time run, we immediately
try to catch up with the present */
if (GNUNET_TIME_absolute_is_zero (s->shard_start))
- s->shard_end = GNUNET_TIME_absolute_get ();
-
+ jump_mode = true;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Starting shard [%llu:%llu]!\n",
+ "Starting shard [%llu:%llu)!\n",
(unsigned long long) s->shard_start.abs_value_us,
(unsigned long long) s->shard_end.abs_value_us);
+ GNUNET_assert (NULL == task);
task = GNUNET_SCHEDULER_add_now (&run_expire,
s);
}
diff --git a/src/exchange/taler-exchange-httpd.h b/src/exchange/taler-exchange-httpd.h
index bb387696d..278a05be9 100644
--- a/src/exchange/taler-exchange-httpd.h
+++ b/src/exchange/taler-exchange-httpd.h
@@ -309,8 +309,8 @@ struct TEH_RequestHandler
* @return MHD result code
*/
MHD_RESULT
- (*get)(struct TEH_RequestContext *rc,
- const char *const args[]);
+ (*get)(struct TEH_RequestContext *rc,
+ const char *const args[]);
/**
diff --git a/src/exchange/taler-exchange-httpd_purses_get.c b/src/exchange/taler-exchange-httpd_purses_get.c
index 656a34dbc..12a244897 100644
--- a/src/exchange/taler-exchange-httpd_purses_get.c
+++ b/src/exchange/taler-exchange-httpd_purses_get.c
@@ -333,6 +333,37 @@ TEH_handler_purses_get (struct TEH_RequestContext *rc,
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
break; /* handled below */
}
+ if (GNUNET_TIME_absolute_cmp (gc->timeout,
+ >,
+ gc->purse_expiration.abs_time))
+ {
+ /* Timeout too high, need to replace event handler */
+ struct TALER_PurseEventP rep = {
+ .header.size = htons (sizeof (rep)),
+ .header.type = htons (
+ gc->wait_for_merge
+ ? TALER_DBEVENT_EXCHANGE_PURSE_MERGED
+ : TALER_DBEVENT_EXCHANGE_PURSE_DEPOSITED),
+ .purse_pub = gc->purse_pub
+ };
+ struct GNUNET_DB_EventHandler *eh2;
+
+ gc->timeout = gc->purse_expiration.abs_time;
+ eh2 = TEH_plugin->event_listen (
+ TEH_plugin->cls,
+ GNUNET_TIME_absolute_get_remaining (gc->timeout),
+ &rep.header,
+ &db_event_cb,
+ rc);
+ if (NULL == eh2)
+ {
+ GNUNET_break (0);
+ gc->timeout = GNUNET_TIME_UNIT_ZERO_ABS;
+ }
+ TEH_plugin->event_listen_cancel (TEH_plugin->cls,
+ gc->eh);
+ gc->eh = eh2;
+ }
}
if (GNUNET_TIME_absolute_is_past (gc->purse_expiration.abs_time))
{
diff --git a/src/exchangedb/common-0001.sql b/src/exchangedb/common-0001.sql
index 05394246c..cb64f446e 100644
--- a/src/exchangedb/common-0001.sql
+++ b/src/exchangedb/common-0001.sql
@@ -1193,6 +1193,14 @@ BEGIN
'(merge_pub);'
);
+ -- FIXME: drop index on master (crosses shards)?
+ -- Or use materialized index? (needed?)
+ EXECUTE FORMAT (
+ 'CREATE INDEX IF NOT EXISTS ' || table_name || '_purse_expiration '
+ 'ON ' || table_name || ' '
+ '(purse_expiration);'
+ );
+
END
$$;
diff --git a/src/exchangedb/exchange-0001-part.sql b/src/exchangedb/exchange-0001-part.sql
index 97c26cb05..1b1d2a38e 100644
--- a/src/exchangedb/exchange-0001-part.sql
+++ b/src/exchangedb/exchange-0001-part.sql
@@ -2505,8 +2505,7 @@ UPDATE known_coins
THEN 1
ELSE 0
END
- WHERE coin_pub=in_coin_pub
- LIMIT 1; -- just to be extra safe
+ WHERE coin_pub=in_coin_pub;
out_conflict=FALSE;
@@ -3363,7 +3362,6 @@ END $$;
CREATE OR REPLACE FUNCTION exchange_do_expire_purse(
- IN in_partner_id INT8,
IN in_start_time INT8,
IN in_end_time INT8,
OUT out_found BOOLEAN)
@@ -3375,25 +3373,26 @@ DECLARE
my_deposit record;
BEGIN
-UPDATE purse_requests
- SET refunded=TRUE,
- finished=TRUE
+SELECT purse_pub
+ INTO my_purse_pub
+ FROM purse_requests
WHERE (purse_expiration >= in_start_time) AND
(purse_expiration < in_end_time) AND
(NOT finished) AND
(NOT refunded)
- RETURNING purse_pub
- ,in_reserve_quota
- ,flags
- INTO my_purse_pub
- ,my_rq
- ,my_flags;
+ ORDER BY purse_expiration ASC
+ LIMIT 1;
out_found = FOUND;
IF NOT FOUND
THEN
RETURN;
END IF;
+UPDATE purse_requests
+ SET refunded=TRUE,
+ finished=TRUE
+ WHERE purse_pub=my_purse_pub;
+
-- restore balance to each coin deposited into the purse
FOR my_deposit IN
SELECT coin_pub
@@ -3402,7 +3401,7 @@ FOR my_deposit IN
FROM purse_deposits
WHERE purse_pub = my_purse_pub
LOOP
- UPDATE
+ UPDATE known_coins SET
remaining_frac=remaining_frac+my_deposit.amount_with_fee_frac
- CASE
WHEN remaining_frac+my_deposit.amount_with_fee_frac >= 100000000
@@ -3415,9 +3414,7 @@ LOOP
THEN 1
ELSE 0
END
- FROM known_coins
- WHERE coin_pub = my_deposit.coin_pub
- LIMIT 1; -- just to be extra safe
+ WHERE coin_pub = my_deposit.coin_pub;
END LOOP;
END $$;
diff --git a/src/include/taler_testing_lib.h b/src/include/taler_testing_lib.h
index 930a8e9a6..b0dee023f 100644
--- a/src/include/taler_testing_lib.h
+++ b/src/include/taler_testing_lib.h
@@ -1212,6 +1212,30 @@ TALER_TESTING_cmd_exec_wirewatch (const char *label,
/**
+ * Make a "expire" CMD.
+ *
+ * @param label command label.
+ * @param config_filename configuration filename.
+ * @return the command.
+ */
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_exec_expire (const char *label,
+ const char *config_filename);
+
+
+/**
+ * Make a "router" CMD.
+ *
+ * @param label command label.
+ * @param config_filename configuration filename.
+ * @return the command.
+ */
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_exec_router (const char *label,
+ const char *config_filename);
+
+
+/**
* Run a "taler-exchange-aggregator" CMD.
*
* @param label command label.
diff --git a/src/lib/exchange_api_purses_get.c b/src/lib/exchange_api_purses_get.c
index db5111287..021954c2d 100644
--- a/src/lib/exchange_api_purses_get.c
+++ b/src/lib/exchange_api_purses_get.c
@@ -169,6 +169,11 @@ handle_purse_get_finished (void *cls,
/* Exchange does not know about transaction;
we should pass the reply to the application */
break;
+ case MHD_HTTP_GONE:
+ /* purse expired */
+ dr.hr.ec = TALER_JSON_get_error_code (j);
+ dr.hr.hint = TALER_JSON_get_error_hint (j);
+ break;
case MHD_HTTP_INTERNAL_SERVER_ERROR:
dr.hr.ec = TALER_JSON_get_error_code (j);
dr.hr.hint = TALER_JSON_get_error_hint (j);
diff --git a/src/testing/Makefile.am b/src/testing/Makefile.am
index 4d3c6061a..42c4d8d6b 100644
--- a/src/testing/Makefile.am
+++ b/src/testing/Makefile.am
@@ -61,6 +61,8 @@ libtalertesting_la_SOURCES = \
testing_api_cmd_exec_aggregator.c \
testing_api_cmd_exec_auditor-offline.c \
testing_api_cmd_exec_closer.c \
+ testing_api_cmd_exec_expire.c \
+ testing_api_cmd_exec_router.c \
testing_api_cmd_exec_transfer.c \
testing_api_cmd_exec_wirewatch.c \
testing_api_cmd_insert_deposit.c \
diff --git a/src/testing/test_exchange_api-cs.conf b/src/testing/test_exchange_api-cs.conf
index 79332d648..204718120 100644
--- a/src/testing/test_exchange_api-cs.conf
+++ b/src/testing/test_exchange_api-cs.conf
@@ -45,6 +45,14 @@ DB = postgres
# exchange (or the twister) is actually listening.
BASE_URL = "http://localhost:8081/"
+# How big is an individual shard to be processed
+# by taler-exchange-expire (in time). It may take
+# this much time for an expired purse to be really
+# cleaned up and the coins refunded.
+EXPIRE_SHARD_SIZE = 300 ms
+
+EXPIRE_IDLE_SLEEP_INTERVAL = 1 s
+
[exchangedb-postgres]
CONFIG = "postgres:///talercheck"
diff --git a/src/testing/test_exchange_api-rsa.conf b/src/testing/test_exchange_api-rsa.conf
index 1d4456623..be0e4bade 100644
--- a/src/testing/test_exchange_api-rsa.conf
+++ b/src/testing/test_exchange_api-rsa.conf
@@ -45,6 +45,15 @@ DB = postgres
# exchange (or the twister) is actually listening.
BASE_URL = "http://localhost:8081/"
+# How big is an individual shard to be processed
+# by taler-exchange-expire (in time). It may take
+# this much time for an expired purse to be really
+# cleaned up and the coins refunded.
+EXPIRE_SHARD_SIZE = 300 ms
+
+EXPIRE_IDLE_SLEEP_INTERVAL = 1 s
+
+
[exchangedb-postgres]
CONFIG = "postgres:///talercheck"
diff --git a/src/testing/test_exchange_p2p.c b/src/testing/test_exchange_p2p.c
index b3e98daa4..ec3c660c4 100644
--- a/src/testing/test_exchange_p2p.c
+++ b/src/testing/test_exchange_p2p.c
@@ -277,6 +277,62 @@ run (void *cls,
TALER_TESTING_cmd_end ()
};
+ struct TALER_TESTING_Command expire[] = {
+ TALER_TESTING_cmd_purse_create_with_reserve (
+ "purse-create-with-reserve-expire",
+ MHD_HTTP_OK,
+ "{\"amount\":\"EUR:1\",\"summary\":\"ice cream\"}",
+ true /* upload contract */,
+ GNUNET_TIME_relative_multiply (
+ GNUNET_TIME_UNIT_SECONDS,
+ 1), /* expiration */
+ "create-reserve-1"),
+ TALER_TESTING_cmd_purse_poll (
+ "pull-poll-purse-before-expire",
+ MHD_HTTP_GONE,
+ "purse-create-with-reserve-expire",
+ "EUR:1",
+ false,
+ GNUNET_TIME_UNIT_MINUTES),
+ TALER_TESTING_cmd_purse_create_with_deposit (
+ "purse-with-deposit-expire",
+ MHD_HTTP_OK,
+ "{\"amount\":\"EUR:1\",\"summary\":\"ice cream\"}",
+ true, /* upload contract */
+ GNUNET_TIME_relative_multiply (
+ GNUNET_TIME_UNIT_SECONDS,
+ 1), /* expiration */
+ "withdraw-coin-1",
+ "EUR:1.01",
+ NULL),
+ TALER_TESTING_cmd_purse_poll (
+ "push-poll-purse-before-expire",
+ MHD_HTTP_GONE,
+ "purse-with-deposit-expire",
+ "EUR:1",
+ true,
+ GNUNET_TIME_UNIT_MINUTES),
+ TALER_TESTING_cmd_sleep ("sleep",
+ 2 /* seconds */),
+ TALER_TESTING_cmd_exec_expire ("exec-expire",
+ config_file),
+ TALER_TESTING_cmd_purse_poll_finish (
+ "push-merge-purse-poll-finish-expire",
+ GNUNET_TIME_relative_multiply (
+ GNUNET_TIME_UNIT_SECONDS,
+ 15),
+ "push-poll-purse-before-expire"),
+ TALER_TESTING_cmd_purse_poll_finish (
+ "pull-deposit-purse-poll-expire-finish",
+ GNUNET_TIME_relative_multiply (
+ GNUNET_TIME_UNIT_SECONDS,
+ 15),
+ "pull-poll-purse-before-expire"),
+ // FIXME: check coin was refunded
+ // FIXME: check reserve purse capacity is back up!
+ TALER_TESTING_cmd_end ()
+ };
+
struct TALER_TESTING_Command commands[] = {
/* setup exchange */
TALER_TESTING_cmd_auditor_add ("add-auditor-OK",
@@ -313,6 +369,8 @@ run (void *cls,
push),
TALER_TESTING_cmd_batch ("pull",
pull),
+ TALER_TESTING_cmd_batch ("expire",
+ expire),
/* End the suite. */
TALER_TESTING_cmd_end ()
};
diff --git a/src/testing/testing_api_cmd_exec_expire.c b/src/testing/testing_api_cmd_exec_expire.c
new file mode 100644
index 000000000..aabf37361
--- /dev/null
+++ b/src/testing/testing_api_cmd_exec_expire.c
@@ -0,0 +1,162 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2022 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 3,
+ or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public
+ License along with TALER; see the file COPYING. If not, see
+ <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file testing/testing_api_cmd_exec_expire.c
+ * @brief run the taler-exchange-expire command
+ * @author Christian Grothoff
+ * @author Marcello Stanisci
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_signatures.h"
+#include "taler_testing_lib.h"
+
+
+/**
+ * State for a "expire" CMD.
+ */
+struct ExpireState
+{
+
+ /**
+ * Process for the expireer.
+ */
+ struct GNUNET_OS_Process *expire_proc;
+
+ /**
+ * Configuration file used by the expireer.
+ */
+ const char *config_filename;
+};
+
+
+/**
+ * Run the command; use the `taler-exchange-expire' program.
+ *
+ * @param cls closure.
+ * @param cmd command currently being executed.
+ * @param is interpreter state.
+ */
+static void
+expire_run (void *cls,
+ const struct TALER_TESTING_Command *cmd,
+ struct TALER_TESTING_Interpreter *is)
+{
+ struct ExpireState *ws = cls;
+
+ (void) cmd;
+ ws->expire_proc
+ = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ALL,
+ NULL, NULL, NULL,
+ "taler-exchange-expire",
+ "taler-exchange-expire",
+ "-L", "INFO",
+ "-c", ws->config_filename,
+ "-t", /* exit when done */
+ NULL);
+ if (NULL == ws->expire_proc)
+ {
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (is);
+ return;
+ }
+ TALER_TESTING_wait_for_sigchld (is);
+}
+
+
+/**
+ * Free the state of a "expire" CMD, and possibly
+ * kills its process if it did not terminate regularly.
+ *
+ * @param cls closure.
+ * @param cmd the command being freed.
+ */
+static void
+expire_cleanup (void *cls,
+ const struct TALER_TESTING_Command *cmd)
+{
+ struct ExpireState *ws = cls;
+
+ (void) cmd;
+ if (NULL != ws->expire_proc)
+ {
+ GNUNET_break (0 ==
+ GNUNET_OS_process_kill (ws->expire_proc,
+ SIGKILL));
+ GNUNET_OS_process_wait (ws->expire_proc);
+ GNUNET_OS_process_destroy (ws->expire_proc);
+ ws->expire_proc = NULL;
+ }
+ GNUNET_free (ws);
+}
+
+
+/**
+ * Offer "expire" CMD internal data to other commands.
+ *
+ * @param cls closure.
+ * @param[out] ret result.
+ * @param trait name of the trait.
+ * @param index index number of the object to offer.
+ * @return #GNUNET_OK on success.
+ */
+static enum GNUNET_GenericReturnValue
+expire_traits (void *cls,
+ const void **ret,
+ const char *trait,
+ unsigned int index)
+{
+ struct ExpireState *ws = cls;
+ struct TALER_TESTING_Trait traits[] = {
+ TALER_TESTING_make_trait_process (&ws->expire_proc),
+ TALER_TESTING_trait_end ()
+ };
+
+ return TALER_TESTING_get_trait (traits,
+ ret,
+ trait,
+ index);
+}
+
+
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_exec_expire (const char *label,
+ const char *config_filename)
+{
+ struct ExpireState *ws;
+
+ ws = GNUNET_new (struct ExpireState);
+ ws->config_filename = config_filename;
+
+ {
+ struct TALER_TESTING_Command cmd = {
+ .cls = ws,
+ .label = label,
+ .run = &expire_run,
+ .cleanup = &expire_cleanup,
+ .traits = &expire_traits
+ };
+
+ return cmd;
+ }
+}
+
+
+/* end of testing_api_cmd_exec_expire.c */
diff --git a/src/testing/testing_api_cmd_exec_router.c b/src/testing/testing_api_cmd_exec_router.c
new file mode 100644
index 000000000..d7f3fe265
--- /dev/null
+++ b/src/testing/testing_api_cmd_exec_router.c
@@ -0,0 +1,161 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2022 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 3,
+ or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public
+ License along with TALER; see the file COPYING. If not, see
+ <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file testing/testing_api_cmd_exec_router.c
+ * @brief run the taler-exchange-router command
+ * @author Christian Grothoff
+ * @author Marcello Stanisci
+ */
+#include "platform.h"
+#include "taler_json_lib.h"
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_signatures.h"
+#include "taler_testing_lib.h"
+
+
+/**
+ * State for a "router" CMD.
+ */
+struct RouterState
+{
+
+ /**
+ * Process for the routerer.
+ */
+ struct GNUNET_OS_Process *router_proc;
+
+ /**
+ * Configuration file used by the routerer.
+ */
+ const char *config_filename;
+};
+
+
+/**
+ * Run the command; use the `taler-exchange-router' program.
+ *
+ * @param cls closure.
+ * @param cmd command currently being executed.
+ * @param is interpreter state.
+ */
+static void
+router_run (void *cls,
+ const struct TALER_TESTING_Command *cmd,
+ struct TALER_TESTING_Interpreter *is)
+{
+ struct RouterState *ws = cls;
+
+ (void) cmd;
+ ws->router_proc
+ = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ALL,
+ NULL, NULL, NULL,
+ "taler-exchange-router",
+ "taler-exchange-router",
+ "-c", ws->config_filename,
+ "-t", /* exit when done */
+ NULL);
+ if (NULL == ws->router_proc)
+ {
+ GNUNET_break (0);
+ TALER_TESTING_interpreter_fail (is);
+ return;
+ }
+ TALER_TESTING_wait_for_sigchld (is);
+}
+
+
+/**
+ * Free the state of a "router" CMD, and possibly
+ * kills its process if it did not terminate regularly.
+ *
+ * @param cls closure.
+ * @param cmd the command being freed.
+ */
+static void
+router_cleanup (void *cls,
+ const struct TALER_TESTING_Command *cmd)
+{
+ struct RouterState *ws = cls;
+
+ (void) cmd;
+ if (NULL != ws->router_proc)
+ {
+ GNUNET_break (0 ==
+ GNUNET_OS_process_kill (ws->router_proc,
+ SIGKILL));
+ GNUNET_OS_process_wait (ws->router_proc);
+ GNUNET_OS_process_destroy (ws->router_proc);
+ ws->router_proc = NULL;
+ }
+ GNUNET_free (ws);
+}
+
+
+/**
+ * Offer "router" CMD internal data to other commands.
+ *
+ * @param cls closure.
+ * @param[out] ret result.
+ * @param trait name of the trait.
+ * @param index index number of the object to offer.
+ * @return #GNUNET_OK on success.
+ */
+static enum GNUNET_GenericReturnValue
+router_traits (void *cls,
+ const void **ret,
+ const char *trait,
+ unsigned int index)
+{
+ struct RouterState *ws = cls;
+ struct TALER_TESTING_Trait traits[] = {
+ TALER_TESTING_make_trait_process (&ws->router_proc),
+ TALER_TESTING_trait_end ()
+ };
+
+ return TALER_TESTING_get_trait (traits,
+ ret,
+ trait,
+ index);
+}
+
+
+struct TALER_TESTING_Command
+TALER_TESTING_cmd_exec_router (const char *label,
+ const char *config_filename)
+{
+ struct RouterState *ws;
+
+ ws = GNUNET_new (struct RouterState);
+ ws->config_filename = config_filename;
+
+ {
+ struct TALER_TESTING_Command cmd = {
+ .cls = ws,
+ .label = label,
+ .run = &router_run,
+ .cleanup = &router_cleanup,
+ .traits = &router_traits
+ };
+
+ return cmd;
+ }
+}
+
+
+/* end of testing_api_cmd_exec_router.c */
diff --git a/src/testing/testing_api_cmd_purse_get.c b/src/testing/testing_api_cmd_purse_get.c
index 3fc576912..61873721b 100644
--- a/src/testing/testing_api_cmd_purse_get.c
+++ b/src/testing/testing_api_cmd_purse_get.c
@@ -261,6 +261,7 @@ TALER_TESTING_cmd_purse_poll (
ss->expected_balance = expected_balance;
ss->expected_response_code = expected_http_status;
ss->timeout = timeout;
+ ss->wait_for_merge = wait_for_merge;
{
struct TALER_TESTING_Command cmd = {
.cls = ss,