initial import
The original osmo-gsm-tester was an internal development at sysmocom, mostly by
D. Laszlo Sitzer <dlsitzer@sysmocom.de>, of which this public osmo-gsm-tester
is a refactoring / rewrite.
This imports an early state of the refactoring and is not functional yet. Bits
from the earlier osmo-gsm-tester will be added as needed. The earlier commit
history is not imported.
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
--- /dev/null
+++ b/test/config_test.err
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
--- /dev/null
+++ b/test/lock_test.err
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
--- /dev/null
+++ b/test/log_test.err
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 <origin>: from log.log()
+01:02:03 tst <origin> DBG: from log.dbg()
+01:02:03 tst <origin> ERR: from log.err()
+- Testing log.Origin functions
+01:02:03 tst some-name(some='detail'): hello log
+01:02:03 tst some-name(some='detail') ERR: hello err
+01:02:03 tst some-name(some='detail'): message {int=3, none=None, str='str\n', tuple=('foo', 42)}
+01:02:03 tst some-name(some='detail') DBG: hello dbg
+- Testing log.style()
+01:02:03: only time
+tst: only category
+DBG: only level
+some-name(some='detail'): only origin
+only src [log_test.py:69]
+- Testing log.style_change()
+no log format
+01:02:03: add time
+but no time format
+01:02:03 DBG: add level
+01:02:03 tst DBG: add category
+01:02:03 tst DBG: add src [log_test.py:84]
+01:02:03 tst some-name(some='detail') DBG: add origin [log_test.py:86]
+- Testing origin_width
+01:02:03 tst shortname: origin str set to 23 chars [log_test.py:93]
+01:02:03 tst very long name(and_some=(3, 'things', 'in a tuple'), some='details'): long origin str [log_test.py:95]
+01:02:03 tst very long name(and_some=(3, 'things', 'in a tuple'), some='details') DBG: long origin str dbg [log_test.py:96]
+01:02:03 tst very long name(and_some=(3, 'things', 'in a tuple'), some='details') ERR: long origin str err [log_test.py:97]
+- Testing log.Origin with omitted info
+01:02:03 tst LogTest: hello log, name implicit from class name [log_test.py:102]
+01:02:03 --- explicit_name: hello log, no category set [log_test.py:106]
+01:02:03 --- LogTest: hello log, no category nor name set [log_test.py:109]
+01:02:03 --- LogTest DBG: debug message, no category nor name set [log_test.py:112]
+- Testing logging of Exceptions, tracing origins
+Not throwing an exception in 'with:' works.
+nested print just prints
+01:02:03 tst level1->level2->level3: nested log() [log_test.py:144]
+01:02:03 tst level1->level2: nested l2 log() from within l3 scope [log_test.py:145]
+01:02:03 tst level1->level2->level3 ERR: ValueError: bork [log_test.py:146: raise ValueError('bork')]
+- Enter the same Origin context twice
+01:02:03 tst level1->level2: nested log [log_test.py:158]
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 <neels@hofmeyr.de>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import _prep
+
+import sys
+import os
+
+from osmo_gsm_tester import log
+
+#log.targets[0].get_time_str = lambda: '01:02:03'
+fake_time = '01:02:03'
+log.style_change(time=True, time_fmt=fake_time)
+
+print('- Testing global log functions')
+log.log('<origin>', log.C_TST, 'from log.log()')
+log.dbg('<origin>', log.C_TST, 'from log.dbg(), not seen')
+log.set_level(log.C_TST, log.L_DBG)
+log.dbg('<origin>', log.C_TST, 'from log.dbg()')
+log.set_level(log.C_TST, log.L_LOG)
+log.err('<origin>', log.C_TST, 'from log.err()')
+
+print('- Testing log.Origin functions')
+class LogTest(log.Origin):
+ pass
+
+t = LogTest()
+t.set_log_category(log.C_TST)
+t.set_name('some', 'name', some="detail")
+
+t.log("hello log")
+t.err("hello err")
+t.dbg("hello dbg not visible")
+
+t.log("message", int=3, tuple=('foo', 42), none=None, str='str\n')
+
+log.set_level(log.C_TST, log.L_DBG)
+t.dbg("hello dbg")
+
+print('- Testing log.style()')
+
+log.style(time=True, category=False, level=False, origin=False, src=False, time_fmt=fake_time)
+t.dbg("only time")
+log.style(time=False, category=True, level=False, origin=False, src=False, time_fmt=fake_time)
+t.dbg("only category")
+log.style(time=False, category=False, level=True, origin=False, src=False, time_fmt=fake_time)
+t.dbg("only level")
+log.style(time=False, category=False, level=False, origin=True, src=False, time_fmt=fake_time)
+t.dbg("only origin")
+log.style(time=False, category=False, level=False, origin=False, src=True, time_fmt=fake_time)
+t.dbg("only src")
+
+print('- Testing log.style_change()')
+log.style(time=False, category=False, level=False, origin=False, src=False, time_fmt=fake_time)
+t.dbg("no log format")
+log.style_change(time=True)
+t.dbg("add time")
+log.style_change(time=True, time_fmt=0)
+t.dbg("but no time format")
+log.style_change(time=True, time_fmt=fake_time)
+log.style_change(level=True)
+t.dbg("add level")
+log.style_change(category=True)
+t.dbg("add category")
+log.style_change(src=True)
+t.dbg("add src")
+log.style_change(origin=True)
+t.dbg("add origin")
+
+print('- Testing origin_width')
+t = LogTest()
+t.set_log_category(log.C_TST)
+t.set_name('shortname')
+log.style(origin_width=23, time_fmt=fake_time)
+t.log("origin str set to 23 chars")
+t.set_name('very long name', some='details', and_some=(3, 'things', 'in a tuple'))
+t.log("long origin str")
+t.dbg("long origin str dbg")
+t.err("long origin str err")
+
+print('- Testing log.Origin with omitted info')
+t = LogTest()
+t.set_log_category(log.C_TST)
+t.log("hello log, name implicit from class name")
+
+t = LogTest()
+t.set_name('explicit_name')
+t.log("hello log, no category set")
+
+t = LogTest()
+t.log("hello log, no category nor name set")
+t.dbg("hello log, no category nor name set, not seen")
+log.set_level(log.C_DEFAULT, log.L_DBG)
+t.dbg("debug message, no category nor name set")
+
+print('- Testing logging of Exceptions, tracing origins')
+log.style(time_fmt=fake_time)
+
+class Thing(log.Origin):
+ def __init__(self, some_path):
+ self.set_log_category(log.C_TST)
+ self.set_name(some_path)
+
+ def say(self, msg):
+ print(msg)
+
+#log.style_change(trace=True)
+
+with Thing('print_redirected'):
+ print("Not throwing an exception in 'with:' works.")
+
+def l1():
+ level1 = Thing('level1')
+ with level1:
+ l2()
+
+def l2():
+ level2 = Thing('level2')
+ with level2:
+ l3(level2)
+
+def l3(level2):
+ level3 = Thing('level3')
+ with level3:
+ print('nested print just prints')
+ level3.log('nested log()')
+ level2.log('nested l2 log() from within l3 scope')
+ raise ValueError('bork')
+
+try:
+ l1()
+except Exception:
+ log.log_exn()
+
+print('- Enter the same Origin context twice')
+with Thing('level1'):
+ l2 = Thing('level2')
+ with l2:
+ with l2:
+ l2.log('nested log')
+
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/test/resource_test.err b/test/resource_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/resource_test.err
diff --git a/test/resource_test.ok b/test/resource_test.ok
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/resource_test.ok
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
--- /dev/null
+++ b/test/suite_test.err
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
--- /dev/null
+++ b/test/suite_test/empty_dir/.unrelated_file
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
--- /dev/null
+++ b/test/template_test.err
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