ms: Add GprsMs class to hold per-MS information

Currently only TBF objects are used to handle the data flow between
the MS and the SGSN. MS specific data (e.g. pending LLC frames, TLLI)
is copied between successive TBFs. If all TBFs (uplink and downlink)
are idle for some time, all information about the MS is discarded in
the PCU. This makes the implementation of some features more
difficult, e.g. proper TLLI and timing advance handling,
connection based CS selection, and proper management of multiple TBF.

This commit adds the GprsMs class that is intended to hold
information directly related to the MS and to keep references to the
active TBFs.

The class is not yet integrated with the other PCU code. A GprsMs
object container and MS specific fields (TA, CS) will be added in
later commits.

Note that calling detach_tbf() can possibly delete the MS object
depending on the callback implementation.

Ticket: #1674
Sponsored-by: On-Waves ehf
This commit is contained in:
Jacob Erlbeck 2015-05-06 18:30:48 +02:00
parent 6eeb7c7e74
commit e04e0b0a20
9 changed files with 597 additions and 2 deletions

1
.gitignore vendored
View File

@ -36,6 +36,7 @@ tests/alloc/AllocTest
tests/rlcmac/RLCMACTest
tests/tbf/TbfTest
tests/types/TypesTest
tests/ms/MsTest
tests/emu/pcu_emu
tests/testsuite
tests/testsuite.log

View File

@ -37,6 +37,7 @@ libgprs_la_SOURCES = \
gprs_rlcmac_sched.cpp \
gprs_rlcmac_meas.cpp \
gprs_rlcmac_ts_alloc.cpp \
gprs_ms.cpp \
gsm_timer.cpp \
bitvector.cpp \
pcu_l1_if.cpp \
@ -77,6 +78,7 @@ noinst_HEADERS = \
gsm_rlcmac.h \
gprs_bssgp_pcu.h \
gprs_rlcmac.h \
gprs_ms.h \
pcuif_proto.h \
pcu_l1_if.h \
gsm_timer.h \

182
src/gprs_ms.cpp Normal file
View File

@ -0,0 +1,182 @@
/* gprs_ms.cpp
*
* Copyright (C) 2015 by Sysmocom s.f.m.c. GmbH
* Author: Jacob Erlbeck <jerlbeck@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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#include "gprs_ms.h"
#include "tbf.h"
#include "gprs_debug.h"
extern "C" {
#include <osmocom/core/talloc.h>
#include <osmocom/core/utils.h>
}
extern void *tall_pcu_ctx;
struct GprsMsDefaultCallback: public GprsMs::Callback {
virtual void ms_idle(class GprsMs *ms) {
delete ms;
}
virtual void ms_active(class GprsMs *) {}
};
static GprsMsDefaultCallback gprs_default_cb;
GprsMs::Guard::Guard(GprsMs *ms) : m_ms(ms)
{
if (m_ms)
m_ms->ref();
}
GprsMs::Guard::~Guard()
{
if (m_ms)
m_ms->unref();
}
GprsMs::GprsMs(uint32_t tlli) :
m_cb(&gprs_default_cb),
m_ul_tbf(NULL),
m_dl_tbf(NULL),
m_tlli(tlli),
m_is_idle(true),
m_ref(0)
{
LOGP(DRLCMAC, LOGL_INFO, "Creating MS object, TLLI = 0x%08x\n", tlli);
}
GprsMs::~GprsMs()
{
LOGP(DRLCMAC, LOGL_INFO, "Destroying MS object, TLLI = 0x%08x\n", tlli());
}
void* GprsMs::operator new(size_t size)
{
static void *tall_ms_ctx = NULL;
if (!tall_ms_ctx)
tall_ms_ctx = talloc_named_const(tall_pcu_ctx, 0, __PRETTY_FUNCTION__);
return talloc_size(tall_ms_ctx, size);
}
void GprsMs::operator delete(void* p)
{
talloc_free(p);
}
void GprsMs::ref()
{
m_ref += 1;
}
void GprsMs::unref()
{
OSMO_ASSERT(m_ref >= 0);
m_ref -= 1;
if (m_ref == 0)
update_status();
}
void GprsMs::attach_tbf(struct gprs_rlcmac_tbf *tbf)
{
if (tbf->direction == GPRS_RLCMAC_DL_TBF)
attach_dl_tbf(static_cast<gprs_rlcmac_dl_tbf *>(tbf));
else
attach_ul_tbf(static_cast<gprs_rlcmac_ul_tbf *>(tbf));
}
void GprsMs::attach_ul_tbf(struct gprs_rlcmac_ul_tbf *tbf)
{
if (m_ul_tbf == tbf)
return;
LOGP(DRLCMAC, LOGL_INFO, "Attaching TBF to MS object, TLLI = 0x%08x, TBF = %s\n",
tlli(), tbf->name());
Guard guard(this);
if (m_ul_tbf)
detach_tbf(m_ul_tbf);
m_ul_tbf = tbf;
}
void GprsMs::attach_dl_tbf(struct gprs_rlcmac_dl_tbf *tbf)
{
if (m_dl_tbf == tbf)
return;
LOGP(DRLCMAC, LOGL_INFO, "Attaching TBF to MS object, TLLI = 0x%08x, TBF = %s\n",
tlli(), tbf->name());
Guard guard(this);
if (m_dl_tbf)
detach_tbf(m_dl_tbf);
m_dl_tbf = tbf;
}
void GprsMs::detach_tbf(gprs_rlcmac_tbf *tbf)
{
if (m_ul_tbf && tbf == static_cast<gprs_rlcmac_tbf *>(m_ul_tbf))
m_ul_tbf = NULL;
else if (m_dl_tbf && tbf == static_cast<gprs_rlcmac_tbf *>(m_dl_tbf))
m_dl_tbf = NULL;
else
return;
LOGP(DRLCMAC, LOGL_INFO, "Detaching TBF from MS object, TLLI = 0x%08x, TBF = %s\n",
tlli(), tbf->name());
update_status();
}
void GprsMs::update_status()
{
if (m_ref > 0)
return;
if (is_idle() && !m_is_idle) {
m_is_idle = true;
m_cb->ms_idle(this);
/* this can be deleted by now, do not access it */
return;
}
if (!is_idle() && m_is_idle) {
m_is_idle = false;
m_cb->ms_active(this);
}
}
void GprsMs::set_tlli(uint32_t tlli)
{
if (tlli == m_tlli)
return;
LOGP(DRLCMAC, LOGL_INFO,
"Modifying MS object, TLLI: 0x%08x -> 0x%08x\n",
m_tlli, tlli);
m_tlli = tlli;
}

79
src/gprs_ms.h Normal file
View File

@ -0,0 +1,79 @@
/* gprs_ms.h
*
* Copyright (C) 2015 by Sysmocom s.f.m.c. GmbH
* Author: Jacob Erlbeck <jerlbeck@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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#pragma once
struct gprs_rlcmac_tbf;
struct gprs_rlcmac_dl_tbf;
struct gprs_rlcmac_ul_tbf;
#include <stdint.h>
#include <stddef.h>
class GprsMs {
public:
struct Callback {
virtual void ms_idle(class GprsMs *) = 0;
virtual void ms_active(class GprsMs *) = 0;
};
class Guard {
public:
Guard(GprsMs *ms);
~Guard();
private:
GprsMs * const m_ms;
};
GprsMs(uint32_t tlli);
~GprsMs();
void set_callback(Callback *cb) {m_cb = cb;}
gprs_rlcmac_ul_tbf *ul_tbf() const {return m_ul_tbf;}
gprs_rlcmac_dl_tbf *dl_tbf() const {return m_dl_tbf;}
uint32_t tlli() const {return m_tlli;}
void set_tlli(uint32_t tlli);
void attach_tbf(gprs_rlcmac_tbf *tbf);
void attach_ul_tbf(gprs_rlcmac_ul_tbf *tbf);
void attach_dl_tbf(gprs_rlcmac_dl_tbf *tbf);
void detach_tbf(gprs_rlcmac_tbf *tbf);
bool is_idle() const {return !m_ul_tbf && !m_dl_tbf && !m_ref;}
void* operator new(size_t num);
void operator delete(void* p);
protected:
void update_status();
void ref();
void unref();
private:
Callback * m_cb;
gprs_rlcmac_ul_tbf *m_ul_tbf;
gprs_rlcmac_dl_tbf *m_dl_tbf;
uint32_t m_tlli;
bool m_is_idle;
int m_ref;
};

View File

@ -1,6 +1,6 @@
AM_CPPFLAGS = $(STD_DEFINES_AND_INCLUDES) $(LIBOSMOCORE_CFLAGS) $(LIBOSMOGB_CFLAGS) $(LIBOSMOGSM_CFLAGS) -I$(top_srcdir)/src/
check_PROGRAMS = rlcmac/RLCMACTest alloc/AllocTest tbf/TbfTest types/TypesTest
check_PROGRAMS = rlcmac/RLCMACTest alloc/AllocTest tbf/TbfTest types/TypesTest ms/MsTest
noinst_PROGRAMS = emu/pcu_emu
rlcmac_RLCMACTest_SOURCES = rlcmac/RLCMACTest.cpp
@ -43,6 +43,16 @@ types_TypesTest_LDADD = \
$(LIBOSMOCORE_LIBS) \
$(COMMON_LA)
ms_MsTest_SOURCES = ms/MsTest.cpp
ms_MsTest_LDADD = \
$(top_builddir)/src/libgprs.la \
$(LIBOSMOGB_LIBS) \
$(LIBOSMOGSM_LIBS) \
$(LIBOSMOCORE_LIBS) \
$(COMMON_LA)
ms_MsTest_LDFLAGS = \
-Wl,-u,bssgp_prim_cb
# The `:;' works around a Bash 3.2 bug when the output is not writeable.
$(srcdir)/package.m4: $(top_srcdir)/configure.ac
@ -67,7 +77,8 @@ EXTRA_DIST = \
rlcmac/RLCMACTest.ok rlcmac/RLCMACTest.err \
alloc/AllocTest.ok alloc/AllocTest.err \
tbf/TbfTest.ok tbf/TbfTest.err \
types/TypesTest.ok types/TypesTest.err
types/TypesTest.ok types/TypesTest.err \
ms/MsTest.ok ms/MsTest.err
DISTCLEANFILES = atconfig

283
tests/ms/MsTest.cpp Normal file
View File

@ -0,0 +1,283 @@
/*
* MsTest.cpp
*
* Copyright (C) 2015 by Sysmocom s.f.m.c. GmbH
*
* 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 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 "tbf.h"
#include "gprs_debug.h"
#include "gprs_ms.h"
extern "C" {
#include "pcu_vty.h"
#include <osmocom/core/application.h>
#include <osmocom/core/msgb.h>
#include <osmocom/core/talloc.h>
#include <osmocom/core/utils.h>
#include <osmocom/vty/vty.h>
}
#include <errno.h>
void *tall_pcu_ctx;
int16_t spoof_mnc = 0, spoof_mcc = 0;
static void test_ms_state()
{
uint32_t tlli = 0xffeeddbb;
gprs_rlcmac_dl_tbf *dl_tbf;
gprs_rlcmac_ul_tbf *ul_tbf;
GprsMs *ms;
printf("=== start %s ===\n", __func__);
ms = new GprsMs(tlli);
OSMO_ASSERT(ms->is_idle());
dl_tbf = talloc_zero(tall_pcu_ctx, struct gprs_rlcmac_dl_tbf);
dl_tbf->direction = GPRS_RLCMAC_DL_TBF;
ul_tbf = talloc_zero(tall_pcu_ctx, struct gprs_rlcmac_ul_tbf);
ul_tbf->direction = GPRS_RLCMAC_UL_TBF;
ms->attach_tbf(ul_tbf);
OSMO_ASSERT(!ms->is_idle());
OSMO_ASSERT(ms->ul_tbf() == ul_tbf);
OSMO_ASSERT(ms->dl_tbf() == NULL);
ms->attach_tbf(dl_tbf);
OSMO_ASSERT(!ms->is_idle());
OSMO_ASSERT(ms->ul_tbf() == ul_tbf);
OSMO_ASSERT(ms->dl_tbf() == dl_tbf);
ms->detach_tbf(ul_tbf);
OSMO_ASSERT(!ms->is_idle());
OSMO_ASSERT(ms->ul_tbf() == NULL);
OSMO_ASSERT(ms->dl_tbf() == dl_tbf);
ms->detach_tbf(dl_tbf);
/* The ms object is freed now */
ms = NULL;
talloc_free(dl_tbf);
talloc_free(ul_tbf);
printf("=== end %s ===\n", __func__);
}
static void test_ms_callback()
{
uint32_t tlli = 0xffeeddbb;
gprs_rlcmac_dl_tbf *dl_tbf;
gprs_rlcmac_ul_tbf *ul_tbf;
GprsMs *ms;
static enum {UNKNOWN, IS_IDLE, IS_ACTIVE} last_cb = UNKNOWN;
struct MyCallback: public GprsMs::Callback {
virtual void ms_idle(class GprsMs *ms) {
OSMO_ASSERT(ms->is_idle());
printf(" ms_idle() was called\n");
last_cb = IS_IDLE;
}
virtual void ms_active(class GprsMs *ms) {
OSMO_ASSERT(!ms->is_idle());
printf(" ms_active() was called\n");
last_cb = IS_ACTIVE;
}
} cb;
printf("=== start %s ===\n", __func__);
ms = new GprsMs(tlli);
ms->set_callback(&cb);
OSMO_ASSERT(ms->is_idle());
dl_tbf = talloc_zero(tall_pcu_ctx, struct gprs_rlcmac_dl_tbf);
dl_tbf->direction = GPRS_RLCMAC_DL_TBF;
ul_tbf = talloc_zero(tall_pcu_ctx, struct gprs_rlcmac_ul_tbf);
ul_tbf->direction = GPRS_RLCMAC_UL_TBF;
OSMO_ASSERT(last_cb == UNKNOWN);
ms->attach_tbf(ul_tbf);
OSMO_ASSERT(!ms->is_idle());
OSMO_ASSERT(ms->ul_tbf() == ul_tbf);
OSMO_ASSERT(ms->dl_tbf() == NULL);
OSMO_ASSERT(last_cb == IS_ACTIVE);
last_cb = UNKNOWN;
ms->attach_tbf(dl_tbf);
OSMO_ASSERT(!ms->is_idle());
OSMO_ASSERT(ms->ul_tbf() == ul_tbf);
OSMO_ASSERT(ms->dl_tbf() == dl_tbf);
OSMO_ASSERT(last_cb == UNKNOWN);
ms->detach_tbf(ul_tbf);
OSMO_ASSERT(!ms->is_idle());
OSMO_ASSERT(ms->ul_tbf() == NULL);
OSMO_ASSERT(ms->dl_tbf() == dl_tbf);
OSMO_ASSERT(last_cb == UNKNOWN);
ms->detach_tbf(dl_tbf);
OSMO_ASSERT(ms->is_idle());
OSMO_ASSERT(ms->ul_tbf() == NULL);
OSMO_ASSERT(ms->dl_tbf() == NULL);
OSMO_ASSERT(last_cb == IS_IDLE);
last_cb = UNKNOWN;
delete ms;
talloc_free(dl_tbf);
talloc_free(ul_tbf);
printf("=== end %s ===\n", __func__);
}
static void test_ms_replace_tbf()
{
uint32_t tlli = 0xffeeddbb;
gprs_rlcmac_dl_tbf *dl_tbf[2];
gprs_rlcmac_ul_tbf *ul_tbf;
GprsMs *ms;
static bool was_idle;
struct MyCallback: public GprsMs::Callback {
virtual void ms_idle(class GprsMs *ms) {
OSMO_ASSERT(ms->is_idle());
printf(" ms_idle() was called\n");
was_idle = true;
}
virtual void ms_active(class GprsMs *ms) {
OSMO_ASSERT(!ms->is_idle());
printf(" ms_active() was called\n");
}
} cb;
printf("=== start %s ===\n", __func__);
ms = new GprsMs(tlli);
ms->set_callback(&cb);
OSMO_ASSERT(ms->is_idle());
was_idle = false;
dl_tbf[0] = talloc_zero(tall_pcu_ctx, struct gprs_rlcmac_dl_tbf);
dl_tbf[0]->direction = GPRS_RLCMAC_DL_TBF;
dl_tbf[1] = talloc_zero(tall_pcu_ctx, struct gprs_rlcmac_dl_tbf);
dl_tbf[1]->direction = GPRS_RLCMAC_DL_TBF;
ul_tbf = talloc_zero(tall_pcu_ctx, struct gprs_rlcmac_ul_tbf);
ul_tbf->direction = GPRS_RLCMAC_UL_TBF;
ms->attach_tbf(dl_tbf[0]);
OSMO_ASSERT(!ms->is_idle());
OSMO_ASSERT(ms->ul_tbf() == NULL);
OSMO_ASSERT(ms->dl_tbf() == dl_tbf[0]);
OSMO_ASSERT(!was_idle);
ms->attach_tbf(dl_tbf[1]);
OSMO_ASSERT(!ms->is_idle());
OSMO_ASSERT(ms->ul_tbf() == NULL);
OSMO_ASSERT(ms->dl_tbf() == dl_tbf[1]);
OSMO_ASSERT(!was_idle);
ms->attach_tbf(ul_tbf);
OSMO_ASSERT(!ms->is_idle());
OSMO_ASSERT(ms->ul_tbf() == ul_tbf);
OSMO_ASSERT(ms->dl_tbf() == dl_tbf[1]);
OSMO_ASSERT(!was_idle);
ms->detach_tbf(ul_tbf);
OSMO_ASSERT(!ms->is_idle());
OSMO_ASSERT(ms->ul_tbf() == NULL);
OSMO_ASSERT(ms->dl_tbf() == dl_tbf[1]);
OSMO_ASSERT(!was_idle);
ms->detach_tbf(dl_tbf[0]);
OSMO_ASSERT(!ms->is_idle());
OSMO_ASSERT(ms->ul_tbf() == NULL);
OSMO_ASSERT(ms->dl_tbf() == dl_tbf[1]);
OSMO_ASSERT(!was_idle);
ms->detach_tbf(dl_tbf[1]);
OSMO_ASSERT(ms->is_idle());
OSMO_ASSERT(ms->ul_tbf() == NULL);
OSMO_ASSERT(ms->dl_tbf() == NULL);
OSMO_ASSERT(was_idle);
delete ms;
talloc_free(dl_tbf[0]);
talloc_free(dl_tbf[1]);
talloc_free(ul_tbf);
printf("=== end %s ===\n", __func__);
}
static const struct log_info_cat default_categories[] = {
{"DPCU", "", "GPRS Packet Control Unit (PCU)", LOGL_INFO, 1},
};
static int filter_fn(const struct log_context *ctx,
struct log_target *tar)
{
return 1;
}
const struct log_info debug_log_info = {
filter_fn,
(struct log_info_cat*)default_categories,
ARRAY_SIZE(default_categories),
};
int main(int argc, char **argv)
{
struct vty_app_info pcu_vty_info = {0};
tall_pcu_ctx = talloc_named_const(NULL, 1, "MsTest context");
if (!tall_pcu_ctx)
abort();
msgb_set_talloc_ctx(tall_pcu_ctx);
osmo_init_logging(&debug_log_info);
log_set_use_color(osmo_stderr_target, 0);
log_set_print_filename(osmo_stderr_target, 0);
log_set_log_level(osmo_stderr_target, LOGL_INFO);
vty_init(&pcu_vty_info);
pcu_vty_init(&debug_log_info);
test_ms_state();
test_ms_callback();
test_ms_replace_tbf();
if (getenv("TALLOC_REPORT_FULL"))
talloc_report_full(tall_pcu_ctx, stderr);
return EXIT_SUCCESS;
}
extern "C" {
void l1if_pdch_req() { abort(); }
void l1if_connect_pdch() { abort(); }
void l1if_close_pdch() { abort(); }
void l1if_open_pdch() { abort(); }
}

20
tests/ms/MsTest.err Normal file
View File

@ -0,0 +1,20 @@
Creating MS object, TLLI = 0xffeeddbb
Attaching TBF to MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=UL STATE=NULL)
Attaching TBF to MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=DL STATE=NULL)
Detaching TBF from MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=UL STATE=NULL)
Detaching TBF from MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=DL STATE=NULL)
Destroying MS object, TLLI = 0xffeeddbb
Creating MS object, TLLI = 0xffeeddbb
Attaching TBF to MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=UL STATE=NULL)
Attaching TBF to MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=DL STATE=NULL)
Detaching TBF from MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=UL STATE=NULL)
Detaching TBF from MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=DL STATE=NULL)
Destroying MS object, TLLI = 0xffeeddbb
Creating MS object, TLLI = 0xffeeddbb
Attaching TBF to MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=DL STATE=NULL)
Attaching TBF to MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=DL STATE=NULL)
Detaching TBF from MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=DL STATE=NULL)
Attaching TBF to MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=UL STATE=NULL)
Detaching TBF from MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=UL STATE=NULL)
Detaching TBF from MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=DL STATE=NULL)
Destroying MS object, TLLI = 0xffeeddbb

10
tests/ms/MsTest.ok Normal file
View File

@ -0,0 +1,10 @@
=== start test_ms_state ===
=== end test_ms_state ===
=== start test_ms_callback ===
ms_active() was called
ms_idle() was called
=== end test_ms_callback ===
=== start test_ms_replace_tbf ===
ms_active() was called
ms_idle() was called
=== end test_ms_replace_tbf ===

View File

@ -29,3 +29,10 @@ cat $abs_srcdir/types/TypesTest.ok > expout
cat $abs_srcdir/types/TypesTest.err > experr
AT_CHECK([$OSMO_QEMU $abs_top_builddir/tests/types/TypesTest], [0], [expout], [experr])
AT_CLEANUP
AT_SETUP([ms])
AT_KEYWORDS([ms])
cat $abs_srcdir/ms/MsTest.ok > expout
cat $abs_srcdir/ms/MsTest.err > experr
AT_CHECK([$OSMO_QEMU $abs_top_builddir/tests/ms/MsTest], [0], [expout], [experr])
AT_CLEANUP