From dae3d3c47906379061d57854fd140e8a7a12a25c Mon Sep 17 00:00:00 2001 From: Neels Hofmeyr Date: Tue, 28 Mar 2017 12:16:58 +0200 Subject: initial import The original osmo-gsm-tester was an internal development at sysmocom, mostly by D. Laszlo Sitzer , 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. --- Makefile | 15 + check_dependencies.py | 26 ++ contrib/jenkins-openbsc-build.sh | 140 +++++++ contrib/jenkins-osmo-bts-octphy.sh | 94 +++++ contrib/jenkins-osmo-bts-sysmo.sh | 68 ++++ contrib/jenkins-osmo-bts-trx.sh | 61 ++++ contrib/ts-dir-cleanup.sh | 30 ++ doc/README-sysmobts.txt | 59 +++ doc/README.txt | 92 +++++ install/ofono.service | 11 + install/org.ofono.conf | 28 ++ install/osmo-gsm-tester-limits.conf | 4 + install/osmo-gsm-tester.service | 11 + src/osmo_gsm_tester/__init__.py | 29 ++ src/osmo_gsm_tester/config.py | 161 ++++++++ src/osmo_gsm_tester/log.py | 405 +++++++++++++++++++++ src/osmo_gsm_tester/process.py | 23 ++ src/osmo_gsm_tester/resource.py | 51 +++ src/osmo_gsm_tester/suite.py | 150 ++++++++ src/osmo_gsm_tester/template.py | 56 +++ src/osmo_gsm_tester/templates/osmo-bts.cfg.tmpl | 21 ++ src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl | 87 +++++ src/osmo_gsm_tester/templates/osmo-pcu.cfg.tmpl | 6 + src/osmo_gsm_tester/templates/osmo-sgsn.cfg.tmpl | 26 ++ .../templates/sysmobts-mgr.cfg.tmpl | 24 ++ src/osmo_gsm_tester/test.py | 43 +++ src/osmo_gsm_tester/utils.py | 118 ++++++ src/run_once.py | 48 +++ test/Makefile | 9 + test/_prep.py | 16 + test/all_tests.py | 111 ++++++ test/config_test.err | 0 test/config_test.ok | 46 +++ test/config_test.py | 70 ++++ test/config_test/test.cfg | 39 ++ test/lock_test.err | 0 test/lock_test.ok | 8 + test/lock_test.sh | 10 + test/lock_test_help.py | 17 + test/log_test.err | 0 test/log_test.ok | 41 +++ test/log_test.py | 160 ++++++++ test/resource_test.err | 0 test/resource_test.ok | 0 test/resource_test.py | 20 + test/resource_test/etc/resources.conf | 115 ++++++ test/suite_test.err | 0 test/suite_test.ok | 24 ++ test/suite_test.py | 29 ++ test/suite_test/empty_dir/.unrelated_file | 0 test/suite_test/test_suite/hello_world.py | 3 + test/suite_test/test_suite/mo_mt_sms.py | 18 + test/suite_test/test_suite/mo_sms.py | 20 + test/suite_test/test_suite/suite.conf | 9 + test/suite_test/test_suite/test_error.py | 2 + test/suite_test/test_suite/test_error2.py | 8 + test/template_test.err | 0 test/template_test.ok | 151 ++++++++ test/template_test.py | 76 ++++ test/template_test/osmo-nitb.cfg.tmpl | 87 +++++ update_version.sh | 10 + 61 files changed, 2986 insertions(+) create mode 100644 Makefile create mode 100755 check_dependencies.py create mode 100755 contrib/jenkins-openbsc-build.sh create mode 100755 contrib/jenkins-osmo-bts-octphy.sh create mode 100755 contrib/jenkins-osmo-bts-sysmo.sh create mode 100755 contrib/jenkins-osmo-bts-trx.sh create mode 100755 contrib/ts-dir-cleanup.sh create mode 100644 doc/README-sysmobts.txt create mode 100644 doc/README.txt create mode 100644 install/ofono.service create mode 100644 install/org.ofono.conf create mode 100644 install/osmo-gsm-tester-limits.conf create mode 100644 install/osmo-gsm-tester.service create mode 100644 src/osmo_gsm_tester/__init__.py create mode 100644 src/osmo_gsm_tester/config.py create mode 100644 src/osmo_gsm_tester/log.py create mode 100644 src/osmo_gsm_tester/process.py create mode 100644 src/osmo_gsm_tester/resource.py create mode 100644 src/osmo_gsm_tester/suite.py create mode 100644 src/osmo_gsm_tester/template.py create mode 100644 src/osmo_gsm_tester/templates/osmo-bts.cfg.tmpl create mode 100644 src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl create mode 100644 src/osmo_gsm_tester/templates/osmo-pcu.cfg.tmpl create mode 100644 src/osmo_gsm_tester/templates/osmo-sgsn.cfg.tmpl create mode 100644 src/osmo_gsm_tester/templates/sysmobts-mgr.cfg.tmpl create mode 100644 src/osmo_gsm_tester/test.py create mode 100644 src/osmo_gsm_tester/utils.py create mode 100755 src/run_once.py create mode 100644 test/Makefile create mode 100644 test/_prep.py create mode 100755 test/all_tests.py create mode 100644 test/config_test.err create mode 100644 test/config_test.ok create mode 100755 test/config_test.py create mode 100644 test/config_test/test.cfg create mode 100644 test/lock_test.err create mode 100644 test/lock_test.ok create mode 100755 test/lock_test.sh create mode 100644 test/lock_test_help.py create mode 100644 test/log_test.err create mode 100644 test/log_test.ok create mode 100755 test/log_test.py create mode 100644 test/resource_test.err create mode 100644 test/resource_test.ok create mode 100755 test/resource_test.py create mode 100644 test/resource_test/etc/resources.conf create mode 100644 test/suite_test.err create mode 100644 test/suite_test.ok create mode 100755 test/suite_test.py create mode 100644 test/suite_test/empty_dir/.unrelated_file create mode 100644 test/suite_test/test_suite/hello_world.py create mode 100644 test/suite_test/test_suite/mo_mt_sms.py create mode 100644 test/suite_test/test_suite/mo_sms.py create mode 100644 test/suite_test/test_suite/suite.conf create mode 100644 test/suite_test/test_suite/test_error.py create mode 100755 test/suite_test/test_suite/test_error2.py create mode 100644 test/template_test.err create mode 100644 test/template_test.ok create mode 100755 test/template_test.py create mode 100644 test/template_test/osmo-nitb.cfg.tmpl create mode 100755 update_version.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f972675 --- /dev/null +++ b/Makefile @@ -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 diff --git a/check_dependencies.py b/check_dependencies.py new file mode 100755 index 0000000..d56e53b --- /dev/null +++ b/check_dependencies.py @@ -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') diff --git a/contrib/jenkins-openbsc-build.sh b/contrib/jenkins-openbsc-build.sh new file mode 100755 index 0000000..e242927 --- /dev/null +++ b/contrib/jenkins-openbsc-build.sh @@ -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" diff --git a/contrib/jenkins-osmo-bts-octphy.sh b/contrib/jenkins-osmo-bts-octphy.sh new file mode 100755 index 0000000..a966083 --- /dev/null +++ b/contrib/jenkins-osmo-bts-octphy.sh @@ -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 diff --git a/contrib/jenkins-osmo-bts-sysmo.sh b/contrib/jenkins-osmo-bts-sysmo.sh new file mode 100755 index 0000000..142eddd --- /dev/null +++ b/contrib/jenkins-osmo-bts-sysmo.sh @@ -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" * diff --git a/contrib/jenkins-osmo-bts-trx.sh b/contrib/jenkins-osmo-bts-trx.sh new file mode 100755 index 0000000..b2b215b --- /dev/null +++ b/contrib/jenkins-osmo-bts-trx.sh @@ -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" diff --git a/contrib/ts-dir-cleanup.sh b/contrib/ts-dir-cleanup.sh new file mode 100755 index 0000000..ae5ea04 --- /dev/null +++ b/contrib/ts-dir-cleanup.sh @@ -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 diff --git a/doc/README-sysmobts.txt b/doc/README-sysmobts.txt new file mode 100644 index 0000000..695c685 --- /dev/null +++ b/doc/README-sysmobts.txt @@ -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/ diff --git a/doc/README.txt b/doc/README.txt new file mode 100644 index 0000000..9d2b91a --- /dev/null +++ b/doc/README.txt @@ -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 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. diff --git a/install/ofono.service b/install/ofono.service new file mode 100644 index 0000000..0aa9fbe --- /dev/null +++ b/install/ofono.service @@ -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 diff --git a/install/org.ofono.conf b/install/org.ofono.conf new file mode 100644 index 0000000..8b13c75 --- /dev/null +++ b/install/org.ofono.conf @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/install/osmo-gsm-tester-limits.conf b/install/osmo-gsm-tester-limits.conf new file mode 100644 index 0000000..1fb0738 --- /dev/null +++ b/install/osmo-gsm-tester-limits.conf @@ -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 diff --git a/install/osmo-gsm-tester.service b/install/osmo-gsm-tester.service new file mode 100644 index 0000000..02225d7 --- /dev/null +++ b/install/osmo-gsm-tester.service @@ -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 diff --git a/src/osmo_gsm_tester/__init__.py b/src/osmo_gsm_tester/__init__.py new file mode 100644 index 0000000..6b6b46e --- /dev/null +++ b/src/osmo_gsm_tester/__init__.py @@ -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 +# Neels Hofmeyr +# +# 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 . + +__version__ = 'UNKNOWN' + +try: + from ._version import _version + __version__ = _version +except: + pass + +# vim: expandtab tabstop=4 shiftwidth=4 diff --git a/src/osmo_gsm_tester/config.py b/src/osmo_gsm_tester/config.py new file mode 100644 index 0000000..18b209e --- /dev/null +++ b/src/osmo_gsm_tester/config.py @@ -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 +# +# 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 . + +# 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 diff --git a/src/osmo_gsm_tester/log.py b/src/osmo_gsm_tester/log.py new file mode 100644 index 0000000..27194a9 --- /dev/null +++ b/src/osmo_gsm_tester/log.py @@ -0,0 +1,405 @@ +# osmo_gsm_tester: global logging +# +# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH +# +# Author: Neels Hofmeyr +# +# 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 . + +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 diff --git a/src/osmo_gsm_tester/process.py b/src/osmo_gsm_tester/process.py new file mode 100644 index 0000000..2e0ff52 --- /dev/null +++ b/src/osmo_gsm_tester/process.py @@ -0,0 +1,23 @@ +# osmo_gsm_tester: process management +# +# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH +# +# Author: Neels Hofmeyr +# +# 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 . + + + + +# vim: expandtab tabstop=4 shiftwidth=4 diff --git a/src/osmo_gsm_tester/resource.py b/src/osmo_gsm_tester/resource.py new file mode 100644 index 0000000..bebc82d --- /dev/null +++ b/src/osmo_gsm_tester/resource.py @@ -0,0 +1,51 @@ +# osmo_gsm_tester: manage resources +# +# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH +# +# Author: Neels Hofmeyr +# +# 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 . + +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 diff --git a/src/osmo_gsm_tester/suite.py b/src/osmo_gsm_tester/suite.py new file mode 100644 index 0000000..fb7c34d --- /dev/null +++ b/src/osmo_gsm_tester/suite.py @@ -0,0 +1,150 @@ +# osmo_gsm_tester: test suite +# +# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH +# +# Author: Neels Hofmeyr +# +# 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 . + +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 diff --git a/src/osmo_gsm_tester/template.py b/src/osmo_gsm_tester/template.py new file mode 100644 index 0000000..434ab62 --- /dev/null +++ b/src/osmo_gsm_tester/template.py @@ -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 +# +# 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 . + +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 diff --git a/src/osmo_gsm_tester/templates/osmo-bts.cfg.tmpl b/src/osmo_gsm_tester/templates/osmo-bts.cfg.tmpl new file mode 100644 index 0000000..20fa57f --- /dev/null +++ b/src/osmo_gsm_tester/templates/osmo-bts.cfg.tmpl @@ -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 diff --git a/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl b/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl new file mode 100644 index 0000000..3404b7f --- /dev/null +++ b/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl @@ -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 diff --git a/src/osmo_gsm_tester/templates/osmo-pcu.cfg.tmpl b/src/osmo_gsm_tester/templates/osmo-pcu.cfg.tmpl new file mode 100644 index 0000000..b88e6e7 --- /dev/null +++ b/src/osmo_gsm_tester/templates/osmo-pcu.cfg.tmpl @@ -0,0 +1,6 @@ +pcu + flow-control-interval 10 + cs 2 + alloc-algorithm dynamic + alpha 0 + gamma 0 diff --git a/src/osmo_gsm_tester/templates/osmo-sgsn.cfg.tmpl b/src/osmo_gsm_tester/templates/osmo-sgsn.cfg.tmpl new file mode 100644 index 0000000..4955983 --- /dev/null +++ b/src/osmo_gsm_tester/templates/osmo-sgsn.cfg.tmpl @@ -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 +! diff --git a/src/osmo_gsm_tester/templates/sysmobts-mgr.cfg.tmpl b/src/osmo_gsm_tester/templates/sysmobts-mgr.cfg.tmpl new file mode 100644 index 0000000..3b28d78 --- /dev/null +++ b/src/osmo_gsm_tester/templates/sysmobts-mgr.cfg.tmpl @@ -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 diff --git a/src/osmo_gsm_tester/test.py b/src/osmo_gsm_tester/test.py new file mode 100644 index 0000000..fd5a640 --- /dev/null +++ b/src/osmo_gsm_tester/test.py @@ -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 +# +# 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 . + +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 diff --git a/src/osmo_gsm_tester/utils.py b/src/osmo_gsm_tester/utils.py new file mode 100644 index 0000000..9992d44 --- /dev/null +++ b/src/osmo_gsm_tester/utils.py @@ -0,0 +1,118 @@ +# osmo_gsm_tester: language snippets +# +# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH +# +# Author: Neels Hofmeyr +# +# 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 . + +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 diff --git a/src/run_once.py b/src/run_once.py new file mode 100755 index 0000000..ff15204 --- /dev/null +++ b/src/run_once.py @@ -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 +# +# 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 . + +'''osmo_gsm_tester: invoke a single test run. + +./run_once.py ~/path/to/test_package/ + +Upon launch, a 'test_package/run-' 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 diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 0000000..692c971 --- /dev/null +++ b/test/Makefile @@ -0,0 +1,9 @@ +.PHONY: check update + +check: + ./all_tests.py + +update: + ./all_tests.py -u + +# vim: noexpandtab tabstop=8 shiftwidth=8 diff --git a/test/_prep.py b/test/_prep.py new file mode 100644 index 0000000..bfbe7b8 --- /dev/null +++ b/test/_prep.py @@ -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) + diff --git a/test/all_tests.py b/test/all_tests.py new file mode 100755 index 0000000..f09fc0e --- /dev/null +++ b/test/all_tests.py @@ -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 diff --git a/test/config_test.err b/test/config_test.err new file mode 100644 index 0000000..e69de29 diff --git a/test/config_test.ok b/test/config_test.ok new file mode 100644 index 0000000..dc88ae2 --- /dev/null +++ b/test/config_test.ok @@ -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 diff --git a/test/config_test.py b/test/config_test.py new file mode 100755 index 0000000..de4ffb9 --- /dev/null +++ b/test/config_test.py @@ -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 diff --git a/test/config_test/test.cfg b/test/config_test/test.cfg new file mode 100644 index 0000000..c6d61bf --- /dev/null +++ b/test/config_test/test.cfg @@ -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 diff --git a/test/lock_test.err b/test/lock_test.err new file mode 100644 index 0000000..e69de29 diff --git a/test/lock_test.ok b/test/lock_test.ok new file mode 100644 index 0000000..2c0f31b --- /dev/null +++ b/test/lock_test.ok @@ -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: diff --git a/test/lock_test.sh b/test/lock_test.sh new file mode 100755 index 0000000..c82d141 --- /dev/null +++ b/test/lock_test.sh @@ -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)" diff --git a/test/lock_test_help.py b/test/lock_test_help.py new file mode 100644 index 0000000..720e100 --- /dev/null +++ b/test/lock_test_help.py @@ -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 diff --git a/test/log_test.err b/test/log_test.err new file mode 100644 index 0000000..e69de29 diff --git a/test/log_test.ok b/test/log_test.ok new file mode 100644 index 0000000..70257d5 --- /dev/null +++ b/test/log_test.ok @@ -0,0 +1,41 @@ +- Testing global log functions +01:02:03 tst : from log.log() +01:02:03 tst DBG: from log.dbg() +01:02:03 tst 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] diff --git a/test/log_test.py b/test/log_test.py new file mode 100755 index 0000000..6eca6aa --- /dev/null +++ b/test/log_test.py @@ -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 +# +# 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 . + +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('', log.C_TST, 'from log.log()') +log.dbg('', log.C_TST, 'from log.dbg(), not seen') +log.set_level(log.C_TST, log.L_DBG) +log.dbg('', log.C_TST, 'from log.dbg()') +log.set_level(log.C_TST, log.L_LOG) +log.err('', 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 diff --git a/test/resource_test.err b/test/resource_test.err new file mode 100644 index 0000000..e69de29 diff --git a/test/resource_test.ok b/test/resource_test.ok new file mode 100644 index 0000000..e69de29 diff --git a/test/resource_test.py b/test/resource_test.py new file mode 100755 index 0000000..87e0473 --- /dev/null +++ b/test/resource_test.py @@ -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 diff --git a/test/resource_test/etc/resources.conf b/test/resource_test/etc/resources.conf new file mode 100644 index 0000000..b6de84a --- /dev/null +++ b/test/resource_test/etc/resources.conf @@ -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 diff --git a/test/suite_test.err b/test/suite_test.err new file mode 100644 index 0000000..e69de29 diff --git a/test/suite_test.ok b/test/suite_test.ok new file mode 100644 index 0000000..173fee9 --- /dev/null +++ b/test/suite_test.ok @@ -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. diff --git a/test/suite_test.py b/test/suite_test.py new file mode 100755 index 0000000..5e6c312 --- /dev/null +++ b/test/suite_test.py @@ -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 diff --git a/test/suite_test/empty_dir/.unrelated_file b/test/suite_test/empty_dir/.unrelated_file new file mode 100644 index 0000000..e69de29 diff --git a/test/suite_test/test_suite/hello_world.py b/test/suite_test/test_suite/hello_world.py new file mode 100644 index 0000000..c992139 --- /dev/null +++ b/test/suite_test/test_suite/hello_world.py @@ -0,0 +1,3 @@ +print('hello world') +print('I am %r / %r' % (this.suite, this.test)) +print('one\ntwo\nthree') diff --git a/test/suite_test/test_suite/mo_mt_sms.py b/test/suite_test/test_suite/mo_mt_sms.py new file mode 100644 index 0000000..cf44357 --- /dev/null +++ b/test/suite_test/test_suite/mo_mt_sms.py @@ -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) diff --git a/test/suite_test/test_suite/mo_sms.py b/test/suite_test/test_suite/mo_sms.py new file mode 100644 index 0000000..d9517dd --- /dev/null +++ b/test/suite_test/test_suite/mo_sms.py @@ -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) diff --git a/test/suite_test/test_suite/suite.conf b/test/suite_test/test_suite/suite.conf new file mode 100644 index 0000000..7596ca0 --- /dev/null +++ b/test/suite_test/test_suite/suite.conf @@ -0,0 +1,9 @@ +resources: + nitb_iface: 1 + nitb: 1 + bts: 1 + msisdn: 2 + modem: 2 + +defaults: + timeout: 60s diff --git a/test/suite_test/test_suite/test_error.py b/test/suite_test/test_suite/test_error.py new file mode 100644 index 0000000..a45f7a6 --- /dev/null +++ b/test/suite_test/test_suite/test_error.py @@ -0,0 +1,2 @@ +print('I am %r' % this.test) +assert(False) diff --git a/test/suite_test/test_suite/test_error2.py b/test/suite_test/test_suite/test_error2.py new file mode 100755 index 0000000..7e04588 --- /dev/null +++ b/test/suite_test/test_suite/test_error2.py @@ -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) diff --git a/test/template_test.err b/test/template_test.err new file mode 100644 index 0000000..e69de29 diff --git a/test/template_test.ok b/test/template_test.ok new file mode 100644 index 0000000..0ccc23a --- /dev/null +++ b/test/template_test.ok @@ -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 + diff --git a/test/template_test.py b/test/template_test.py new file mode 100755 index 0000000..38495bf --- /dev/null +++ b/test/template_test.py @@ -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 diff --git a/test/template_test/osmo-nitb.cfg.tmpl b/test/template_test/osmo-nitb.cfg.tmpl new file mode 100644 index 0000000..3404b7f --- /dev/null +++ b/test/template_test/osmo-nitb.cfg.tmpl @@ -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 diff --git a/update_version.sh b/update_version.sh new file mode 100755 index 0000000..3d5fe42 --- /dev/null +++ b/update_version.sh @@ -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 -- cgit v1.2.3