initial import

The original osmo-gsm-tester was an internal development at sysmocom, mostly by
D. Laszlo Sitzer <dlsitzer@sysmocom.de>, of which this public osmo-gsm-tester
is a refactoring / rewrite.

This imports an early state of the refactoring and is not functional yet. Bits
from the earlier osmo-gsm-tester will be added as needed. The earlier commit
history is not imported.
This commit is contained in:
Neels Hofmeyr 2017-03-28 12:16:58 +02:00
parent 0f2f19e9aa
commit dae3d3c479
61 changed files with 2986 additions and 0 deletions

15
Makefile Normal file
View File

@ -0,0 +1,15 @@
all: deps version check
.PHONY: version check
deps:
./check_dependencies.py
version:
./update_version.sh
check:
$(MAKE) -C test check
@echo "make check: success"
# vim: noexpandtab tabstop=8 shiftwidth=8

26
check_dependencies.py Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
# just import all python3 modules used by osmo-gsm-tester to make sure they are
# installed.
from inspect import getframeinfo, stack
from mako.lookup import TemplateLookup
from mako.template import Template
import argparse
import contextlib
import copy
import difflib
import fcntl
import inspect
import io
import os
import pprint
import re
import subprocess
import sys
import tempfile
import time
import traceback
import yaml
print('ok')

140
contrib/jenkins-openbsc-build.sh Executable file
View File

@ -0,0 +1,140 @@
set -e -x
prefix_base="`pwd`"
prefix_dirname="inst-openbsc"
prefix="$prefix_base/$prefix_dirname"
reposes="
libosmocore
libosmo-abis
libosmo-netif
openggsn
libsmpp34
libosmo-sccp
openbsc/openbsc
"
osmo_gsm_tester_host=root@10.9.1.190
osmo_gsm_tester_dir="/var/tmp/osmo-gsm-tester"
tmp_dir="/var/tmp/prep-osmo-gsm-tester"
arch="x86_64"
archive_name="openbsc-$arch-build-$BUILD_NUMBER"
archive="$archive_name.tgz"
manifest="manifest.txt"
test_report="test-report.xml"
test_timeout_sec=120
rm -rf $prefix
mkdir -p $prefix
opt_prefix=""
if [ -n "$prefix" ]; then
export LD_LIBRARY_PATH="$prefix"/lib
export PKG_CONFIG_PATH="$prefix"/lib/pkgconfig
opt_prefix="--prefix=$prefix"
fi
for r in $reposes; do
make -C "$r" clean || true
done
for r in $reposes; do
cd "$r"
echo "$(git rev-parse HEAD) $r" >> "$prefix/openbsc_git_hashes.txt"
autoreconf -fi
opt_enable=""
if [ "$r" = 'openbsc/openbsc' ]; then
opt_enable="--enable-smpp --enable-osmo-bsc --enable-nat"
fi
./configure "$opt_prefix" $opt_enable
make -j || make || make
if [ "$r" != asn1c ]; then
if [ "$r" = 'libosmo-netif' ]; then
# skip clock dependent test in libosmo-netif
make check TESTSUITEFLAGS='-k !osmux_test'
else
make check
fi
fi
make install
cd ..
done
# create test session directory, archive and manifest
cd $prefix_base
ts_name="$NODE_NAME-$BUILD_TAG"
local_ts_base="./compose_ts"
local_ts_dir="$local_ts_base/$ts_name"
rm -rf "$local_ts_base" || true
mkdir -p "$local_ts_dir"
# create archive of openbsc build
tar czf "$local_ts_dir/$archive" "$prefix_dirname"/*
# move archived bts builds into test session directory
mv $WORKSPACE/osmo-bts-*.tgz "$local_ts_dir"
cd "$local_ts_dir"
md5sum *.tgz > $manifest
cd -
# transfer test session directory to temporary dir on osmo-gsm-tester host
# when transfer is complete, move the directory to its final location (where
# the osmo-gsm-tester will recognize the session directory and start the session
ssh $osmo_gsm_tester_host "mkdir -p $tmp_dir"
scp -r "$local_ts_dir" $osmo_gsm_tester_host:$tmp_dir/
ssh $osmo_gsm_tester_host "mv $tmp_dir/$ts_name $osmo_gsm_tester_dir"
# poll for test status
ts_dir="$osmo_gsm_tester_dir/$ts_name"
set +x
ts_log=$ts_dir/test-session.log
echo "Waiting for test session log to be created"
while /bin/true; do
if ssh $osmo_gsm_tester_host "test -e $ts_log"; then
break
fi
sleep 1
done
echo "Following test session log"
# NOTE this will leave dead ssh session with tail running
ssh $osmo_gsm_tester_host "tail -f $ts_log" &
echo "Waiting for test session to complete"
while /bin/true; do
# if [ "$test_timeout_sec" = "0" ]; then
# echo "TIMEOUT test execution timeout ($test_timeout_sec seconds) exceeded!"
# exit 1
# fi
if ssh $osmo_gsm_tester_host "test -e $ts_dir/$test_report"; then
break
fi
sleep 1
# test_timeout_sec="$(($test_timeout_sec - 1))"
done
set -x
# use pgrep to terminate the ssh/tail (if it still exists)
remote_tail_pid=`ssh $osmo_gsm_tester_host "pgrep -fx 'tail -f $ts_log'"`
echo "remote_tail_pid = $remote_tail_pid"
ssh $osmo_gsm_tester_host "kill $remote_tail_pid"
# copy contents of test session directory back and remove it from the osmo-gsm-tester host
rsync -av -e ssh --exclude='inst-*' --exclude='tmp*' $osmo_gsm_tester_host:$ts_dir/ "$local_ts_dir/"
ssh $osmo_gsm_tester_host "/usr/local/src/osmo-gsm-tester/contrib/ts-dir-cleanup.sh"
# touch test-report.xml (to make up for clock drift between jenkins and build slave)
touch "$local_ts_dir/$test_report"

View File

@ -0,0 +1,94 @@
#!/bin/sh
set -e
OPTION_DO_CLONE=0
OPTION_DO_CLEAN=0
OPTION_DO_TEST=1
PREFIX=`pwd`/inst-osmo-bts-octphy
# NOTE Make sure either 'octphy-2g-headers' (prefered) or
# 'octsdr-2g' is listed among the repositories
octbts_repos="libosmocore
libosmo-abis
openbsc/openbsc
octphy-2g-headers
osmo-bts"
clone_repos() {
repos="$1"
for repo in $repos; do
if [ -e $repo ]; then
continue
fi
if [ "$repo" = "libosmocore" ]; then
url="git://git.osmocom.org/libosmocore.git"
elif [ "$repo" = "libosmo-abis" ]; then
url="git://git.osmocom.org/libosmo-abis.git"
elif [ "$repo" = "libosmo-netif" ]; then
url="git://git.osmocom.org/libosmo-netif.git"
elif [ "$repo" = "openbsc/openbsc" ]; then
url="git://git.osmocom.org/openbsc"
elif [ "$repo" = "octphy-2g-headers" ]; then
url="git://git.osmocom.org/octphy-2g-headers"
elif [ "$repo" = "octsdr-2g" ]; then
# NOTE acutally we only need the headers from the octphy-2g-headers
# repository but this (private) repository contains more recent versions
url="ssh://git@git.admin.sysmocom.de/octasic/octsdr-2g"
elif [ "$repo" = "osmo-bts" ]; then
url="git://git.osmocom.org/osmo-bts.git"
else
exit 2
fi
git clone $url
done
}
main() {
repos="$1"
if [ $OPTION_DO_CLONE -eq 1 ]; then clone_repos "$repos"; fi
rm -rf $PREFIX
mkdir -p $PREFIX
for repo in $repos; do
if [ "$repo" = "openbsc/openbsc" ]; then
continue
fi
if [ "$repo" = "octphy-2g-headers" ]; then
OCTPHY_INCDIR=`pwd`/octphy-2g-headers
continue
fi
if [ "$repo" = "octsdr-2g" ]; then
cd $repo
git checkout 5c7166bab0a0f2d8a9664213d18642ae305e7004
cd -
OCTPHY_INCDIR=`pwd`/octsdr-2g/software/include
continue
fi
cd $repo
if [ $OPTION_DO_CLEAN -eq 1 ]; then git clean -dxf; fi
echo "$(git rev-parse HEAD) $repo" >> "$PREFIX/osmo-bts-octphy_git_hashes.txt"
autoreconf -fi
if [ "$repo" != "libosmocore" ]; then
export PKG_CONFIG_PATH=$PREFIX/lib/pkgconfig
export LD_LIBRARY_PATH=$PREFIX/lib:/usr/local/lib
fi
config_opts=""
case "$repo" in
'osmo-bts') config_opts="$config_opts --enable-octphy --with-octsdr-2g=$OCTPHY_INCDIR"
esac
./configure --prefix=$PREFIX $config_opts
make -j8
if [ $OPTION_DO_TEST -eq 1 ]; then make check; fi
make install
cd ..
done
}
set -x
main "$octbts_repos"
# build the archive that is going to be copied to the tester and then to the BTS
rm -f $WORKSPACE/osmo-bts-octphy*.tgz
tar czf $WORKSPACE/osmo-bts-octphy-build-$BUILD_NUMBER.tgz inst-osmo-bts-octphy

View File

@ -0,0 +1,68 @@
set -e -x
deps="
libosmocore
libosmo-abis
osmo-bts
"
base="$PWD"
have_repo() {
repo="$1"
cd "$base"
if [ ! -e "$repo" ]; then
set +x
echo "MISSING REPOSITORY: $repo"
echo "should be provided by the jenkins workspace"
exit 1
fi
cd "$repo"
git clean -dxf
cd "$base"
}
for dep in $deps; do
have_repo "$dep"
done
# for gsm_data_shared.h
have_repo openbsc
. /opt/poky/1.5.4/environment-setup-armv5te-poky-linux-gnueabi
export DESTDIR=/opt/poky/1.5.4/sysroots/armv5te-poky-linux-gnueabi
prefix_base="/usr/local/jenkins-build"
prefix_base_real="$DESTDIR$prefix_base"
rm -rf "$prefix_base_real"
prefix="$prefix_base/inst-osmo-bts-sysmo"
prefix_real="$DESTDIR$prefix"
mkdir -p "$prefix_real"
for dep in $deps; do
cd "$base/$dep"
echo "$(git rev-parse HEAD) $dep" >> "$prefix_real/osmo-bts-sysmo_git_hashes.txt"
autoreconf -fi
config_opts=""
case "$dep" in
'libosmocore') config_opts="--disable-pcsc" ;;
'osmo-bts') config_opts="--enable-sysmocom-bts --with-openbsc=$base/openbsc/openbsc/include" ;;
esac
./configure --prefix="$prefix" $CONFIGURE_FLAGS $config_opts
make -j8
make install
done
# build the archive that is going to be copied to the tester and then to the BTS
tar_name="osmo-bts-sysmo-build-"
if ls "$base/$tar_name"* ; then
rm -f "$base/$tar_name"*
fi
cd "$prefix_base_real"
tar cvzf "$base/$tar_name${BUILD_NUMBER}.tgz" *

61
contrib/jenkins-osmo-bts-trx.sh Executable file
View File

@ -0,0 +1,61 @@
set -x -e
base="$PWD"
inst="inst-osmo-bts-trx"
prefix="$base/$inst"
deps="
libosmocore
libosmo-abis
osmo-trx
osmo-bts
"
have_repo() {
repo="$1"
cd "$base"
if [ ! -e "$repo" ]; then
set +x
echo "MISSING REPOSITORY: $repo"
echo "should be provided by the jenkins workspace"
exit 1
fi
cd "$repo"
git clean -dxf
cd "$base"
}
# for gsm_data_shared.*
have_repo openbsc
rm -rf "$prefix"
mkdir -p "$prefix"
export PKG_CONFIG_PATH="$prefix/lib/pkgconfig"
export LD_LIBRARY_PATH="$prefix/lib"
for dep in $deps; do
have_repo "$dep"
cd "$dep"
echo "$(git rev-parse HEAD) $dep" >> "$prefix/osmo-bts-trx_osmo-trx_git_hashes.txt"
autoreconf -fi
config_opts=""
case "$repo" in
'osmo-bts') config_opts="--enable-trx --with-openbsc=$base/openbsc/openbsc/include" ;;
'osmo-trx') config_opts="--without-sse" ;;
esac
./configure --prefix="$prefix" $config_opts
make -j8
make install
done
# build the archive that is going to be copied to the tester
cd "$base"
rm -f osmo-bts-trx*.tgz
tar czf "osmo-bts-trx-build-${BUILD_NUMBER}.tgz" "$inst"

30
contrib/ts-dir-cleanup.sh Executable file
View File

@ -0,0 +1,30 @@
#!/bin/sh
# Remove all but the N newest test run dirs (that have been started)
ts_rx_dir="$1"
ts_prep_dir="$2"
if [ -z "$ts_rx_dir" ]; then
ts_rx_dir="/var/tmp/osmo-gsm-tester"
fi
if [ -z "$ts_prep_dir" ]; then
ts_prep_dir="/var/tmp/prep-osmo-gsm-tester"
fi
mkdir -p "$ts_prep_dir"
rm_ts() {
ts_dir="$1"
ts_name="$(basename "$ts_dir")"
echo "Removing: $(ls -ld "$ts_dir")"
# ensure atomic removal, so that the gsm-tester doesn't take it as a
# newly added dir (can happen when the 'SEEN' marker is removed first).
mv "$ts_dir" "$ts_prep_dir/"
rm -rf "$ts_prep_dir/$ts_name"
}
# keep the N newest test session dirs that have been started: find all that
# have been started sorted by time, then discard all but the N newest ones.
for seen in $(ls -1t "$ts_rx_dir"/*/SEEN | tail -n +31); do
rm_ts "$(dirname "$seen")"
done

59
doc/README-sysmobts.txt Normal file
View File

@ -0,0 +1,59 @@
SETTING UP sysmobts
PACKAGE VERSIONS
Depending on the code to be tested, select the stable, testing or nightly opkg
feed:
To change the feed and packages installed on the sysmobts edit the
following files in /etc/opkg/
* all-feed.conf
* armv5te-feed.conf
* sysmobts-v2-feed.conf
and adjust the URL. For example, to move to the testing feeds:
sed -i 's/201310/201310-testing/g' /etc/opkg/*.conf
Then run 'opkg update', 'opkg upgrade' and finally 'reboot'.
DISABLE SERVICES
To use the sysmobts together with the tester, the following systemd services must be disabled
but using the mask and not using the disable option. You can use the following lines:
systemctl mask osmo-nitb
systemctl mask sysmobts
systemctl mask sysmobts-mgr
SSH ACCESS
Copy the SSH public key from the system/user that runs the tester to the BTS
authorized keys file so the tester will be able to deploy binaries.
It is also advisable to configure the eth0 network interface of the BTS to a
static IP address instead of using DHCP. To do so adjust /etc/network/interfaces
and change the line
iface eth0 inet dhcp
to
iface eth0 inet static
address 10.42.42.114
netmask 255.255.255.0
gateway 10.42.42.1
Set the name server in /etc/resolve.conf (most likely to the IP of the
gateway).
ALLOW CORE FILES
In case a binary run for the test crashes, we allow it to write a core file, to
be able to analyze the crash later. This requires a limit rule:
scp install/osmo-gsm-tester-limits.conf sysmobts:/etc/security/limits.d/

92
doc/README.txt Normal file
View File

@ -0,0 +1,92 @@
INSTALLATION
So far the osmo-gsm-tester directory is manually placed in /usr/local/src
DEPENDENCIES
Packages required to run the osmo-gsm-tester:
dbus
python3
python3-dbus
python3-pip
python3-mako
tcpdump
smpplib (pip install git+git://github.com/podshumok/python-smpplib.git)
ofono
To build ofono:
libglib2.0-dev
libdbus-1-dev
libudev-dev
mobile-broadband-provider-info
INSTALLATION
Place a copy of the osmo-gsm-tester repository in /usr/local/src/
cp install/osmo-gsm-tester-limits.conf /etc/security/limits.d/
cp install/*.service /lib/systemd/system/
cp install/org.ofono.conf /etc/dbus-1/system.d/
systemctl daemon-reload
To run:
systemctl enable ofono
systemctl start ofono
systemctl status ofono
systemctl enable osmo-gsm-tester
systemctl start osmo-gsm-tester
systemctl status osmo-gsm-tester
To stop:
systemctl stop osmo-gsm-tester
After ofonod has been started and modems have been connected to the system,
you can run the 'list-modems' script located in /usr/local/src/ofono/test to get
a list of the modems that have been detected by ofono.
CONFIGURATION
Host System configuration
Create the /var/tmp/osmo-gsm-tester directory. It will be used to accept new test jobs.
Test resources (NITB, BTS and modems) are currently configured in the test_manager.py.
For every nitb resource that can be allocated, one alias IP address needs
to be set up in /etc/network/interfaces on the interface that is connected to the BTSes.
By add the following lines for each nitb instance that can be allocated (while making
sure each interface alias and IP is unique)
auto eth1:0
allow-hotplug eth1:0
iface eth1:0 inet static
address 10.42.42.2
netmask 255.255.255.0
Also make sure, the user executing the tester is allowed to run tcpdump. If
the user is not root, we have used the folloing line to get proper permissions:
groupadd pcap
addgroup <your-user-name> pcap
setcap cap_net_raw,cap_net_admin=eip /usr/sbin/tcpdump
chgroup pcap /usr/sbin/tcpdump
chmod 0750 /usr/sbin/tcpdump
The tester main unit must be able to ssh without password to the sysmobts (and
possibly other) hardware: place the main unit's public SSH key on the sysmoBTS.
Log in via SSH at least once to accept the BTS' host key.
LAUNCHING A TEST RUN
osmo-gsm-tester watches /var/tmp/osmo-gsm-tester for instructions to launch
test runs. A test run is triggered by a subdirectory containing binaries and a
manifest file, typically created by jenkins using the enclosed scripts.

11
install/ofono.service Normal file
View File

@ -0,0 +1,11 @@
# systemd service file for the ofono daemon
[Unit]
Description=oFono
[Service]
ExecStart=/usr/local/src/ofono/src/ofonod -n
Restart=always
StartLimitInterval=0
[Install]
WantedBy=multi-user.target

28
install/org.ofono.conf Normal file
View File

@ -0,0 +1,28 @@
<!-- This configuration file specifies the required security policies
for oFono core daemon to work. It lives in /etc/dbus-1/system.d/ -->
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<!-- ../system.conf have denied everything, so we just punch some holes -->
<policy user="root">
<allow own="org.ofono"/>
<allow send_destination="org.ofono"/>
<allow send_interface="org.ofono.SimToolkitAgent"/>
<allow send_interface="org.ofono.PushNotificationAgent"/>
<allow send_interface="org.ofono.SmartMessagingAgent"/>
<allow send_interface="org.ofono.PositioningRequestAgent"/>
<allow send_interface="org.ofono.HandsfreeAudioAgent"/>
</policy>
<policy at_console="true">
<allow send_destination="org.ofono"/>
</policy>
<policy context="default">
<deny send_destination="org.ofono"/>
</policy>
</busconfig>

View File

@ -0,0 +1,4 @@
# place this file in /etc/security/limits.d to allow core files when a program
# crashes; for osmo-gsm-tester.
root - core unlimited
* - core unlimited

View File

@ -0,0 +1,11 @@
# systemd service file for the osmo-gsm-tester daemon
[Unit]
Description=Osmocom GSM Tester
[Service]
ExecStart=/usr/local/src/osmo-gsm-tester/osmo-gsm-tester
Restart=on-abort
StartLimitInterval=0
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,29 @@
# osmo_gsm_tester: automated cellular network hardware tests
#
# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
#
# Authors: D. Lazlo Sitzer <dlsitzer@sysmocom.de>
# Neels Hofmeyr <neels@hofmeyr.de>
#
# 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/>.
__version__ = 'UNKNOWN'
try:
from ._version import _version
__version__ = _version
except:
pass
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -0,0 +1,161 @@
# osmo_gsm_tester: read and validate config files
#
# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
#
# Author: Neels Hofmeyr <neels@hofmeyr.de>
#
# 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/>.
# discussion for choice of config file format:
#
# Python syntax is insane, because it allows the config file to run arbitrary
# python commands.
#
# INI file format is nice and simple, but it doesn't allow having the same
# section numerous times (e.g. to define several modems or BTS models) and does
# not support nesting.
#
# JSON has too much braces and quotes to be easy to type
#
# YAML formatting is lean, but too powerful. The normal load() allows arbitrary
# code execution. There is safe_load(). But YAML also allows several
# alternative ways of formatting, better to have just one authoritative style.
# Also it would be better to receive every setting as simple string rather than
# e.g. an IMSI as an integer.
#
# The Python ConfigParserShootout page has numerous contestants, but it we want
# to use widely used, standardized parsing code without re-inventing the wheel.
# https://wiki.python.org/moin/ConfigParserShootout
#
# The optimum would be a stripped down YAML format.
# In the lack of that, we shall go with yaml.load_safe() + a round trip
# (feeding back to itself), converting keys to lowercase and values to string.
import yaml
import re
import os
from . import log
def read(path, schema=None):
with log.Origin(path):
with open(path, 'r') as f:
config = yaml.safe_load(f)
config = _standardize(config)
if schema:
validate(config, schema)
return config
def tostr(config):
return _tostr(_standardize(config))
def _tostr(config):
return yaml.dump(config, default_flow_style=False)
def _standardize_item(item):
if isinstance(item, (tuple, list)):
return [_standardize_item(i) for i in item]
if isinstance(item, dict):
return dict([(key.lower(), _standardize_item(val)) for key,val in item.items()])
return str(item)
def _standardize(config):
config = yaml.safe_load(_tostr(_standardize_item(config)))
return config
KEY_RE = re.compile('[a-zA-Z][a-zA-Z0-9_]*')
def band(val):
if val in ('GSM-1800', 'GSM-1900'):
return
raise ValueError('Unknown GSM band: %r' % val)
INT = 'int'
STR = 'str'
BAND = 'band'
SCHEMA_TYPES = {
INT: int,
STR: str,
BAND: band,
}
def is_dict(l):
return isinstance(l, dict)
def is_list(l):
return isinstance(l, (list, tuple))
def validate(config, schema):
'''Make sure the given config dict adheres to the schema.
The schema is a dict of 'dict paths' in dot-notation with permitted
value type. All leaf nodes are validated, nesting dicts are implicit.
validate( { 'a': 123, 'b': { 'b1': 'foo', 'b2': [ 1, 2, 3 ] } },
{ 'a': int,
'b.b1': str,
'b.b2[]': int } )
Raise a ValueError in case the schema is violated.
'''
def validate_item(path, value, schema):
want_type = schema.get(path)
if is_list(value):
if want_type:
raise ValueError('config item is a list, should be %r: %r' % (want_type, path))
path = path + '[]'
want_type = schema.get(path)
if not want_type:
if is_dict(value):
nest(path, value, schema)
return
if is_list(value) and value:
for list_v in value:
validate_item(path, list_v, schema)
return
raise ValueError('config item not known: %r' % path)
if want_type not in SCHEMA_TYPES:
raise ValueError('unknown type %r at %r' % (want_type, path))
if is_dict(value):
raise ValueError('config item is dict but should be a leaf node of type %r: %r'
% (want_type, path))
if is_list(value):
for list_v in value:
validate_item(path, list_v, schema)
return
with log.Origin(item=path):
type_validator = SCHEMA_TYPES.get(want_type)
type_validator(value)
def nest(parent_path, config, schema):
if parent_path:
parent_path = parent_path + '.'
else:
parent_path = ''
for k,v in config.items():
if not KEY_RE.fullmatch(k):
raise ValueError('invalid config key: %r' % k)
path = parent_path + k
validate_item(path, v, schema)
nest(None, config, schema)
# vim: expandtab tabstop=4 shiftwidth=4

405
src/osmo_gsm_tester/log.py Normal file
View File

@ -0,0 +1,405 @@
# osmo_gsm_tester: global logging
#
# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
#
# Author: Neels Hofmeyr <neels@hofmeyr.de>
#
# 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/>.
import os
import sys
import time
import traceback
import contextlib
from inspect import getframeinfo, stack
L_ERR = 30
L_LOG = 20
L_DBG = 10
L_TRACEBACK = 'TRACEBACK'
C_NET = 'net'
C_RUN = 'run'
C_TST = 'tst'
C_CNF = 'cnf'
C_DEFAULT = '---'
LONG_DATEFMT = '%Y-%m-%d_%H:%M:%S'
DATEFMT = '%H:%M:%S'
class LogTarget:
do_log_time = None
do_log_category = None
do_log_level = None
do_log_origin = None
do_log_traceback = None
do_log_src = None
origin_width = None
origin_fmt = None
# redirected by logging test
get_time_str = lambda self: time.strftime(self.log_time_fmt)
# sink that gets each complete logging line
log_sink = sys.stderr.write
category_levels = None
def __init__(self):
self.category_levels = {}
self.style()
def style(self, time=True, time_fmt=DATEFMT, category=True, level=True, origin=True, origin_width=0, src=True, trace=False):
'''
set all logging format aspects, to defaults if not passed:
time: log timestamps;
time_fmt: format of timestamps;
category: print the logging category (three letters);
level: print the logging level, unless it is L_LOG;
origin: print which object(s) the message originated from;
origin_width: fill up the origin string with whitespace to this witdh;
src: log the source file and line number the log comes from;
trace: on exceptions, log the full stack trace;
'''
self.log_time_fmt = time_fmt
self.do_log_time = bool(time)
if not self.log_time_fmt:
self.do_log_time = False
self.do_log_category = bool(category)
self.do_log_level = bool(level)
self.do_log_origin = bool(origin)
self.origin_width = int(origin_width)
self.origin_fmt = '{:>%ds}' % self.origin_width
self.do_log_src = src
self.do_log_traceback = trace
def style_change(self, time=None, time_fmt=None, category=None, level=None, origin=None, origin_width=None, src=None, trace=None):
'modify only the given aspects of the logging format'
self.style(
time=(time if time is not None else self.do_log_time),
time_fmt=(time_fmt if time_fmt is not None else self.log_time_fmt),
category=(category if category is not None else self.do_log_category),
level=(level if level is not None else self.do_log_level),
origin=(origin if origin is not None else self.do_log_origin),
origin_width=(origin_width if origin_width is not None else self.origin_width),
src=(src if src is not None else self.do_log_src),
trace=(trace if trace is not None else self.do_log_traceback),
)
def set_level(self, category, level):
'set global logging log.L_* level for a given log.C_* category'
self.category_levels[category] = level
def is_enabled(self, category, level):
if level == L_TRACEBACK:
return self.do_log_traceback
is_level = self.category_levels.get(category)
if is_level is None:
is_level = L_LOG
if level < is_level:
return False
return True
def log(self, origin, category, level, src, messages, named_items):
if category and len(category) != 3:
self.log_sink('WARNING: INVALID LOG SUBSYSTEM %r\n' % category)
self.log_sink('origin=%r category=%r level=%r\n' % (origin, category, level));
if not category:
category = C_DEFAULT
if not self.is_enabled(category, level):
return
log_pre = []
if self.do_log_time:
log_pre.append(self.get_time_str())
if self.do_log_category:
log_pre.append(category)
if self.do_log_origin:
if origin is None:
name = '-'
elif isinstance(origin, str):
name = origin or None
elif hasattr(origin, '_name'):
name = origin._name
if not name:
name = str(origin.__class__.__name__)
log_pre.append(self.origin_fmt.format(name))
if self.do_log_level and level != L_LOG:
log_pre.append(level_str(level) or ('loglevel=' + str(level)) )
log_line = [str(m) for m in messages]
if named_items:
# unfortunately needs to be sorted to get deterministic results
log_line.append('{%s}' %
(', '.join(['%s=%r' % (k,v)
for k,v in sorted(named_items.items())])))
if self.do_log_src and src:
log_line.append(' [%s]' % str(src))
log_str = '%s%s%s' % (' '.join(log_pre),
': ' if log_pre else '',
' '.join(log_line))
self.log_sink(log_str.strip() + '\n')
targets = [ LogTarget() ]
def level_str(level):
if level == L_TRACEBACK:
return L_TRACEBACK
if level <= L_DBG:
return 'DBG'
if level <= L_LOG:
return 'LOG'
return 'ERR'
def _log_all_targets(origin, category, level, src, messages, named_items=None):
global targets
if isinstance(src, int):
src = get_src_from_caller(src + 1)
for target in targets:
target.log(origin, category, level, src, messages, named_items)
def get_src_from_caller(levels_up=1):
caller = getframeinfo(stack()[levels_up][0])
return '%s:%d' % (os.path.basename(caller.filename), caller.lineno)
def get_src_from_tb(tb, levels_up=1):
ftb = traceback.extract_tb(tb)
f,l,m,c = ftb[-levels_up]
f = os.path.basename(f)
return '%s:%s: %s' % (f, l, c)
class Origin:
'''
Base class for all classes that want to log,
and to add an origin string to a code path:
with log.Origin('my name'):
raise Problem()
This will log 'my name' as an origin for the Problem.
'''
_log_category = None
_src = None
_name = None
_log_line_buf = None
_prev_stdout = None
_global_current_origin = None
_parent_origin = None
def __init__(self, *name_items, category=None, **detail_items):
self.set_log_category(category)
self.set_name(*name_items, **detail_items)
def set_name(self, *name_items, **detail_items):
if name_items:
name = '-'.join([str(i) for i in name_items])
elif not detail_items:
name = self.__class__.__name__
else:
name = ''
if detail_items:
details = '(%s)' % (', '.join([("%s=%r" % (k,v))
for k,v in sorted(detail_items.items())]))
else:
details = ''
self._name = name + details
def name(self):
return self._name
def set_log_category(self, category):
self._log_category = category
def _log(self, level, messages, named_items=None, src_levels_up=3, origins=None):
src = self._src or src_levels_up
origin = origins or self.gather_origins()
_log_all_targets(origin, self._log_category, level, src, messages, named_items)
def dbg(self, *messages, **named_items):
self._log(L_DBG, messages, named_items)
def log(self, *messages, **named_items):
self._log(L_LOG, messages, named_items)
def err(self, *messages, **named_items):
self._log(L_ERR, messages, named_items)
def log_exn(self, exc_info=None):
log_exn(self, self._log_category, exc_info)
def __enter__(self):
if self._parent_origin is not None:
return
if Origin._global_current_origin == self:
return
self._parent_origin, Origin._global_current_origin = Origin._global_current_origin, self
def __exit__(self, *exc_info):
rc = None
if exc_info[0] is not None:
rc = exn_add_info(exc_info, self)
Origin._global_current_origin, self._parent_origin = self._parent_origin, None
return rc
def redirect_stdout(self):
return contextlib.redirect_stdout(self)
def write(self, message):
'to redirect stdout to the log'
lines = message.splitlines()
if not lines:
return
if self._log_line_buf:
lines[0] = self._log_line_buf + lines[0]
self._log_line_buf = None
if not message.endswith('\n'):
self._log_line_buf = lines[-1]
lines = lines[:-1]
origins = self.gather_origins()
for line in lines:
self._log(L_LOG, (line,), origins=origins)
def flush(self):
pass
def gather_origins(self):
origins = Origins()
origin = self
while origin:
origins.add(origin)
origin = origin._parent_origin
return str(origins)
def dbg(origin, category, *messages, **named_items):
_log_all_targets(origin, category, L_DBG, 2, messages, named_items)
def log(origin, category, *messages, **named_items):
_log_all_targets(origin, category, L_LOG, 2, messages, named_items)
def err(origin, category, *messages, **named_items):
_log_all_targets(origin, category, L_ERR, 2, messages, named_items)
def trace(origin, category, exc_info):
_log_all_targets(origin, category, L_TRACEBACK, None,
traceback.format_exception(*exc_info))
def resolve_category(origin, category):
if category is not None:
return category
if not hasattr(origin, '_log_category'):
return None
return origin._log_category
def exn_add_info(exc_info, origin, category=None):
etype, exception, tb = exc_info
if not hasattr(exception, 'origins'):
exception.origins = Origins()
if not hasattr(exception, 'category'):
# only remember the deepest category
exception.category = resolve_category(origin, category)
if not hasattr(exception, 'src'):
exception.src = get_src_from_tb(tb)
exception.origins.add(origin)
return False
def log_exn(origin=None, category=None, exc_info=None):
if not (exc_info is not None and len(exc_info) == 3):
exc_info = sys.exc_info()
if not (exc_info is not None and len(exc_info) == 3):
raise RuntimeError('invalid call to log_exn() -- no valid exception info')
etype, exception, tb = exc_info
# if there are origins recorded with the Exception, prefer that
if hasattr(exception, 'origins'):
origin = str(exception.origins)
# if there is a category recorded with the Exception, prefer that
if hasattr(exception, 'category'):
category = exception.category
if hasattr(exception, 'msg'):
msg = exception.msg
else:
msg = str(exception)
if hasattr(exception, 'src'):
src = exception.src
else:
src = 2
trace(origin, category, exc_info)
_log_all_targets(origin, category, L_ERR, src,
('%s:' % str(etype.__name__), msg))
class Origins(list):
def __init__(self, origin=None):
if origin is not None:
self.add(origin)
def add(self, origin):
if hasattr(origin, '_name'):
origin_str = origin._name
else:
origin_str = str(origin)
self.insert(0, origin_str)
def __str__(self):
return '->'.join(self)
def set_level(category, level):
global targets
for target in targets:
target.set_level(category, level)
def style(**kwargs):
global targets
for target in targets:
target.style(**kwargs)
def style_change(**kwargs):
global targets
for target in targets:
target.style_change(**kwargs)
class TestsTarget(LogTarget):
'LogTarget producing deterministic results for regression tests'
def __init__(self, out=sys.stdout):
super().__init__()
self.style(time=False, src=False)
self.log_sink = out.write
def run_logging_exceptions(func, *func_args, return_on_failure=None, **func_kwargs):
try:
return func(*func_args, **func_kwargs)
except:
log_exn()
return return_on_failure
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -0,0 +1,23 @@
# osmo_gsm_tester: process management
#
# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
#
# Author: Neels Hofmeyr <neels@hofmeyr.de>
#
# 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/>.
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -0,0 +1,51 @@
# osmo_gsm_tester: manage resources
#
# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
#
# Author: Neels Hofmeyr <neels@hofmeyr.de>
#
# 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/>.
import os
from . import log
from . import config
from .utils import listdict, FileLock
class Resources(log.Origin):
def __init__(self, config_path, lock_dir):
self.config_path = config_path
self.lock_dir = lock_dir
self.set_name(conf=self.config_path, lock=self.lock_dir)
def ensure_lock_dir_exists(self):
if not os.path.isdir(self.lock_dir):
os.makedirs(self.lock_dir)
global_resources = listdict()
def register(kind, instance):
global global_resources
global_resources.add(kind, instance)
def reserve(user, config):
asdf
def read_conf(path):
with open(path, 'r') as f:
conf = f.read()
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -0,0 +1,150 @@
# osmo_gsm_tester: test suite
#
# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
#
# Author: Neels Hofmeyr <neels@hofmeyr.de>
#
# 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/>.
import os
from . import config, log, template, utils
class Suite(log.Origin):
'''A test suite reserves resources for a number of tests.
Each test requires a specific number of modems, BTSs etc., which are
reserved beforehand by a test suite. This way several test suites can be
scheduled dynamically without resource conflicts arising halfway through
the tests.'''
CONF_FILENAME = 'suite.conf'
CONF_SCHEMA = {
'resources.nitb_iface': config.INT,
'resources.nitb': config.INT,
'resources.bts': config.INT,
'resources.msisdn': config.INT,
'resources.modem': config.INT,
'defaults.timeout': config.STR,
}
class Results:
def __init__(self):
self.passed = []
self.failed = []
self.all_passed = None
def add_pass(self, test):
self.passed.append(test)
def add_fail(self, test):
self.failed.append(test)
def conclude(self):
self.all_passed = bool(self.passed) and not bool(self.failed)
return self
def __init__(self, suite_dir):
self.set_log_category(log.C_CNF)
self.suite_dir = suite_dir
self.set_name(os.path.basename(self.suite_dir))
self.read_conf()
def read_conf(self):
with self:
if not os.path.isdir(self.suite_dir):
raise RuntimeError('No such directory: %r' % self.suite_dir)
self.conf = config.read(os.path.join(self.suite_dir,
Suite.CONF_FILENAME),
Suite.CONF_SCHEMA)
self.load_tests()
def load_tests(self):
with self:
self.tests = []
for basename in os.listdir(self.suite_dir):
if not basename.endswith('.py'):
continue
self.tests.append(Test(self, basename))
def add_test(self, test):
with self:
if not isinstance(test, Test):
raise ValueError('add_test(): pass a Test() instance, not %s' % type(test))
if test.suite is None:
test.suite = self
if test.suite is not self:
raise ValueError('add_test(): test already belongs to another suite')
self.tests.append(test)
def run_tests(self):
results = Suite.Results()
for test in self.tests:
self._run_test(test, results)
return results.conclude()
def run_tests_by_name(self, *names):
results = Suite.Results()
for name in names:
basename = name
if not basename.endswith('.py'):
basename = name + '.py'
for test in self.tests:
if basename == test.basename:
self._run_test(test, results)
break
return results.conclude()
def _run_test(self, test, results):
try:
with self:
test.run()
results.add_pass(test)
except:
results.add_fail(test)
self.log_exn()
class Test(log.Origin):
def __init__(self, suite, test_basename):
self.suite = suite
self.basename = test_basename
self.set_name(self.basename)
self.set_log_category(log.C_TST)
self.path = os.path.join(self.suite.suite_dir, self.basename)
with self:
with open(self.path, 'r') as f:
self.script = f.read()
def run(self):
with self:
self.code = compile(self.script, self.path, 'exec')
with self.redirect_stdout():
exec(self.code, self.test_globals())
self._success = True
def test_globals(self):
test_globals = {
'this': utils.dict2obj({
'suite': self.suite.suite_dir,
'test': self.basename,
}),
'resources': utils.dict2obj({
}),
}
return test_globals
def load(suite_dir):
return Suite(suite_dir)
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -0,0 +1,56 @@
# osmo_gsm_tester: automated cellular network hardware tests
# Proxy to templating engine to handle files
#
# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
#
# Author: Neels Hofmeyr <neels@hofmeyr.de>
#
# 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/>.
import os, sys
from mako.template import Template
from mako.lookup import TemplateLookup
from . import log
from .utils import dict2obj
_lookup = None
_logger = log.Origin('no templates dir set')
def set_templates_dir(*templates_dirs):
global _lookup
global _logger
if not templates_dirs:
# default templates dir is relative to this source file
templates_dirs = [os.path.join(os.path.dirname(__file__), 'templates')]
for d in templates_dirs:
if not os.path.isdir(d):
raise RuntimeError('templates dir is not a dir: %r'
% os.path.abspath(d))
_lookup = TemplateLookup(directories=templates_dirs)
_logger = log.Origin('Templates', category=log.C_CNF)
def render(name, values):
'''feed values dict into template and return rendered result.
".tmpl" is added to the name to look it up in the templates dir.'''
global _lookup
if _lookup is None:
set_templates_dir()
with _logger:
tmpl_name = name + '.tmpl'
template = _lookup.get_template(tmpl_name)
_logger.dbg('rendering', tmpl_name)
return template.render(**dict2obj(values))
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -0,0 +1,21 @@
!
! OsmoBTS () configuration saved from vty
!!
!
log stderr
logging color 1
logging timestamp 1
logging print extended-timestamp 1
logging print category 1
logging level all debug
logging level l1c info
logging level linp info
!
phy 0
instance 0
bts 0
band {band}
ipa unit-id {ipa_unit_id} 0
oml remote-ip {oml_remote_ip}
trx 0
phy 0 instance 0

View File

@ -0,0 +1,87 @@
!
! OpenBSC configuration saved from vty
!
password foo
!
log stderr
logging filter all 1
logging color 0
logging print category 0
logging print extended-timestamp 1
logging level all debug
!
line vty
no login
bind ${vty_bind_ip}
!
e1_input
e1_line 0 driver ipa
ipa bind ${abis_bind_ip}
network
network country code ${mcc}
mobile network code ${mnc}
short name ${net_name_short}
long name ${net_name_long}
auth policy ${net_auth_policy}
location updating reject cause 13
encryption a5 ${encryption}
neci 1
rrlp mode none
mm info 1
handover 0
handover window rxlev averaging 10
handover window rxqual averaging 1
handover window rxlev neighbor averaging 10
handover power budget interval 6
handover power budget hysteresis 3
handover maximum distance 9999
timer t3101 10
timer t3103 0
timer t3105 0
timer t3107 0
timer t3109 4
timer t3111 0
timer t3113 60
timer t3115 0
timer t3117 0
timer t3119 0
timer t3141 0
smpp
local-tcp-ip ${smpp_bind_ip} 2775
system-id test
policy closed
esme test
password test
default-route
ctrl
bind ${ctrl_bind_ip}
%for bts in bts_list:
bts ${loop.index}
type ${bts.type}
band ${bts.band}
cell_identity 0
location_area_code ${bts.location_area_code}
training_sequence_code 7
base_station_id_code ${bts.base_station_id_code}
ms max power 15
cell reselection hysteresis 4
rxlev access min 0
channel allocator ascending
rach tx integer 9
rach max transmission 7
ip.access unit_id ${bts.unit_id} 0
oml ip.access stream_id ${bts.stream_id} line 0
gprs mode none
% for trx in bts.trx_list:
trx ${loop.index}
rf_locked 0
arfcn ${trx.arfcn}
nominal power 23
max_power_red ${trx.max_power_red}
rsl e1 tei 0
% for ts in trx.timeslot_list:
timeslot ${loop.index}
phys_chan_config ${ts.phys_chan_config}
% endfor
% endfor
%endfor

View File

@ -0,0 +1,6 @@
pcu
flow-control-interval 10
cs 2
alloc-algorithm dynamic
alpha 0
gamma 0

View File

@ -0,0 +1,26 @@
!
! Osmocom SGSN configuration
!
!
line vty
no login
!
sgsn
gtp local-ip 127.0.0.1
ggsn 0 remote-ip 127.0.0.1
ggsn 0 gtp-version 1
!
ns
timer tns-block 3
timer tns-block-retries 3
timer tns-reset 3
timer tns-reset-retries 3
timer tns-test 30
timer tns-alive 3
timer tns-alive-retries 10
encapsulation udp local-ip 127.0.0.1
encapsulation udp local-port 23000
encapsulation framerelay-gre enabled 0
!
bssgp
!

View File

@ -0,0 +1,24 @@
!
! SysmoMgr (0.3.0.141-33e5) configuration saved from vty
!!
!
log stderr
logging filter all 1
logging color 1
logging timestamp 0
logging level all everything
logging level temp info
logging level fw info
logging level find info
logging level lglobal notice
logging level llapd notice
logging level linp notice
logging level lmux notice
logging level lmi notice
logging level lmib notice
logging level lsms notice
!
line vty
no login
!
sysmobts-mgr

View File

@ -0,0 +1,43 @@
# osmo_gsm_tester: prepare a test run and provide test API
#
# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
#
# Author: Neels Hofmeyr <neels@hofmeyr.de>
#
# 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/>.
import sys, os
import pprint
import inspect
from . import suite as _suite
from . import log
from . import resource
# load the configuration for the test
suite = _suite.Suite(sys.path[0])
test = _suite.Test(suite, os.path.basename(inspect.stack()[-1][1]))
def test_except_hook(*exc_info):
log.exn_add_info(exc_info, test)
log.exn_add_info(exc_info, suite)
log.log_exn(exc_info=exc_info)
sys.excepthook = test_except_hook
orig_stdout, sys.stdout = sys.stdout, test
resources = {}
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -0,0 +1,118 @@
# osmo_gsm_tester: language snippets
#
# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
#
# Author: Neels Hofmeyr <neels@hofmeyr.de>
#
# 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/>.
import os
import fcntl
class listdict:
'a dict of lists { "a": [1, 2, 3], "b": [1, 2] }'
def __getattr__(ld, name):
if name == 'add':
return ld.__getattribute__(name)
return ld.__dict__.__getattribute__(name)
def add(ld, name, item):
l = ld.__dict__.get(name)
if not l:
l = []
ld.__dict__[name] = l
l.append(item)
return l
def add_dict(ld, d):
for k,v in d.items():
ld.add(k, v)
def __setitem__(ld, name, val):
return ld.__dict__.__setitem__(name, val)
def __getitem__(ld, name):
return ld.__dict__.__getitem__(name)
def __str__(ld):
return ld.__dict__.__str__()
class DictProxy:
'''
allow accessing dict entries like object members
syntactical sugar, adapted from http://stackoverflow.com/a/31569634
so that e.g. templates can do ${bts.member} instead of ${bts['member']}
'''
def __init__(self, obj):
self.obj = obj
def __getitem__(self, key):
return dict2obj(self.obj[key])
def __getattr__(self, key):
try:
return dict2obj(getattr(self.obj, key))
except AttributeError:
try:
return self[key]
except KeyError:
raise AttributeError(key)
class ListProxy:
'allow nesting for DictProxy'
def __init__(self, obj):
self.obj = obj
def __getitem__(self, key):
return dict2obj(self.obj[key])
def dict2obj(value):
if isinstance(value, dict):
return DictProxy(value)
if isinstance(value, (tuple, list)):
return ListProxy(value)
return value
class FileLock:
def __init__(self, path, owner):
self.path = path
self.owner = owner
self.f = None
def __enter__(self):
if self.f is not None:
return
self.fd = os.open(self.path, os.O_CREAT | os.O_WRONLY | os.O_TRUNC)
fcntl.flock(self.fd, fcntl.LOCK_EX)
os.truncate(self.fd, 0)
os.write(self.fd, str(self.owner).encode('utf-8'))
os.fsync(self.fd)
def __exit__(self, *exc_info):
#fcntl.flock(self.fd, fcntl.LOCK_UN)
os.truncate(self.fd, 0)
os.fsync(self.fd)
os.close(self.fd)
self.fd = -1
def lock(self):
self.__enter__()
def unlock(self):
self.__exit__()
# vim: expandtab tabstop=4 shiftwidth=4

48
src/run_once.py Executable file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env python3
# osmo_gsm_tester: invoke a single test run
#
# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
#
# Author: Neels Hofmeyr <neels@hofmeyr.de>
#
# 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/>.
'''osmo_gsm_tester: invoke a single test run.
./run_once.py ~/path/to/test_package/
Upon launch, a 'test_package/run-<date>' directory will be created.
When complete, a symbolic link 'test_package/last_run' will point at this dir.
The run dir then contains logs and test results.
'''
import osmo_gsm_tester
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-V', '--version', action='store_true',
help='Show version')
parser.add_argument('test_package', nargs='*',
help='Directory containing binaries to test')
args = parser.parse_args()
if args.version:
print(osmo_gsm_tester.__version__)
exit(0)
# vim: expandtab tabstop=4 shiftwidth=4

9
test/Makefile Normal file
View File

@ -0,0 +1,9 @@
.PHONY: check update
check:
./all_tests.py
update:
./all_tests.py -u
# vim: noexpandtab tabstop=8 shiftwidth=8

16
test/_prep.py Normal file
View File

@ -0,0 +1,16 @@
import sys, os
script_dir = sys.path[0]
top_dir = os.path.join(script_dir, '..')
src_dir = os.path.join(top_dir, 'src')
# to find the osmo_gsm_tester py module
sys.path.append(src_dir)
from osmo_gsm_tester import log
log.targets = [ log.TestsTarget() ]
if '-v' in sys.argv:
log.style_change(trace=True)

111
test/all_tests.py Executable file
View File

@ -0,0 +1,111 @@
#!/usr/bin/env python3
import os
import sys
import subprocess
import time
import difflib
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('testdir_or_test', nargs='*',
help='subdir name or test script name')
parser.add_argument('-u', '--update', action='store_true',
help='Update test expecations instead of verifying them')
args = parser.parse_args()
def run_test(path):
print(path)
p = subprocess.Popen(path, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
o,e = p.communicate()
while True:
retval = p.poll()
if retval is not None:
break;
p.kill()
time.sleep(.1)
return retval, o.decode('utf-8'), e.decode('utf-8')
def udiff(expect, got, expect_path):
expect = expect.splitlines(1)
got = got.splitlines(1)
for line in difflib.unified_diff(expect, got,
fromfile=expect_path, tofile='got'):
sys.stderr.write(line)
if not line.endswith('\n'):
sys.stderr.write('[no-newline]\n')
def verify_output(got, expect_file, update=False):
if os.path.isfile(expect_file):
if update:
with open(expect_file, 'w') as f:
f.write(got)
return True
with open(expect_file, 'r') as f:
expect = f.read()
if expect != got:
udiff(expect, got, expect_file)
sys.stderr.write('output mismatch: %r\n'
% os.path.basename(expect_file))
return False
return True
script_dir = sys.path[0]
tests = []
for f in os.listdir(script_dir):
file_path = os.path.join(script_dir, f)
if not os.path.isfile(file_path):
continue
if not (file_path.endswith('_test.py') or file_path.endswith('_test.sh')):
continue
tests.append(file_path)
ran = []
errors = []
for test in sorted(tests):
if args.testdir_or_test:
if not any([t in test for t in args.testdir_or_test]):
continue
ran.append(test)
success = True
name, ext = os.path.splitext(test)
ok_file = name + '.ok'
err_file = name + '.err'
rc, out, err = run_test(test)
if rc != 0:
sys.stderr.write('%r: returned %d\n' % (os.path.basename(test), rc))
success = False
if not verify_output(out, ok_file, args.update):
success = False
if not verify_output(err, err_file, args.update):
success = False
if not success:
sys.stderr.write('--- stdout ---\n')
sys.stderr.write(out)
sys.stderr.write('--- stderr ---\n')
sys.stderr.write(err)
sys.stderr.write('---\n')
sys.stderr.write('Test failed: %r\n\n' % os.path.basename(test))
errors.append(test)
if errors:
print('%d of %d TESTS FAILED:\n %s' % (len(errors), len(ran), '\n '.join(errors)))
exit(1)
print('%d tests ok' % len(ran))
exit(0)
# vim: expandtab tabstop=4 shiftwidth=4

0
test/config_test.err Normal file
View File

46
test/config_test.ok Normal file
View File

@ -0,0 +1,46 @@
{'bts': [{'addr': '10.42.42.114',
'name': 'sysmoBTS 1002',
'trx': [{'band': 'GSM-1800',
'timeslots': ['CCCH+SDCCH4',
'SDCCH8',
'TCH/F_TCH/H_PDCH',
'TCH/F_TCH/H_PDCH',
'TCH/F_TCH/H_PDCH',
'TCH/F_TCH/H_PDCH',
'TCH/F_TCH/H_PDCH',
'TCH/F_TCH/H_PDCH']},
{'band': 'GSM-1900',
'timeslots': ['SDCCH8',
'PDCH',
'PDCH',
'PDCH',
'PDCH',
'PDCH',
'PDCH',
'PDCH']}],
'type': 'sysmobts'}],
'modems': [{'dbus_path': '/sierra_0',
'imsi': '901700000009001',
'ki': 'D620F48487B1B782DA55DF6717F08FF9',
'msisdn': '7801'},
{'dbus_path': '/sierra_1',
'imsi': '901700000009002',
'ki': 'D620F48487B1B782DA55DF6717F08FF9',
'msisdn': '7802'}]}
- expect validation success:
Validation: OK
- unknown item:
--- - ERR: ValueError: config item not known: 'bts[].unknown_item'
Validation: Error
- wrong type modems[].imsi:
--- - ERR: ValueError: config item is dict but should be a leaf node of type 'str': 'modems[].imsi'
Validation: Error
- invalid key with space:
--- - ERR: ValueError: invalid config key: 'imsi '
Validation: Error
- list instead of dict:
--- - ERR: ValueError: config item not known: 'a_dict[]'
Validation: Error
- unknown band:
--- (item='bts[].trx[].band') ERR: ValueError: Unknown GSM band: 'what'
Validation: Error

70
test/config_test.py Executable file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
import _prep
import sys
import os
import io
import pprint
import copy
from osmo_gsm_tester import config, log
example_config_file = 'test.cfg'
example_config = os.path.join(_prep.script_dir, 'config_test', example_config_file)
cfg = config.read(example_config)
pprint.pprint(cfg)
test_schema = {
'modems[].dbus_path': config.STR,
'modems[].msisdn': config.STR,
'modems[].imsi': config.STR,
'modems[].ki': config.STR,
'bts[].name' : config.STR,
'bts[].type' : config.STR,
'bts[].addr' : config.STR,
'bts[].trx[].timeslots[]' : config.STR,
'bts[].trx[].band' : config.BAND,
'a_dict.foo' : config.INT,
}
def val(which):
try:
config.validate(which, test_schema)
print('Validation: OK')
except ValueError:
log.log_exn()
print('Validation: Error')
print('- expect validation success:')
val(cfg)
print('- unknown item:')
c = copy.deepcopy(cfg)
c['bts'][0]['unknown_item'] = 'no'
val(c)
print('- wrong type modems[].imsi:')
c = copy.deepcopy(cfg)
c['modems'][0]['imsi'] = {'no':'no'}
val(c)
print('- invalid key with space:')
c = copy.deepcopy(cfg)
c['modems'][0]['imsi '] = '12345'
val(c)
print('- list instead of dict:')
c = copy.deepcopy(cfg)
c['a_dict'] = [ 1, 2, 3 ]
val(c)
print('- unknown band:')
c = copy.deepcopy(cfg)
c['bts'][0]['trx'][0]['band'] = 'what'
val(c)
exit(0)
# vim: expandtab tabstop=4 shiftwidth=4

39
test/config_test/test.cfg Normal file
View File

@ -0,0 +1,39 @@
modems:
- dbus_path: /sierra_0
msisdn: 7801
imsi: 901700000009001
ki: D620F48487B1B782DA55DF6717F08FF9
- dbus_path: /sierra_1
msisdn: '7802'
imsi: '901700000009002'
ki: D620F48487B1B782DA55DF6717F08FF9
# comment
BTS:
- name: sysmoBTS 1002
TYPE: sysmobts
addr: 10.42.42.114
trx:
- timeslots:
- CCCH+SDCCH4
- SDCCH8
- TCH/F_TCH/H_PDCH
- TCH/F_TCH/H_PDCH
- TCH/F_TCH/H_PDCH
- TCH/F_TCH/H_PDCH
- TCH/F_TCH/H_PDCH
- TCH/F_TCH/H_PDCH
band: GSM-1800
- timeslots:
- SDCCH8
- PDCH
- PDCH
- PDCH
- PDCH
- PDCH
- PDCH
- PDCH
band: GSM-1900

0
test/lock_test.err Normal file
View File

8
test/lock_test.ok Normal file
View File

@ -0,0 +1,8 @@
acquired lock: 'long_name'
launched first, locked by: long_name
launched second, locked by: long_name
leaving lock: 'long_name'
acquired lock: 'shorter'
waited, locked by: shorter
leaving lock: 'shorter'
waited more, locked by:

10
test/lock_test.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/sh
python3 ./lock_test_help.py long name &
sleep .2
echo "launched first, locked by: $(cat /tmp/lock_test)"
python3 ./lock_test_help.py shorter &
echo "launched second, locked by: $(cat /tmp/lock_test)"
sleep .4
echo "waited, locked by: $(cat /tmp/lock_test)"
sleep .5
echo "waited more, locked by: $(cat /tmp/lock_test)"

17
test/lock_test_help.py Normal file
View File

@ -0,0 +1,17 @@
import sys
import time
import _prep
from osmo_gsm_tester.utils import FileLock
fl = FileLock('/tmp/lock_test', '_'.join(sys.argv[1:]))
with fl:
print('acquired lock: %r' % fl.owner)
sys.stdout.flush()
time.sleep(0.5)
print('leaving lock: %r' % fl.owner)
sys.stdout.flush()
# vim: expandtab tabstop=4 shiftwidth=4

0
test/log_test.err Normal file
View File

41
test/log_test.ok Normal file
View File

@ -0,0 +1,41 @@
- Testing global log functions
01:02:03 tst <origin>: from log.log()
01:02:03 tst <origin> DBG: from log.dbg()
01:02:03 tst <origin> ERR: from log.err()
- Testing log.Origin functions
01:02:03 tst some-name(some='detail'): hello log
01:02:03 tst some-name(some='detail') ERR: hello err
01:02:03 tst some-name(some='detail'): message {int=3, none=None, str='str\n', tuple=('foo', 42)}
01:02:03 tst some-name(some='detail') DBG: hello dbg
- Testing log.style()
01:02:03: only time
tst: only category
DBG: only level
some-name(some='detail'): only origin
only src [log_test.py:69]
- Testing log.style_change()
no log format
01:02:03: add time
but no time format
01:02:03 DBG: add level
01:02:03 tst DBG: add category
01:02:03 tst DBG: add src [log_test.py:84]
01:02:03 tst some-name(some='detail') DBG: add origin [log_test.py:86]
- Testing origin_width
01:02:03 tst shortname: origin str set to 23 chars [log_test.py:93]
01:02:03 tst very long name(and_some=(3, 'things', 'in a tuple'), some='details'): long origin str [log_test.py:95]
01:02:03 tst very long name(and_some=(3, 'things', 'in a tuple'), some='details') DBG: long origin str dbg [log_test.py:96]
01:02:03 tst very long name(and_some=(3, 'things', 'in a tuple'), some='details') ERR: long origin str err [log_test.py:97]
- Testing log.Origin with omitted info
01:02:03 tst LogTest: hello log, name implicit from class name [log_test.py:102]
01:02:03 --- explicit_name: hello log, no category set [log_test.py:106]
01:02:03 --- LogTest: hello log, no category nor name set [log_test.py:109]
01:02:03 --- LogTest DBG: debug message, no category nor name set [log_test.py:112]
- Testing logging of Exceptions, tracing origins
Not throwing an exception in 'with:' works.
nested print just prints
01:02:03 tst level1->level2->level3: nested log() [log_test.py:144]
01:02:03 tst level1->level2: nested l2 log() from within l3 scope [log_test.py:145]
01:02:03 tst level1->level2->level3 ERR: ValueError: bork [log_test.py:146: raise ValueError('bork')]
- Enter the same Origin context twice
01:02:03 tst level1->level2: nested log [log_test.py:158]

160
test/log_test.py Executable file
View File

@ -0,0 +1,160 @@
#!/usr/bin/env python3
# osmo_gsm_tester: logging tests
#
# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
#
# Author: Neels Hofmeyr <neels@hofmeyr.de>
#
# 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/>.
import _prep
import sys
import os
from osmo_gsm_tester import log
#log.targets[0].get_time_str = lambda: '01:02:03'
fake_time = '01:02:03'
log.style_change(time=True, time_fmt=fake_time)
print('- Testing global log functions')
log.log('<origin>', log.C_TST, 'from log.log()')
log.dbg('<origin>', log.C_TST, 'from log.dbg(), not seen')
log.set_level(log.C_TST, log.L_DBG)
log.dbg('<origin>', log.C_TST, 'from log.dbg()')
log.set_level(log.C_TST, log.L_LOG)
log.err('<origin>', log.C_TST, 'from log.err()')
print('- Testing log.Origin functions')
class LogTest(log.Origin):
pass
t = LogTest()
t.set_log_category(log.C_TST)
t.set_name('some', 'name', some="detail")
t.log("hello log")
t.err("hello err")
t.dbg("hello dbg not visible")
t.log("message", int=3, tuple=('foo', 42), none=None, str='str\n')
log.set_level(log.C_TST, log.L_DBG)
t.dbg("hello dbg")
print('- Testing log.style()')
log.style(time=True, category=False, level=False, origin=False, src=False, time_fmt=fake_time)
t.dbg("only time")
log.style(time=False, category=True, level=False, origin=False, src=False, time_fmt=fake_time)
t.dbg("only category")
log.style(time=False, category=False, level=True, origin=False, src=False, time_fmt=fake_time)
t.dbg("only level")
log.style(time=False, category=False, level=False, origin=True, src=False, time_fmt=fake_time)
t.dbg("only origin")
log.style(time=False, category=False, level=False, origin=False, src=True, time_fmt=fake_time)
t.dbg("only src")
print('- Testing log.style_change()')
log.style(time=False, category=False, level=False, origin=False, src=False, time_fmt=fake_time)
t.dbg("no log format")
log.style_change(time=True)
t.dbg("add time")
log.style_change(time=True, time_fmt=0)
t.dbg("but no time format")
log.style_change(time=True, time_fmt=fake_time)
log.style_change(level=True)
t.dbg("add level")
log.style_change(category=True)
t.dbg("add category")
log.style_change(src=True)
t.dbg("add src")
log.style_change(origin=True)
t.dbg("add origin")
print('- Testing origin_width')
t = LogTest()
t.set_log_category(log.C_TST)
t.set_name('shortname')
log.style(origin_width=23, time_fmt=fake_time)
t.log("origin str set to 23 chars")
t.set_name('very long name', some='details', and_some=(3, 'things', 'in a tuple'))
t.log("long origin str")
t.dbg("long origin str dbg")
t.err("long origin str err")
print('- Testing log.Origin with omitted info')
t = LogTest()
t.set_log_category(log.C_TST)
t.log("hello log, name implicit from class name")
t = LogTest()
t.set_name('explicit_name')
t.log("hello log, no category set")
t = LogTest()
t.log("hello log, no category nor name set")
t.dbg("hello log, no category nor name set, not seen")
log.set_level(log.C_DEFAULT, log.L_DBG)
t.dbg("debug message, no category nor name set")
print('- Testing logging of Exceptions, tracing origins')
log.style(time_fmt=fake_time)
class Thing(log.Origin):
def __init__(self, some_path):
self.set_log_category(log.C_TST)
self.set_name(some_path)
def say(self, msg):
print(msg)
#log.style_change(trace=True)
with Thing('print_redirected'):
print("Not throwing an exception in 'with:' works.")
def l1():
level1 = Thing('level1')
with level1:
l2()
def l2():
level2 = Thing('level2')
with level2:
l3(level2)
def l3(level2):
level3 = Thing('level3')
with level3:
print('nested print just prints')
level3.log('nested log()')
level2.log('nested l2 log() from within l3 scope')
raise ValueError('bork')
try:
l1()
except Exception:
log.log_exn()
print('- Enter the same Origin context twice')
with Thing('level1'):
l2 = Thing('level2')
with l2:
with l2:
l2.log('nested log')
# vim: expandtab tabstop=4 shiftwidth=4

0
test/resource_test.err Normal file
View File

0
test/resource_test.ok Normal file
View File

20
test/resource_test.py Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
import tempfile
import os
import _prep
from osmo_gsm_tester import config, log, resource
workdir = tempfile.mkdtemp()
try:
r = resource.Resources(os.path.join(_prep.script_dir, 'etc', 'resources.conf'),
workdir)
finally:
os.removedirs(workdir)
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -0,0 +1,115 @@
# all hardware and interfaces available to this osmo-gsm-tester
nitb_iface:
- 10.42.42.1
- 10.42.42.2
- 10.42.42.3
bts:
- label: sysmoBTS 1002
type: sysmo
unit_id: 1
addr: 10.42.42.114
trx:
- band: GSM-1800
- label: octBTS 3000
type: oct
unit_id: 5
addr: 10.42.42.115
trx:
- band: GSM-1800
hwaddr: 00:0c:90:32:b5:8a
- label: nanoBTS 1900
type: nanobts
unit_id: 1902
addr: 10.42.42.190
trx:
- band: GSM-1900
hwaddr: 00:02:95:00:41:b3
arfcn:
- GSM-1800: [512, 514, 516, 518, 520]
- GSM-1900: [540, 542, 544, 546, 548]
modem:
- label: m7801
path: '/wavecom_0'
imsi: 901700000007801
ki: D620F48487B1B782DA55DF6717F08FF9
- label: m7802
path: '/wavecom_1'
imsi: 901700000007802
ki: 47FDB2D55CE6A10A85ABDAD034A5B7B3
- label: m7803
path: '/wavecom_2'
imsi: 901700000007803
ki: ABBED4C91417DF710F60675B6EE2C8D2
- label: m7804
path: '/wavecom_3'
imsi: 901700000007804
ki: 8BA541179156F2BF0918CA3CFF9351B0
- label: m7805
path: '/wavecom_4'
imsi: 901700000007805
ki: 82BEC24B5B50C9FAA69D17DEC0883A23
- label: m7806
path: '/wavecom_5'
imsi: 901700000007806
ki: DAF6BD6A188F7A4F09866030BF0F723D
- label: m7807
path: '/wavecom_6'
imsi: 901700000007807
ki: AEB411CFE39681A6352A1EAE4DDC9DBA
- label: m7808
path: '/wavecom_7'
imsi: 901700000007808
ki: F5DEF8692B305D7A65C677CA9EEE09C4
- label: m7809
path: '/wavecom_8'
imsi: 901700000007809
ki: A644F4503E812FD75329B1C8D625DA44
- label: m7810
path: '/wavecom_9'
imsi: 901700000007810
ki: EF663BDF3477DCD18D3D2293A2BAED67
- label: m7811
path: '/wavecom_10'
imsi: 901700000007811
ki: E88F37F048A86A9BC4D652539228C039
- label: m7812
path: '/wavecom_11'
imsi: 901700000007812
ki: E8D940DD66FCF6F1CD2C0F8F8C45633D
- label: m7813
path: '/wavecom_12'
imsi: 901700000007813
ki: DBF534700C10141C49F699B0419107E3
- label: m7814
path: '/wavecom_13'
imsi: 901700000007814
ki: B36021DEB90C4EA607E408A92F3B024D
- label: m7815
path: '/wavecom_14'
imsi: 901700000007815
ki: 1E209F6F839F9195778C4F96BE281A24
- label: m7816
path: '/wavecom_15'
imsi: 901700000007816
ki: BF827D219E739DD189F6F59E60D6455C

0
test/suite_test.err Normal file
View File

24
test/suite_test.ok Normal file
View File

@ -0,0 +1,24 @@
- non-existing suite dir
cnf does_not_exist ERR: RuntimeError: No such directory: 'does_not_exist'
- no suite.conf
--- empty_dir->suite_test/empty_dir/suite.conf ERR: FileNotFoundError: [Errno 2] No such file or directory: 'suite_test/empty_dir/suite.conf'
- valid suite dir
defaults:
timeout: 60s
resources:
bts: '1'
modem: '2'
msisdn: '2'
nitb: '1'
nitb_iface: '1'
- run hello world test
tst test_suite->hello_world.py: hello world
tst test_suite->hello_world.py: I am 'suite_test/test_suite' / 'hello_world.py'
tst test_suite->hello_world.py: one
tst test_suite->hello_world.py: two
tst test_suite->hello_world.py: three
- a test with an error
tst test_suite->test_error.py: I am 'test_error.py' [test_error.py:1]
tst test_suite->test_error.py ERR: AssertionError: [test_error.py:2: assert(False)]
- graceful exit.

29
test/suite_test.py Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env python3
import os
import _prep
from osmo_gsm_tester import log, suite, config
#log.style_change(trace=True)
print('- non-existing suite dir')
assert(log.run_logging_exceptions(suite.load, 'does_not_exist') == None)
print('- no suite.conf')
assert(log.run_logging_exceptions(suite.load, os.path.join('suite_test', 'empty_dir')) == None)
print('- valid suite dir')
example_suite_dir = os.path.join('suite_test', 'test_suite')
s = suite.load(example_suite_dir)
assert(isinstance(s, suite.Suite))
print(config.tostr(s.conf))
print('- run hello world test')
s.run_tests_by_name('hello_world')
log.style_change(src=True)
#log.style_change(trace=True)
print('- a test with an error')
s.run_tests_by_name('test_error')
print('- graceful exit.')
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -0,0 +1,3 @@
print('hello world')
print('I am %r / %r' % (this.suite, this.test))
print('one\ntwo\nthree')

View File

@ -0,0 +1,18 @@
nitb_iface = resources.nitb_iface()
nitb = resources.nitb()
bts = resources.bts()
ms_mo = resources.modem()
ms_mt = resources.modem()
nitb.start(nitb_iface)
bts.start(nitb)
nitb.add_subscriber(ms_mo, resources.msisdn())
nitb.add_subscriber(ms_mt, resources.msisdn())
ms_mo.start()
ms_mt.start()
wait(nitb.subscriber_attached, ms_mo, ms_mt)
sms = ms_mo.sms_send(ms_mt.msisdn)
wait(nitb.sms_received, sms)

View File

@ -0,0 +1,20 @@
nitb_iface = resources.nitb_iface()
nitb = resources.nitb()
bts = resources.bts()
ms_ext = resources.msisdn()
fake_ext = resources.msisdn()
ms = resources.modem()
nitb.configure(nitb_iface, bts)
bts.configure(nitb)
nitb.start()
bts.start()
nitb.add_fake_ext(fake_ext)
nitb.add_subscriber(ms, ms_ext)
ms.start()
wait(nitb.subscriber_attached, ms)
sms = ms.sms_send(fake_ext)
wait(nitb.sms_received, sms)

View File

@ -0,0 +1,9 @@
resources:
nitb_iface: 1
nitb: 1
bts: 1
msisdn: 2
modem: 2
defaults:
timeout: 60s

View File

@ -0,0 +1,2 @@
print('I am %r' % this.test)
assert(False)

View File

@ -0,0 +1,8 @@
#!/usr/bin/env python3
from osmo_gsm_tester import test
from osmo_gsm_tester.test import resources
print('I am %r / %r' % (test.suite.name(), test.test.name()))
assert(False)

0
test/template_test.err Normal file
View File

151
test/template_test.ok Normal file
View File

@ -0,0 +1,151 @@
- Testing: fill a config file with values
cnf Templates DBG: rendering osmo-nitb.cfg.tmpl
!
! OpenBSC configuration saved from vty
!
password foo
!
log stderr
logging filter all 1
logging color 0
logging print category 0
logging print extended-timestamp 1
logging level all debug
!
line vty
no login
bind val_vty_bind_ip
!
e1_input
e1_line 0 driver ipa
ipa bind val_abis_bind_ip
network
network country code val_mcc
mobile network code val_mnc
short name val_net_name_short
long name val_net_name_long
auth policy val_net_auth_policy
location updating reject cause 13
encryption a5 val_encryption
neci 1
rrlp mode none
mm info 1
handover 0
handover window rxlev averaging 10
handover window rxqual averaging 1
handover window rxlev neighbor averaging 10
handover power budget interval 6
handover power budget hysteresis 3
handover maximum distance 9999
timer t3101 10
timer t3103 0
timer t3105 0
timer t3107 0
timer t3109 4
timer t3111 0
timer t3113 60
timer t3115 0
timer t3117 0
timer t3119 0
timer t3141 0
smpp
local-tcp-ip val_smpp_bind_ip 2775
system-id test
policy closed
esme test
password test
default-route
ctrl
bind val_ctrl_bind_ip
bts 0
type val_type_bts0
band val_band_bts0
cell_identity 0
location_area_code val_bts.location_area_code_bts0
training_sequence_code 7
base_station_id_code val_bts.base_station_id_code_bts0
ms max power 15
cell reselection hysteresis 4
rxlev access min 0
channel allocator ascending
rach tx integer 9
rach max transmission 7
ip.access unit_id val_bts.unit_id_bts0 0
oml ip.access stream_id val_bts.stream_id_bts0 line 0
gprs mode none
trx 0
rf_locked 0
arfcn val_trx_arfcn_trx0
nominal power 23
max_power_red val_trx_max_power_red_trx0
rsl e1 tei 0
timeslot 0
phys_chan_config val_phys_chan_config_0
timeslot 1
phys_chan_config val_phys_chan_config_1
timeslot 2
phys_chan_config val_phys_chan_config_2
timeslot 3
phys_chan_config val_phys_chan_config_3
trx 1
rf_locked 0
arfcn val_trx_arfcn_trx1
nominal power 23
max_power_red val_trx_max_power_red_trx1
rsl e1 tei 0
timeslot 0
phys_chan_config val_phys_chan_config_0
timeslot 1
phys_chan_config val_phys_chan_config_1
timeslot 2
phys_chan_config val_phys_chan_config_2
timeslot 3
phys_chan_config val_phys_chan_config_3
bts 1
type val_type_bts1
band val_band_bts1
cell_identity 0
location_area_code val_bts.location_area_code_bts1
training_sequence_code 7
base_station_id_code val_bts.base_station_id_code_bts1
ms max power 15
cell reselection hysteresis 4
rxlev access min 0
channel allocator ascending
rach tx integer 9
rach max transmission 7
ip.access unit_id val_bts.unit_id_bts1 0
oml ip.access stream_id val_bts.stream_id_bts1 line 0
gprs mode none
trx 0
rf_locked 0
arfcn val_trx_arfcn_trx0
nominal power 23
max_power_red val_trx_max_power_red_trx0
rsl e1 tei 0
timeslot 0
phys_chan_config val_phys_chan_config_0
timeslot 1
phys_chan_config val_phys_chan_config_1
timeslot 2
phys_chan_config val_phys_chan_config_2
timeslot 3
phys_chan_config val_phys_chan_config_3
trx 1
rf_locked 0
arfcn val_trx_arfcn_trx1
nominal power 23
max_power_red val_trx_max_power_red_trx1
rsl e1 tei 0
timeslot 0
phys_chan_config val_phys_chan_config_0
timeslot 1
phys_chan_config val_phys_chan_config_1
timeslot 2
phys_chan_config val_phys_chan_config_2
timeslot 3
phys_chan_config val_phys_chan_config_3
- Testing: expect to fail on invalid templates dir
sucess: setting non-existing templates dir raised RuntimeError

76
test/template_test.py Executable file
View File

@ -0,0 +1,76 @@
#!/usr/bin/env python3
import _prep
import sys
import os
from osmo_gsm_tester import template, log
log.set_level(log.C_CNF, log.L_DBG)
print('- Testing: fill a config file with values')
mock_timeslot_list=(
{ 'phys_chan_config': 'val_phys_chan_config_0' },
{ 'phys_chan_config': 'val_phys_chan_config_1' },
{ 'phys_chan_config': 'val_phys_chan_config_2' },
{ 'phys_chan_config': 'val_phys_chan_config_3' },
)
mock_bts = {
'type': 'val_type',
'band': 'val_band',
'location_area_code': 'val_bts.location_area_code',
'base_station_id_code': 'val_bts.base_station_id_code',
'unit_id': 'val_bts.unit_id',
'stream_id': 'val_bts.stream_id',
'trx_list': (
dict(arfcn='val_trx_arfcn_trx0',
max_power_red='val_trx_max_power_red_trx0',
timeslot_list=mock_timeslot_list),
dict(arfcn='val_trx_arfcn_trx1',
max_power_red='val_trx_max_power_red_trx1',
timeslot_list=mock_timeslot_list),
)
}
def clone_mod(d, val_ext):
c = dict(d)
for name in c.keys():
if isinstance(c[name], str):
c[name] = c[name] + val_ext
elif isinstance(c[name], dict):
c[name] = clone_mod(c[name], val_ext)
return c
mock_bts0 = clone_mod(mock_bts, '_bts0')
mock_bts1 = clone_mod(mock_bts, '_bts1')
vals = dict(
vty_bind_ip='val_vty_bind_ip',
abis_bind_ip='val_abis_bind_ip',
mcc='val_mcc',
mnc='val_mnc',
net_name_short='val_net_name_short',
net_name_long='val_net_name_long',
net_auth_policy='val_net_auth_policy',
encryption='val_encryption',
smpp_bind_ip='val_smpp_bind_ip',
ctrl_bind_ip='val_ctrl_bind_ip',
bts_list=(mock_bts0, mock_bts1)
)
print(template.render('osmo-nitb.cfg', vals))
print('- Testing: expect to fail on invalid templates dir')
try:
template.set_templates_dir('non-existing dir')
sys.stderr.write('Error: setting non-existing templates dir should raise RuntimeError\n')
assert(False)
except RuntimeError:
# not logging exception to omit non-constant path name from expected output
print('sucess: setting non-existing templates dir raised RuntimeError\n')
pass
# vim: expandtab tabstop=4 shiftwidth=4

View File

@ -0,0 +1,87 @@
!
! OpenBSC configuration saved from vty
!
password foo
!
log stderr
logging filter all 1
logging color 0
logging print category 0
logging print extended-timestamp 1
logging level all debug
!
line vty
no login
bind ${vty_bind_ip}
!
e1_input
e1_line 0 driver ipa
ipa bind ${abis_bind_ip}
network
network country code ${mcc}
mobile network code ${mnc}
short name ${net_name_short}
long name ${net_name_long}
auth policy ${net_auth_policy}
location updating reject cause 13
encryption a5 ${encryption}
neci 1
rrlp mode none
mm info 1
handover 0
handover window rxlev averaging 10
handover window rxqual averaging 1
handover window rxlev neighbor averaging 10
handover power budget interval 6
handover power budget hysteresis 3
handover maximum distance 9999
timer t3101 10
timer t3103 0
timer t3105 0
timer t3107 0
timer t3109 4
timer t3111 0
timer t3113 60
timer t3115 0
timer t3117 0
timer t3119 0
timer t3141 0
smpp
local-tcp-ip ${smpp_bind_ip} 2775
system-id test
policy closed
esme test
password test
default-route
ctrl
bind ${ctrl_bind_ip}
%for bts in bts_list:
bts ${loop.index}
type ${bts.type}
band ${bts.band}
cell_identity 0
location_area_code ${bts.location_area_code}
training_sequence_code 7
base_station_id_code ${bts.base_station_id_code}
ms max power 15
cell reselection hysteresis 4
rxlev access min 0
channel allocator ascending
rach tx integer 9
rach max transmission 7
ip.access unit_id ${bts.unit_id} 0
oml ip.access stream_id ${bts.stream_id} line 0
gprs mode none
% for trx in bts.trx_list:
trx ${loop.index}
rf_locked 0
arfcn ${trx.arfcn}
nominal power 23
max_power_red ${trx.max_power_red}
rsl e1 tei 0
% for ts in trx.timeslot_list:
timeslot ${loop.index}
phys_chan_config ${ts.phys_chan_config}
% endfor
% endfor
%endfor

10
update_version.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/sh
set -e
git describe --abbrev=8 --dirty | sed 's/v\([^-]*\)-\([^-]*\)-\(.*\)/\1.dev\2.\3/' > version
cat version
echo "# osmo-gsm-tester version.
# Automatically generated by update_version.sh.
# Gets imported by __init__.py.
_version = '$(cat version)'" \
> src/osmo_gsm_tester/_version.py