From 2f2a009e7a38da4fbdd66ea1a1d9b2f26cf3897c Mon Sep 17 00:00:00 2001 From: Martine Lenders Date: Thu, 21 Jan 2021 17:31:15 +0100 Subject: [PATCH 1/5] congure: initial import of a congestion control framework --- sys/Kconfig | 1 + sys/Makefile.dep | 4 + sys/congure/Kconfig | 22 ++++ sys/congure/Makefile | 1 + sys/include/congure.h | 224 +++++++++++++++++++++++++++++++++++ sys/include/congure/config.h | 32 +++++ 6 files changed, 284 insertions(+) create mode 100644 sys/congure/Kconfig create mode 100644 sys/congure/Makefile create mode 100644 sys/include/congure.h create mode 100644 sys/include/congure/config.h diff --git a/sys/Kconfig b/sys/Kconfig index e1b64ba492..02a636d7c1 100644 --- a/sys/Kconfig +++ b/sys/Kconfig @@ -18,6 +18,7 @@ rsource "cb_mux/Kconfig" rsource "checksum/Kconfig" rsource "color/Kconfig" rsource "crypto/Kconfig" +rsource "congure/Kconfig" rsource "div/Kconfig" rsource "embunit/Kconfig" rsource "entropy_source/Kconfig" diff --git a/sys/Makefile.dep b/sys/Makefile.dep index 1af2827552..54c243b363 100644 --- a/sys/Makefile.dep +++ b/sys/Makefile.dep @@ -21,6 +21,10 @@ ifneq (,$(filter arduino_pwm,$(FEATURES_USED))) FEATURES_REQUIRED += periph_pwm endif +ifneq (,$(filter congure_%,$(USEMODULE))) + USEMODULE += congure +endif + ifneq (,$(filter eepreg,$(USEMODULE))) FEATURES_REQUIRED += periph_eeprom endif diff --git a/sys/congure/Kconfig b/sys/congure/Kconfig new file mode 100644 index 0000000000..a4c788b615 --- /dev/null +++ b/sys/congure/Kconfig @@ -0,0 +1,22 @@ +# Copyright (c) 2021 Freie Universität Berlin +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +if !TEST_KCONFIG +menu "CongURE congestion control abstraction" + depends on USEMODULE_CONGURE + +endmenu # CongURE congestion control abstraction +endif # !TEST_KCONFIG + +if TEST_KCONFIG +menuconfig MODULE_CONGURE + bool "CongURE congestion control abstraction" + depends on TEST_KCONFIG + +if MODULE_CONGURE + +endif # MODULE_CONGURE +endif # TEST_KCONFIG diff --git a/sys/congure/Makefile b/sys/congure/Makefile new file mode 100644 index 0000000000..48422e909a --- /dev/null +++ b/sys/congure/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/sys/include/congure.h b/sys/include/congure.h new file mode 100644 index 0000000000..9031b54b05 --- /dev/null +++ b/sys/include/congure.h @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup sys_congure CongURE - A Congestion control framework + * @ingroup sys + * @brief Congestion control utilizing re-usable + * elements + * @{ + * + * @file + * + * @author Martine S. Lenders + */ +#ifndef CONGURE_H +#define CONGURE_H + +#include + +#include "clist.h" +#include "ztimer.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Maximum value the window size can take + */ +#define CONGURE_WND_SIZE_MAX (UINT16_MAX) + +/** + * @brief Define type for window size to allow for possible window size + * scaling + */ +typedef uint16_t congure_wnd_size_t; + +/** + * @brief Forward-declariton for the driver struct + */ +typedef struct congure_snd_driver congure_snd_driver_t; + +/** + * @brief Base state object for CongURE implementations + */ +typedef struct { + /** + * @brief Driver for the state object. See @ref congure_snd_driver_t. + */ + const congure_snd_driver_t *driver; + /** + * @brief Context for callbacks specific to the congestion control + * + * E.g. A TCP PCB. + */ + void *ctx; + congure_wnd_size_t cwnd; /**< Congestion window size */ +} congure_snd_t; + +/** + * @brief Object to represent a collection of sent messages. + */ +typedef struct { + clist_node_t super; /**< see @ref clist_node_t */ + /** + * @brief timestamp in milliseconds of when the message was sent. + */ + ztimer_now_t send_time; + congure_wnd_size_t size; /**< size in initiator-defined units */ + /** + * @brief number of times the message has already been resent. + * + * This does not include the first send. + */ + uint8_t resends; +} congure_snd_msg_t; + +/** + * @brief Object to represent an ACK to a message + */ +typedef struct { + /** + * @brief Timestamp in milliseconds of when the ACK was received. + */ + ztimer_now_t recv_time; + uint32_t id; /**< ID of the message the ACK is for */ + /** + * @brief size of the ACK in initiator-defined units + */ + congure_wnd_size_t size; + /** + * @brief the peer-reported window size in caller defined units. Leave + * 0 if not supplied by the protocol. + */ + congure_wnd_size_t wnd; + /** + * @brief true, if ACK only contains an ACK, false if not + * + * This e.g. can be used to tell the congestion control mechanism that the + * SYN or FIN tag are cleared in the ACK with TCP. + */ + uint16_t clean; + /** + * @brief the peer-reported time in milliseconds the ACK was delayed + * since message reception. Leave 0 if not supplied by the + * protocol. + */ + uint16_t delay; +} congure_snd_ack_t; + +/** + * @brief Driver for CongURE objects. + */ +struct congure_snd_driver { + /** + * @brief Initializes a CongURE object. + * + * @param[in,out] c The CongURE object to initialize. + * @param[in] ctx Context for callbacks specific to the + * congestion control (such as a TCP PCB). + * May be NULL. + */ + void (*init)(congure_snd_t *c, void *ctx); + + /** + * @brief Get current interval between messages for pacing. + * + * @param[in] c The CongURE state object. + * @param[in] msg_size The size of the next message to send in + * caller-defined unit. + * + * @return The current interval between sent messages in microseconds + * when pacing supported by congestion control implementation. + * @return -1, if pacing is not supported by the congestion control + * implementation. + */ + int32_t (*inter_msg_interval)(congure_snd_t *c, unsigned msg_size); + + /** + * @brief Report that a message was sent. + * + * @param[in] c The CongURE state object. + * @param[in] msg_size Size of the message in caller-defined unit. + */ + void (*report_msg_sent)(congure_snd_t *c, unsigned msg_size); + + /** + * @brief Report message as discarded. + * + * Discarded messages are not further taken into account for congestion + * control. + * + * @param[in] c The CongURE state object. + * @param[in] msg_size Size of the discarded message in caller-defined + * unit. + */ + void (*report_msg_discarded)(congure_snd_t *c, unsigned msg_size); + + /** + * @brief Report that the ACKs for a collection of messages timed out. + * + * @note As many congestion control algorithms do not distinguish loss + * and ACK timeout, this method and + * congure_snd_t::report_msgs_lost need to have the same + * signature so the same function can be used here + * + * @param[in] c The CongURE state object. + * @param[in] msgs A collection of messages for which the ACK + * timed out. The list may be changed by the + * method. + */ + void (*report_msgs_timeout)(congure_snd_t *c, congure_snd_msg_t *msgs); + + /** + * @brief Report that a collection of messages that is known to be lost. + * + * One indicator for a lost message may e.g. be the reception of an ACK of a + * later sent packet, but not a ACK timeout (see + * congure_snd_driver_t::report_msgs_timeout() for that). + * + * @note As many congestion control algorithms do not distinguish loss + * and ACK timeout, this method and + * congure_snd_t::report_msgs_timeout need to have the same + * signature so the same function can be used here + * + * @param[in] c The CongURE state object. + * @param[in] msgs A collection of messages that are known to + * be lost. The list may be changed by the method. + */ + void (*report_msgs_lost)(congure_snd_t *c, congure_snd_msg_t *msgs); + + /** + * @brief Report that the ACK for a message was received. + * + * @param[in] c The CongURE state object. + * @param[in] msg The ACK'd message. + * @param[in] ack The received ACK. + */ + void (*report_msg_acked)(congure_snd_t *c, congure_snd_msg_t *msg, + congure_snd_ack_t *ack); + + /** + * @brief Report that "congestion encountered" CE signals were received + * for a message by means of explicit congestion notification + * (ECN). + * + * @param[in] c The CongURE state object. + * @param[in] time Timestamp in milliseconds of the message the CE + * event occurred for was sent. + */ + void (*report_ecn_ce)(congure_snd_t *c, ztimer_now_t time); +}; + +#ifdef __cplusplus +} +#endif + +#endif /* CONGURE_H */ +/** @} */ diff --git a/sys/include/congure/config.h b/sys/include/congure/config.h new file mode 100644 index 0000000000..d9b3e080ec --- /dev/null +++ b/sys/include/congure/config.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup sys_congure_config CongURE compile time configuration + * @ingroup sys_congure + * @ingroup config + * @brief Configuration for congestion control using @ref sys_congure + * @{ + * + * @file + * + * @author Martine S. Lenders + */ +#ifndef CONGURE_CONFIG_H +#define CONGURE_CONFIG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* CONGURE_CONFIG_H */ +/** @} */ From 28592c203ce964a0f24b5aa88f8fcb4646dcd669 Mon Sep 17 00:00:00 2001 From: Martine Lenders Date: Thu, 21 Jan 2021 17:43:54 +0100 Subject: [PATCH 2/5] uncrustify: whitelist congure --- dist/tools/uncrustify/whitelist.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dist/tools/uncrustify/whitelist.txt b/dist/tools/uncrustify/whitelist.txt index d3aa5210b2..aa2d387309 100644 --- a/dist/tools/uncrustify/whitelist.txt +++ b/dist/tools/uncrustify/whitelist.txt @@ -8,5 +8,7 @@ cpu/riscv_common/include/.*\.h cpu/riscv_common/periph/.*\.c sys/riotboot/.*\.h sys/riotboot/.*\.c +sys/congure.*\.c +sys/include/congure.*\.h sys/ztimer/.*\.c sys/include/ztimer.*\.h From e65fee45874c4318c3cd74ccbf269f788997ab57 Mon Sep 17 00:00:00 2001 From: Martine Lenders Date: Mon, 1 Feb 2021 20:08:51 +0100 Subject: [PATCH 3/5] congure_test: initial import of CongURE test framework --- sys/Makefile.dep | 4 + sys/congure/Kconfig | 4 + sys/congure/Makefile | 4 + sys/congure/test/Kconfig | 30 +++ sys/congure/test/Kconfig.config | 19 ++ sys/congure/test/Makefile | 3 + sys/congure/test/congure_test.c | 369 ++++++++++++++++++++++++++++ sys/include/congure.h | 5 +- sys/include/congure/test.h | 319 ++++++++++++++++++++++++ sys/shell/commands/shell_commands.c | 14 ++ 10 files changed, 769 insertions(+), 2 deletions(-) create mode 100644 sys/congure/test/Kconfig create mode 100644 sys/congure/test/Kconfig.config create mode 100644 sys/congure/test/Makefile create mode 100644 sys/congure/test/congure_test.c create mode 100644 sys/include/congure/test.h diff --git a/sys/Makefile.dep b/sys/Makefile.dep index 54c243b363..019d117ef1 100644 --- a/sys/Makefile.dep +++ b/sys/Makefile.dep @@ -25,6 +25,10 @@ ifneq (,$(filter congure_%,$(USEMODULE))) USEMODULE += congure endif +ifneq (,$(filter congure_test,$(USEMODULE))) + USEMODULE += fmt +endif + ifneq (,$(filter eepreg,$(USEMODULE))) FEATURES_REQUIRED += periph_eeprom endif diff --git a/sys/congure/Kconfig b/sys/congure/Kconfig index a4c788b615..2cc77d5001 100644 --- a/sys/congure/Kconfig +++ b/sys/congure/Kconfig @@ -8,6 +8,8 @@ if !TEST_KCONFIG menu "CongURE congestion control abstraction" depends on USEMODULE_CONGURE +rsource "test/Kconfig" + endmenu # CongURE congestion control abstraction endif # !TEST_KCONFIG @@ -18,5 +20,7 @@ menuconfig MODULE_CONGURE if MODULE_CONGURE +rsource "test/Kconfig" + endif # MODULE_CONGURE endif # TEST_KCONFIG diff --git a/sys/congure/Makefile b/sys/congure/Makefile index 48422e909a..12697067ff 100644 --- a/sys/congure/Makefile +++ b/sys/congure/Makefile @@ -1 +1,5 @@ +ifneq (,$(filter congure_test,$(USEMODULE))) + DIRS += test +endif + include $(RIOTBASE)/Makefile.base diff --git a/sys/congure/test/Kconfig b/sys/congure/test/Kconfig new file mode 100644 index 0000000000..f710d4f23d --- /dev/null +++ b/sys/congure/test/Kconfig @@ -0,0 +1,30 @@ +# Copyright (c) 2021 Freie Universität Berlin +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +if !TEST_KCONFIG + +menuconfig KCONFIG_USEMODULE_CONGURE_TEST + bool "Configure CongURE test framework" + depends on USEMODULE_CONGURE_TEST + help + Configure CongURE test framework via Kconfig. +if KCONFIG_USEMODULE_CONGURE_TEST +rsource "Kconfig.config" +endif # KCONFIG_USEMODULE_CONGURE_TEST + +endif # !TEST_KCONFIG +if TEST_KCONFIG + +menuconfig MODULE_CONGURE_TEST + bool "CongURE test framework" + depends on TEST_KCONFIG + select MODULE_FMT + +if MODULE_CONGURE_TEST +rsource "Kconfig.config" +endif # MODULE_CONGURE_TEST + +endif # TEST_KCONFIG diff --git a/sys/congure/test/Kconfig.config b/sys/congure/test/Kconfig.config new file mode 100644 index 0000000000..d33ebccd2f --- /dev/null +++ b/sys/congure/test/Kconfig.config @@ -0,0 +1,19 @@ +# Copyright (c) 2021 Freie Universität Berlin +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. +# + +# XXX This file only is required since there is no easy way to use these config +# options with both the final MODULE_SHELL and KCONFIG_USEMODULE_SHELL in a +# nicely looking and easy to migrate way. After migration, the content of this +# file can be folded back into `sys/shell/Kconfig` + +config CONGURE_TEST_LOST_MSG_POOL_SIZE + int "Pool size for the list elements for a lost message report" + default 4 + help + @see congure_snd_driver_t::report_msg_lost + This defines the maximum number of 3-tuples you can use with + @ref congure_test_call_report() when argv[1] is msg_lost. diff --git a/sys/congure/test/Makefile b/sys/congure/test/Makefile new file mode 100644 index 0000000000..2c32d373f3 --- /dev/null +++ b/sys/congure/test/Makefile @@ -0,0 +1,3 @@ +MODULE := congure_test + +include $(RIOTBASE)/Makefile.base diff --git a/sys/congure/test/congure_test.c b/sys/congure/test/congure_test.c new file mode 100644 index 0000000000..675dd18b9f --- /dev/null +++ b/sys/congure/test/congure_test.c @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @{ + * + * @file + * @author Martine S. Lenders + */ + +#include +#include +#include +#include + +#include "fmt.h" + +#include "congure/test.h" + +static bool _scn_u32_dec_with_zero(const char *str, size_t n, uint32_t *res) +{ + if ((n == 1) && str[0] == '0') { + *res = 0; + } + else if ((*res = scn_u32_dec(str, n)) == 0) { + return false; + } + return true; +} + +int congure_test_clear_state(int argc, char **argv) +{ + (void)argc; + (void)argv; + memset(congure_test_get_state(), 0, sizeof(congure_test_snd_t)); + print_str("{\"success\":null}\n"); + return 0; +} + +int congure_test_call_setup(int argc, char **argv) +{ + congure_test_snd_t *c = congure_test_get_state(); + uint32_t id = 0; + + if (argc > 1) { + if (!_scn_u32_dec_with_zero(argv[1], strlen(argv[1]), &id)) { + print_str("{\"error\":\"`id` expected to be integer\"}\n"); + return 1; + } + } + if (congure_test_snd_setup(c, (unsigned)id) < 0) { + print_str("{\"error\":\"`id` is invalid\"}"); + return 1; + } + + print_str("{"); + + print_str("\"success\":\"0x"); + print_u32_hex((intptr_t)c); + + print_str("\"}\n"); + return 0; +} + +static inline bool _check_driver(congure_test_snd_t *c) +{ + if (c->super.driver == NULL) { + print_str("{\"error\":\"State object not set up\"}\n"); + return false; + } + return true; +} + +int congure_test_call_init(int argc, char **argv) +{ + congure_test_snd_t *c = congure_test_get_state(); + uint32_t ctx; + size_t arglen; + + if (!_check_driver(c)) { + return 1; + } + if (argc < 2) { + print_str("{\"error\":\"`ctx` argument expected\"}\n"); + return 1; + } + arglen = strlen(argv[1]); + if ((arglen < 3) || ((argv[1][0] != '0') && (argv[1][1] != 'x'))) { + print_str("{\"error\":\"`ctx` expected to be hex\"}\n"); + return 1; + } + ctx = scn_u32_hex(&argv[1][2], arglen - 2); + c->super.driver->init(&c->super, (void *)((intptr_t)ctx)); + print_str("{\"success\":null}\n"); + return 0; +} + +int congure_test_call_inter_msg_interval(int argc, char **argv) +{ + congure_test_snd_t *c = congure_test_get_state(); + uint32_t msg_size; + int32_t res; + + (void)argc; + (void)argv; + if (!_check_driver(c)) { + return 1; + } + if (argc < 2) { + print_str("{\"error\":\"`msg_size` argument expected\"}\n"); + return 1; + } + if (!_scn_u32_dec_with_zero(argv[1], strlen(argv[1]), &msg_size)) { + print_str("{\"error\":\"`msg_size` expected to be integer\"}\n"); + return 1; + } + res = c->super.driver->inter_msg_interval(&c->super, msg_size); + print_str("{\"success\":"); + print_s32_dec(res); + print_str("}\n"); + return 0; +} + +static int _call_report_msg_sent(int argc, char **argv) +{ + congure_test_snd_t *c = congure_test_get_state(); + uint32_t msg_size; + + if (argc < 2) { + print_str("{\"error\":\"`msg_size` argument expected\"}\n"); + return 1; + } + if (!_scn_u32_dec_with_zero(argv[1], strlen(argv[1]), &msg_size)) { + print_str("{\"error\":\"`msg_size` expected to be integer\"}\n"); + return 1; + } + c->super.driver->report_msg_sent(&c->super, (unsigned)msg_size); + print_str("{\"success\":null}\n"); + return 0; +} + +static int _call_report_msg_discarded(int argc, char **argv) +{ + congure_test_snd_t *c = congure_test_get_state(); + uint32_t msg_size; + + if (argc < 2) { + print_str("{\"error\":\"`msg_size` argument expected\"}\n"); + return 1; + } + if (!_scn_u32_dec_with_zero(argv[1], strlen(argv[1]), &msg_size)) { + print_str("{\"error\":\"`msg_size` expected to be integer\"}\n"); + return 1; + } + c->super.driver->report_msg_discarded(&c->super, (unsigned)msg_size); + print_str("{\"success\":null}\n"); + return 0; +} + +static int _call_report_msgs_timeout_lost(void (*method)(congure_snd_t *, + congure_snd_msg_t *), + int argc, char **argv) +{ + static congure_snd_msg_t list_pool[CONFIG_CONGURE_TEST_LOST_MSG_POOL_SIZE]; + clist_node_t msgs = { .next = NULL }; + congure_test_snd_t *c = congure_test_get_state(); + + if (argc < 4) { + print_str("{\"error\":\"At least 3 arguments `msg_send_time`, " + "`msg_size`, `msg_resends` expected\"}\n"); + return 1; + } + if ((argc - 1) % 3) { + print_str("{\"error\":\"Number of arguments must be divisible " + "by 3\"}\n"); + return 1; + } + if ((unsigned)((argc - 1) / 3) >= ARRAY_SIZE(list_pool)) { + print_str("{\"error\":\"List element pool depleted\"}"); + return 1; + } + for (int i = 1; i < argc; i += 3) { + uint32_t tmp; + unsigned pool_idx = ((i - 1) / 3); + + list_pool[pool_idx].super.next = NULL; + + if (!_scn_u32_dec_with_zero(argv[i], strlen(argv[i]), &tmp)) { + print_str("{\"error\":\"`msg_send_time` expected to be " + "integer\"}\n"); + return 1; + } + list_pool[pool_idx].send_time = tmp; + + if (!_scn_u32_dec_with_zero(argv[i + 1], strlen(argv[i + 1]), &tmp)) { + print_str("{\"error\":\"`msg_size` expected to be integer\"}\n"); + return 1; + } + list_pool[pool_idx].size = tmp; + + if (!_scn_u32_dec_with_zero(argv[i + 2], strlen(argv[i + 2]), &tmp)) { + print_str("{\"error\":\"`msg_resends` expected to be " + "integer\"}\n"); + return 1; + } + list_pool[pool_idx].resends = tmp; + + clist_rpush(&msgs, &list_pool[pool_idx].super); + } + method(&c->super, (congure_snd_msg_t *)msgs.next); + print_str("{\"success\":null}\n"); + return 0; +} + +static int _call_report_msgs_timeout(int argc, char **argv) +{ + congure_test_snd_t *c = congure_test_get_state(); + + return _call_report_msgs_timeout_lost(c->super.driver->report_msgs_timeout, + argc, argv); +} + +static int _call_report_msgs_lost(int argc, char **argv) +{ + congure_test_snd_t *c = congure_test_get_state(); + + return _call_report_msgs_timeout_lost(c->super.driver->report_msgs_lost, + argc, argv); +} + +static int _call_report_msg_acked(int argc, char **argv) +{ + static congure_snd_msg_t msg = { .size = 0 }; + static congure_snd_ack_t ack = { .size = 0 }; + congure_test_snd_t *c = congure_test_get_state(); + uint32_t tmp; + + if (argc < 10) { + print_str("{\"error\":\"At least 9 arguments `msg_send_time`, " + "`msg_size`, `msg_resends`, `ack_recv_time`, `ack_id`, " + "`ack_size`, `ack_clean`, `ack_wnd`, `ack_delay` " + "expected\"}\n"); + return 1; + } + if (!_scn_u32_dec_with_zero(argv[1], strlen(argv[1]), &tmp)) { + print_str("{\"error\":\"`msg_send_time` expected to be " + "integer\"}\n"); + return 1; + } + msg.send_time = tmp; + + if (!_scn_u32_dec_with_zero(argv[2], strlen(argv[2]), &tmp)) { + print_str("{\"error\":\"`msg_size` expected to be integer\"}\n"); + return 1; + } + msg.size = tmp; + + if (!_scn_u32_dec_with_zero(argv[3], strlen(argv[3]), &tmp)) { + print_str("{\"error\":\"`msg_resends` expected to be integer\"}\n"); + return 1; + } + msg.resends = tmp; + + if (!_scn_u32_dec_with_zero(argv[4], strlen(argv[4]), &tmp)) { + print_str("{\"error\":\"`ack_recv_time` expected to be integer\"}\n"); + return 1; + } + ack.recv_time = tmp; + + if (!_scn_u32_dec_with_zero(argv[5], strlen(argv[5]), &tmp)) { + print_str("{\"error\":\"`ack_id` expected to be integer\"}\n"); + return 1; + } + ack.id = tmp; + + if (!_scn_u32_dec_with_zero(argv[6], strlen(argv[6]), &tmp)) { + print_str("{\"error\":\"`ack_size` expected to be integer\"}\n"); + return 1; + } + ack.size = tmp; + + if (!_scn_u32_dec_with_zero(argv[7], strlen(argv[7]), &tmp)) { + print_str("{\"error\":\"`ack_clean` expected to be integer\"}\n"); + return 1; + } + ack.clean = (bool)tmp; + + if (!_scn_u32_dec_with_zero(argv[8], strlen(argv[8]), &tmp)) { + print_str("{\"error\":\"`ack_wnd` expected to be integer\"}\n"); + return 1; + } + if (tmp > CONGURE_WND_SIZE_MAX) { + print_str("{\"error\":\"`ack_wnd` not 16 bit wide\"}\n"); + return 1; + } + ack.wnd = (uint16_t)tmp; + + if (!_scn_u32_dec_with_zero(argv[9], strlen(argv[9]), &tmp)) { + print_str("{\"error\":\"`ack_delay` expected to be integer\"}\n"); + return 1; + } + if (tmp > UINT16_MAX) { + print_str("{\"error\":\"`ack_delay` not 16 bit wide\"}\n"); + return 1; + } + ack.delay = (uint16_t)tmp; + + c->super.driver->report_msg_acked(&c->super, &msg, &ack); + print_str("{\"success\":null}\n"); + return 0; +} + +static int _call_report_ecn_ce(int argc, char **argv) +{ + congure_test_snd_t *c = congure_test_get_state(); + uint32_t time; + + if (argc < 2) { + print_str("{\"error\":\"`time` argument expected\"}\n"); + return 1; + } + if (!_scn_u32_dec_with_zero(argv[1], strlen(argv[1]), &time)) { + print_str("{\"error\":\"`time` expected to be integer\"}\n"); + return 1; + } + c->super.driver->report_ecn_ce(&c->super, time); + print_str("{\"success\":null}\n"); + return 0; +} + +int congure_test_call_report(int argc, char **argv) +{ + if (!_check_driver(congure_test_get_state())) { + return 1; + } + if (argc < 2) { + print_str("{\"error\":\"No report command provided\"}\n"); + return 1; + } + if (strcmp(argv[1], "msg_sent") == 0) { + return _call_report_msg_sent(argc - 1, &argv[1]); + } + else if (strcmp(argv[1], "msg_discarded") == 0) { + return _call_report_msg_discarded(argc - 1, &argv[1]); + } + else if (strcmp(argv[1], "msgs_timeout") == 0) { + return _call_report_msgs_timeout(argc - 1, &argv[1]); + } + else if (strcmp(argv[1], "msgs_lost") == 0) { + return _call_report_msgs_lost(argc - 1, &argv[1]); + } + else if (strcmp(argv[1], "msg_acked") == 0) { + return _call_report_msg_acked(argc - 1, &argv[1]); + } + else if (strcmp(argv[1], "ecn_ce") == 0) { + return _call_report_ecn_ce(argc - 1, &argv[1]); + } + print_str("{\"error\":\"Unknown command `"); + print_str(argv[1]); + print_str("`\"}\n"); + return 1; +} + +/** @} */ diff --git a/sys/include/congure.h b/sys/include/congure.h index 9031b54b05..d993002893 100644 --- a/sys/include/congure.h +++ b/sys/include/congure.h @@ -171,7 +171,7 @@ struct congure_snd_driver { * * @param[in] c The CongURE state object. * @param[in] msgs A collection of messages for which the ACK - * timed out. The list may be changed by the + * timed out. The list must not be changed by the * method. */ void (*report_msgs_timeout)(congure_snd_t *c, congure_snd_msg_t *msgs); @@ -190,7 +190,8 @@ struct congure_snd_driver { * * @param[in] c The CongURE state object. * @param[in] msgs A collection of messages that are known to - * be lost. The list may be changed by the method. + * be lost. The list must not be be changed by the + * method. */ void (*report_msgs_lost)(congure_snd_t *c, congure_snd_msg_t *msgs); diff --git a/sys/include/congure/test.h b/sys/include/congure/test.h new file mode 100644 index 0000000000..3383aa6962 --- /dev/null +++ b/sys/include/congure/test.h @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup sys_congure_test CongURE test framework shell commands + * @ingroup sys_congure + * @brief Shell commands to test a CongURE implementation + * + * This module requires an application defined `congure_impl.h` which defines + * the congure_snd_t extension of the CongURE implementation as + * @ref congure_test_snd_t and provides a function declaration + * @ref congure_test_snd_setup() setup said type. E.g. + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ {.c} + * typedef congure_reno_snd_t congure_test_snd_t; + * + * void congure_test_snd_setup(congure_test_snd_t *c, int id); + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * All constants and initial values can then be set within the application + * specific definition of @ref congure_test_snd_setup(). + * + * @{ + * + * @file + * @brief Definitions for the CongURE test framework + * + * @author Martine S Lenders + */ + +#ifndef CONGURE_TEST_H +#define CONGURE_TEST_H + +#include "congure_impl.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef DOXYGEN +/** + * @brief Application-defined type for the CongURE state object under test + * + * @extends congure_snd_t + * + * @note Needs to be set within an application-provided `congure_impl.h` to + * the state object of the CongURE implementation you want to test. + */ +typedef congure_snd_t congure_test_snd_t; + +/** + * @brief Setup the application-defined CongURE state object under test + * + * @note Needs to be defined by the application and declare it within + * an application-provided `congure_impl.h` + * + * @param[in,out] c The CongURE state object under test. May not be NULL. + * @param[in] id And application-specific ID that may identify different + * setup parameters, e.g. a set of different constants to + * use when setting up @p c. If not applicable to your + * application just ignore @p id. + * + * @retval 0 on success + * @retval -1 when @p id is not ignored and is an unknown value to the + * application. + */ +int congure_test_snd_setup(void congure_test_snd_t *c, unsigned id); +#endif /* DOXYGEN */ + +/** + * @brief Pool size for the message list elements for a lost message report + * + * @see congure_snd_driver_t::report_msg_lost + * + * This defines the maximum number of 3-tuples you can use with + * @ref congure_test_call_report() when `argv[1] = "msg_lost"`. + */ +#ifndef CONFIG_CONGURE_TEST_LOST_MSG_POOL_SIZE +#define CONFIG_CONGURE_TEST_LOST_MSG_POOL_SIZE (4U) +#endif + +/** + * @brief Get the application-defined CongURE state object + * + * @note Needs to be defined by the application + * + * @return The CongURE state object. + */ +congure_test_snd_t *congure_test_get_state(void); + +/** + * @brief Clears the CongURE state object + * + * Every byte in the state object is set to 0. + * + * @param[in] argc Number of @p argv. Needs to be at least 1. + * @param[in] argv Command line arguments. No extra arguments are required + * except for the command name in `argv[0]`. + * + * Always generates the following JSON object in STDOUT: + * @code {"success": null} @endcode + * + * @return Always 0. + */ +int congure_test_clear_state(int argc, char **argv); + +/** + * @brief Setup the CongURE state object + * + * Calls application-defined @ref congure_test_snd_setup() + * + * @param[in] argc Number of @p argv. Needs to be at least 1. + * @param[in] argv Command line arguments. The command name is expected in + * `argv[0]`. If @p argc > 1, a integer is expected in + * `argv[1]` for the `id` parameter of + * @ref congure_test_snd_setup(). + * + * This function will generate the following JSON objects in STDOUT: + * - @code {"success": "0x12345678"} @endcode + * On success, with `0x12345678` being replaced with the memory address of the + * state object. + * - @code {"error":"`id` expected to be integer"} @endcode + * On error, when `argv[1]` is not parsable to an unsigned integer. + * - @code {"error":"`id` is invalid"} @endcode + * On error, when `argv[1]` is a valid unsigned integer but is an unknown + * value to the application. + * + * @retval 0 on success. + * @retval 1 on error. Only can happen if `argv[1]` is provided and an invalid + * or unexpected value. + */ +int congure_test_call_setup(int argc, char **argv); + +/** + * @brief Calls `init()` method for CongURE state object. + * + * @see congure_snd_driver_t::init() + * + * @param[in] argc Number of @p argv. Needs to be at least 2. + * @param[in] argv Command line arguments. `argv[0]` needs to be the command + * name and `argv[1]` needs to be a hexadecimal integer of + * format 0xXXXX, represending a pointer to the object used as + * the `ctx` parameter for `init()`. + * + * This function will generate the following JSON objects in STDOUT on error: + * - @code {"error":"State object not set up"} @endcode + * When @ref congure_test_snd_setup() was not called before calling this + * command (i.e. the `driver` member of the state object is `NULL`). + * - @code {"error":"`ctx` argument expected"} @endcode + * When @p argc < 2. + * - @code {"error":"`ctx` expected to be hex"} @endcode + * When `argv[1]` is not parseable as a hexadecimal integer. + * + * Always generates the following JSON object in STDOUT: + * @code {"success": null} @endcode + * + * @retval 0 on success. + * @retval 1 on error. + */ +int congure_test_call_init(int argc, char **argv); + +/** + * @brief Calls `inter_msg_interval()` method for CongURE state object. + * + * @see congure_snd_driver_t::inter_msg_interval() + * + * @param[in] argc Number of @p argv. Needs to be at least 1. + * @param[in] argv Command line arguments. No extra arguments are required + * except for the command name in `argv[0]`. + * + * This function will generate the following JSON objects in STDOUT: + * - @code {"success":X} @endcode + * On success, with `X` being replaced with the return value of + * congure_snd_driver_t::inter_msg_interval(). + * - @code {"error":"State object not set up"} @endcode + * When @ref congure_test_snd_setup() was not called before calling this + * command (i.e. the `driver` member of the state object is `NULL`). + * + * + * @retval 0 on success. + * @retval 1 on error. + */ +int congure_test_call_inter_msg_interval(int argc, char **argv); + +/** + * @brief Calls one of the `report_*()` methods for CongURE state object. + * + * @see congure_snd_driver_t::report_msg_sent() + * @see congure_snd_driver_t::report_msg_discarded() + * @see congure_snd_driver_t::report_msg_timeout() + * @see congure_snd_driver_t::report_msg_lost() + * @see congure_snd_driver_t::report_msg_acked() + * @see congure_snd_driver_t::report_ecn_ce() + * + * @param[in] argc Number of @p argv. Needs to be at least 3. + * @param[in] argv Command line arguments. `argv[0]` needs to be the command + * name and `argv[1]` needs to one of the following + * sub-commands that may require at least one extra arguments: + * - `msg_sent`: `argv[2]` is expected to be an integer for the + * `msg_size` parameter of + * congure_snd_driver_t::report_msg_sent() + * - `msg_sent`: `argv[2]` is expected to be an integer for the + * `msg_size` parameter of + * congure_snd_driver_t::report_msg_discarded() + * - `msg_timeout`: `argv` is expected to have a number of + * parameters divisible by 3 after `argv[1]` (i.e. + * `(argc - 2) % 3 == 0` must hold). Each group of 3 + * `argv[2+i]`, `argv[3+i]`, argv[4+i] (with `i` being the + * offset of the group) represents an element in the `msgs` + * list parameter of + * congure_snd_driver_t::report_msg_timeout(): + * - `argv[2+i]` (`msg_send_time`) is expected to be an + * integer for the `send_time` member of + * @ref congure_snd_msg_t, + * - `argv[3+i]` (`msg_size`) is expected to be a an integer + * for the `size` member of @ref congure_snd_msg_t, and + * - `argv[4+i]` (`msg_resends`) is expected to be an integer + * integer for the `resends` member of + * @ref congure_snd_msg_t. + * - `msg_lost`: `argv` is expected to have a number of + * parameters divisible by 3 after `argv[1]` (i.e. + * `(argc - 2) % 3 == 0` must hold. Each group of 3 + * `argv[2+i]`, `argv[3+i]`, argv[4+i] (with `i` being the + * offset of the group) represents an element in the `msgs` + * list parameter of + * congure_snd_driver_t::report_msg_lost(): + * - `argv[2+i]` (`msg_send_time`) is expected to be an + * integer for the `send_time` member of + * @ref congure_snd_msg_t, + * - `argv[3+i]` (`msg_size`) is expected to be an integer + * for the `size` member of @ref congure_snd_msg_t, and + * - `argv[4+i]` (`msg_resends`) is expected to be a an integer + * integer for the `resends` member of + * @ref congure_snd_msg_t. + * - `msg_acked`: `argc` must be 11. The first three arguments + * after `argv[1]` represent members of the `msg` parameter + * of congure_snd_driver_t::report_msg_acked(): + * - `argv[2]` (`msg_send_time`) is expected to be an + * integer for the `send_time` member of + * @ref congure_snd_msg_t, + * - `argv[3]` (`msg_size`) is expected to be an integer + * for the `size` member of @ref congure_snd_msg_t, and + * - `argv[4]` (`msg_resends`) is expected to be an integer + * for the `resends` member of @ref congure_snd_msg_t. + * + * The `next` member of @ref congure_snd_msg_t will be + * initialized with `NULL`. + * + * The remaining 6 arguments represent members of the `ack` + * parameter of congure_snd_driver_t::report_msg_acked(): + * - `argv[5]` (`ack_recv_time`) is expected to be a an + * integer for the `recv_time` member of + * @ref congure_snd_ack_t, + * - `argv[6]` (`ack_id`) is expected to be a an integer + * for the `ack_id` member of @ref congure_snd_ack_t, and + * - `argv[7]` (`ack_size`) is expected to be a an integer + * integer for the `size` member of @ref congure_snd_ack_t. + * - `argv[8]` (`ack_clean`) is expected to be a an integer + * for the `clean` member of @ref congure_snd_ack_t. If + * `argv[8]` is `"0"`, `clean` will be set to `false` and to + * `true` otherwise. + * - `argv[9]` (`ack_wnd`) is expected to be a 16-bit integer + * for the `wnd` member of @ref congure_snd_ack_t. + * - `argv[10]` (`ack_delay`) is expected to be a 16-bit + * integer for the `delay` member of + * @ref congure_snd_ack_t. + * - `ecn_ce`: `argv[2]` is expected to be an integer for the + * `time` parameter of congure_snd_driver_t::report_ecn_ce() + * + * This function will generate the following JSON objects in STDOUT: + * - @code {"success":null} @endcode + * On success + * - @code {"error":"State object not set up"} @endcode + * When @ref congure_test_snd_setup() was not called before calling this + * command (i.e. the `driver` member of the state object is `NULL`). + * - @code {"error":"No report command provided"} @endcode + * When @p argc < 2. + * - @code {"error":"Unknown command ``"} @endcode + * When `argv[1] = ""` is not a known sub-command. + * - @code {"error":"`` argument expected"} @endcode + * When `argv[i] = ""` is expected but `argc <= i`. + * - @code {"error":"`` expected to be integer"} @endcode + * When `argv[i] = ""` is expected to be an integer but is not + * parseable + * - @code {"error":"`` expected not 16 bit wide"} @endcode + * When `argv[i] = ""` is expected to be an 16-bit unsigned integer + * but is is greater than or equal to $2^{16}$ + * - @code {"error":"At least `` arguments expecdted"} + * @endcode + * When `argc` is smaller than expected. `` provides the number of + * arguments beyond the sub-command (i.e. `argc` needs at least to be + * ` + 2`), with the names of the arguments expected provided in + * `` as a comma-seperated list of ``. + * - @code {"error":"Number of arguments must be divisible by 3"} @endcode + * When `argv[1] == "msg_timeout"` or `argv[1] == "msg_lost"` but + * the length of the argument list after `argv[1]` is not divisible by 3 (i.e. + * `(argc - 2) % 3 != 0`). + * - @code {"error":"List element pool depleted"} @endcode + * When `argv[1] == "msg_timeout"` or `argv[1] == "msg_lost"` and + * `(argc - 2) / 3` >= @ref CONFIG_CONGURE_TEST_LOST_MSG_POOL_SIZE). + * + * Provides no output on success. + * + * @retval 0 on success. + * @retval 1 on error. + */ +int congure_test_call_report(int argc, char **argv); + +#ifdef __cplusplus +} +#endif + +#endif /* CONGURE_TEST_H */ +/** @} */ diff --git a/sys/shell/commands/shell_commands.c b/sys/shell/commands/shell_commands.c index f2d5a13a25..50dc2ce4b1 100644 --- a/sys/shell/commands/shell_commands.c +++ b/sys/shell/commands/shell_commands.c @@ -22,6 +22,9 @@ #include #include "shell_commands.h" +#ifdef MODULE_CONGURE_TEST +#include "congure/test.h" +#endif extern int _reboot_handler(int argc, char **argv); extern int _version_handler(int argc, char **argv); @@ -338,6 +341,17 @@ const shell_command_t _shell_command_list[] = { #endif #ifdef MODULE_DFPLAYER {"dfplayer", "Control a DFPlayer Mini MP3 player", _sc_dfplayer}, +#endif +#ifdef MODULE_CONGURE_TEST + { "cong_clear", "Clears CongURE state object", congure_test_clear_state }, + { "cong_setup", "Calls the setup function for the CongURE state object", + congure_test_call_setup }, + { "cong_init", "Calls init method of the CongURE state object", + congure_test_call_init }, + { "cong_imi", "Calls inter_message_interval method of the CongURE state object", + congure_test_call_inter_msg_interval }, + { "cong_report", "Calls a report_* method of the CongURE state object", + congure_test_call_report }, #endif {NULL, NULL, NULL} }; From f5bae1a8fb2aff2224817cc980f18825ecf29152 Mon Sep 17 00:00:00 2001 From: Martine Lenders Date: Fri, 12 Feb 2021 11:03:38 +0100 Subject: [PATCH 4/5] congure_mock: initial import of a CongURE mock implementation --- sys/congure/Kconfig | 2 + sys/congure/Makefile | 3 + sys/congure/mock/Kconfig | 3 + sys/congure/mock/Makefile | 3 + sys/congure/mock/congure_mock.c | 121 +++++++++++++++++++++++ sys/include/congure/mock.h | 164 ++++++++++++++++++++++++++++++++ 6 files changed, 296 insertions(+) create mode 100644 sys/congure/mock/Kconfig create mode 100644 sys/congure/mock/Makefile create mode 100644 sys/congure/mock/congure_mock.c create mode 100644 sys/include/congure/mock.h diff --git a/sys/congure/Kconfig b/sys/congure/Kconfig index 2cc77d5001..ce189fc382 100644 --- a/sys/congure/Kconfig +++ b/sys/congure/Kconfig @@ -8,6 +8,7 @@ if !TEST_KCONFIG menu "CongURE congestion control abstraction" depends on USEMODULE_CONGURE +rsource "mock/Kconfig" rsource "test/Kconfig" endmenu # CongURE congestion control abstraction @@ -20,6 +21,7 @@ menuconfig MODULE_CONGURE if MODULE_CONGURE +rsource "mock/Kconfig" rsource "test/Kconfig" endif # MODULE_CONGURE diff --git a/sys/congure/Makefile b/sys/congure/Makefile index 12697067ff..37c69ac62f 100644 --- a/sys/congure/Makefile +++ b/sys/congure/Makefile @@ -1,3 +1,6 @@ +ifneq (,$(filter congure_mock,$(USEMODULE))) + DIRS += mock +endif ifneq (,$(filter congure_test,$(USEMODULE))) DIRS += test endif diff --git a/sys/congure/mock/Kconfig b/sys/congure/mock/Kconfig new file mode 100644 index 0000000000..20355df2a8 --- /dev/null +++ b/sys/congure/mock/Kconfig @@ -0,0 +1,3 @@ +config MODULE_CONGURE_MOCK + bool "CongURE mock implementation for testing" + depends on MODULE_CONGURE diff --git a/sys/congure/mock/Makefile b/sys/congure/mock/Makefile new file mode 100644 index 0000000000..ce7732052a --- /dev/null +++ b/sys/congure/mock/Makefile @@ -0,0 +1,3 @@ +MODULE := congure_mock + +include $(RIOTBASE)/Makefile.base diff --git a/sys/congure/mock/congure_mock.c b/sys/congure/mock/congure_mock.c new file mode 100644 index 0000000000..840e3b734b --- /dev/null +++ b/sys/congure/mock/congure_mock.c @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @{ + * + * @file + * @author Martine Lenders + */ + +#include "congure/mock.h" + +static void _snd_init(congure_snd_t *cong, void *ctx); +static int32_t _snd_inter_msg_interval(congure_snd_t *cong, unsigned msg_size); +static void _snd_report_msg_sent(congure_snd_t *cong, unsigned msg_size); +static void _snd_report_msg_discarded(congure_snd_t *cong, unsigned msg_size); +static void _snd_report_msgs_lost(congure_snd_t *cong, congure_snd_msg_t *msgs); +static void _snd_report_msgs_timeout(congure_snd_t *cong, + congure_snd_msg_t *msgs); +static void _snd_report_msg_acked(congure_snd_t *cong, congure_snd_msg_t *msg, + congure_snd_ack_t *ack); +static void _snd_report_ecn_ce(congure_snd_t *cong, ztimer_now_t time); + +static const congure_snd_driver_t _driver = { + .init = _snd_init, + .inter_msg_interval = _snd_inter_msg_interval, + .report_msg_sent = _snd_report_msg_sent, + .report_msg_discarded = _snd_report_msg_discarded, + .report_msgs_timeout = _snd_report_msgs_timeout, + .report_msgs_lost = _snd_report_msgs_lost, + .report_msg_acked = _snd_report_msg_acked, + .report_ecn_ce = _snd_report_ecn_ce, +}; + +void congure_mock_snd_setup(congure_mock_snd_t *c) +{ + c->super.driver = &_driver; +} + +static void _snd_init(congure_snd_t *cong, void *ctx) +{ + congure_mock_snd_t *c = (congure_mock_snd_t *)cong; + + c->init_calls++; + c->init_args.c = &c->super; + c->init_args.ctx = ctx; +} + +static int32_t _snd_inter_msg_interval(congure_snd_t *cong, unsigned msg_size) +{ + congure_mock_snd_t *c = (congure_mock_snd_t *)cong; + + c->inter_msg_interval_calls++; + c->inter_msg_interval_args.c = &c->super; + c->inter_msg_interval_args.msg_size = msg_size; + return -1; +} + +static void _snd_report_msg_sent(congure_snd_t *cong, unsigned msg_size) +{ + congure_mock_snd_t *c = (congure_mock_snd_t *)cong; + + c->report_msg_sent_calls++; + c->report_msg_sent_args.c = &c->super; + c->report_msg_sent_args.msg_size = msg_size; +} + +static void _snd_report_msg_discarded(congure_snd_t *cong, unsigned msg_size) +{ + congure_mock_snd_t *c = (congure_mock_snd_t *)cong; + + c->report_msg_discarded_calls++; + c->report_msg_discarded_args.c = &c->super; + c->report_msg_discarded_args.msg_size = msg_size; +} + +static void _snd_report_msgs_lost(congure_snd_t *cong, congure_snd_msg_t *msgs) +{ + congure_mock_snd_t *c = (congure_mock_snd_t *)cong; + + c->report_msgs_lost_calls++; + c->report_msgs_lost_args.c = &c->super; + c->report_msgs_lost_args.msgs = msgs; +} + +static void _snd_report_msgs_timeout(congure_snd_t *cong, + congure_snd_msg_t *msgs) +{ + congure_mock_snd_t *c = (congure_mock_snd_t *)cong; + + c->report_msgs_timeout_calls++; + c->report_msgs_timeout_args.c = &c->super; + c->report_msgs_timeout_args.msgs = msgs; +} + +static void _snd_report_msg_acked(congure_snd_t *cong, congure_snd_msg_t *msg, + congure_snd_ack_t *ack) +{ + congure_mock_snd_t *c = (congure_mock_snd_t *)cong; + + c->report_msg_acked_calls++; + c->report_msg_acked_args.c = &c->super; + c->report_msg_acked_args.msg = msg; + c->report_msg_acked_args.ack = ack; +} + +static void _snd_report_ecn_ce(congure_snd_t *cong, ztimer_now_t time) +{ + congure_mock_snd_t *c = (congure_mock_snd_t *)cong; + + c->report_ecn_ce_calls++; + c->report_ecn_ce_args.c = &c->super; + c->report_ecn_ce_args.time = time; +} + +/** @} */ diff --git a/sys/include/congure/mock.h b/sys/include/congure/mock.h new file mode 100644 index 0000000000..d93ea49a1a --- /dev/null +++ b/sys/include/congure/mock.h @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup sys_congure_mock CongURE mock implementation + * @ingroup sys_congure + * @brief A mock for testing @ref sys_congure + * @{ + * + * @file + * + * @author Martine S. Lenders + */ +#ifndef CONGURE_MOCK_H +#define CONGURE_MOCK_H + +#include + +#include "congure.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief A mock CongURE state object + * + * @extends congure_snd_t + */ +typedef struct { + congure_snd_t super; /**< see @ref congure_snd_t */ + /** + * @brief How often was the congure_snd_driver_t::init() method called? + */ + uint8_t init_calls; + /** + * @brief How often was the congure_snd_driver_t::inter_msg_interval() + * method called? + */ + uint8_t inter_msg_interval_calls; + /** + * @brief How often was the congure_snd_driver_t::report_msg_sent() + * method called? + */ + uint8_t report_msg_sent_calls; + /** + * @brief How often was the congure_snd_driver_t::report_msg_discarded() + * method called? + */ + uint8_t report_msg_discarded_calls; + /** + * @brief How often was the congure_snd_driver_t::report_msgs_timeout() + * method called? + */ + uint8_t report_msgs_timeout_calls; + /** + * @brief How often was the congure_snd_driver_t::report_msgs_lost() + * method called? + */ + uint8_t report_msgs_lost_calls; + /** + * @brief How often was the congure_snd_driver_t::report_msg_acked() + * method called? + */ + uint8_t report_msg_acked_calls; + /** + * @brief How often was the congure_snd_driver_t::report_ecn_ce() + * method called? + */ + uint8_t report_ecn_ce_calls; + /** + * @brief What were the arguments for the last + * congure_snd_driver_t::init() call? + */ + struct { + congure_snd_t *c; /**< The CongURE object to initialize */ + void *ctx; /**< Context for callbacks specific to CC */ + } init_args; + /** + * @brief What were the arguments for the last + * congure_snd_driver_t::inter_msg_interval() call? + */ + struct { + congure_snd_t *c; /**< The CongURE state object */ + unsigned msg_size; /**< The size of the next message to send */ + } inter_msg_interval_args; + /** + * @brief What were the arguments for the last + * congure_snd_driver_t::report_msg_sent() call? + */ + struct { + congure_snd_t *c; /**< The CongURE state object */ + unsigned msg_size; /**< Size of the message */ + } report_msg_sent_args; + /** + * @brief What were the arguments for the last + * congure_snd_driver_t::report_msg_discarded() call? + */ + struct { + congure_snd_t *c; /**< The CongURE state object */ + unsigned msg_size; /**< Size of the message */ + } report_msg_discarded_args; + /** + * @brief What were the arguments for the last + * congure_snd_driver_t::report_msgs_timeout() call? + */ + struct { + congure_snd_t *c; /**< The CongURE state object */ + /** + * @brief A collection of messages for which the ACK timed out + */ + congure_snd_msg_t *msgs; + } report_msgs_timeout_args; + /** + * @brief What were the arguments for the last + * congure_snd_driver_t::report_msgs_lost() call? + */ + struct { + congure_snd_t *c; /**< The CongURE state object */ + /** + * @brief A collection of messages that are known to be lost + */ + congure_snd_msg_t *msgs; + } report_msgs_lost_args; + /** + * @brief What were the arguments for the last + * congure_snd_driver_t::report_msg_acked() call? + */ + struct { + congure_snd_t *c; /**< The CongURE state object */ + congure_snd_msg_t *msg; /**< The ACK'd message */ + congure_snd_ack_t *ack; /**< The received ACK */ + } report_msg_acked_args; + /** + * @brief What were the arguments for the last + * congure_snd_driver_t::report_ecn_ce() call? + */ + struct { + congure_snd_t *c; /**< The CongURE state object */ + /** + * @brief Timestamp of the message the CE event occurred for was sent + */ + ztimer_now_t time; + } report_ecn_ce_args; +} congure_mock_snd_t; + +/** + * @brief Sets up the driver for CongURE mock object + * + * @param[in] c A CongURE mock object + */ +void congure_mock_snd_setup(congure_mock_snd_t *c); + +#ifdef __cplusplus +} +#endif + +#endif /* CONGURE_MOCK_H */ +/** @} */ From 90a1e4d5c503c8251bd32f170c2d3396fda734c1 Mon Sep 17 00:00:00 2001 From: Martine Lenders Date: Wed, 3 Feb 2021 10:45:19 +0100 Subject: [PATCH 5/5] tests: initial import of tests for CongURE and its test framework --- .murdock | 8 +- tests/congure_test/Makefile | 29 ++ tests/congure_test/Makefile.ci | 7 + tests/congure_test/README.md | 40 +++ tests/congure_test/app.config | 2 + tests/congure_test/app.config.test | 7 + tests/congure_test/congure_impl.c | 29 ++ tests/congure_test/congure_impl.h | 34 +++ tests/congure_test/main.c | 333 +++++++++++++++++++++ tests/congure_test/tests/01-run.py | 450 +++++++++++++++++++++++++++++ 10 files changed, 935 insertions(+), 4 deletions(-) create mode 100644 tests/congure_test/Makefile create mode 100644 tests/congure_test/Makefile.ci create mode 100644 tests/congure_test/README.md create mode 100644 tests/congure_test/app.config create mode 100644 tests/congure_test/app.config.test create mode 100644 tests/congure_test/congure_impl.c create mode 100644 tests/congure_test/congure_impl.h create mode 100644 tests/congure_test/main.c create mode 100755 tests/congure_test/tests/01-run.py diff --git a/.murdock b/.murdock index bb099d6801..c4c2c5a289 100755 --- a/.murdock +++ b/.murdock @@ -8,7 +8,7 @@ : ${TEST_BOARDS_LLVM_COMPILE:=""} : ${TEST_KCONFIG_samr21_xpro:="examples/hello-world tests/periph_* -tests/test_tools tests/xtimer_* tests/ztimer_* +tests/test_tools tests/congure_* tests/xtimer_* tests/ztimer_* tests/driver_ad7746 tests/driver_adcxx1c tests/driver_ads101x tests/driver_adt101x tests/driver_adt7310 tests/driver_adxl345 tests/driver_aip31068 tests/driver_apa102 tests/driver_apds99xx tests/driver_apds99xx_full tests/driver_at tests/driver_at24cxxx @@ -25,9 +25,9 @@ tests/mtd_mapper tests/driver_o* tests/driver_p* tests/driver_q* tests/driver_r* tests/driver_s* tests/driver_t* tests/driver_u* tests/driver_v*"} : ${TEST_KCONFIG_native:="examples/hello-world tests/periph_* tests/sys_crypto -tests/test_tools tests/prng_* tests/xtimer_* tests/ztimer_* tests/driver_ws281x -tests/posix_sleep tests/pkg_umorse tests/cb_mux* tests/eepreg tests/shell -tests/struct_tm_utility"} +tests/test_tools tests/congure_* tests/prng_* tests/xtimer_* tests/ztimer_* +tests/driver_ws281x tests/posix_sleep tests/pkg_umorse tests/cb_mux* tests/eepreg +tests/shell tests/struct_tm_utility"} : ${TEST_WITH_CONFIG_SUPPORTED:="examples/suit_update tests/driver_at86rf2xx_aes"} diff --git a/tests/congure_test/Makefile b/tests/congure_test/Makefile new file mode 100644 index 0000000000..cce77d5867 --- /dev/null +++ b/tests/congure_test/Makefile @@ -0,0 +1,29 @@ +include ../Makefile.tests_common + +USEMODULE += congure_mock +USEMODULE += congure_test +USEMODULE += fmt +USEMODULE += shell +USEMODULE += shell_commands + +INCLUDES += -I$(CURDIR) + +# Use a terminal that does not introduce extra characters into the stream. +RIOT_TERMINAL ?= socat + +# As there is an 'app.config' we want to explicitly disable Kconfig by setting +# the variable to empty +SHOULD_RUN_KCONFIG ?= + +include $(RIOTBASE)/Makefile.include + +ifndef CONFIG_SHELL_NO_ECHO + CFLAGS += -DCONFIG_SHELL_NO_ECHO=1 +endif + +ifndef CONFIG_CONGURE_TEST_LOST_MSG_POOL_SIZE + CFLAGS += -DCONFIG_CONGURE_TEST_LOST_MSG_POOL_SIZE=4 + export LOST_MSG_POOL_SIZE=4 +else + export LOST_MSG_POOL_SIZE=$(CONFIG_CONGURE_TEST_LOST_MSG_POOL_SIZE) +endif diff --git a/tests/congure_test/Makefile.ci b/tests/congure_test/Makefile.ci new file mode 100644 index 0000000000..75912a97e3 --- /dev/null +++ b/tests/congure_test/Makefile.ci @@ -0,0 +1,7 @@ +BOARD_INSUFFICIENT_MEMORY := \ + arduino-duemilanove \ + arduino-leonardo \ + arduino-nano \ + arduino-uno \ + atmega328p \ + # diff --git a/tests/congure_test/README.md b/tests/congure_test/README.md new file mode 100644 index 0000000000..656a441c76 --- /dev/null +++ b/tests/congure_test/README.md @@ -0,0 +1,40 @@ +Basic tests for the CongURE API +=============================== + +This test tests the `congure_test` test framework used for the other CongURE +tests. + +Usage +----- + +The test requires an up-to-date version of `riotctrl` with `rapidjson` support: + +```console +$ pip install --upgrade riotctrl[rapidjson] +``` + +Then simply run the application using: + +```console +$ BOARD="" make flash test +``` + +It can also executed with pytest: + +```console +$ pytest tests/01-run.py +``` + +Note that this only works from within the directory of the test, so if you are +somewhere else, use + +```console +$ cd tests/congure_test +``` + +first to change into that. + +Expected result +--------------- + +The application's test script passes without error code. diff --git a/tests/congure_test/app.config b/tests/congure_test/app.config new file mode 100644 index 0000000000..2d84fd3dac --- /dev/null +++ b/tests/congure_test/app.config @@ -0,0 +1,2 @@ +CONFIG_KCONFIG_USEMODULE_SHELL=y +CONFIG_SHELL_NO_ECHO=y diff --git a/tests/congure_test/app.config.test b/tests/congure_test/app.config.test new file mode 100644 index 0000000000..ec56840c46 --- /dev/null +++ b/tests/congure_test/app.config.test @@ -0,0 +1,7 @@ +CONFIG_MODULE_CONGURE=y +CONFIG_MODULE_CONGURE_MOCK=y +CONFIG_MODULE_CONGURE_TEST=y +CONFIG_MODULE_FMT=y +CONFIG_MODULE_SHELL=y +CONFIG_MODULE_SHELL_COMMANDS=y +CONFIG_SHELL_NO_ECHO=y diff --git a/tests/congure_test/congure_impl.c b/tests/congure_test/congure_impl.c new file mode 100644 index 0000000000..24732600e6 --- /dev/null +++ b/tests/congure_test/congure_impl.c @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @{ + * + * @file + * @author Martine Lenders + */ + +#include "congure/mock.h" + +#include "congure_impl.h" + +int congure_test_snd_setup(congure_test_snd_t *c, unsigned id) +{ + if (id > 0) { + return -1; + } + congure_mock_snd_setup(c); + return 0; +} + +/** @} */ diff --git a/tests/congure_test/congure_impl.h b/tests/congure_test/congure_impl.h new file mode 100644 index 0000000000..9d8e190e51 --- /dev/null +++ b/tests/congure_test/congure_impl.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @{ + * + * @file + * + * @author Martine Lenders + */ +#ifndef CONGURE_IMPL_H +#define CONGURE_IMPL_H + +#include "congure/mock.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef congure_mock_snd_t congure_test_snd_t; + +int congure_test_snd_setup(congure_test_snd_t *c, unsigned id); + +#ifdef __cplusplus +} +#endif + +#endif /* CONGURE_IMPL_H */ +/** @} */ diff --git a/tests/congure_test/main.c b/tests/congure_test/main.c new file mode 100644 index 0000000000..bc72ab50f1 --- /dev/null +++ b/tests/congure_test/main.c @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @{ + * + * @file + * @author Martine S. Lenders + */ + +#include + +#include "clist.h" +#include "congure/test.h" +#include "fmt.h" +#include "shell.h" + +#include "congure_impl.h" + +static int _json_statham(int argc, char **argv); + +static congure_test_snd_t _congure_state; +static const shell_command_t shell_commands[] = { + { "state", "Prints current CongURE state object as JSON", _json_statham }, + { NULL, NULL, NULL } +}; + +int main(void) +{ + char line_buf[SHELL_DEFAULT_BUFSIZE]; + shell_run(shell_commands, line_buf, SHELL_DEFAULT_BUFSIZE); + return 0; +} + +congure_test_snd_t *congure_test_get_state(void) +{ + return &_congure_state; +} + +static int _print_congure_snd_msg(clist_node_t *node, void *arg) +{ + congure_snd_msg_t *msg = (congure_snd_msg_t *)node; + + (void)arg; + print_str("{"); + + print_str("\"send_time\":"); + print_u64_dec(msg->send_time); + print_str(","); + + print_str("\"size\":"); + print_u32_dec(msg->size); + print_str(","); + + print_str("\"resends\":"); + print_u32_dec(msg->resends); + + print_str("},"); + + return 0; +} + +static int _print_congure_snd_ack(congure_snd_ack_t *ack) +{ + print_str("{"); + + print_str("\"recv_time\":"); + print_u64_dec(ack->recv_time); + print_str(","); + + print_str("\"id\":"); + print_u32_dec(ack->id); + print_str(","); + + print_str("\"size\":"); + print_u32_dec(ack->size); + print_str(","); + + print_str("\"clean\":"); + print_str(ack->clean ? "true" : "false"); + print_str(","); + + print_str("\"wnd\":"); + print_u32_dec(ack->wnd); + print_str(","); + + print_str("\"delay\":"); + print_u32_dec(ack->delay); + print_str(","); + + print_str("},"); + + return 0; +} + +static void _print_init_state(void) +{ + print_str("\"init\":{"); + + print_str("\"calls\":"); + print_u32_dec(_congure_state.init_calls); + print_str(","); + + print_str("\"last_args\":{"); + + print_str("\"c\":\"0x"); + print_u32_hex((intptr_t)_congure_state.init_args.c); + print_str("\","); + + print_str("\"ctx\":\"0x"); + print_u32_hex((intptr_t)_congure_state.init_args.ctx); + print_str("\""); + + print_str("}"); + + print_str("},"); +} + +static void _print_inter_msg_interval_state(void) +{ + print_str("\"inter_msg_interval\":{"); + + print_str("\"calls\":"); + print_u32_dec(_congure_state.inter_msg_interval_calls); + print_str(","); + + print_str("\"last_args\":{"); + + print_str("\"c\":\"0x"); + print_u32_hex((intptr_t)_congure_state.inter_msg_interval_args.c); + print_str("\","); + + print_str("\"msg_size\":"); + print_u32_dec(_congure_state.inter_msg_interval_args.msg_size); + print_str(""); + + print_str("}"); + + print_str("},"); +} + +static void _print_report_msg_sent_state(void) +{ + print_str("\"report_msg_sent\":{"); + + print_str("\"calls\":"); + print_u32_dec(_congure_state.report_msg_sent_calls); + print_str(","); + + print_str("\"last_args\":{"); + + print_str("\"c\":\"0x"); + print_u32_hex((intptr_t)_congure_state.report_msg_sent_args.c); + print_str("\","); + + print_str("\"msg_size\":"); + print_u32_dec(_congure_state.report_msg_sent_args.msg_size); + print_str(""); + + print_str("}"); + + print_str("},"); +} + +static void _print_report_msg_discarded_state(void) +{ + print_str("\"report_msg_discarded\":{"); + + print_str("\"calls\":"); + print_u32_dec(_congure_state.report_msg_discarded_calls); + print_str(","); + + print_str("\"last_args\":{"); + + print_str("\"c\":\"0x"); + print_u32_hex((intptr_t)_congure_state.report_msg_discarded_args.c); + print_str("\","); + + print_str("\"msg_size\":"); + print_u32_dec(_congure_state.report_msg_discarded_args.msg_size); + print_str(""); + + print_str("}"); + + print_str("},"); +} + +static void _print_report_msgs_timeout_state(void) +{ + print_str("\"report_msgs_timeout\":{"); + + print_str("\"calls\":"); + print_u32_dec(_congure_state.report_msgs_timeout_calls); + print_str(","); + + print_str("\"last_args\":{"); + + print_str("\"c\":\"0x"); + print_u32_hex((intptr_t)_congure_state.report_msgs_timeout_args.c); + print_str("\","); + + print_str("\"msgs\":["); + clist_foreach((clist_node_t *)&_congure_state.report_msgs_timeout_args.msgs, + _print_congure_snd_msg, NULL); + print_str("]"); + + print_str("}"); + + print_str("},"); +} + +static void _print_report_msgs_lost_state(void) +{ + print_str("\"report_msgs_lost\":{"); + + print_str("\"calls\":"); + print_u32_dec(_congure_state.report_msgs_lost_calls); + print_str(","); + + print_str("\"last_args\":{"); + + print_str("\"c\":\"0x"); + print_u32_hex((intptr_t)_congure_state.report_msgs_lost_args.c); + print_str("\","); + + print_str("\"msgs\":["); + clist_foreach((clist_node_t *)&_congure_state.report_msgs_lost_args.msgs, + _print_congure_snd_msg, NULL); + print_str("]"); + + print_str("}"); + + print_str("},"); +} + +static void _print_report_msg_acked_state(void) +{ + print_str("\"report_msg_acked\":{"); + + print_str("\"calls\":"); + print_u32_dec(_congure_state.report_msg_acked_calls); + print_str(","); + + print_str("\"last_args\":{"); + + print_str("\"c\":\"0x"); + print_u32_hex((intptr_t)_congure_state.report_msg_acked_args.c); + print_str("\","); + + assert((_congure_state.report_msg_acked_args.msg == NULL) || + (_congure_state.report_msg_acked_args.msg->super.next == NULL)); + print_str("\"msg\":"); + if (_congure_state.report_msg_acked_args.msg) { + _print_congure_snd_msg( + (clist_node_t *)_congure_state.report_msg_acked_args.msg, NULL + ); + } + else { + print_str("null,"); + } + + print_str("\"ack\":"); + if (_congure_state.report_msg_acked_args.ack) { + _print_congure_snd_ack(_congure_state.report_msg_acked_args.ack); + } + else { + print_str("null"); + } + + print_str("}"); + + print_str("},"); +} + +static void _print_report_ecn_ce_state(void) +{ + print_str("\"report_ecn_ce\":{"); + + print_str("\"calls\":"); + print_u32_dec(_congure_state.report_ecn_ce_calls); + print_str(","); + + print_str("\"last_args\":{"); + + print_str("\"c\":\"0x"); + print_u32_hex((intptr_t)_congure_state.report_ecn_ce_args.c); + print_str("\","); + + print_str("\"time\":"); + print_u64_dec((intptr_t)_congure_state.report_ecn_ce_args.time); + print_str(","); + + print_str("}"); + + print_str("},"); +} + +static int _json_statham(int argc, char **argv) +{ + (void)argc; + (void)argv; + print_str("{"); + print_str("\"driver\":\"0x"); + print_u32_hex((intptr_t)_congure_state.super.driver); + print_str("\","); + + print_str("\"ctx\":\"0x"); + print_u32_hex((intptr_t)_congure_state.super.ctx); + print_str("\","); + + print_str("\"cwnd\":"); + print_u32_dec(_congure_state.super.cwnd); + print_str(","); + + _print_init_state(); + _print_inter_msg_interval_state(); + _print_report_msg_sent_state(); + _print_report_msg_discarded_state(); + _print_report_msgs_timeout_state(); + _print_report_msgs_lost_state(); + _print_report_msg_acked_state(); + _print_report_ecn_ce_state(); + + print_str("}\n"); + return 0; +} + +/** @} */ diff --git a/tests/congure_test/tests/01-run.py b/tests/congure_test/tests/01-run.py new file mode 100755 index 0000000000..dd4e24d55a --- /dev/null +++ b/tests/congure_test/tests/01-run.py @@ -0,0 +1,450 @@ +#! /usr/bin/env python3 + +# Copyright (C) 2021 Freie Universität Berlin +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +import logging +import os +import sys +import unittest + +from riotctrl.ctrl import RIOTCtrl +from riotctrl.shell import ShellInteraction +from riotctrl.shell.json import RapidJSONShellInteractionParser, rapidjson + + +class TestCongUREBase(unittest.TestCase): + DEBUG = False + + @classmethod + def setUpClass(cls): + cls.ctrl = RIOTCtrl() + cls.ctrl.start_term() + if cls.DEBUG: + cls.ctrl.term.logfile = sys.stdout + cls.ctrl.reset() + cls.shell = ShellInteraction(cls.ctrl) + cls.json_parser = RapidJSONShellInteractionParser() + cls.json_parser.set_parser_args( + parse_mode=rapidjson.PM_TRAILING_COMMAS + ) + cls.logger = logging.getLogger(cls.__name__) + if cls.DEBUG: + cls.logger.setLevel(logging.DEBUG) + + @classmethod + def tearDownClass(cls): + cls.ctrl.stop_term() + + def setUp(self): + self.shell.cmd('cong_clear') + + def exec_cmd(self, cmd, timeout=-1, async_=False): + res = self.shell.cmd(cmd, timeout, async_) + self.logger.debug(repr(res)) + if res.strip(): + return self.json_parser.parse(res) + return None + + +class TestCongUREWithoutSetup(TestCongUREBase): + def test_no_setup(self): + state = self.exec_cmd('state') + self.assertEqual(state, { + 'driver': '0x00000000', + 'ctx': '0x00000000', + 'cwnd': 0, + 'init': { + 'calls': 0, + 'last_args': { + 'c': '0x00000000', + 'ctx': '0x00000000', + }, + }, + 'inter_msg_interval': { + 'calls': 0, + 'last_args': { + 'c': '0x00000000', + 'msg_size': 0, + }, + }, + 'report_msg_sent': { + 'calls': 0, + 'last_args': { + 'c': '0x00000000', + 'msg_size': 0, + }, + }, + 'report_msg_discarded': { + 'calls': 0, + 'last_args': { + 'c': '0x00000000', + 'msg_size': 0, + }, + }, + 'report_msgs_timeout': { + 'calls': 0, + 'last_args': { + 'c': '0x00000000', + 'msgs': [], + }, + }, + 'report_msgs_lost': { + 'calls': 0, + 'last_args': { + 'c': '0x00000000', + 'msgs': [], + }, + }, + 'report_msg_acked': { + 'calls': 0, + 'last_args': { + 'c': '0x00000000', + 'msg': None, + 'ack': None, + }, + }, + 'report_ecn_ce': { + 'calls': 0, + 'last_args': { + 'c': '0x00000000', + 'time': 0, + }, + }, + }) + + def test_setup_invalid_id(self): + self.test_no_setup() + res = self.exec_cmd('cong_setup abcd') + self.assertEqual(res, {'error': "`id` expected to be integer"}) + res = self.exec_cmd('cong_setup 1') + self.assertEqual(res, {'error': "`id` is invalid"}) + + def test_setup(self): + self.test_no_setup() + res = self.exec_cmd('cong_setup') + # res['success'] is 32-bit hexadecimal number greater zero, representing + # the pointer to the congure state object + self.assertGreater(int(res['success'], base=16), 0) + self.assertEqual(len(res['success']), len('0x00000000')) + res = self.exec_cmd('cong_setup 0') + # res['success'] is 32-bit hexadecimal number greater zero, representing + # the pointer to the congure state object + self.assertGreater(int(res['success'], base=16), 0) + self.assertEqual(len(res['success']), len('0x00000000')) + + def test_init_wo_setup(self): + res = self.exec_cmd('cong_init 0x12345') + self.assertEqual(res, {'error': "State object not set up"}) + + def test_inter_msg_interval_wo_setup(self): + res = self.exec_cmd('cong_imi 689') + self.assertEqual(res, {'error': "State object not set up"}) + + def test_report_wo_setup(self): + res = self.exec_cmd('cong_report') + self.assertEqual(res, {'error': "State object not set up"}) + + +class TestCongUREWithSetup(TestCongUREBase): + def setUp(self): + super().setUp() + res = self.exec_cmd('cong_setup') + self.congure_state_ptr = int(res['success'], base=16) + + def test_init_no_args(self): + res = self.exec_cmd('cong_init') + self.assertEqual(res, {'error': "`ctx` argument expected"}) + + def test_init_arg_not_hex(self): + res = self.exec_cmd('cong_init foobar') + self.assertEqual(res, {'error': "`ctx` expected to be hex"}) + res = self.exec_cmd('cong_init 123456') + self.assertEqual(res, {'error': "`ctx` expected to be hex"}) + + def test_init_success(self): + ctx = 0x12345 + res = self.exec_cmd('cong_init 0x{ctx:x}'.format(ctx=ctx)) + self.assertIsNone(res['success']) + res = self.exec_cmd('state') + self.assertEqual(res['init']['calls'], 1) + self.assertEqual(int(res['init']['last_args']['c'], base=16), + self.congure_state_ptr) + self.assertEqual(int(res['init']['last_args']['ctx'], base=16), + ctx) + + def test_inter_msg_interval_no_args(self): + res = self.exec_cmd('cong_imi') + self.assertEqual(res, {'error': '`msg_size` argument expected'}) + + def test_inter_msg_interval_not_int(self): + res = self.exec_cmd('cong_imi foobar') + self.assertEqual(res, {'error': '`msg_size` expected to be integer'}) + + def test_inter_msg_interval_success(self): + msg_size = 521 + res = self.exec_cmd('cong_imi {msg_size}'.format(msg_size=msg_size)) + assert res == {'success': -1} + res = self.exec_cmd('state') + self.assertEqual(res['inter_msg_interval']['calls'], 1) + self.assertEqual(int(res['inter_msg_interval']['last_args']['c'], + base=16), + self.congure_state_ptr) + self.assertEqual(res['inter_msg_interval']['last_args']['msg_size'], + msg_size) + + def test_report_no_args(self): + res = self.exec_cmd('cong_report foobar') + self.assertEqual(res, {'error': 'Unknown command `foobar`'}) + + def test_report_unknown_command(self): + res = self.exec_cmd('cong_report') + self.assertEqual(res, {'error': 'No report command provided'}) + + def test_report_msg_sent_no_args(self): + res = self.exec_cmd('cong_report msg_sent') + self.assertEqual(res, {'error': '`msg_size` argument expected'}) + + def test_report_msg_sent_not_int(self): + res = self.exec_cmd('cong_report msg_sent this one') + self.assertEqual(res, {'error': '`msg_size` expected to be integer'}) + + def test_report_msg_sent_success(self): + msg_size = 1234 + res = self.exec_cmd('cong_report msg_sent {msg_size}' + .format(msg_size=msg_size)) + self.assertIsNone(res['success']) + res = self.exec_cmd('state') + self.assertEqual(res['report_msg_sent']['calls'], 1) + self.assertEqual(int(res['report_msg_sent']['last_args']['c'], + base=16), + self.congure_state_ptr) + self.assertEqual(res['report_msg_sent']['last_args']['msg_size'], + msg_size) + + def test_report_msg_discarded_no_args(self): + res = self.exec_cmd('cong_report msg_discarded') + self.assertEqual(res, {'error': "`msg_size` argument expected"}) + + def test_report_msg_discarded_not_int(self): + res = self.exec_cmd('cong_report msg_discarded this one') + self.assertEqual(res, {'error': "`msg_size` expected to be integer"}) + + def test_report_msg_discarded_success(self): + msg_size = 1234 + res = self.exec_cmd('cong_report msg_discarded {msg_size}' + .format(msg_size=msg_size)) + self.assertIsNone(res['success']) + res = self.exec_cmd('state') + self.assertEqual(res['report_msg_discarded']['calls'], 1) + self.assertEqual(int(res['report_msg_discarded']['last_args']['c'], + base=16), + self.congure_state_ptr) + self.assertEqual(res['report_msg_discarded']['last_args']['msg_size'], + msg_size) + + def _report_msgs_timeout_lost_acked_not_enough_args(self, cmd, exp_params): + args = "" + # gradually append more arguments but never get full set + for i in range(len(exp_params) - 1): + args += ' {}'.format(i + 1) + res = self.exec_cmd('cong_report {cmd} {args}' + .format(cmd=cmd, args=args)) + self.assertEqual(res, { + 'error': 'At least {} arguments {} expected' + .format(len(exp_params), + ', '.join('`{}`'.format(p) + for p in exp_params)) + }) + + def _report_msgs_timeout_lost_argc_not_mod_3(self, cmd): + res = self.exec_cmd('cong_report {cmd} 1 2 3 4'.format(cmd=cmd)) + self.assertEqual(res, { + 'error': 'Number of arguments must be divisible by 3' + }) + res = self.exec_cmd('cong_report {cmd} 1 2 3 4 5'.format(cmd=cmd)) + self.assertEqual(res, { + 'error': 'Number of arguments must be divisible by 3' + }) + + def _report_msgs_timeout_lost_acked_args_not_int(self, cmd, exp_params): + # generate list of arguments that are exp_params string parameters + args = [chr(i + ord('a')) for i in range(len(exp_params))] + if cmd != 'msg_acked': + # and exp_params integer parameters for msgs_timeout and msgs_lost + args += [str(i + len(exp_params)) for i in range(len(exp_params))] + res = self.exec_cmd('cong_report {} {}'.format(cmd, ' '.join(args))) + self.assertEqual(res, { + 'error': '`{}` expected to be integer'.format(exp_params[0]) + }) + # gradually transform all but the last string to integer and test again + for i in range(len(exp_params) - 1): + args[i] = str(i + 1) + res = self.exec_cmd( + 'cong_report {} {}'.format(cmd, ' '.join(args)) + ) + self.assertEqual(res, { + 'error': '`{}` expected to be integer' + .format(exp_params[i + 1]) + }) + + def _report_msgs_timeout_lost_exceed_msg_pool_size(self, cmd): + # expected to be set by Makefile + pool_size = int(os.environ.get('LOST_MSG_POOL_SIZE', 4)) + args = ' '.join('1' for _ in range(3 * pool_size)) + args += ' 1 1 1' + res = self.exec_cmd('cong_report {cmd} {args}' + .format(cmd=cmd, args=args)) + self.assertEqual(res, { + 'error': 'List element pool depleted' + }) + + def _report_msgs_timeout_lost_success(self, cmd): + msgs = [{'send_time': 76543, 'size': 1234, 'resends': 2}, + {'send_time': 5432, 'size': 987, 'resends': 32}] + res = self.exec_cmd( + 'cong_report {cmd} ' + '{msgs[0][send_time]} {msgs[0][size]} {msgs[0][resends]} ' + '{msgs[1][send_time]} {msgs[1][size]} {msgs[1][resends]}' + .format(cmd=cmd, msgs=msgs) + ) + self.assertIsNone(res['success']) + res = self.exec_cmd('state') + self.assertEqual(res['report_{}'.format(cmd)]['calls'], 1) + self.assertEqual(int(res['report_{}'.format(cmd)]['last_args']['c'], + base=16), + self.congure_state_ptr) + self.assertEqual(res['report_{}'.format(cmd)]['last_args']['msgs'], + msgs) + + def test_report_msgs_timeout_not_enough_args(self): + self._report_msgs_timeout_lost_acked_not_enough_args( + 'msgs_timeout', ['msg_send_time', 'msg_size', 'msg_resends'] + ) + + def test_report_msgs_timeout_argc_not_mod_3(self): + self._report_msgs_timeout_lost_argc_not_mod_3('msgs_timeout') + + def test_report_msgs_timeout_args_not_int(self): + self._report_msgs_timeout_lost_acked_args_not_int( + 'msgs_timeout', ['msg_send_time', 'msg_size', 'msg_resends'] + ) + + def test_report_msgs_timeout_exceed_msg_pool_size(self): + self._report_msgs_timeout_lost_exceed_msg_pool_size('msgs_timeout') + + def test_report_msgs_timeout_success(self): + self._report_msgs_timeout_lost_success('msgs_timeout') + + def test_report_msgs_lost_not_enough_args(self): + self._report_msgs_timeout_lost_acked_not_enough_args( + 'msgs_lost', ['msg_send_time', 'msg_size', 'msg_resends'] + ) + + def test_report_msgs_lost_argc_not_mod_3(self): + self._report_msgs_timeout_lost_argc_not_mod_3('msgs_lost') + + def test_report_msgs_lost_msg_args_not_int(self): + self._report_msgs_timeout_lost_acked_args_not_int( + 'msgs_lost', ['msg_send_time', 'msg_size', 'msg_resends'] + ) + + def test_report_msgs_lost_exceed_msg_pool_size(self): + self._report_msgs_timeout_lost_exceed_msg_pool_size('msgs_lost') + + def test_report_msgs_lost_success(self): + self._report_msgs_timeout_lost_success('msgs_lost') + + def test_report_msg_acked_not_enough_args(self): + self._report_msgs_timeout_lost_acked_not_enough_args( + 'msg_acked', [ + 'msg_send_time', 'msg_size', 'msg_resends', 'ack_recv_time', + 'ack_id', 'ack_size', 'ack_clean', 'ack_wnd', 'ack_delay', + ] + + ) + + def test_report_msg_acked_msg_args_not_int(self): + self._report_msgs_timeout_lost_acked_args_not_int( + 'msg_acked', [ + 'msg_send_time', 'msg_size', 'msg_resends', 'ack_recv_time', + 'ack_id', 'ack_size', 'ack_clean', 'ack_wnd', 'ack_delay', + ] + ) + + def test_report_msg_acked_wnd_not_wnd_size(self): + msg = {'send_time': 12345, 'size': 456, 'resends': 0} + ack = {'recv_time': 12432, 'id': 18846, 'size': 12, + 'clean': 1, 'wnd': (1 << 16) + 7642, 'delay': 1235} + res = self.exec_cmd( + 'cong_report msg_acked ' + '{msg[send_time]} {msg[size]} {msg[resends]} ' + '{ack[recv_time]} {ack[id]} {ack[size]} {ack[clean]} ' + '{ack[wnd]} {ack[delay]}' + .format(msg=msg, ack=ack) + ) + self.assertEqual(res, { + 'error': '`ack_wnd` not 16 bit wide' + }) + + def test_report_msg_acked_delay_not_uint16(self): + msg = {'send_time': 12345, 'size': 456, 'resends': 0} + ack = {'recv_time': 12432, 'id': 18846, 'size': 12, + 'clean': 1, 'wnd': 7642, 'delay': (1 << 16) + 1235} + res = self.exec_cmd( + 'cong_report msg_acked ' + '{msg[send_time]} {msg[size]} {msg[resends]} ' + '{ack[recv_time]} {ack[id]} {ack[size]} {ack[clean]} ' + '{ack[wnd]} {ack[delay]}' + .format(msg=msg, ack=ack) + ) + self.assertEqual(res, { + 'error': '`ack_delay` not 16 bit wide' + }) + + def test_report_msg_acked_success(self): + msg = {'send_time': 12345, 'size': 456, 'resends': 0} + ack = {'recv_time': 12432, 'id': 18846, 'size': 12, + 'clean': 1, 'wnd': 742, 'delay': 1235} + res = self.exec_cmd( + 'cong_report msg_acked ' + '{msg[send_time]} {msg[size]} {msg[resends]} ' + '{ack[recv_time]} {ack[id]} {ack[size]} {ack[clean]} ' + '{ack[wnd]} {ack[delay]}' + .format(msg=msg, ack=ack) + ) + self.assertIsNone(res['success']) + res = self.exec_cmd('state') + self.assertEqual(res['report_msg_acked']['calls'], 1) + self.assertEqual(int(res['report_msg_acked']['last_args']['c'], + base=16), + self.congure_state_ptr) + self.assertEqual(res['report_msg_acked']['last_args']['msg'], msg) + self.assertEqual(res['report_msg_acked']['last_args']['ack'], ack) + + def test_report_ecn_ce_no_args(self): + res = self.exec_cmd('cong_report ecn_ce') + self.assertEqual(res, {'error': '`time` argument expected'}) + + def test_report_ecn_ce_not_int(self): + res = self.exec_cmd('cong_report ecn_ce this one') + self.assertEqual(res, {'error': '`time` expected to be integer'}) + + def test_report_ecn_ce_success(self): + time = 64352 + res = self.exec_cmd('cong_report ecn_ce {time}'.format(time=time)) + self.assertIsNone(res['success']) + res = self.exec_cmd('state') + self.assertEqual(res['report_ecn_ce']['calls'], 1) + self.assertEqual(int(res['report_ecn_ce']['last_args']['c'], + base=16), + self.congure_state_ptr) + self.assertEqual(res['report_ecn_ce']['last_args']['time'], + time) + + +if __name__ == '__main__': + unittest.main()