Change in libosmocore[master]: add osmo_tdef API, moved from osmo-bsc T_def

This is merely a historical archive of years 2008-2021, before the migration to mailman3.

A maintained and still updated list archive can be found at https://lists.osmocom.org/hyperkitty/list/gerrit-log@lists.osmocom.org/.

Neels Hofmeyr gerrit-no-reply at lists.osmocom.org
Mon Jan 28 23:57:04 UTC 2019


Neels Hofmeyr has uploaded this change for review. ( https://gerrit.osmocom.org/12717


Change subject: add osmo_tdef API, moved from osmo-bsc T_def
......................................................................

add osmo_tdef API, moved from osmo-bsc T_def

Move T_def from osmo-bsc to libosmocore as osmo_tdef. Adjust naming to be more
consistent. Upgrade to first class API: add mising API doc, add both a C test
and a VTY transcript test.

>From osmo_fsm_inst_state_chg() API doc, cross reference to osmo_tdef API.

The root reason for moving to libosmocore is that I want to use the
mgw_endpoint_fsm in osmo-msc for inter-MSC handover, and hence want to move the
FSM to libosmo-mgcp-client. This FSM uses the T_def from osmo-bsc. Though the
mgw_endpoint_fsm's use of T_def is minimal, I intend to use the osmo_tdef API
in osmo-msc (and probably elsewhere) as well. libosmocore is the most sensible
place for this.

osmo_tdef provides:

- a list of Tnnnn (GSM) timers with description, unit and default value.
- vty UI to allow users to configure non-default timeouts.
- API to tie T timers to osmo_fsm states and set them on state transitions.

- a few standard units (minute, second, millisecond) as well as a custom unit
  (which relies on the timer's human readable description to indicate the
  meaning of the value).
- conversion for standard units: for example, some GSM timers are defined in
  minutes, while our FSM definitions need timeouts in seconds. Conversion is
  for convenience only and can be easily avoided via the custom unit.

T_def was introduced during the recent osmo-bsc refactoring for inter-BSC
handover, and has proven useful:

- without osmo_tdef, each invocation of osmo_fsm_inst_state_chg() needs to be
  programmed with the right timeout value, for all code paths that invoke this
  state change. It is a likely source of errors to get one of them wrong.  By
  defining a T timer exactly for an FSM state, the caller can merely invoke the
  state change and trust on the original state definition to apply the correct
  timeout.

- it is helpful to have a standardized config file UI to provide user
  configurable timeouts, instead of inventing new VTY commands for each
  separate application of T timer numbers.

Change-Id: Ibd6b1ed7f1bd6e1f2e0fde53352055a4468f23e5
---
M include/Makefile.am
A include/osmocom/core/tdef.h
A include/osmocom/vty/tdef.h
M src/Makefile.am
M src/fsm.c
A src/tdef.c
M src/vty/Makefile.am
A src/vty/tdef_vty.c
M tests/Makefile.am
A tests/tdef/tdef_test.c
A tests/tdef/tdef_test.ok
A tests/tdef/tdef_vty_test.c
A tests/tdef/tdef_vty_test.vty
M tests/testsuite.at
14 files changed, 1,494 insertions(+), 4 deletions(-)



  git pull ssh://gerrit.osmocom.org:29418/libosmocore refs/changes/17/12717/1

diff --git a/include/Makefile.am b/include/Makefile.am
index 25a6d75..ca1f8f0 100644
--- a/include/Makefile.am
+++ b/include/Makefile.am
@@ -47,6 +47,7 @@
                        osmocom/core/statistics.h \
                        osmocom/core/strrb.h \
                        osmocom/core/talloc.h \
+                       osmocom/core/tdef.h \
                        osmocom/core/timer.h \
                        osmocom/core/timer_compat.h \
                        osmocom/core/utils.h \
@@ -154,6 +155,7 @@
                           osmocom/vty/vector.h \
                           osmocom/vty/vty.h \
                           osmocom/vty/ports.h \
+                          osmocom/vty/tdef.h \
                           osmocom/ctrl/control_vty.h
 endif
 
diff --git a/include/osmocom/core/tdef.h b/include/osmocom/core/tdef.h
new file mode 100644
index 0000000..701ce54
--- /dev/null
+++ b/include/osmocom/core/tdef.h
@@ -0,0 +1,113 @@
+/* API to define Tnnn timers globally, configure in VTY and use for FSM state changes. */
+#pragma once
+
+#include <stdint.h>
+#include <osmocom/core/utils.h>
+
+struct osmo_fsm_inst;
+
+enum osmo_tdef_unit {
+	OSMO_TDEF_S = 0,	/*< most T are in seconds, keep 0 as default. */
+	OSMO_TDEF_MS,		/*< milliseconds */
+	OSMO_TDEF_M,		/*< minutes */
+	OSMO_TDEF_CUSTOM,
+};
+
+extern const struct value_string osmo_tdef_unit_names[];
+static inline const char *osmo_tdef_unit_name(enum osmo_tdef_unit val)
+{ return get_value_string(osmo_tdef_unit_names, val); }
+
+/*! Define a GSM timer of the form Tnnn, with unit, default value and doc string.
+ * Typically used as an array with the last entry being left zero-initialized, e.g.:
+ *
+ *         struct osmo_tdef tdefs[] = {
+ *                 { .T=10, .default_val=6, .desc="RR Assignment" },
+ *                 { .T=101, .default_val=10, .desc="inter-BSC Handover MT, HO Request to HO Accept" },
+ *                 { .T=3101, .default_val=3, .desc="RR Immediate Assignment" },
+ *                 {}
+ *         };
+ *
+ * Program initialization should call osmo_tdefs_reset() so that all timers return the default_val, until e.g. the VTY
+ * configuration sets user-defined values (see osmo_tdef_vty_init()). See also the examples at osmo_tdef_get().
+ */
+struct osmo_tdef {
+	/*! T1234 number; type corresponds to struct osmo_fsm_inst.T. */
+	const int T;
+	/*! Timeout duration (according to unit), default value; type corresponds to osmo_fsm_inst_state_chg()'s
+	 * timeout_secs argument. Note that osmo_fsm_inst_state_chg() clamps the range to 0x7fffffff. */
+	const unsigned long default_val;
+	const enum osmo_tdef_unit unit;
+	const char *desc;
+	/*! Currently active timeout value, e.g. set by user config. This is the only mutable member: a user may
+	 * configure the timeout value, but neither unit nor any other field. */
+	unsigned long val;
+};
+
+/*! Iterate an array of struct osmo_tdef, the last item should be fully zero, i.e. "{}".
+ * \param[inout] t  A struct osmo_tdef *t used for interation, points at the current entry.
+ * \param[in] tdefs  Array of struct osmo_tdef to iterate.
+ */
+#define osmo_tdef_for_each(t, tdefs) \
+	for (t = tdefs; t && (t->T || t->default_val || t->desc); t++)
+
+void osmo_tdefs_reset(struct osmo_tdef *tdefs);
+unsigned long osmo_tdef_get(const struct osmo_tdef *tdefs, int T, enum osmo_tdef_unit as_unit,
+			    unsigned long val_if_not_present);
+struct osmo_tdef *osmo_tdef_get_entry(struct osmo_tdef *tdefs, int T);
+
+/*! Using osmo_tdef for osmo_fsm_inst: array entry for a mapping of state numbers to timeout definitions.
+ * For a usage example, see osmo_tdef_get_state_timeout(). */
+struct osmo_tdef_state_timeout {
+	/*! Timer number to match struct osmo_tdef.T, and to pass to osmo_fsm_inst_state_chg(). */
+	int T;
+	/*! If true, call osmo_fsm_inst_state_chg_keep_timer().
+	 * If T == 0, keep previous T number, otherwise also set fi->T. */
+	bool keep_timer;
+};
+
+const struct osmo_tdef_state_timeout *osmo_tdef_get_state_timeout(uint32_t state,
+								  const struct osmo_tdef_state_timeout *timeouts_array);
+
+/*! Call osmo_fsm_inst_state_chg() or osmo_fsm_inst_state_chg_keep_timer(), depending on the timeouts_array, tdefs and
+ * default_timeout.
+ *
+ * A T timer configured in sub-second precision is rounded up to the next full second. A timer in unit =
+ * OSMO_TDEF_CUSTOM is applied as if the unit is in seconds (i.e. this macro does not make sense for custom units!).
+ *
+ * See osmo_tdef_get_state_timeout() and osmo_tdef_get().
+ *
+ * If no T timer is defined for the given state, invoke the state change without a timeout.
+ *
+ * Should a T number be defined in timeouts_array that is not defined in tdefs, use default_timeout (in seconds). If
+ * default_timeout is negative, a missing T definition in tdefs instead causes a program abort.
+ *
+ * This is best used by wrapping this function call in a macro suitable for a specific FSM implementation, which can
+ * become as short as: my_fsm_state_chg(fi, NEXT_STATE):
+ *
+ * #define my_fsm_state_chg(fi, NEXT_STATE) \
+ * 	osmo_tdef_fsm_inst_state_chg(fi, NEXT_STATE, my_fsm_timeouts, global_T_defs, 5)
+ *
+ * my_fsm_state_chg(fi, MY_FSM_STATE_1);
+ * // -> No timeout configured, will enter state without timeout.
+ *
+ * my_fsm_state_chg(fi, MY_FSM_STATE_3);
+ * // T423 configured for this state, will look up T423 in tdefs, or use 5 seconds if unset.
+ *
+ * my_fsm_state_chg(fi, MY_FSM_STATE_8);
+ * // keep_timer == true for this state, will invoke osmo_fsm_inst_state_chg_keep_timer().
+ *
+ * \param[inout] fi  osmo_fsm_inst to transition to another state.
+ * \param[in] state  State number to transition to.
+ * \param[in] timeouts_array  Array of struct osmo_tdef_state_timeout[32] to look up state in.
+ * \param[in] tdefs  Array of struct osmo_tdef (last entry zero initialized) to look up T in.
+ * \param[in] default_timeout  If a T is set in timeouts_array, but no timeout value is configured for T, then use this
+ *                             default timeout value as fallback, or pass -1 to abort the program.
+ * \return Return value from osmo_fsm_inst_state_chg() or osmo_fsm_inst_state_chg_keep_timer().
+ */
+#define osmo_tdef_fsm_inst_state_chg(fi, state, timeouts_array, tdefs, default_timeout) \
+	_osmo_tdef_fsm_inst_state_chg(fi, state, timeouts_array, tdefs, default_timeout, \
+				      __FILE__, __LINE__)
+int _osmo_tdef_fsm_inst_state_chg(struct osmo_fsm_inst *fi, uint32_t state,
+				  const struct osmo_tdef_state_timeout *timeouts_array,
+				  const struct osmo_tdef *tdefs, unsigned long default_timeout,
+				  const char *file, int line);
diff --git a/include/osmocom/vty/tdef.h b/include/osmocom/vty/tdef.h
new file mode 100644
index 0000000..7f7b338
--- /dev/null
+++ b/include/osmocom/vty/tdef.h
@@ -0,0 +1,11 @@
+/* API to define Tnnn timers globally, configure in VTY and use for FSM state changes. */
+#pragma once
+
+#include <stdint.h>
+
+struct vty;
+struct osmo_tdef;
+
+void osmo_tdef_vty_init(struct osmo_tdef *tdefs, int cfg_parent_node);
+void osmo_tdef_vty_write(struct vty *vty, const char *indent);
+struct osmo_tdef *osmo_tdef_vty_parse_T_arg(struct vty *vty, const char *osmo_tdef_str);
diff --git a/src/Makefile.am b/src/Makefile.am
index 6840f79..27ab702 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -23,7 +23,8 @@
 			 loggingrb.c crc8gen.c crc16gen.c crc32gen.c crc64gen.c \
 			 macaddr.c stat_item.c stats.c stats_statsd.c prim.c \
 			 conv_acc.c conv_acc_generic.c sercomm.c prbs.c \
-			 isdnhdlc.c
+			 isdnhdlc.c \
+			 tdef.c
 
 if HAVE_SSSE3
 libosmocore_la_SOURCES += conv_acc_sse.c
diff --git a/src/fsm.c b/src/fsm.c
index 33a5bbd..e4e2711 100644
--- a/src/fsm.c
+++ b/src/fsm.c
@@ -498,6 +498,10 @@
  *  timer_cb. If passing timeout_secs == 0, it is recommended to also pass T ==
  *  0, so that fi->T is reset to 0 when no timeout is invoked.
  *
+ *  See also osmo_tdef_fsm_inst_state_chg() from the osmo_tdef API, which
+ *  provides a unified way to configure and apply GSM style Tnnnn timers to FSM
+ *  state transitions.
+ *
  *  Range: since time_t's maximum value is not well defined in a cross platform
  *  way, clamp timeout_secs to the maximum of the signed 64bit range, or roughly
  *  68 years (float(0x7fffffff) / (60. * 60 * 24 * 365.25) = 68.0497). Thus
diff --git a/src/tdef.c b/src/tdef.c
new file mode 100644
index 0000000..43f3893
--- /dev/null
+++ b/src/tdef.c
@@ -0,0 +1,220 @@
+/* Implementation to define Tnnn timers globally and use for FSM state changes. */
+/* (C) 2018-2019 by sysmocom - s.f.m.c. GmbH <info at sysmocom.de>
+ *
+ * Author: Neels Hofmeyr <neels at hofmeyr.de>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <limits.h>
+
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/tdef.h>
+
+/* a = return_val * b. Return 0 if factor is below 1. */
+static unsigned long osmo_tdef_factor(enum osmo_tdef_unit a, enum osmo_tdef_unit b)
+{
+	if (b == a
+	    || b == OSMO_TDEF_CUSTOM || a == OSMO_TDEF_CUSTOM)
+		return 1;
+
+	switch (b) {
+	case OSMO_TDEF_MS:
+		switch (a) {
+		case OSMO_TDEF_S:
+			return 1000;
+		case OSMO_TDEF_M:
+			return 60*1000;
+		default:
+			return 0;
+		}
+	case OSMO_TDEF_S:
+		switch (a) {
+		case OSMO_TDEF_M:
+			return 60;
+		default:
+			return 0;
+		}
+	default:
+		return 0;
+	}
+}
+
+static unsigned long osmo_tdef_round(unsigned long val, enum osmo_tdef_unit from_unit, enum osmo_tdef_unit to_unit)
+{
+	unsigned long f;
+	if (!val)
+		return 0;
+
+	f = osmo_tdef_factor(from_unit, to_unit);
+	if (f == 1)
+		return val;
+	if (f < 1) {
+		f = osmo_tdef_factor(to_unit, from_unit);
+		return (val / f) + (val % f? 1 : 0);
+	}
+	/* range checking */
+	if (f > (ULONG_MAX / val))
+		return ULONG_MAX;
+	return val * f;
+}
+
+/*! Set all osmo_tdef values to the default_val.
+ * It is convenient to define a tdefs array by setting only the default_val, and calling osmo_tdefs_reset() once for
+ * program startup. (See also osmo_tdef_vty_init())
+ * \param[in] tdefs  Array of timer definitions, last entry being fully zero.
+ */
+void osmo_tdefs_reset(struct osmo_tdef *tdefs)
+{
+	struct osmo_tdef *t;
+	osmo_tdef_for_each(t, tdefs)
+		t->val = t->default_val;
+}
+
+/*! Return the value of a T timer from a list of osmo_tdef, in the given unit.
+ * If no such timer is defined, return the default value passed, or abort the program if default < 0.
+ *
+ * Round up any value match as_unit: 1100 ms as OSMO_TDEF_S becomes 2 seconds, as OSMO_TDEF_M becomes one minute.
+ * However, always return a value of zero as zero (0 ms as OSMO_TDEF_M still is 0 m).
+ *
+ * Range: even though the value range is unsigned long here, in practice, using ULONG_MAX as value for a timeout in
+ * seconds may actually wrap to negative or low timeout values (e.g. in struct timeval). It is recommended to stay below
+ * INT_MAX seconds. See also osmo_fsm_inst_state_chg().
+ *
+ * Usage example:
+ *
+ * 	struct osmo_tdef global_T_defs[] = {
+ * 		{ .T=7, .default_val=50, .desc="Water Boiling Timeout" },  // default is .unit=OSMO_TDEF_S == 0
+ * 		{ .T=8, .default_val=300, .desc="Tea brewing" },
+ * 		{ .T=9, .default_val=5, .unit=OSMO_TDEF_M, .desc="Let tea cool down before drinking" },
+ * 		{ .T=10, .default_val=20, .unit=OSMO_TDEF_M, .desc="Forgot to drink tea while it's warm" },
+ * 		{}  //  <-- important! last entry shall be zero
+ * 	};
+ * 	osmo_tdefs_reset(global_T_defs); // make all values the default
+ * 	osmo_tdef_vty_init(global_T_defs, CONFIG_NODE);
+ *
+ * 	val = osmo_tdef_get(global_T_defs, 7, OSMO_TDEF_S, -1); // -> 50
+ * 	sleep(val);
+ *
+ * 	val = osmo_tdef_get(global_T_defs, 7, OSMO_TDEF_M, -1); // 50 seconds becomes 1 minute -> 1
+ * 	sleep_minutes(val);
+ *
+ * 	val = osmo_tdef_get(global_T_defs, 99, OSMO_TDEF_S, 3); // not defined, returns 3
+ *
+ * 	val = osmo_tdef_get(global_T_defs, 99, OSMO_TDEF_S, -1); // not defined, program aborts!
+ *
+ * \param[in] tdefs  Array of timer definitions, last entry must be fully zero initialized.
+ * \param[in] T  Timer number to get the value for.
+ * \param[in] as_unit  Return timeout value in this unit.
+ * \param[in] val_if_not_present  Fallback value to return if no timeout is defined.
+ * \return Timeout value in the unit given by as_unit, rounded up if necessary, or val_if_not_present.
+ */
+unsigned long osmo_tdef_get(const struct osmo_tdef *tdefs, int T, enum osmo_tdef_unit as_unit, unsigned long val_if_not_present)
+{
+	const struct osmo_tdef *t = osmo_tdef_get_entry((struct osmo_tdef*)tdefs, T);
+	if (!t) {
+		OSMO_ASSERT(val_if_not_present >= 0);
+		return val_if_not_present;
+	}
+	return osmo_tdef_round(t->val, t->unit, as_unit);
+}
+
+/*! Find tdef entry matching T.
+ * This is useful for manipulation, which is usually limited to the VTY configuration. To retrieve a timeout value,
+ * most callers probably should use osmo_tdef_get() instead.
+ * \param[in] tdefs  Array of timer definitions, last entry being fully zero.
+ * \param[in] T  Timer number to get the entry for.
+ * \return osmo_tdef entry matching T in given array, or NULL if no match is found.
+ */
+struct osmo_tdef *osmo_tdef_get_entry(struct osmo_tdef *tdefs, int T)
+{
+	struct osmo_tdef *t;
+	osmo_tdef_for_each(t, tdefs) {
+		if (t->T == T)
+			return t;
+	}
+	return NULL;
+}
+
+/*! Using osmo_tdef for osmo_fsm_inst: find a given state's osmo_tdef_state_timeout entry.
+ *
+ * The timeouts_array shall contain exactly 32 elements, regardless whether only some of them are actually populated
+ * with nonzero values. 32 corresponds to the number of states allowed by the osmo_fsm_* API. Lookup is by array index.
+ * Not populated entries imply a state change invocation without timeout.
+ *
+ * For example:
+ *
+ * 	struct osmo_tdef_state_timeout my_fsm_timeouts[32] = {
+ * 		[MY_FSM_STATE_3] = { .T = 423 }, // look up timeout configured for T423
+ * 		[MY_FSM_STATE_7] = { .T = 235 },
+ * 		[MY_FSM_STATE_8] = { .keep_timer = true }, // keep previous state's T number, continue timeout.
+ * 		// any state that is omitted will remain zero == no timeout
+ *	};
+ *	osmo_tdef_get_state_timeout(MY_FSM_STATE_0, &my_fsm_timeouts) -> NULL,
+ *	osmo_tdef_get_state_timeout(MY_FSM_STATE_7, &my_fsm_timeouts) -> { .T = 235 }
+ *
+ * The intention is then to obtain the timer like osmo_tdef_get(global_T_defs, T=235); see also
+ * fsm_inst_state_chg_T() below.
+ *
+ * \param[in] state  State constant to look up.
+ * \param[in] timeouts_array  Array[32] of struct osmo_tdef_state_timeout defining which timer number to use per state.
+ * \return A struct osmo_tdef_state_timeout entry, or NULL if that entry is zero initialized.
+ */
+const struct osmo_tdef_state_timeout *osmo_tdef_get_state_timeout(uint32_t state, const struct osmo_tdef_state_timeout *timeouts_array)
+{
+	const struct osmo_tdef_state_timeout *t;
+	OSMO_ASSERT(state < 32);
+	t = &timeouts_array[state];
+	if (!t->keep_timer && !t->T)
+		return NULL;
+	return t;
+}
+
+/*! See invocation macro osmo_tdef_fsm_inst_state_chg() instead.
+ * \param[in] file  Source file name, like __FILE__.
+ * \param[in] line  Source file line number, like __LINE__.
+ */
+int _osmo_tdef_fsm_inst_state_chg(struct osmo_fsm_inst *fi, uint32_t state,
+				  const struct osmo_tdef_state_timeout *timeouts_array,
+				  const struct osmo_tdef *tdefs, unsigned long default_timeout,
+				  const char *file, int line)
+{
+	const struct osmo_tdef_state_timeout *t = osmo_tdef_get_state_timeout(state, timeouts_array);
+	unsigned long val;
+
+	/* No timeout defined for this state? */
+	if (!t)
+		return _osmo_fsm_inst_state_chg(fi, state, 0, 0, file, line);
+
+	if (t->keep_timer) {
+		int rc = _osmo_fsm_inst_state_chg_keep_timer(fi, state, file, line);
+		if (t->T && !rc)
+			fi->T = t->T;
+		return rc;
+	}
+
+	val = osmo_tdef_get(tdefs, t->T, OSMO_TDEF_S, default_timeout);
+	return _osmo_fsm_inst_state_chg(fi, state, val, t->T, file, line);
+}
+
+const struct value_string osmo_tdef_unit_names[] = {
+	{ OSMO_TDEF_S, "s" },
+	{ OSMO_TDEF_MS, "ms" },
+	{ OSMO_TDEF_M, "m" },
+	{ OSMO_TDEF_CUSTOM, "custom-unit" },
+	{ 0, NULL }
+};
diff --git a/src/vty/Makefile.am b/src/vty/Makefile.am
index 2e49498..cdde0fa 100644
--- a/src/vty/Makefile.am
+++ b/src/vty/Makefile.am
@@ -11,7 +11,8 @@
 
 libosmovty_la_SOURCES = buffer.c command.c vty.c vector.c utils.c \
 			telnet_interface.c logging_vty.c stats_vty.c \
-			fsm_vty.c talloc_ctx_vty.c
+			fsm_vty.c talloc_ctx_vty.c \
+			tdef_vty.c
 libosmovty_la_LDFLAGS = -version-info $(LIBVERSION) -no-undefined
 libosmovty_la_LIBADD = $(top_builddir)/src/libosmocore.la $(TALLOC_LIBS)
 endif
diff --git a/src/vty/tdef_vty.c b/src/vty/tdef_vty.c
new file mode 100644
index 0000000..da0a198
--- /dev/null
+++ b/src/vty/tdef_vty.c
@@ -0,0 +1,138 @@
+/* Implementation to configure Tnnn timers in VTY */
+/* (C) 2018-2019 by sysmocom - s.f.m.c. GmbH <info at sysmocom.de>
+ *
+ * Author: Neels Hofmeyr <neels at hofmeyr.de>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <string.h>
+
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+
+#include <osmocom/vty/tdef.h>
+#include <osmocom/core/tdef.h>
+
+/* Global singleton list used for the VTY configuration. See osmo_tdef_vty_init(). */
+static struct osmo_tdef *g_vty_tdefs = NULL;
+
+/*! Parse an argument like "T1234", "t1234" or "1234".
+ * \param[in] vty  VTY context.
+ * \param[in] T_str  Argument string.
+ * \return the corresponding osmo_tdef entry from the global tdefs list originally passed to osmo_tdef_vty_init(),
+ *         or NULL if no such entry exists.
+ */
+struct osmo_tdef *osmo_tdef_vty_parse_T_arg(struct vty *vty, const char *T_str)
+{
+	int T;
+	struct osmo_tdef *t;
+
+	if (T_str[0] == 't' || T_str[0] == 'T')
+		T_str++;
+	T = atoi(T_str);
+
+	t = osmo_tdef_get_entry(g_vty_tdefs, T);
+	if (!t)
+		vty_out(vty, "No such timer: T%d%s", T, VTY_NEWLINE);
+	return t;
+}
+
+static void vty_out_timer(struct vty *vty, struct osmo_tdef *t)
+{
+	OSMO_ASSERT(t);
+	vty_out(vty, "T%d = %lu %s\t%s (default: %lu %s)%s",
+		t->T, t->val, osmo_tdef_unit_name(t->unit), t->desc, t->default_val, osmo_tdef_unit_name(t->unit),
+		VTY_NEWLINE);
+}
+
+/* Installed in the VTY on osmo_tdef_vty_init(). */
+DEFUN(cfg_timer, cfg_timer_cmd,
+      "timer TNNNN (default|<1-65535>)",
+      "Configure GSM Timers\n"
+      "T-number, optionally preceded by 't' or 'T'."
+      "See also 'show timer' for a list of available timers.\n"
+      "Set to default timer value\n" "Timer value\n")
+{
+	const char *val_str = argv[1];
+	struct osmo_tdef *t;
+
+	t = osmo_tdef_vty_parse_T_arg(vty, argv[0]);
+	if (!t)
+		return CMD_WARNING;
+
+	if (!strcmp(val_str, "default"))
+		t->val = t->default_val;
+	else
+		t->val = atoi(val_str);
+
+	vty_out_timer(vty, t);
+	return CMD_SUCCESS;
+}
+
+/* Installed in the VTY on osmo_tdef_vty_init(). */
+DEFUN(show_timer, show_timer_cmd,
+      "show timer [TNNNN]",
+      SHOW_STR "GSM Timers\n"
+      "Specific timer to show, or all timers if omitted.\n")
+{
+	struct osmo_tdef *t;
+
+	if (argc) {
+		t = osmo_tdef_vty_parse_T_arg(vty, argv[0]);
+		if (!t)
+			return CMD_WARNING;
+		vty_out_timer(vty, t);
+		return CMD_SUCCESS;
+	}
+
+	osmo_tdef_for_each(t, g_vty_tdefs)
+		vty_out_timer(vty, t);
+	return CMD_SUCCESS;
+}
+
+/*! Install GSM timer configuration commands in the VTY. This calls osmo_tdefs_reset(tdefs) to ensure that the timer
+ * values reflect the defaults on program startup.
+ * \param[in] tdefs  Array of Tnnn definitions, terminated by a zero-initialized array member. This array is
+ *                   subsequently used for all VTY commands, hence only one osmo_tdef_vty_init() should be called per
+ *                   main() context.
+ * \param[in] cfg_parent_node  VTY node number below which to add the "timer T123" commands. The "show timer" commands
+ *                             are added only on the VIEW_NODE and ENABLE_NODE, regardless.
+ */
+void osmo_tdef_vty_init(struct osmo_tdef *tdefs, int cfg_parent_node)
+{
+	g_vty_tdefs = tdefs;
+	
+	/* Make sure the timer values initially reflect exactly their defaults. */
+	osmo_tdefs_reset(tdefs);
+
+	install_element_ve(&show_timer_cmd);
+	install_element(cfg_parent_node, &cfg_timer_cmd);
+}
+
+/*! Write current GSM timer configuration to the vty. Skip all entries that reflect their default value.
+ * \param[in] vty  VTY context.
+ * \param[in] indent  Indentation string to prepend to each VTY command printed on the vty.
+ */
+void osmo_tdef_vty_write(struct vty *vty, const char *indent)
+{
+	struct osmo_tdef *t;
+	osmo_tdef_for_each(t, g_vty_tdefs) {
+		if (t->val != t->default_val)
+			vty_out(vty, "%stimer T%d %lu%s", indent, t->T, t->val, VTY_NEWLINE);
+	}
+}
diff --git a/tests/Makefile.am b/tests/Makefile.am
index a307886..2fde901 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -26,6 +26,7 @@
 		 codec/codec_ecu_fr_test timer/clk_override_test	\
 		 oap/oap_client_test gsm29205/gsm29205_test		\
 		 logging/logging_vty_test				\
+		 tdef/tdef_test tdef/tdef_vty_test			\
 		 $(NULL)
 
 if ENABLE_MSGFILE
@@ -217,6 +218,12 @@
 gsm23003_gsm23003_test_SOURCES = gsm23003/gsm23003_test.c
 gsm23003_gsm23003_test_LDADD = $(LDADD) $(top_builddir)/src/gsm/libosmogsm.la
 
+tdef_tdef_test_SOURCES = tdef/tdef_test.c
+tdef_tdef_test_LDADD = $(LDADD)
+
+tdef_tdef_vty_test_SOURCES = tdef/tdef_vty_test.c
+tdef_tdef_vty_test_LDADD = $(LDADD) $(top_builddir)/src/vty/libosmovty.la
+
 # The `:;' works around a Bash 3.2 bug when the output is not writeable.
 $(srcdir)/package.m4: $(top_srcdir)/configure.ac
 	:;{ \
@@ -278,7 +285,9 @@
 	     sercomm/sercomm_test.ok prbs/prbs_test.ok			\
 	     gsm29205/gsm29205_test.ok gsm23003/gsm23003_test.ok        \
 	     timer/clk_override_test.ok					\
-	     oap/oap_client_test.ok oap/oap_client_test.err
+	     oap/oap_client_test.ok oap/oap_client_test.err		\
+	     tdef/tdef_test.ok tdef/tdef_vty_test.vty			\
+	     $(NULL)
 
 DISTCLEANFILES = atconfig atlocal conv/gsm0503_test_vectors.c
 BUILT_SOURCES = conv/gsm0503_test_vectors.c
@@ -322,11 +331,22 @@
 # To update the VTY script from current application behavior,
 # pass -u to osmo_verify_transcript_vty.py by doing:
 #   make vty-test U=-u
-vty-test:
+
+vty-test-logging:
 	osmo_verify_transcript_vty.py -v \
 		-p 42042 \
 		-r "$(top_builddir)/tests/logging/logging_vty_test" \
 		$(U) $(srcdir)/logging/*.vty
 
+vty-test-tdef:
+	osmo_verify_transcript_vty.py -v \
+		-p 42042 \
+		-r "$(top_builddir)/tests/tdef/tdef_vty_test" \
+		$(U) $(srcdir)/tdef/*.vty
+
+vty-test:
+	$(MAKE) vty-test-logging
+	$(MAKE) vty-test-tdef
+
 ctrl-test:
 	echo "No CTRL tests exist currently"
diff --git a/tests/tdef/tdef_test.c b/tests/tdef/tdef_test.c
new file mode 100644
index 0000000..263ebc3
--- /dev/null
+++ b/tests/tdef/tdef_test.c
@@ -0,0 +1,444 @@
+/*
+ * (C) 2019 by sysmocom s.f.m.c. GmbH <info at sysmocom.de>
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * Author: Neels Hofmeyr <nhofmeyr at sysmocom.de>
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <limits.h>
+
+#include <osmocom/core/logging.h>
+#include <osmocom/core/application.h>
+#include <osmocom/core/fsm.h>
+
+#include <osmocom/core/tdef.h>
+
+static void *ctx = NULL;
+
+static struct osmo_tdef tdefs[] = {
+	{ .T=1, .default_val=100, .desc="100s" },
+	{ .T=2, .default_val=100, .unit=OSMO_TDEF_MS, .desc="100ms" },
+	{ .T=3, .default_val=100, .unit=OSMO_TDEF_M, .desc="100m" },
+	{ .T=4, .default_val=100, .unit=OSMO_TDEF_CUSTOM, .desc="100 potatoes" },
+
+	{ .T=7, .default_val=50, .desc="Water Boiling Timeout" },  // default is .unit=OSMO_TDEF_S == 0
+	{ .T=8, .default_val=300, .desc="Tea brewing" },
+	{ .T=9, .default_val=5, .unit=OSMO_TDEF_M, .desc="Let tea cool down before drinking" },
+	{ .T=10, .default_val=20, .unit=OSMO_TDEF_M, .desc="Forgot to drink tea while it's warm" },
+
+	/* test conversions */
+	{ .T=1000, .default_val=2*1000, .unit=OSMO_TDEF_MS, .desc="two seconds from ms" },
+	{ .T=1001, .default_val=60*1000, .unit=OSMO_TDEF_MS, .desc="one minute from ms" },
+	{ .T=1002, .default_val=(ULONG_MAX/60), .unit=OSMO_TDEF_M, .desc="almost too many seconds" },
+	{ .T=1003, .default_val=ULONG_MAX, .unit=OSMO_TDEF_M, .desc="too many seconds" },
+	{ .T=1004, .default_val=1, .unit=OSMO_TDEF_MS, .desc="one ms" },
+	{ .T=1005, .default_val=0, .unit=OSMO_TDEF_MS, .desc="zero ms" },
+	{ .T=1006, .default_val=0, .unit=OSMO_TDEF_S, .desc="zero s" },
+	{ .T=1007, .default_val=0, .unit=OSMO_TDEF_M, .desc="zero m" },
+	{ .T=1008, .default_val=0, .unit=OSMO_TDEF_CUSTOM, .desc="zero" },
+
+	/* test range */
+	{ .T=INT_MAX, .default_val=ULONG_MAX, .unit=OSMO_TDEF_S, .desc="very large" },
+	{ .T=INT_MAX-1, .default_val=ULONG_MAX-1, .unit=OSMO_TDEF_S, .desc="very large" },
+	{ .T=INT_MAX-2, .default_val=LONG_MAX, .unit=OSMO_TDEF_S, .desc="very large" },
+	{ .T=INT_MAX-3, .default_val=ULONG_MAX, .unit=OSMO_TDEF_M, .desc="very large in minutes" },
+	{ .T=INT_MIN, .default_val=ULONG_MAX, .unit=OSMO_TDEF_S, .desc="negative" },
+
+	{ .T=0, .default_val=1, .unit=OSMO_TDEF_CUSTOM, .desc="zero" },
+
+	/* no desc */
+	{ .T=123, .default_val=1 },
+
+	{}  //  <-- important! last entry shall be zero
+};
+
+#define print_tdef_get(T, AS_UNIT) do { \
+		unsigned long val = osmo_tdef_get(tdefs, T, AS_UNIT, 999); \
+		printf("osmo_tdef_get(tdefs, %d, %s, 999)\t= %lu\n", T, osmo_tdef_unit_name(AS_UNIT), val); \
+	} while (0)
+
+#define print_tdef_get_short(T, AS_UNIT) do { \
+		unsigned long val = osmo_tdef_get(tdefs, T, AS_UNIT, 999); \
+		printf("osmo_tdef_get(%d, %s)\t= %lu\n", T, osmo_tdef_unit_name(AS_UNIT), val); \
+	} while (0)
+
+void print_tdef_info(unsigned int T)
+{
+	const struct osmo_tdef *t = osmo_tdef_get_entry(tdefs, T);
+	if (!t) {
+		printf("T%d=NULL", T);
+		return;
+	}
+	printf("T%d=%lu%s", T, t->val, osmo_tdef_unit_name(t->unit));
+	if (t->val != t->default_val)
+		printf("(def=%lu)", t->default_val);
+	printf("\n");
+}
+
+static void test_tdef_get()
+{
+	int i;
+	enum osmo_tdef_unit as_unit;
+
+	printf("\n%s()\n", __func__);
+
+	osmo_tdefs_reset(tdefs); // make all values the default
+
+	for (i = 0; i < ARRAY_SIZE(tdefs)-1; i++) {
+		unsigned int T = tdefs[i].T;
+		print_tdef_info(T);
+		for (as_unit = OSMO_TDEF_S; as_unit <= OSMO_TDEF_CUSTOM; as_unit++) {
+			print_tdef_get_short(T, as_unit);
+		}
+	}
+}
+
+static void test_tdef_get_nonexisting()
+{
+	printf("\n%s()\n", __func__);
+
+	print_tdef_get(5, OSMO_TDEF_S);
+	print_tdef_get(5, OSMO_TDEF_MS);
+	print_tdef_get(5, OSMO_TDEF_M);
+	print_tdef_get(5, OSMO_TDEF_CUSTOM);
+}
+
+static void test_tdef_set_and_get()
+{
+	struct osmo_tdef *t;
+	printf("\n%s()\n", __func__);
+
+	t = osmo_tdef_get_entry(tdefs, 7);
+	printf("setting 7 = 42\n");
+	t->val = 42;
+	print_tdef_info(7);
+	print_tdef_get_short(7, OSMO_TDEF_MS);
+	print_tdef_get_short(7, OSMO_TDEF_S);
+	print_tdef_get_short(7, OSMO_TDEF_M);
+	print_tdef_get_short(7, OSMO_TDEF_CUSTOM);
+
+	printf("setting 7 = 420\n");
+	t->val = 420;
+	print_tdef_info(7);
+	print_tdef_get_short(7, OSMO_TDEF_MS);
+	print_tdef_get_short(7, OSMO_TDEF_S);
+	print_tdef_get_short(7, OSMO_TDEF_M);
+	print_tdef_get_short(7, OSMO_TDEF_CUSTOM);
+
+	printf("resetting\n");
+	osmo_tdefs_reset(tdefs);
+	print_tdef_info(7);
+	print_tdef_get_short(7, OSMO_TDEF_S);
+}
+
+enum test_tdef_fsm_states {
+	S_A = 0,
+	S_B,
+	S_C,
+	S_D,
+	S_G,
+	S_H,
+	S_I,
+	S_J,
+	S_K,
+	S_L,
+	S_M,
+	S_N,
+	S_O,
+	S_X,
+	S_Y,
+	S_Z,
+};
+
+static const struct osmo_tdef_state_timeout test_tdef_state_timeouts[32] = {
+	[S_A] = { .T = 1 },
+	[S_B] = { .T = 2 },
+	[S_C] = { .T = 3 },
+	[S_D] = { .T = 4 },
+
+	[S_G] = { .T = 7 },
+	[S_H] = { .T = 8 },
+	[S_I] = { .T = 9 },
+	[S_J] = { .T = 10 },
+
+	/* keep_timer: adopt whichever T was running before and continue the timeout. */
+	[S_K] = { .keep_timer = true },
+	/* S_F defines an undefined T, but should continue previous state's timeout. */
+	[S_L] = { .T = 123, .keep_timer = true },
+
+	/* range */
+	[S_M] = { .T = INT_MAX },
+	[S_N] = { .T = INT_MIN },
+
+	/* T0 is not addressable from osmo_tdef_state_timeout, since it is indistinguishable from an unset entry. Even
+	 * though a timeout value is set for T=0, the transition to state S_O will show "no timer configured". */
+	[S_O] = { .T = 0 },
+
+	/* S_X undefined on purpose */
+	/* S_Y defines a T that does not exist */
+	[S_Y] = { .T = 666 },
+	/* S_Z undefined on purpose */
+};
+
+#define S(x)	(1 << (x))
+
+static const struct osmo_fsm_state test_tdef_fsm_states[] = {
+#define DEF_STATE(NAME) \
+	[S_##NAME] = { \
+		.name = #NAME, \
+		.out_state_mask = 0 \
+			| S(S_A) \
+			| S(S_B) \
+			| S(S_C) \
+			| S(S_D) \
+			| S(S_G) \
+			| S(S_H) \
+			| S(S_I) \
+			| S(S_J) \
+			| S(S_K) \
+			| S(S_L) \
+			| S(S_M) \
+			| S(S_N) \
+			| S(S_O) \
+			| S(S_X) \
+			| S(S_Y) \
+			| S(S_Z) \
+			, \
+	}
+
+	DEF_STATE(A),
+	DEF_STATE(B),
+	DEF_STATE(C),
+	DEF_STATE(D),
+
+	DEF_STATE(G),
+	DEF_STATE(H),
+	DEF_STATE(I),
+	DEF_STATE(J),
+
+	DEF_STATE(K),
+	DEF_STATE(L),
+
+	DEF_STATE(M),
+	DEF_STATE(N),
+	DEF_STATE(O),
+
+	DEF_STATE(X),
+	DEF_STATE(Y),
+	/* Z: test not being allowed to transition to other states. */
+	[S_Z] = {
+		.name = "Z",
+		.out_state_mask = 0
+			| S(S_A)
+			,
+	},
+};
+
+static const struct value_string test_tdef_fsm_event_names[] = { {} };
+
+static struct osmo_fsm test_tdef_fsm = {
+	.name = "tdef_test",
+	.states = test_tdef_fsm_states,
+	.event_names = test_tdef_fsm_event_names,
+	.num_states = ARRAY_SIZE(test_tdef_fsm_states),
+	.log_subsys = DLGLOBAL,
+};
+
+const struct timeval fake_time_start_time = { 123, 456 };
+
+#define fake_time_passes(secs, usecs) do \
+{ \
+	struct timeval diff; \
+	osmo_gettimeofday_override_add(secs, usecs); \
+	osmo_clock_override_add(CLOCK_MONOTONIC, secs, usecs * 1000); \
+	timersub(&osmo_gettimeofday_override_time, &fake_time_start_time, &diff); \
+	printf("Total time passed: %ld.%06ld s\n", diff.tv_sec, diff.tv_usec); \
+	osmo_timers_prepare(); \
+	osmo_timers_update(); \
+} while (0)
+
+void fake_time_start()
+{
+	struct timespec *clock_override;
+
+	osmo_gettimeofday_override_time = fake_time_start_time;
+	osmo_gettimeofday_override = true;
+	clock_override = osmo_clock_override_gettimespec(CLOCK_MONOTONIC);
+	OSMO_ASSERT(clock_override);
+	clock_override->tv_sec = fake_time_start_time.tv_sec;
+	clock_override->tv_nsec = fake_time_start_time.tv_usec * 1000;
+	osmo_clock_override_enable(CLOCK_MONOTONIC, true);
+	fake_time_passes(0, 0);
+}
+
+static void print_fsm_state(struct osmo_fsm_inst *fi)
+{
+	struct timeval remaining;
+	printf("state=%s T=%d", osmo_fsm_inst_state_name(fi), fi->T);
+
+	if (!osmo_timer_pending(&fi->timer)) {
+		printf(", no timeout\n");
+		return;
+	}
+
+	osmo_timer_remaining(&fi->timer, &osmo_gettimeofday_override_time, &remaining);
+	printf(", %lu.%06lu s remaining\n", remaining.tv_sec, remaining.tv_usec);
+}
+
+
+#define test_tdef_fsm_state_chg(NEXT_STATE) do { \
+		const struct osmo_tdef_state_timeout *st = osmo_tdef_get_state_timeout(NEXT_STATE, \
+										       test_tdef_state_timeouts); \
+		if (!st) { \
+			printf(" --> %s (no timer configured for this state)\n", \
+			       osmo_fsm_state_name(&test_tdef_fsm, NEXT_STATE)); \
+		} else { \
+			struct osmo_tdef *t = osmo_tdef_get_entry(tdefs, st->T); \
+			int rc = osmo_tdef_fsm_inst_state_chg(fi, NEXT_STATE, test_tdef_state_timeouts, tdefs, 999); \
+			printf(" --> %s (configured as T%d%s %lu %s) rc=%d;\t", osmo_fsm_state_name(&test_tdef_fsm, \
+												    NEXT_STATE), \
+			       st->T, st->keep_timer ? "(keep_timer)" : "", \
+			       t? t->val : -1, t? osmo_tdef_unit_name(t->unit) : "-", \
+			       rc); \
+			print_fsm_state(fi); \
+		} \
+	} while(0)
+
+
+
+static void test_tdef_state_timeout(bool test_range)
+{
+	struct osmo_fsm_inst *fi;
+	struct osmo_tdef *m = osmo_tdef_get_entry(tdefs, INT_MAX);
+	unsigned long m_secs;
+	printf("\n%s()\n", __func__);
+
+	osmo_tdefs_reset(tdefs);
+
+	fake_time_start();
+
+	fi = osmo_fsm_inst_alloc(&test_tdef_fsm, ctx, NULL, LOGL_DEBUG, __func__);
+	OSMO_ASSERT(fi);
+	print_fsm_state(fi);
+
+	test_tdef_fsm_state_chg(S_A);
+	test_tdef_fsm_state_chg(S_B);
+	test_tdef_fsm_state_chg(S_C);
+	test_tdef_fsm_state_chg(S_D);
+
+	test_tdef_fsm_state_chg(S_G);
+	test_tdef_fsm_state_chg(S_H);
+	test_tdef_fsm_state_chg(S_I);
+	test_tdef_fsm_state_chg(S_J);
+
+	printf("- test keep_timer:\n");
+	fake_time_passes(123, 45678);
+	print_fsm_state(fi);
+	test_tdef_fsm_state_chg(S_K);
+	test_tdef_fsm_state_chg(S_A);
+	fake_time_passes(23, 45678);
+	print_fsm_state(fi);
+	test_tdef_fsm_state_chg(S_K);
+
+	test_tdef_fsm_state_chg(S_A);
+	fake_time_passes(23, 45678);
+	print_fsm_state(fi);
+	test_tdef_fsm_state_chg(S_L);
+
+	printf("- test large T:\n");
+	test_tdef_fsm_state_chg(S_M);
+
+	printf("- test T<0:\n");
+	test_tdef_fsm_state_chg(S_N);
+
+	printf("- test T=0:\n");
+	test_tdef_fsm_state_chg(S_O);
+
+	printf("- test no timer:\n");
+	test_tdef_fsm_state_chg(S_X);
+
+	printf("- test undefined timer, using default_val arg of osmo_tdef_fsm_inst_state_chg(), here passed as 999:\n");
+	test_tdef_fsm_state_chg(S_Y);
+
+	/* the range of unsigned long is architecture dependent. This test can be invoked manually to see whether
+	 * clamping the timeout values works, but the output will be of varying lengths depending on the system's
+	 * unsigned long range, and would cause differences in expected output. */
+	if (test_range) {
+		printf("- test range:\n");
+		test_tdef_fsm_state_chg(S_M);
+		/* sweep through all the bits, shifting in 0xfffff.. from the right. */
+		m_secs = 0;
+		do {
+			m_secs = (m_secs << 1) + 1;
+			switch (m_secs) {
+			case 0x7fff:
+				printf("--- int32_t max ---\n");
+				break;
+			case 0xffff:
+				printf("--- uint32_t max ---\n");
+				break;
+			case 0x7fffffff:
+				printf("--- int64_t max ---\n");
+				break;
+			case 0xffffffff:
+				printf("--- uint64_t max ---\n");
+				break;
+			default:
+				break;
+			}
+
+			m->val = m_secs - 1;
+			test_tdef_fsm_state_chg(S_M);
+			m->val = m_secs;
+			test_tdef_fsm_state_chg(S_M);
+			m->val = m_secs + 1;
+			test_tdef_fsm_state_chg(S_M);
+		} while (m_secs < ULONG_MAX);
+	}
+
+	printf("- test disallowed transition:\n");
+	test_tdef_fsm_state_chg(S_Z);
+	test_tdef_fsm_state_chg(S_B);
+	test_tdef_fsm_state_chg(S_C);
+	test_tdef_fsm_state_chg(S_D);
+}
+
+int main(int argc, char **argv)
+{
+	ctx = talloc_named_const(NULL, 0, "tdef_test.c");
+	osmo_init_logging2(ctx, NULL);
+
+	log_set_print_filename(osmo_stderr_target, 0);
+	log_set_print_category(osmo_stderr_target, 1);
+	log_set_use_color(osmo_stderr_target, 0);
+
+	osmo_fsm_register(&test_tdef_fsm);
+
+	test_tdef_get();
+	test_tdef_get_nonexisting();
+	test_tdef_set_and_get();
+	/* Run range test iff any argument is passed on the cmdline. For the rationale, see the comment in
+	 * test_tdef_state_timeout(). */
+	test_tdef_state_timeout(argc > 1);
+
+	return EXIT_SUCCESS;
+}
diff --git a/tests/tdef/tdef_test.ok b/tests/tdef/tdef_test.ok
new file mode 100644
index 0000000..cf4b77f
--- /dev/null
+++ b/tests/tdef/tdef_test.ok
@@ -0,0 +1,184 @@
+
+test_tdef_get()
+T1=100s
+osmo_tdef_get(1, s)	= 100
+osmo_tdef_get(1, ms)	= 100000
+osmo_tdef_get(1, m)	= 2
+osmo_tdef_get(1, custom-unit)	= 100
+T2=100ms
+osmo_tdef_get(2, s)	= 1
+osmo_tdef_get(2, ms)	= 100
+osmo_tdef_get(2, m)	= 1
+osmo_tdef_get(2, custom-unit)	= 100
+T3=100m
+osmo_tdef_get(3, s)	= 6000
+osmo_tdef_get(3, ms)	= 6000000
+osmo_tdef_get(3, m)	= 100
+osmo_tdef_get(3, custom-unit)	= 100
+T4=100custom-unit
+osmo_tdef_get(4, s)	= 100
+osmo_tdef_get(4, ms)	= 100
+osmo_tdef_get(4, m)	= 100
+osmo_tdef_get(4, custom-unit)	= 100
+T7=50s
+osmo_tdef_get(7, s)	= 50
+osmo_tdef_get(7, ms)	= 50000
+osmo_tdef_get(7, m)	= 1
+osmo_tdef_get(7, custom-unit)	= 50
+T8=300s
+osmo_tdef_get(8, s)	= 300
+osmo_tdef_get(8, ms)	= 300000
+osmo_tdef_get(8, m)	= 5
+osmo_tdef_get(8, custom-unit)	= 300
+T9=5m
+osmo_tdef_get(9, s)	= 300
+osmo_tdef_get(9, ms)	= 300000
+osmo_tdef_get(9, m)	= 5
+osmo_tdef_get(9, custom-unit)	= 5
+T10=20m
+osmo_tdef_get(10, s)	= 1200
+osmo_tdef_get(10, ms)	= 1200000
+osmo_tdef_get(10, m)	= 20
+osmo_tdef_get(10, custom-unit)	= 20
+T1000=2000ms
+osmo_tdef_get(1000, s)	= 2
+osmo_tdef_get(1000, ms)	= 2000
+osmo_tdef_get(1000, m)	= 1
+osmo_tdef_get(1000, custom-unit)	= 2000
+T1001=60000ms
+osmo_tdef_get(1001, s)	= 60
+osmo_tdef_get(1001, ms)	= 60000
+osmo_tdef_get(1001, m)	= 1
+osmo_tdef_get(1001, custom-unit)	= 60000
+T1002=307445734561825860m
+osmo_tdef_get(1002, s)	= 18446744073709551600
+osmo_tdef_get(1002, ms)	= 18446744073709551615
+osmo_tdef_get(1002, m)	= 307445734561825860
+osmo_tdef_get(1002, custom-unit)	= 307445734561825860
+T1003=18446744073709551615m
+osmo_tdef_get(1003, s)	= 18446744073709551615
+osmo_tdef_get(1003, ms)	= 18446744073709551615
+osmo_tdef_get(1003, m)	= 18446744073709551615
+osmo_tdef_get(1003, custom-unit)	= 18446744073709551615
+T1004=1ms
+osmo_tdef_get(1004, s)	= 1
+osmo_tdef_get(1004, ms)	= 1
+osmo_tdef_get(1004, m)	= 1
+osmo_tdef_get(1004, custom-unit)	= 1
+T1005=0ms
+osmo_tdef_get(1005, s)	= 0
+osmo_tdef_get(1005, ms)	= 0
+osmo_tdef_get(1005, m)	= 0
+osmo_tdef_get(1005, custom-unit)	= 0
+T1006=0s
+osmo_tdef_get(1006, s)	= 0
+osmo_tdef_get(1006, ms)	= 0
+osmo_tdef_get(1006, m)	= 0
+osmo_tdef_get(1006, custom-unit)	= 0
+T1007=0m
+osmo_tdef_get(1007, s)	= 0
+osmo_tdef_get(1007, ms)	= 0
+osmo_tdef_get(1007, m)	= 0
+osmo_tdef_get(1007, custom-unit)	= 0
+T1008=0custom-unit
+osmo_tdef_get(1008, s)	= 0
+osmo_tdef_get(1008, ms)	= 0
+osmo_tdef_get(1008, m)	= 0
+osmo_tdef_get(1008, custom-unit)	= 0
+T2147483647=18446744073709551615s
+osmo_tdef_get(2147483647, s)	= 18446744073709551615
+osmo_tdef_get(2147483647, ms)	= 18446744073709551615
+osmo_tdef_get(2147483647, m)	= 307445734561825861
+osmo_tdef_get(2147483647, custom-unit)	= 18446744073709551615
+T2147483646=18446744073709551614s
+osmo_tdef_get(2147483646, s)	= 18446744073709551614
+osmo_tdef_get(2147483646, ms)	= 18446744073709551615
+osmo_tdef_get(2147483646, m)	= 307445734561825861
+osmo_tdef_get(2147483646, custom-unit)	= 18446744073709551614
+T2147483645=9223372036854775807s
+osmo_tdef_get(2147483645, s)	= 9223372036854775807
+osmo_tdef_get(2147483645, ms)	= 18446744073709551615
+osmo_tdef_get(2147483645, m)	= 153722867280912931
+osmo_tdef_get(2147483645, custom-unit)	= 9223372036854775807
+T2147483644=18446744073709551615m
+osmo_tdef_get(2147483644, s)	= 18446744073709551615
+osmo_tdef_get(2147483644, ms)	= 18446744073709551615
+osmo_tdef_get(2147483644, m)	= 18446744073709551615
+osmo_tdef_get(2147483644, custom-unit)	= 18446744073709551615
+T-2147483648=18446744073709551615s
+osmo_tdef_get(-2147483648, s)	= 18446744073709551615
+osmo_tdef_get(-2147483648, ms)	= 18446744073709551615
+osmo_tdef_get(-2147483648, m)	= 307445734561825861
+osmo_tdef_get(-2147483648, custom-unit)	= 18446744073709551615
+T0=1custom-unit
+osmo_tdef_get(0, s)	= 1
+osmo_tdef_get(0, ms)	= 1
+osmo_tdef_get(0, m)	= 1
+osmo_tdef_get(0, custom-unit)	= 1
+T123=1s
+osmo_tdef_get(123, s)	= 1
+osmo_tdef_get(123, ms)	= 1000
+osmo_tdef_get(123, m)	= 1
+osmo_tdef_get(123, custom-unit)	= 1
+
+test_tdef_get_nonexisting()
+osmo_tdef_get(tdefs, 5, s, 999)	= 999
+osmo_tdef_get(tdefs, 5, ms, 999)	= 999
+osmo_tdef_get(tdefs, 5, m, 999)	= 999
+osmo_tdef_get(tdefs, 5, custom-unit, 999)	= 999
+
+test_tdef_set_and_get()
+setting 7 = 42
+T7=42s(def=50)
+osmo_tdef_get(7, ms)	= 42000
+osmo_tdef_get(7, s)	= 42
+osmo_tdef_get(7, m)	= 1
+osmo_tdef_get(7, custom-unit)	= 42
+setting 7 = 420
+T7=420s(def=50)
+osmo_tdef_get(7, ms)	= 420000
+osmo_tdef_get(7, s)	= 420
+osmo_tdef_get(7, m)	= 7
+osmo_tdef_get(7, custom-unit)	= 420
+resetting
+T7=50s
+osmo_tdef_get(7, s)	= 50
+
+test_tdef_state_timeout()
+Total time passed: 0.000000 s
+state=A T=0, no timeout
+ --> A (configured as T1 100 s) rc=0;	state=A T=1, 100.000000 s remaining
+ --> B (configured as T2 100 ms) rc=0;	state=B T=2, 1.000000 s remaining
+ --> C (configured as T3 100 m) rc=0;	state=C T=3, 6000.000000 s remaining
+ --> D (configured as T4 100 custom-unit) rc=0;	state=D T=4, 100.000000 s remaining
+ --> G (configured as T7 50 s) rc=0;	state=G T=7, 50.000000 s remaining
+ --> H (configured as T8 300 s) rc=0;	state=H T=8, 300.000000 s remaining
+ --> I (configured as T9 5 m) rc=0;	state=I T=9, 300.000000 s remaining
+ --> J (configured as T10 20 m) rc=0;	state=J T=10, 1200.000000 s remaining
+- test keep_timer:
+Total time passed: 123.045678 s
+state=J T=10, 1076.954322 s remaining
+ --> K (configured as T0(keep_timer) 1 custom-unit) rc=0;	state=K T=10, 1076.954322 s remaining
+ --> A (configured as T1 100 s) rc=0;	state=A T=1, 100.000000 s remaining
+Total time passed: 146.091356 s
+state=A T=1, 76.954322 s remaining
+ --> K (configured as T0(keep_timer) 1 custom-unit) rc=0;	state=K T=1, 76.954322 s remaining
+ --> A (configured as T1 100 s) rc=0;	state=A T=1, 100.000000 s remaining
+Total time passed: 169.137034 s
+state=A T=1, 76.954322 s remaining
+ --> L (configured as T123(keep_timer) 1 s) rc=0;	state=L T=123, 76.954322 s remaining
+- test large T:
+ --> M (configured as T2147483647 18446744073709551615 s) rc=0;	state=M T=2147483647, 2147483647.000000 s remaining
+- test T<0:
+ --> N (configured as T-2147483648 18446744073709551615 s) rc=0;	state=N T=-2147483648, 2147483647.000000 s remaining
+- test T=0:
+ --> O (no timer configured for this state)
+- test no timer:
+ --> X (no timer configured for this state)
+- test undefined timer, using default_val arg of osmo_tdef_fsm_inst_state_chg(), here passed as 999:
+ --> Y (configured as T666 18446744073709551615 -) rc=0;	state=Y T=666, 999.000000 s remaining
+- test disallowed transition:
+ --> Z (no timer configured for this state)
+ --> B (configured as T2 100 ms) rc=0;	state=B T=2, 1.000000 s remaining
+ --> C (configured as T3 100 m) rc=0;	state=C T=3, 6000.000000 s remaining
+ --> D (configured as T4 100 custom-unit) rc=0;	state=D T=4, 100.000000 s remaining
diff --git a/tests/tdef/tdef_vty_test.c b/tests/tdef/tdef_vty_test.c
new file mode 100644
index 0000000..6dd0a90
--- /dev/null
+++ b/tests/tdef/tdef_vty_test.c
@@ -0,0 +1,252 @@
+/* test program with a vty interface to test osmo_tdef behavior */
+/*
+ * (C) 2019 by sysmocom - s.f.m.c. GmbH <info at sysmocom.de>
+ * All Rights Reserved
+ *
+ * Author: Neels Hofmeyr <neels at hofmeyr.de>
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#define _GNU_SOURCE
+#include <getopt.h>
+#include <signal.h>
+#include <limits.h>
+
+#include <osmocom/core/application.h>
+
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/misc.h>
+#include <osmocom/vty/telnet_interface.h>
+
+#include <osmocom/core/tdef.h>
+#include <osmocom/vty/tdef.h>
+
+#include <stdlib.h>
+
+#include "config.h"
+
+void *root_ctx = NULL;
+
+static void print_help()
+{
+	printf( "options:\n"
+		"  -h	--help		this text\n"
+		"  -d	--debug MASK	Enable debugging (e.g. -d DRSL:DOML:DLAPDM)\n"
+		"  -D	--daemonize	For the process into a background daemon\n"
+		"  -c	--config-file	Specify the filename of the config file\n"
+		"  -s	--disable-color	Don't use colors in stderr log output\n"
+		"  -T	--timestamp	Prefix every log line with a timestamp\n"
+		"  -V	--version	Print version information and exit\n"
+		"  -e	--log-level	Set a global log-level\n"
+		);
+}
+
+static struct {
+	const char *config_file;
+	int daemonize;
+} cmdline_config = {};
+
+static void handle_options(int argc, char **argv)
+{
+	while (1) {
+		int option_idx = 0, c;
+		static const struct option long_options[] = {
+			{ "help", 0, 0, 'h' },
+			{ "debug", 1, 0, 'd' },
+			{ "daemonize", 0, 0, 'D' },
+			{ "config-file", 1, 0, 'c' },
+			{ "disable-color", 0, 0, 's' },
+			{ "timestamp", 0, 0, 'T' },
+			{ "version", 0, 0, 'V' },
+			{ "log-level", 1, 0, 'e' },
+			{}
+		};
+
+		c = getopt_long(argc, argv, "hc:d:Dc:sTVe:",
+				long_options, &option_idx);
+		if (c == -1)
+			break;
+
+		switch (c) {
+		case 'h':
+			print_help();
+			exit(0);
+		case 's':
+			log_set_use_color(osmo_stderr_target, 0);
+			break;
+		case 'd':
+			log_parse_category_mask(osmo_stderr_target, optarg);
+			break;
+		case 'D':
+			cmdline_config.daemonize = 1;
+			break;
+		case 'c':
+			cmdline_config.config_file = optarg;
+			break;
+		case 'T':
+			log_set_print_timestamp(osmo_stderr_target, 1);
+			break;
+		case 'e':
+			log_set_log_level(osmo_stderr_target, atoi(optarg));
+			break;
+		case 'V':
+			print_version(1);
+			exit(0);
+			break;
+		default:
+			/* catch unknown options *as well as* missing arguments. */
+			fprintf(stderr, "Error in command line options. Exiting.\n");
+			exit(-1);
+		}
+	}
+}
+
+static int quit = 0;
+
+static void signal_handler(int signal)
+{
+	fprintf(stdout, "signal %u received\n", signal);
+
+	switch (signal) {
+	case SIGINT:
+	case SIGTERM:
+		quit++;
+		break;
+	case SIGABRT:
+		osmo_generate_backtrace();
+		/* in case of abort, we want to obtain a talloc report
+		 * and then return to the caller, who will abort the process */
+	case SIGUSR1:
+		talloc_report(tall_vty_ctx, stderr);
+		talloc_report_full(root_ctx, stderr);
+		break;
+	case SIGUSR2:
+		talloc_report_full(tall_vty_ctx, stderr);
+		break;
+	default:
+		break;
+	}
+}
+
+static struct vty_app_info vty_info = {
+	.name		= "tdef_vty_test",
+	.version	= PACKAGE_VERSION,
+};
+
+static const struct log_info_cat default_categories[] = {};
+
+const struct log_info log_info = {
+	.cat = default_categories,
+	.num_cat = ARRAY_SIZE(default_categories),
+};
+
+static struct osmo_tdef tdefs[] = {
+	{ .T=1, .default_val=100, .desc="Testing a hundred seconds" },
+	{ .T=2, .default_val=100, .unit=OSMO_TDEF_MS, .desc="Testing a hundred milliseconds" },
+	{ .T=3, .default_val=100, .unit=OSMO_TDEF_M, .desc="Testing a hundred minutes" },
+	{ .T=4, .default_val=100, .unit=OSMO_TDEF_CUSTOM, .desc="Testing a hundred potatoes" },
+
+	{ .T=7, .default_val=50, .desc="Water Boiling Timeout" },  // default is .unit=OSMO_TDEF_S == 0
+	{ .T=8, .default_val=300, .desc="Tea brewing" },
+	{ .T=9, .default_val=5, .unit=OSMO_TDEF_M, .desc="Let tea cool down before drinking" },
+	{ .T=10, .default_val=20, .unit=OSMO_TDEF_M, .desc="Forgot to drink tea while it's warm" },
+	{ .T=INT_MAX, .default_val=ULONG_MAX, .unit=OSMO_TDEF_M, .desc="Very large" },
+	{ .T=-23, .default_val=-15, .desc="Negative T number" },
+	{}  //  <-- important! last entry shall be zero
+};
+
+enum tdef_vty_node {
+	TDEF_NODE = _LAST_OSMOVTY_NODE + 1,
+};
+
+static struct cmd_node tdef_node = {
+	TDEF_NODE,
+	"%s(config-tdef)# ",
+	1,
+};
+
+DEFUN(cfg_tdef,
+      cfg_tdef_cmd,
+      "tdef", "enter tdef test node")
+{
+	vty->node = TDEF_NODE;
+	return CMD_SUCCESS;
+}
+
+static int config_write_tdef(struct vty *vty)
+{
+	vty_out(vty, "tdef%s", VTY_NEWLINE);
+	osmo_tdef_vty_write(vty, " ");
+	return CMD_SUCCESS;
+}
+
+int main(int argc, char **argv)
+{
+	int rc;
+
+	root_ctx = talloc_named_const(NULL, 0, "tdef_vty_test");
+
+	vty_info.tall_ctx = root_ctx;
+	vty_init(&vty_info);
+
+	osmo_init_logging2(root_ctx, &log_info);
+
+	install_element(CONFIG_NODE, &cfg_tdef_cmd);
+	install_node(&tdef_node, config_write_tdef);
+	osmo_tdef_vty_init(tdefs, TDEF_NODE);
+
+	handle_options(argc, argv);
+
+	osmo_talloc_vty_add_cmds();
+
+	if (cmdline_config.config_file) {
+		rc = vty_read_config_file(cmdline_config.config_file, NULL);
+		if (rc < 0) {
+			fprintf(stderr, "Failed to parse the config file: '%s'\n", cmdline_config.config_file);
+			return 1;
+		}
+	}
+
+	rc = telnet_init_dynif(root_ctx, NULL, vty_get_bind_addr(), 42042);
+	if (rc < 0)
+		return 2;
+
+	signal(SIGINT, &signal_handler);
+	signal(SIGTERM, &signal_handler);
+	signal(SIGABRT, &signal_handler);
+	signal(SIGUSR1, &signal_handler);
+	signal(SIGUSR2, &signal_handler);
+	osmo_init_ignore_signals();
+
+	if (cmdline_config.daemonize) {
+		rc = osmo_daemonize();
+		if (rc < 0) {
+			perror("Error during daemonize");
+			return 6;
+		}
+	}
+
+	while (!quit) {
+		log_reset_context();
+		osmo_select_main(0);
+	}
+
+	talloc_free(root_ctx);
+	talloc_free(tall_vty_ctx);
+
+	return 0;
+}
diff --git a/tests/tdef/tdef_vty_test.vty b/tests/tdef/tdef_vty_test.vty
new file mode 100644
index 0000000..d2a260d
--- /dev/null
+++ b/tests/tdef/tdef_vty_test.vty
@@ -0,0 +1,94 @@
+tdef_vty_test> enable
+tdef_vty_test# configure terminal
+
+tdef_vty_test(config)# tdef
+tdef_vty_test(config-tdef)# show running-config
+... !timer
+
+tdef_vty_test(config-tdef)# do show timer
+T1 = 100 s	Testing a hundred seconds (default: 100 s)
+T2 = 100 ms	Testing a hundred milliseconds (default: 100 ms)
+T3 = 100 m	Testing a hundred minutes (default: 100 m)
+T4 = 100 (custom)	Testing a hundred potatoes (default: 100 (custom))
+T7 = 50 s	Water Boiling Timeout (default: 50 s)
+T8 = 300 s	Tea brewing (default: 300 s)
+T9 = 5 m	Let tea cool down before drinking (default: 5 m)
+T10 = 20 m	Forgot to drink tea while it's warm (default: 20 m)
+T2147483647 = 18446744073709551615 m	Very large (default: 18446744073709551615 m)
+T-23 = 18446744073709551601 s	Negative T number (default: 18446744073709551601 s)
+
+tdef_vty_test(config-tdef)# do show timer T7
+T7 = 50 s	Water Boiling Timeout (default: 50 s)
+tdef_vty_test(config-tdef)# do show timer 7
+T7 = 50 s	Water Boiling Timeout (default: 50 s)
+tdef_vty_test(config-tdef)# do show timer t7
+T7 = 50 s	Water Boiling Timeout (default: 50 s)
+
+tdef_vty_test(config-tdef)# timer T10 5
+T10 = 5 m	Forgot to drink tea while it's warm (default: 20 m)
+tdef_vty_test(config-tdef)# do show timer 10
+T10 = 5 m	Forgot to drink tea while it's warm (default: 20 m)
+
+tdef_vty_test(config-tdef)# timer t10 25
+T10 = 25 m	Forgot to drink tea while it's warm (default: 20 m)
+tdef_vty_test(config-tdef)# do show timer 10
+T10 = 25 m	Forgot to drink tea while it's warm (default: 20 m)
+
+tdef_vty_test(config-tdef)# timer 10 55
+T10 = 55 m	Forgot to drink tea while it's warm (default: 20 m)
+tdef_vty_test(config-tdef)# do show timer 10
+T10 = 55 m	Forgot to drink tea while it's warm (default: 20 m)
+
+tdef_vty_test(config-tdef)# do show timer T666
+No such timer: T666
+tdef_vty_test(config-tdef)# timer T666 5
+No such timer: T666
+
+tdef_vty_test(config-tdef)# timer T-23 42
+T-23 = 42 s	Negative T number (default: 18446744073709551601 s)
+tdef_vty_test(config-tdef)# timer t-23 42
+T-23 = 42 s	Negative T number (default: 18446744073709551601 s)
+tdef_vty_test(config-tdef)# timer -23 42
+T-23 = 42 s	Negative T number (default: 18446744073709551601 s)
+
+tdef_vty_test(config-tdef)# do show timer
+T1 = 100 s	Testing a hundred seconds (default: 100 s)
+T2 = 100 ms	Testing a hundred milliseconds (default: 100 ms)
+T3 = 100 m	Testing a hundred minutes (default: 100 m)
+T4 = 100 (custom)	Testing a hundred potatoes (default: 100 (custom))
+T7 = 50 s	Water Boiling Timeout (default: 50 s)
+T8 = 300 s	Tea brewing (default: 300 s)
+T9 = 5 m	Let tea cool down before drinking (default: 5 m)
+T10 = 55 m	Forgot to drink tea while it's warm (default: 20 m)
+T2147483647 = 18446744073709551615 m	Very large (default: 18446744073709551615 m)
+T-23 = 42 s	Negative T number (default: 18446744073709551601 s)
+
+tdef_vty_test(config-tdef)# show running-config
+
+Current configuration:
+!
+!
+line vty
+ no login
+!
+tdef
+ timer T10 55
+ timer T-23 42
+end
+
+tdef_vty_test(config-tdef)# timer 10 default
+T10 = 20 m	Forgot to drink tea while it's warm (default: 20 m)
+tdef_vty_test(config-tdef)# do show timer 10
+T10 = 20 m	Forgot to drink tea while it's warm (default: 20 m)
+
+tdef_vty_test(config-tdef)# show running-config
+
+Current configuration:
+!
+!
+line vty
+ no login
+!
+tdef
+ timer T-23 42
+end
diff --git a/tests/testsuite.at b/tests/testsuite.at
index 6aaaa78..0093403 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -325,3 +325,9 @@
 cat $abs_srcdir/gsm23003/gsm23003_test.ok > expout
 AT_CHECK([$abs_top_builddir/tests/gsm23003/gsm23003_test], [0], [expout], [ignore])
 AT_CLEANUP
+
+AT_SETUP([tdef])
+AT_KEYWORDS([tdef])
+cat $abs_srcdir/tdef/tdef_test.ok > expout
+AT_CHECK([$abs_top_builddir/tests/tdef/tdef_test], [0], [expout], [ignore])
+AT_CLEANUP

-- 
To view, visit https://gerrit.osmocom.org/12717
To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings

Gerrit-Project: libosmocore
Gerrit-Branch: master
Gerrit-MessageType: newchange
Gerrit-Change-Id: Ibd6b1ed7f1bd6e1f2e0fde53352055a4468f23e5
Gerrit-Change-Number: 12717
Gerrit-PatchSet: 1
Gerrit-Owner: Neels Hofmeyr <nhofmeyr at sysmocom.de>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20190128/57ef5291/attachment.htm>


More information about the gerrit-log mailing list