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:
parent
0f2f19e9aa
commit
dae3d3c479
|
@ -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
|
|
@ -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')
|
|
@ -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"
|
|
@ -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
|
|
@ -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" *
|
|
@ -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"
|
|
@ -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
|
|
@ -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/
|
|
@ -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.
|
|
@ -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
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
pcu
|
||||
flow-control-interval 10
|
||||
cs 2
|
||||
alloc-algorithm dynamic
|
||||
alpha 0
|
||||
gamma 0
|
|
@ -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
|
||||
!
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,9 @@
|
|||
.PHONY: check update
|
||||
|
||||
check:
|
||||
./all_tests.py
|
||||
|
||||
update:
|
||||
./all_tests.py -u
|
||||
|
||||
# vim: noexpandtab tabstop=8 shiftwidth=8
|
|
@ -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)
|
||||
|
|
@ -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,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
|
|
@ -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
|
|
@ -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,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:
|
|
@ -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)"
|
|
@ -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,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]
|
|
@ -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,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
|
|
@ -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,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.
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
print('hello world')
|
||||
print('I am %r / %r' % (this.suite, this.test))
|
||||
print('one\ntwo\nthree')
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,9 @@
|
|||
resources:
|
||||
nitb_iface: 1
|
||||
nitb: 1
|
||||
bts: 1
|
||||
msisdn: 2
|
||||
modem: 2
|
||||
|
||||
defaults:
|
||||
timeout: 60s
|
|
@ -0,0 +1,2 @@
|
|||
print('I am %r' % this.test)
|
||||
assert(False)
|
|
@ -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,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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue