aboutsummaryrefslogtreecommitdiffstats
path: root/selftest
diff options
context:
space:
mode:
authorNeels Hofmeyr <neels@hofmeyr.de>2017-03-28 14:30:28 +0200
committerNeels Hofmeyr <nhofmeyr@sysmocom.de>2017-04-08 15:43:19 +0200
commit3531a192ae8eeb78c53342454f65327bce4fa57a (patch)
tree2791d8927038eb0e3ffc8c4371955051161a6a5d /selftest
parentdae3d3c47906379061d57854fd140e8a7a12a25c (diff)
core implementation
code bomb implementing the bulk of the osmo-gsm-tester Change-Id: I53610becbf643ed51b90cfd9debc6992fe211ec9
Diffstat (limited to 'selftest')
-rw-r--r--selftest/Makefile12
-rw-r--r--selftest/_prep.py17
-rwxr-xr-xselftest/all_tests.py123
-rw-r--r--selftest/conf/paths.conf2
-rw-r--r--selftest/conf/resources.conf133
-rw-r--r--selftest/config_test.err0
-rw-r--r--selftest/config_test.ok95
-rwxr-xr-xselftest/config_test.py115
-rw-r--r--selftest/config_test/test.cfg59
-rwxr-xr-xselftest/dbus_test/dbus_server.py44
-rwxr-xr-xselftest/dbus_test/ofono_client.py57
-rw-r--r--selftest/dbus_test/ofono_client_one_thread.py71
-rw-r--r--selftest/lock_test.err0
-rw-r--r--selftest/lock_test.ok8
-rwxr-xr-xselftest/lock_test.sh10
-rw-r--r--selftest/lock_test_help.py17
-rw-r--r--selftest/log_test.err0
-rw-r--r--selftest/log_test.ok41
-rwxr-xr-xselftest/log_test.py161
-rwxr-xr-xselftest/misc.py7
-rw-r--r--selftest/process_test.err0
-rw-r--r--selftest/process_test.ok33
-rw-r--r--selftest/process_test.ok.ign7
-rwxr-xr-xselftest/process_test.py51
-rwxr-xr-xselftest/process_test/foo.py25
-rwxr-xr-xselftest/py_import_test/invocation.py24
-rw-r--r--selftest/py_import_test/subdir/script.py9
-rw-r--r--selftest/py_import_test/support.py2
-rw-r--r--selftest/real_suite/README.txt18
-rw-r--r--selftest/real_suite/default.conf31
-rw-r--r--selftest/real_suite/env4
-rw-r--r--selftest/real_suite/paths.conf3
-rw-r--r--selftest/real_suite/resources.conf139
-rw-r--r--selftest/real_suite/scenarios/trx.conf3
-rwxr-xr-xselftest/real_suite/suites/sms/mo_mt_sms.py26
-rw-r--r--selftest/real_suite/suites/sms/suite.conf10
-rw-r--r--selftest/resource_test.err0
-rw-r--r--selftest/resource_test.ok207
-rwxr-xr-xselftest/resource_test.py97
-rw-r--r--selftest/suite_test.err0
-rw-r--r--selftest/suite_test.ok40
-rwxr-xr-xselftest/suite_test.py34
-rw-r--r--selftest/suite_test/empty_dir/.unrelated_file0
-rw-r--r--selftest/suite_test/test_suite/hello_world.py5
-rw-r--r--selftest/suite_test/test_suite/mo_mt_sms.py18
-rw-r--r--selftest/suite_test/test_suite/mo_sms.py20
-rw-r--r--selftest/suite_test/test_suite/suite.conf10
-rwxr-xr-xselftest/suite_test/test_suite/test_error.py5
-rw-r--r--selftest/template_test.err0
-rw-r--r--selftest/template_test.ok149
-rwxr-xr-xselftest/template_test.py76
-rw-r--r--selftest/template_test/osmo-nitb.cfg.tmpl87
-rw-r--r--selftest/trial_test.err0
-rw-r--r--selftest/trial_test.ok16
-rw-r--r--selftest/trial_test.ok.ign2
-rwxr-xr-xselftest/trial_test.py49
-rw-r--r--selftest/trial_test/invalid_checksum/checksums.md53
-rw-r--r--selftest/trial_test/invalid_checksum/file11
-rw-r--r--selftest/trial_test/invalid_checksum/file21
-rw-r--r--selftest/trial_test/invalid_checksum/file31
-rw-r--r--selftest/trial_test/missing_file/checksums.md53
-rw-r--r--selftest/trial_test/missing_file/file11
-rw-r--r--selftest/trial_test/missing_file/file31
-rw-r--r--selftest/trial_test/valid_checksums/checksums.md53
-rw-r--r--selftest/trial_test/valid_checksums/file11
-rw-r--r--selftest/trial_test/valid_checksums/file21
-rw-r--r--selftest/trial_test/valid_checksums/file31
-rw-r--r--selftest/util_test.err0
-rw-r--r--selftest/util_test.ok5
-rwxr-xr-xselftest/util_test.py12
70 files changed, 2206 insertions, 0 deletions
diff --git a/selftest/Makefile b/selftest/Makefile
new file mode 100644
index 0000000..f0c8c69
--- /dev/null
+++ b/selftest/Makefile
@@ -0,0 +1,12 @@
+.PHONY: check update
+
+check: set_pythonpath
+ ./all_tests.py
+
+update:
+ ./all_tests.py -u
+
+set_pythonpath:
+ echo "export PYTHONPATH=\"$(PWD)/../src\"" > set_pythonpath
+
+# vim: noexpandtab tabstop=8 shiftwidth=8
diff --git a/selftest/_prep.py b/selftest/_prep.py
new file mode 100644
index 0000000..e89c5a7
--- /dev/null
+++ b/selftest/_prep.py
@@ -0,0 +1,17 @@
+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() ]
+log.set_all_levels(log.L_DBG)
+
+if '-v' in sys.argv:
+ log.style_change(trace=True)
+
diff --git a/selftest/all_tests.py b/selftest/all_tests.py
new file mode 100755
index 0000000..5c1ce59
--- /dev/null
+++ b/selftest/all_tests.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+import subprocess
+import time
+import difflib
+import argparse
+import re
+
+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):
+ ign_file = expect_file + '.ign'
+ if os.path.isfile(ign_file):
+ with open(ign_file, 'r') as f:
+ ign_rules = f.readlines()
+ for ign_rule in ign_rules:
+ if not ign_rule:
+ continue
+ if '\t' in ign_rule:
+ ign_rule, repl = ign_rule.split('\t')
+ repl = repl.strip()
+ else:
+ repl = '*'
+ ir = re.compile(ign_rule)
+ got = repl.join(ir.split(got))
+
+ 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('\nTest 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/selftest/conf/paths.conf b/selftest/conf/paths.conf
new file mode 100644
index 0000000..0b2d035
--- /dev/null
+++ b/selftest/conf/paths.conf
@@ -0,0 +1,2 @@
+state_dir: ./test_work/state_dir
+suites_dir: ./suite_test
diff --git a/selftest/conf/resources.conf b/selftest/conf/resources.conf
new file mode 100644
index 0000000..84c85d0
--- /dev/null
+++ b/selftest/conf/resources.conf
@@ -0,0 +1,133 @@
+# all hardware and interfaces available to this osmo-gsm-tester
+
+nitb_iface:
+- addr: 10.42.42.1
+- addr: 10.42.42.2
+- addr: 10.42.42.3
+
+bts:
+- label: sysmoBTS 1002
+ type: sysmo
+ unit_id: 1
+ addr: 10.42.42.114
+ band: GSM-1800
+
+- label: octBTS 3000
+ type: oct
+ unit_id: 5
+ addr: 10.42.42.115
+ band: GSM-1800
+ trx:
+ - hwaddr: 00:0c:90:32:b5:8a
+
+- label: nanoBTS 1900
+ type: nanobts
+ unit_id: 1902
+ addr: 10.42.42.190
+ band: GSM-1900
+ trx:
+ - hwaddr: 00:02:95:00:41:b3
+
+arfcn:
+ - arfcn: 512
+ band: GSM-1800
+ - arfcn: 514
+ band: GSM-1800
+ - arfcn: 516
+ band: GSM-1800
+ - arfcn: 518
+ band: GSM-1800
+ - arfcn: 520
+ band: GSM-1800
+
+ - arfcn: 540
+ band: GSM-1900
+ - arfcn: 542
+ band: GSM-1900
+ - arfcn: 544
+ band: GSM-1900
+ - arfcn: 546
+ band: GSM-1900
+ - arfcn: 548
+ band: GSM-1900
+
+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/selftest/config_test.err b/selftest/config_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/selftest/config_test.err
diff --git a/selftest/config_test.ok b/selftest/config_test.ok
new file mode 100644
index 0000000..40a5dcb
--- /dev/null
+++ b/selftest/config_test.ok
@@ -0,0 +1,95 @@
+{'addr': ['0.0.0.0',
+ '255.255.255.255',
+ '10.11.12.13',
+ '10.0.99.1',
+ '192.168.0.14'],
+ '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'}],
+ 'hwaddr': ['ca:ff:ee:ba:aa:be',
+ '00:00:00:00:00:00',
+ 'CA:FF:EE:BA:AA:BE',
+ 'cA:Ff:eE:Ba:aA:Be',
+ 'ff:ff:ff:ff:ff:ff'],
+ 'imsi': ['012345', '012345678', '012345678912345'],
+ 'ki': ['000102030405060708090a0b0c0d0e0f', '000102030405060708090a0b0c0d0e0f'],
+ '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 'imsi': '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
+- invalid v4 addrs:
+--- (item='addr[]'): ERR: ValueError: Invalid IPv4 address: '1.2.3'
+Validation: Error
+--- (item='addr[]'): ERR: ValueError: Invalid IPv4 address: '1.2.3 .4'
+Validation: Error
+--- (item='addr[]'): ERR: ValueError: Invalid IPv4 address: '91.2.3'
+Validation: Error
+--- (item='addr[]'): ERR: ValueError: Invalid IPv4 address: 'go away'
+Validation: Error
+--- (item='addr[]'): ERR: ValueError: Invalid IPv4 address: ''
+Validation: Error
+--- (item='addr[]'): ERR: ValueError: Invalid IPv4 address: None
+Validation: Error
+- invalid hw addrs:
+--- (item='hwaddr[]'): ERR: ValueError: Invalid hardware address: '1.2.3'
+Validation: Error
+--- (item='hwaddr[]'): ERR: ValueError: Invalid hardware address: '0b:0c:0d:0e:0f:0g'
+Validation: Error
+--- (item='hwaddr[]'): ERR: ValueError: Invalid hardware address: '0b:0c:0d:0e : 0f:0f'
+Validation: Error
+--- (item='hwaddr[]'): ERR: ValueError: Invalid hardware address: 'go away'
+Validation: Error
+--- (item='hwaddr[]'): ERR: ValueError: Invalid hardware address: ''
+Validation: Error
+--- (item='hwaddr[]'): ERR: ValueError: Invalid hardware address: None
+Validation: Error
+- invalid imsis:
+--- (item='imsi[]'): ERR: ValueError: Invalid IMSI: '99999999x9'
+Validation: Error
+--- (item='imsi[]'): ERR: ValueError: Invalid IMSI: '123 456 789 123'
+Validation: Error
+--- (item='imsi[]'): ERR: ValueError: Invalid IMSI: 'go away'
+Validation: Error
+--- (item='imsi[]'): ERR: ValueError: Invalid IMSI: ''
+Validation: Error
+--- (item='imsi[]'): ERR: ValueError: Invalid IMSI: None
+Validation: Error
diff --git a/selftest/config_test.py b/selftest/config_test.py
new file mode 100755
index 0000000..ce4d939
--- /dev/null
+++ b/selftest/config_test.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python3
+
+import _prep
+
+import sys
+import os
+import io
+import pprint
+import copy
+
+from osmo_gsm_tester import config, log, schema
+
+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': schema.STR,
+ 'modems[].msisdn': schema.STR,
+ 'modems[].imsi': schema.IMSI,
+ 'modems[].ki': schema.STR,
+ 'bts[].name' : schema.STR,
+ 'bts[].type' : schema.STR,
+ 'bts[].addr' : schema.STR,
+ 'bts[].trx[].timeslots[]' : schema.STR,
+ 'bts[].trx[].band' : schema.BAND,
+ 'a_dict.foo' : schema.INT,
+ 'addr[]' : schema.IPV4,
+ 'hwaddr[]' : schema.HWADDR,
+ 'imsi[]' : schema.IMSI,
+ 'ki[]' : schema.KI,
+ }
+
+def val(which):
+ try:
+ schema.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)
+
+print('- invalid v4 addrs:')
+c = copy.deepcopy(cfg)
+c['addr'][3] = '1.2.3'
+val(c)
+c['addr'][3] = '1.2.3 .4'
+val(c)
+c['addr'][3] = '91.2.3'
+val(c)
+c['addr'][3] = 'go away'
+val(c)
+c['addr'][3] = ''
+val(c)
+c['addr'][3] = None
+val(c)
+
+print('- invalid hw addrs:')
+c = copy.deepcopy(cfg)
+c['hwaddr'][3] = '1.2.3'
+val(c)
+c['hwaddr'][3] = '0b:0c:0d:0e:0f:0g'
+val(c)
+c['hwaddr'][3] = '0b:0c:0d:0e : 0f:0f'
+val(c)
+c['hwaddr'][3] = 'go away'
+val(c)
+c['hwaddr'][3] = ''
+val(c)
+c['hwaddr'][3] = None
+val(c)
+
+print('- invalid imsis:')
+c = copy.deepcopy(cfg)
+c['imsi'][2] = '99999999x9'
+val(c)
+c['imsi'][2] = '123 456 789 123'
+val(c)
+c['imsi'][2] = 'go away'
+val(c)
+c['imsi'][2] = ''
+val(c)
+c['imsi'][2] = None
+val(c)
+
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/selftest/config_test/test.cfg b/selftest/config_test/test.cfg
new file mode 100644
index 0000000..cc62182
--- /dev/null
+++ b/selftest/config_test/test.cfg
@@ -0,0 +1,59 @@
+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
+
+addr:
+- 0.0.0.0
+- 255.255.255.255
+- 10.11.12.13
+- 10.0.99.1
+- 192.168.0.14
+hwaddr:
+- ca:ff:ee:ba:aa:be
+- 00:00:00:00:00:00
+- CA:FF:EE:BA:AA:BE
+- cA:Ff:eE:Ba:aA:Be
+- ff:ff:ff:ff:ff:ff
+imsi:
+- '012345'
+- '012345678'
+- '012345678912345'
+ki:
+- 000102030405060708090a0b0c0d0e0f
+- 000102030405060708090a0b0c0d0e0f
diff --git a/selftest/dbus_test/dbus_server.py b/selftest/dbus_test/dbus_server.py
new file mode 100755
index 0000000..222b28b
--- /dev/null
+++ b/selftest/dbus_test/dbus_server.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+
+# Based on http://stackoverflow.com/questions/22390064/use-dbus-to-just-send-a-message-in-python
+
+# Python DBUS Test Server
+# runs until the Quit() method is called via DBUS
+
+from gi.repository import GLib
+from pydbus import SessionBus
+
+loop = GLib.MainLoop()
+
+class MyDBUSService(object):
+ """
+ <node>
+ <interface name='net.lew21.pydbus.ClientServerExample'>
+ <method name='Hello'>
+ <arg type='s' name='response' direction='out'/>
+ </method>
+ <method name='EchoString'>
+ <arg type='s' name='a' direction='in'/>
+ <arg type='s' name='response' direction='out'/>
+ </method>
+ <method name='Quit'/>
+ </interface>
+ </node>
+ """
+
+ def Hello(self):
+ """returns the string 'Hello, World!'"""
+ return "Hello, World!"
+
+ def EchoString(self, s):
+ """returns whatever is passed to it"""
+ return s
+
+ def Quit(self):
+ """removes this object from the DBUS connection and exits"""
+ loop.quit()
+
+bus = SessionBus()
+bus.publish("net.lew21.pydbus.ClientServerExample", MyDBUSService())
+loop.run()
+
diff --git a/selftest/dbus_test/ofono_client.py b/selftest/dbus_test/ofono_client.py
new file mode 100755
index 0000000..6b60f98
--- /dev/null
+++ b/selftest/dbus_test/ofono_client.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+
+'''
+Power on and off some modem on ofono, while running the glib main loop in a
+thread and receiving modem state changes by dbus signals.
+'''
+
+from pydbus import SystemBus, Variant
+import time
+import threading
+import pprint
+
+from gi.repository import GLib
+loop = GLib.MainLoop()
+
+def propchanged(*args, **kwargs):
+ print('-> PROP CHANGED: %r %r' % (args, kwargs))
+
+class GlibMainloop(threading.Thread):
+ def run(self):
+ loop.run()
+
+ml = GlibMainloop()
+ml.start()
+
+try:
+ bus = SystemBus()
+
+ print('\n- list modems')
+ root = bus.get("org.ofono", '/')
+ print(root.Introspect())
+ modems = sorted(root.GetModems())
+ pprint.pprint(modems)
+
+ first_modem_path = modems[0][0]
+ print('\n- first modem %r' % first_modem_path)
+ modem = bus.get("org.ofono", first_modem_path)
+ modem.PropertyChanged.connect(propchanged)
+
+ print(modem.Introspect())
+ print(modem.GetProperties())
+
+ print('\n- set Powered = True')
+ modem.SetProperty('Powered', Variant('b', True))
+ print('call returned')
+ print(modem.GetProperties())
+
+ time.sleep(1)
+
+ print('\n- set Powered = False')
+ modem.SetProperty('Powered', Variant('b', False))
+ print('call returned')
+
+ print(modem.GetProperties())
+finally:
+ loop.quit()
+ml.join()
diff --git a/selftest/dbus_test/ofono_client_one_thread.py b/selftest/dbus_test/ofono_client_one_thread.py
new file mode 100644
index 0000000..96d54bc
--- /dev/null
+++ b/selftest/dbus_test/ofono_client_one_thread.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+
+'''
+Power on and off some modem on ofono, while running the glib main loop in a
+thread and receiving modem state changes by dbus signals.
+'''
+
+from pydbus import SystemBus, Variant
+import time
+import pprint
+
+from gi.repository import GLib
+glib_main_loop = GLib.MainLoop()
+glib_main_ctx = glib_main_loop.get_context()
+
+def propchanged(*args, **kwargs):
+ print('-> PROP CHANGED: %r %r' % (args, kwargs))
+
+
+def pump():
+ global glib_main_ctx
+ print('pump?')
+ while glib_main_ctx.pending():
+ print('* pump')
+ glib_main_ctx.iteration()
+
+def wait(condition):
+ pump()
+ while not condition():
+ time.sleep(.1)
+ pump()
+
+bus = SystemBus()
+
+print('\n- list modems')
+root = bus.get("org.ofono", '/')
+print(root.Introspect())
+modems = sorted(root.GetModems())
+pprint.pprint(modems)
+pump()
+
+first_modem_path = modems[0][0]
+print('\n- first modem %r' % first_modem_path)
+modem = bus.get("org.ofono", first_modem_path)
+modem.PropertyChanged.connect(propchanged)
+
+print(modem.Introspect())
+print(modem.GetProperties())
+
+print('\n- set Powered = True')
+modem.SetProperty('Powered', Variant('b', True))
+print('call returned')
+print('- pump dbus events')
+pump()
+pump()
+print('sleep 1')
+time.sleep(1)
+pump()
+
+
+print('- modem properties:')
+print(modem.GetProperties())
+
+
+print('\n- set Powered = False')
+modem.SetProperty('Powered', Variant('b', False))
+print('call returned')
+
+print(modem.GetProperties())
+
+# vim: tabstop=4 shiftwidth=4 expandtab
diff --git a/selftest/lock_test.err b/selftest/lock_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/selftest/lock_test.err
diff --git a/selftest/lock_test.ok b/selftest/lock_test.ok
new file mode 100644
index 0000000..2c0f31b
--- /dev/null
+++ b/selftest/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/selftest/lock_test.sh b/selftest/lock_test.sh
new file mode 100755
index 0000000..c82d141
--- /dev/null
+++ b/selftest/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/selftest/lock_test_help.py b/selftest/lock_test_help.py
new file mode 100644
index 0000000..0549981
--- /dev/null
+++ b/selftest/lock_test_help.py
@@ -0,0 +1,17 @@
+import sys
+import time
+
+import _prep
+
+from osmo_gsm_tester.util 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/selftest/log_test.err b/selftest/log_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/selftest/log_test.err
diff --git a/selftest/log_test.ok b/selftest/log_test.ok
new file mode 100644
index 0000000..b2fdd69
--- /dev/null
+++ b/selftest/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:70]
+- 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:85]
+01:02:03 tst some-name(some='detail'): DBG: add origin [log_test.py:87]
+- Testing origin_width
+01:02:03 tst shortname: origin str set to 23 chars [log_test.py:94]
+01:02:03 tst very long name(and_some=(3, 'things', 'in a tuple'), some='details'): long origin str [log_test.py:96]
+01:02:03 tst very long name(and_some=(3, 'things', 'in a tuple'), some='details'): DBG: long origin str dbg [log_test.py:97]
+01:02:03 tst very long name(and_some=(3, 'things', 'in a tuple'), some='details'): ERR: long origin str err [log_test.py:98]
+- Testing log.Origin with omitted info
+01:02:03 tst LogTest: hello log, name implicit from class name [log_test.py:103]
+01:02:03 --- explicit_name: hello log, no category set [log_test.py:107]
+01:02:03 --- LogTest: hello log, no category nor name set [log_test.py:110]
+01:02:03 --- LogTest: DBG: debug message, no category nor name set [log_test.py:113]
+- Testing logging of Exceptions, tracing origins
+Not throwing an exception in 'with:' works.
+nested print just prints
+01:02:03 tst level3: nested log() [level1↪level2↪level3] [log_test.py:145]
+01:02:03 tst level2: nested l2 log() from within l3 scope [level1↪level2] [log_test.py:146]
+01:02:03 tst level3: ERR: ValueError: bork [level1↪level2↪level3] [log_test.py:147: raise ValueError('bork')]
+- Enter the same Origin context twice
+01:02:03 tst level2: nested log [level1↪level2] [log_test.py:159]
diff --git a/selftest/log_test.py b/selftest/log_test.py
new file mode 100755
index 0000000..2ec8635
--- /dev/null
+++ b/selftest/log_test.py
@@ -0,0 +1,161 @@
+#!/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)
+log.set_all_levels(None)
+
+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/selftest/misc.py b/selftest/misc.py
new file mode 100755
index 0000000..e57a48c
--- /dev/null
+++ b/selftest/misc.py
@@ -0,0 +1,7 @@
+#!/usr/bin/env python3
+
+msisdn = '0000'
+
+l = len(msisdn)
+next_msisdn = ('%%0%dd' % l) % (int(msisdn) + 1)
+print(next_msisdn)
diff --git a/selftest/process_test.err b/selftest/process_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/selftest/process_test.err
diff --git a/selftest/process_test.ok b/selftest/process_test.ok
new file mode 100644
index 0000000..4245eeb
--- /dev/null
+++ b/selftest/process_test.ok
@@ -0,0 +1,33 @@
+run foo: DBG: cd '[TMP]'; PATH=[$PATH] foo.py arg1 arg2 [foo↪foo]
+run foo: DBG: [TMP]/stdout [foo↪foo]
+run foo: DBG: [TMP]/stderr [foo↪foo]
+run foo(pid=[PID]): Launched [foo(pid=[PID])↪foo(pid=[PID])]
+stdout:
+(launched: [DATETIME])
+foo stdout
+[[$0], 'arg1', 'arg2']
+
+stderr:
+(launched: [DATETIME])
+foo stderr
+
+run foo(pid=[PID]): Terminating (SIGINT)
+run foo(pid=[PID]): DBG: Cleanup
+run foo(pid=[PID]): Terminated {rc=1}
+result: 1
+stdout:
+(launched: [DATETIME])
+foo stdout
+[[$0], 'arg1', 'arg2']
+Exiting (stdout)
+
+stderr:
+(launched: [DATETIME])
+foo stderr
+Traceback (most recent call last):
+ File [$0], line [LINE], in <module>
+ time.sleep(1)
+KeyboardInterrupt
+Exiting (stderr)
+
+done.
diff --git a/selftest/process_test.ok.ign b/selftest/process_test.ok.ign
new file mode 100644
index 0000000..0abd7d5
--- /dev/null
+++ b/selftest/process_test.ok.ign
@@ -0,0 +1,7 @@
+PATH='[^']*' PATH=[$PATH]
+/tmp/[^/ '"]* [TMP]
+pid=[0-9]* pid=[PID]
+....-..-.._..:..:.. [DATETIME]
+'[^']*/selftest/process_test/foo.py' [$0]
+"[^"]*/selftest/process_test/foo.py" [$0]
+, line [0-9]* , line [LINE]
diff --git a/selftest/process_test.py b/selftest/process_test.py
new file mode 100755
index 0000000..9ad082b
--- /dev/null
+++ b/selftest/process_test.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+
+import _prep
+import time
+import os
+
+from osmo_gsm_tester import process, util, log
+
+tmpdir = util.Dir(util.get_tempdir())
+
+dollar_path = '%s:%s' % (
+ os.path.join(os.getcwd(), 'process_test'),
+ os.getenv('PATH'))
+
+p = process.Process('foo', tmpdir, ('foo.py', 'arg1', 'arg2'),
+ env={'PATH': dollar_path})
+
+p.launch()
+time.sleep(.5)
+p.poll()
+print('stdout:')
+print(p.get_stdout())
+print('stderr:')
+print(p.get_stderr())
+
+assert not p.terminated()
+p.terminate()
+assert p.terminated()
+print('result: %r' % p.result)
+
+print('stdout:')
+print(p.get_stdout())
+print('stderr:')
+print(p.get_stderr())
+print('done.')
+
+test_ssh = True
+test_ssh = False
+if test_ssh:
+ # this part of the test requires ability to ssh to localhost
+ p = process.RemoteProcess('localhost', '/tmp', 'ssh-test', tmpdir,
+ ('ls', '-al'))
+ p.launch()
+ p.wait()
+ assert p.terminated()
+ print('stdout:')
+ print(p.get_stdout())
+ print('stderr:')
+ print(p.get_stderr())
+
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/selftest/process_test/foo.py b/selftest/process_test/foo.py
new file mode 100755
index 0000000..4abe887
--- /dev/null
+++ b/selftest/process_test/foo.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+
+import sys
+import atexit
+import time
+
+
+sys.stdout.write('foo stdout\n')
+sys.stderr.write('foo stderr\n')
+
+print(repr(sys.argv))
+sys.stdout.flush()
+sys.stderr.flush()
+
+def x():
+ sys.stdout.write('Exiting (stdout)\n')
+ sys.stdout.flush()
+ sys.stderr.write('Exiting (stderr)\n')
+ sys.stderr.flush()
+atexit.register(x)
+
+while True:
+ time.sleep(1)
+
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/selftest/py_import_test/invocation.py b/selftest/py_import_test/invocation.py
new file mode 100755
index 0000000..ad58b80
--- /dev/null
+++ b/selftest/py_import_test/invocation.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+
+import support
+import importlib.util
+
+if hasattr(importlib.util, 'module_from_spec'):
+ def run_test(path):
+ print('py 3.5+')
+ spec = importlib.util.spec_from_file_location("tests.script", path)
+ spec.loader.exec_module( importlib.util.module_from_spec(spec) )
+else:
+ def run_test(path):
+ print('py 3.4-')
+ from importlib.machinery import SourceFileLoader
+ SourceFileLoader("tests.script", path).load_module()
+
+path = './subdir/script.py'
+
+support.config = 'specifics'
+run_test(path)
+
+support.config = 'specifics2'
+run_test(path)
+
diff --git a/selftest/py_import_test/subdir/script.py b/selftest/py_import_test/subdir/script.py
new file mode 100644
index 0000000..1b57c20
--- /dev/null
+++ b/selftest/py_import_test/subdir/script.py
@@ -0,0 +1,9 @@
+from support import *
+
+print('hello')
+
+def run(what):
+ print(what)
+ print(what)
+
+run(config)
diff --git a/selftest/py_import_test/support.py b/selftest/py_import_test/support.py
new file mode 100644
index 0000000..aceedb8
--- /dev/null
+++ b/selftest/py_import_test/support.py
@@ -0,0 +1,2 @@
+
+config = None
diff --git a/selftest/real_suite/README.txt b/selftest/real_suite/README.txt
new file mode 100644
index 0000000..f18840a
--- /dev/null
+++ b/selftest/real_suite/README.txt
@@ -0,0 +1,18 @@
+This a real gsm test suite configured and ready to use.
+The only thing missing is a trial dir containing binaries.
+
+If you have your trial with binary tar archives in ~/my_trial
+you can run the suite for example like this:
+
+ . ./env # point your environment at all the right places
+ run_once.py ~/my_trial -s sms:trx
+
+This combines the suites/sms test suite with the scenarios/trx choice of
+osmo-bts-trx and runs all tests in the 'sms' suite.
+
+A ./state dir will be created to store the current osmo-gsm-tester state. If
+you prefer not to write to this dir, set up an own configuration pointing at a
+different path (see paths.conf: 'state_dir' and the env file). When there is
+no OSMO_GSM_TESTER_CONF set (from ./env), osmo-gsm-tester will instead look for
+conf files in several locations like ~/.config/osmo-gsm-tester,
+/usr/local/etc/osmo-gsm-tester, /etc/osmo-gsm-tester
diff --git a/selftest/real_suite/default.conf b/selftest/real_suite/default.conf
new file mode 100644
index 0000000..b247722
--- /dev/null
+++ b/selftest/real_suite/default.conf
@@ -0,0 +1,31 @@
+nitb:
+ net:
+ mcc: 1
+ mnc: 868
+ short_name: osmo-gsm-tester
+ long_name: osmo-gsm-tester
+ auth_policy: closed
+ encryption: a5 0
+
+nitb_bts:
+ location_area_code: 23
+ base_station_id_code: 63
+ stream_id: 255
+ trx_list:
+ - max_power_red: 22
+ arfcn: 868
+ timeslot_list:
+ - phys_chan_config: CCCH+SDCCH4
+ - phys_chan_config: SDCCH8
+ - phys_chan_config: TCH_F/TCH_H/PDCH
+ - phys_chan_config: TCH_F/TCH_H/PDCH
+ - phys_chan_config: TCH_F/TCH_H/PDCH
+ - phys_chan_config: TCH_F/TCH_H/PDCH
+ - phys_chan_config: TCH_F/TCH_H/PDCH
+ - phys_chan_config: TCH_F/TCH_H/PDCH
+
+osmo_bts_sysmo:
+ ipa_unit_id: 1123
+
+osmo_bts_trx:
+ ipa_unit_id: 1124
diff --git a/selftest/real_suite/env b/selftest/real_suite/env
new file mode 100644
index 0000000..1d9cc0a
--- /dev/null
+++ b/selftest/real_suite/env
@@ -0,0 +1,4 @@
+OSMO_GSM_TESTER_SRC="$(readlink -f ../../src)"
+export PYTHONPATH="$OSMO_GSM_TESTER_SRC"
+export PATH="$OSMO_GSM_TESTER_SRC:$PATH"
+export OSMO_GSM_TESTER_CONF="$PWD"
diff --git a/selftest/real_suite/paths.conf b/selftest/real_suite/paths.conf
new file mode 100644
index 0000000..bb7316c
--- /dev/null
+++ b/selftest/real_suite/paths.conf
@@ -0,0 +1,3 @@
+state_dir: './state'
+suites_dir: './suites'
+scenarios_dir: './scenarios'
diff --git a/selftest/real_suite/resources.conf b/selftest/real_suite/resources.conf
new file mode 100644
index 0000000..a6c396b
--- /dev/null
+++ b/selftest/real_suite/resources.conf
@@ -0,0 +1,139 @@
+# all hardware and interfaces available to this osmo-gsm-tester
+
+nitb_iface:
+- addr: 127.0.0.10
+- addr: 127.0.0.11
+- addr: 127.0.0.12
+
+bts:
+- label: sysmoBTS 1002
+ type: sysmo
+ unit_id: 1
+ addr: 10.42.42.114
+ band: GSM-1800
+
+- label: octBTS 3000
+ type: oct
+ unit_id: 5
+ addr: 10.42.42.115
+ band: GSM-1800
+ trx:
+ - hwaddr: 00:0c:90:32:b5:8a
+
+- label: Ettus B210
+ type: osmotrx
+ unit_id: 6
+ addr: 10.42.42.116
+ band: GSM-1800
+
+- label: nanoBTS 1900
+ type: nanobts
+ unit_id: 1902
+ addr: 10.42.42.190
+ band: GSM-1900
+ trx:
+ - hwaddr: 00:02:95:00:41:b3
+
+arfcn:
+ - arfcn: 512
+ band: GSM-1800
+ - arfcn: 514
+ band: GSM-1800
+ - arfcn: 516
+ band: GSM-1800
+ - arfcn: 518
+ band: GSM-1800
+ - arfcn: 520
+ band: GSM-1800
+
+ - arfcn: 540
+ band: GSM-1900
+ - arfcn: 542
+ band: GSM-1900
+ - arfcn: 544
+ band: GSM-1900
+ - arfcn: 546
+ band: GSM-1900
+ - arfcn: 548
+ band: GSM-1900
+
+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/selftest/real_suite/scenarios/trx.conf b/selftest/real_suite/scenarios/trx.conf
new file mode 100644
index 0000000..98065aa
--- /dev/null
+++ b/selftest/real_suite/scenarios/trx.conf
@@ -0,0 +1,3 @@
+resources:
+ bts:
+ - type: osmotrx
diff --git a/selftest/real_suite/suites/sms/mo_mt_sms.py b/selftest/real_suite/suites/sms/mo_mt_sms.py
new file mode 100755
index 0000000..05be48c
--- /dev/null
+++ b/selftest/real_suite/suites/sms/mo_mt_sms.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+from osmo_gsm_tester.test import *
+
+print('use resources...')
+nitb = suite.nitb()
+bts = suite.bts()
+ms_mo = suite.modem()
+ms_mt = suite.modem()
+
+print('start nitb and bts...')
+nitb.add_bts(bts)
+nitb.start()
+sleep(.1)
+assert nitb.running()
+bts.start()
+
+nitb.add_subscriber(ms_mo)
+nitb.add_subscriber(ms_mt)
+
+ms_mo.connect(nitb)
+ms_mt.connect(nitb)
+wait(nitb.subscriber_attached, ms_mo, ms_mt)
+
+sms = ms_mo.sms_send(ms_mt.msisdn)
+sleep(3)
+wait(nitb.sms_received, sms)
diff --git a/selftest/real_suite/suites/sms/suite.conf b/selftest/real_suite/suites/sms/suite.conf
new file mode 100644
index 0000000..4a03379
--- /dev/null
+++ b/selftest/real_suite/suites/sms/suite.conf
@@ -0,0 +1,10 @@
+resources:
+ nitb_iface:
+ - times: 1
+ bts:
+ - times: 1
+ modem:
+ - times: 2
+
+defaults:
+ timeout: 60s
diff --git a/selftest/resource_test.err b/selftest/resource_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/selftest/resource_test.err
diff --git a/selftest/resource_test.ok b/selftest/resource_test.ok
new file mode 100644
index 0000000..008c447
--- /dev/null
+++ b/selftest/resource_test.ok
@@ -0,0 +1,207 @@
+- expect solutions:
+[0, 1, 2]
+[0, 1, 2]
+[1, 0, 2]
+[1, 2, 0]
+- expect failure to solve:
+The requested resource requirements are not solvable [[0, 2], [2], [0, 2]]
+- test removing a Resources list from itself
+ok, caused exception: RuntimeError('Refusing to drop a list of resources from itself. This is probably a bug where a list of Resources() should have been copied but is passed as-is. use Resources.clear() instead.',)
+- test removing a Resources list from one with the same list in it
+- test resources config and state dir:
+*** all resources:
+{'arfcn': [{'_hash': 'e620569450f8259b3f0212ec19c285dd07df063c',
+ 'arfcn': '512',
+ 'band': 'GSM-1800'},
+ {'_hash': '022621e513c5a5bf33b77430a1e9c886be676fa1',
+ 'arfcn': '514',
+ 'band': 'GSM-1800'},
+ {'_hash': '3199abf375a1dd899e554e9d63a552e06d7f38bf',
+ 'arfcn': '516',
+ 'band': 'GSM-1800'},
+ {'_hash': '57aa7bd1da62495f2857ae6b859193dd592a0a02',
+ 'arfcn': '518',
+ 'band': 'GSM-1800'},
+ {'_hash': '53dd2e2682b736f427abd2ce59a9a50ca8130678',
+ 'arfcn': '520',
+ 'band': 'GSM-1800'},
+ {'_hash': '31687a5e6d5140a4b3877606ca5f18244f11d706',
+ 'arfcn': '540',
+ 'band': 'GSM-1900'},
+ {'_hash': '1def43a5c88a83cdb21279eacab0679ea08ffaf3',
+ 'arfcn': '542',
+ 'band': 'GSM-1900'},
+ {'_hash': '1d6e3b08a3861fd4d748f111295ec5a93ecd3d23',
+ 'arfcn': '544',
+ 'band': 'GSM-1900'},
+ {'_hash': '8fb36927de15466fcdbee01f7f65704c312cb36c',
+ 'arfcn': '546',
+ 'band': 'GSM-1900'},
+ {'_hash': 'dc9ce027a257da087f31a5bc1ee6b4abd2637369',
+ 'arfcn': '548',
+ 'band': 'GSM-1900'}],
+ 'bts': [{'_hash': 'a7c6d2ebaeb139e8c2e7d45c3495d046d7439007',
+ 'addr': '10.42.42.114',
+ 'band': 'GSM-1800',
+ 'label': 'sysmoBTS 1002',
+ 'type': 'sysmo',
+ 'unit_id': '1'},
+ {'_hash': '02540ab9eb556056a0b4d28443bc9f4793f6d549',
+ 'addr': '10.42.42.115',
+ 'band': 'GSM-1800',
+ 'label': 'octBTS 3000',
+ 'trx': [{'hwaddr': '00:0c:90:32:b5:8a'}],
+ 'type': 'oct',
+ 'unit_id': '5'},
+ {'_hash': '556c954d475d12cf0dc622c0df5743cac5543fa0',
+ 'addr': '10.42.42.190',
+ 'band': 'GSM-1900',
+ 'label': 'nanoBTS 1900',
+ 'trx': [{'hwaddr': '00:02:95:00:41:b3'}],
+ 'type': 'nanobts',
+ 'unit_id': '1902'}],
+ 'modem': [{'_hash': '19c69e45aa090fb511446bd00797690aa82ff52f',
+ 'imsi': '901700000007801',
+ 'ki': 'D620F48487B1B782DA55DF6717F08FF9',
+ 'label': 'm7801',
+ 'path': '/wavecom_0'},
+ {'_hash': 'e1a46516a1fb493b2617ab14fc1693a9a45ec254',
+ 'imsi': '901700000007802',
+ 'ki': '47FDB2D55CE6A10A85ABDAD034A5B7B3',
+ 'label': 'm7802',
+ 'path': '/wavecom_1'},
+ {'_hash': '4fe91500a309782bb0fd8ac6fc827834089f8b00',
+ 'imsi': '901700000007803',
+ 'ki': 'ABBED4C91417DF710F60675B6EE2C8D2',
+ 'label': 'm7803',
+ 'path': '/wavecom_2'},
+ {'_hash': 'c895badf0c2faaa4a997cd9f2313b5ebda7486e4',
+ 'imsi': '901700000007804',
+ 'ki': '8BA541179156F2BF0918CA3CFF9351B0',
+ 'label': 'm7804',
+ 'path': '/wavecom_3'},
+ {'_hash': '60f182abed05adb530e3d06d88cc47703b65d7d8',
+ 'imsi': '901700000007805',
+ 'ki': '82BEC24B5B50C9FAA69D17DEC0883A23',
+ 'label': 'm7805',
+ 'path': '/wavecom_4'},
+ {'_hash': 'd1f0fbf089a4bf32dd566af956d23b89e3d60821',
+ 'imsi': '901700000007806',
+ 'ki': 'DAF6BD6A188F7A4F09866030BF0F723D',
+ 'label': 'm7806',
+ 'path': '/wavecom_5'},
+ {'_hash': '2445e3b5949d15f4351c0db1d3f3f593f9d73aa5',
+ 'imsi': '901700000007807',
+ 'ki': 'AEB411CFE39681A6352A1EAE4DDC9DBA',
+ 'label': 'm7807',
+ 'path': '/wavecom_6'},
+ {'_hash': '80247388b2ca382382c4aec678102355b7922965',
+ 'imsi': '901700000007808',
+ 'ki': 'F5DEF8692B305D7A65C677CA9EEE09C4',
+ 'label': 'm7808',
+ 'path': '/wavecom_7'},
+ {'_hash': '5b9e4e117a8889430542d22a9693e7b999362856',
+ 'imsi': '901700000007809',
+ 'ki': 'A644F4503E812FD75329B1C8D625DA44',
+ 'label': 'm7809',
+ 'path': '/wavecom_8'},
+ {'_hash': '219a7abb057050eef3ce4b99c487f32bbaae9a41',
+ 'imsi': '901700000007810',
+ 'ki': 'EF663BDF3477DCD18D3D2293A2BAED67',
+ 'label': 'm7810',
+ 'path': '/wavecom_9'},
+ {'_hash': '75d45c2d975b893da34c7cae827c25a2039cecd2',
+ 'imsi': '901700000007811',
+ 'ki': 'E88F37F048A86A9BC4D652539228C039',
+ 'label': 'm7811',
+ 'path': '/wavecom_10'},
+ {'_hash': '1777362f556b249a5c1d6a83110704dbd037bc20',
+ 'imsi': '901700000007812',
+ 'ki': 'E8D940DD66FCF6F1CD2C0F8F8C45633D',
+ 'label': 'm7812',
+ 'path': '/wavecom_11'},
+ {'_hash': '21d7eb4b0c782e004821a9f7f778891c93956924',
+ 'imsi': '901700000007813',
+ 'ki': 'DBF534700C10141C49F699B0419107E3',
+ 'label': 'm7813',
+ 'path': '/wavecom_12'},
+ {'_hash': 'f53e4e79bdbc63eb2845de671007d4f733f28409',
+ 'imsi': '901700000007814',
+ 'ki': 'B36021DEB90C4EA607E408A92F3B024D',
+ 'label': 'm7814',
+ 'path': '/wavecom_13'},
+ {'_hash': 'df1abec7704ebc89b2c062a69bd299cf3663ed9e',
+ 'imsi': '901700000007815',
+ 'ki': '1E209F6F839F9195778C4F96BE281A24',
+ 'label': 'm7815',
+ 'path': '/wavecom_14'},
+ {'_hash': '11df1e4c7708157e5b89020c757763f58d6e610b',
+ 'imsi': '901700000007816',
+ 'ki': 'BF827D219E739DD189F6F59E60D6455C',
+ 'label': 'm7816',
+ 'path': '/wavecom_15'}],
+ 'nitb_iface': [{'_hash': 'cde1debf28f07f94f92c761b4b7c6bf35785ced4',
+ 'addr': '10.42.42.1'},
+ {'_hash': 'fd103b22c7cf2480d609150e06f4bbd92ac78d8c',
+ 'addr': '10.42.42.2'},
+ {'_hash': '1c614d6210c551d142aadca8f25e1534ebb2a70f',
+ 'addr': '10.42.42.3'}]}
+*** end: all resources
+
+- request some resources
+--- (want='nitb_iface'): DBG: Looking for 1 x nitb_iface , candidates: 3
+--- (want='arfcn'): DBG: Looking for 2 x arfcn , candidates: 10
+--- (want='bts'): DBG: Looking for 2 x bts , candidates: 3
+--- (want='modem'): DBG: Looking for 2 x modem , candidates: 16
+~~~ currently reserved:
+arfcn:
+- _hash: e620569450f8259b3f0212ec19c285dd07df063c
+ _reserved_by: testowner-123-1490837279
+ arfcn: '512'
+ band: GSM-1800
+- _hash: 022621e513c5a5bf33b77430a1e9c886be676fa1
+ _reserved_by: testowner-123-1490837279
+ arfcn: '514'
+ band: GSM-1800
+bts:
+- _hash: a7c6d2ebaeb139e8c2e7d45c3495d046d7439007
+ _reserved_by: testowner-123-1490837279
+ addr: 10.42.42.114
+ band: GSM-1800
+ label: sysmoBTS 1002
+ type: sysmo
+ unit_id: '1'
+- _hash: 02540ab9eb556056a0b4d28443bc9f4793f6d549
+ _reserved_by: testowner-123-1490837279
+ addr: 10.42.42.115
+ band: GSM-1800
+ label: octBTS 3000
+ trx:
+ - hwaddr: 00:0c:90:32:b5:8a
+ type: oct
+ unit_id: '5'
+modem:
+- _hash: 19c69e45aa090fb511446bd00797690aa82ff52f
+ _reserved_by: testowner-123-1490837279
+ imsi: '901700000007801'
+ ki: D620F48487B1B782DA55DF6717F08FF9
+ label: m7801
+ path: /wavecom_0
+- _hash: e1a46516a1fb493b2617ab14fc1693a9a45ec254
+ _reserved_by: testowner-123-1490837279
+ imsi: '901700000007802'
+ ki: 47FDB2D55CE6A10A85ABDAD034A5B7B3
+ label: m7802
+ path: /wavecom_1
+nitb_iface:
+- _hash: cde1debf28f07f94f92c761b4b7c6bf35785ced4
+ _reserved_by: testowner-123-1490837279
+ addr: 10.42.42.1
+
+~~~ end: currently reserved
+
+~~~ currently reserved:
+{}
+
+~~~ end: currently reserved
+
diff --git a/selftest/resource_test.py b/selftest/resource_test.py
new file mode 100755
index 0000000..2d0f880
--- /dev/null
+++ b/selftest/resource_test.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+
+import tempfile
+import os
+import pprint
+import shutil
+import atexit
+import _prep
+from osmo_gsm_tester import config, log, resource, util
+
+workdir = util.get_tempdir()
+
+# override config locations to make sure we use only the test conf
+config.ENV_CONF = './conf'
+
+log.get_process_id = lambda: '123-1490837279'
+
+print('- expect solutions:')
+pprint.pprint(
+ resource.solve([ [0, 1, 2],
+ [0, 1, 2],
+ [0, 1, 2] ]) )
+pprint.pprint(
+ resource.solve([ [0, 1, 2],
+ [0, 1],
+ [0, 2] ]) ) # == [0, 1, 2]
+pprint.pprint(
+ resource.solve([ [0, 1, 2],
+ [0],
+ [0, 2] ]) ) # == [1, 0, 2]
+pprint.pprint(
+ resource.solve([ [0, 1, 2],
+ [2],
+ [0, 2] ]) ) # == [1, 2, 0]
+
+print('- expect failure to solve:')
+try:
+ resource.solve([ [0, 2],
+ [2],
+ [0, 2] ])
+ assert False
+except resource.NoResourceExn as e:
+ print(e)
+
+print('- test removing a Resources list from itself')
+try:
+ r = resource.Resources({ 'k': [ {'a': 1, 'b': 2}, {'a': 3, 'b': 4}, ],
+ 'i': [ {'c': 1, 'd': 2}, {'c': 3, 'd': 4}, ] })
+ r.drop(r)
+ assert False
+except RuntimeError as e:
+ print('ok, caused exception: %r' % e)
+
+print('- test removing a Resources list from one with the same list in it')
+r = resource.Resources({ 'k': [ {'a': 1, 'b': 2}, {'a': 3, 'b': 4}, ],
+ 'i': [ {'c': 1, 'd': 2}, {'c': 3, 'd': 4}, ] })
+r.drop({ 'k': r.get('k'), 'i': r.get('i') })
+assert not r
+
+print('- test resources config and state dir:')
+resources_conf = os.path.join(_prep.script_dir, 'resource_test', 'etc',
+ 'resources.conf')
+
+state_dir = config.get_state_dir()
+rrfile = state_dir.child(resource.RESERVED_RESOURCES_FILE)
+
+pool = resource.ResourcesPool()
+
+print('*** all resources:')
+pprint.pprint(pool.all_resources)
+print('*** end: all resources\n')
+
+print('- request some resources')
+want = {
+ 'nitb_iface': [ { 'times': 1 } ],
+ 'bts': [ { 'type': 'sysmo', 'times': 1 }, { 'type': 'oct', 'times': 1 } ],
+ 'arfcn': [ { 'band': 'GSM-1800', 'times': 2 } ],
+ 'modem': [ { 'times': 2 } ],
+ }
+
+origin = log.Origin('testowner')
+
+resources = pool.reserve(origin, want)
+
+print('~~~ currently reserved:')
+with open(rrfile, 'r') as f:
+ print(f.read())
+print('~~~ end: currently reserved\n')
+
+resources.free()
+
+print('~~~ currently reserved:')
+with open(rrfile, 'r') as f:
+ print(f.read())
+print('~~~ end: currently reserved\n')
+
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/selftest/suite_test.err b/selftest/suite_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/selftest/suite_test.err
diff --git a/selftest/suite_test.ok b/selftest/suite_test.ok
new file mode 100644
index 0000000..c0232dd
--- /dev/null
+++ b/selftest/suite_test.ok
@@ -0,0 +1,40 @@
+- non-existing suite dir
+--- -: ERR: RuntimeError: Suite not found: 'does_not_exist' in ./suite_test
+- no suite.conf
+cnf empty_dir: DBG: reading suite.conf [empty_dir↪empty_dir]
+--- ./suite_test/empty_dir/suite.conf: ERR: FileNotFoundError: [Errno 2] No such file or directory: './suite_test/empty_dir/suite.conf' [empty_dir↪./suite_test/empty_dir/suite.conf]
+- valid suite dir
+cnf test_suite: DBG: reading suite.conf [test_suite↪test_suite]
+defaults:
+ timeout: 60s
+resources:
+ bts:
+ - times: '1'
+ modem:
+ - times: '2'
+ nitb_iface:
+ - times: '1'
+
+- run hello world test
+tst test_suite: reserving resources...
+--- (want='nitb_iface'): DBG: Looking for 1 x nitb_iface , candidates: 3
+--- (want='modem'): DBG: Looking for 2 x modem , candidates: 16
+--- (want='bts'): DBG: Looking for 1 x bts , candidates: 3
+tst hello_world.py: START [test_suite↪hello_world.py]
+tst hello_world.py:3: hello world [test_suite↪hello_world.py:3]
+tst hello_world.py:4: I am 'test_suite' / 'hello_world.py:4' [test_suite↪hello_world.py:4]
+tst hello_world.py:5: one [test_suite↪hello_world.py:5]
+tst hello_world.py:5: two [test_suite↪hello_world.py:5]
+tst hello_world.py:5: three [test_suite↪hello_world.py:5]
+tst hello_world.py: PASS [test_suite↪hello_world.py]
+pass: all 1 tests passed.
+
+- a test with an error
+tst test_error.py: START [test_suite↪test_error.py] [suite.py:96]
+tst test_error.py:3: I am 'test_suite' / 'test_error.py:3' [test_suite↪test_error.py:3] [test_error.py:3]
+tst test_error.py:5: FAIL [test_suite↪test_error.py:5] [suite.py:108]
+tst test_error.py:5: ERR: AssertionError: [test_suite↪test_error.py:5] [test_error.py:5: assert False]
+FAIL: 1 of 1 tests failed:
+ test_error.py
+
+- graceful exit.
diff --git a/selftest/suite_test.py b/selftest/suite_test.py
new file mode 100755
index 0000000..8c0e6e8
--- /dev/null
+++ b/selftest/suite_test.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+import os
+import _prep
+from osmo_gsm_tester import log, suite, config
+
+config.ENV_CONF = os.path.join(os.getcwd(), 'conf')
+
+#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, 'empty_dir') == None)
+
+print('- valid suite dir')
+example_suite_dir = os.path.join('test_suite')
+s_def = suite.load(example_suite_dir)
+assert(isinstance(s_def, suite.SuiteDefinition))
+print(config.tostr(s_def.conf))
+
+print('- run hello world test')
+s = suite.SuiteRun(None, s_def)
+results = s.run_tests('hello_world.py')
+print(str(results))
+
+log.style_change(src=True)
+#log.style_change(trace=True)
+print('\n- a test with an error')
+results = s.run_tests('test_error.py')
+print(str(results))
+
+print('\n- graceful exit.')
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/selftest/suite_test/empty_dir/.unrelated_file b/selftest/suite_test/empty_dir/.unrelated_file
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/selftest/suite_test/empty_dir/.unrelated_file
diff --git a/selftest/suite_test/test_suite/hello_world.py b/selftest/suite_test/test_suite/hello_world.py
new file mode 100644
index 0000000..9f3bf4a
--- /dev/null
+++ b/selftest/suite_test/test_suite/hello_world.py
@@ -0,0 +1,5 @@
+from osmo_gsm_tester.test import *
+
+print('hello world')
+print('I am %r / %r' % (suite.name(), test.name()))
+print('one\ntwo\nthree')
diff --git a/selftest/suite_test/test_suite/mo_mt_sms.py b/selftest/suite_test/test_suite/mo_mt_sms.py
new file mode 100644
index 0000000..cf44357
--- /dev/null
+++ b/selftest/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/selftest/suite_test/test_suite/mo_sms.py b/selftest/suite_test/test_suite/mo_sms.py
new file mode 100644
index 0000000..d9517dd
--- /dev/null
+++ b/selftest/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/selftest/suite_test/test_suite/suite.conf b/selftest/suite_test/test_suite/suite.conf
new file mode 100644
index 0000000..4a03379
--- /dev/null
+++ b/selftest/suite_test/test_suite/suite.conf
@@ -0,0 +1,10 @@
+resources:
+ nitb_iface:
+ - times: 1
+ bts:
+ - times: 1
+ modem:
+ - times: 2
+
+defaults:
+ timeout: 60s
diff --git a/selftest/suite_test/test_suite/test_error.py b/selftest/suite_test/test_suite/test_error.py
new file mode 100755
index 0000000..17b05f4
--- /dev/null
+++ b/selftest/suite_test/test_suite/test_error.py
@@ -0,0 +1,5 @@
+from osmo_gsm_tester.test import *
+
+print('I am %r / %r' % (suite.name(), test.name()))
+
+assert False
diff --git a/selftest/template_test.err b/selftest/template_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/selftest/template_test.err
diff --git a/selftest/template_test.ok b/selftest/template_test.ok
new file mode 100644
index 0000000..879907d
--- /dev/null
+++ b/selftest/template_test.ok
@@ -0,0 +1,149 @@
+- Testing: fill a config file with values
+cnf Templates: DBG: rendering osmo-nitb.cfg.tmpl [osmo-nitb.cfg.tmpl↪Templates]
+! Configuration rendered by osmo-gsm-tester
+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_nitb_iface_addr
+!
+e1_input
+ e1_line 0 driver ipa
+ ipa bind val_nitb_iface_addr
+network
+ network country code val_mcc
+ mobile network code val_mnc
+ short name val_short_name
+ long name val_long_name
+ auth policy val_auth_policy
+ location updating reject cause 13
+ encryption 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
+ 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
+smpp
+ local-tcp-ip val_nitb_iface_addr 2775
+ system-id test
+ policy closed
+ esme test
+ password test
+ default-route
+ctrl
+ bind val_nitb_iface_addr
+
+- Testing: expect to fail on invalid templates dir
+sucess: setting non-existing templates dir raised RuntimeError
+
diff --git a/selftest/template_test.py b/selftest/template_test.py
new file mode 100755
index 0000000..2b44ae5
--- /dev/null
+++ b/selftest/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',
+ 'ipa_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(nitb=dict(
+ net=dict(
+ mcc='val_mcc',
+ mnc='val_mnc',
+ short_name='val_short_name',
+ long_name='val_long_name',
+ auth_policy='val_auth_policy',
+ encryption='val_encryption',
+ bts_list=(mock_bts0, mock_bts1)
+ ),
+ ),
+ nitb_iface=dict(addr='val_nitb_iface_addr'),
+ )
+
+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/selftest/template_test/osmo-nitb.cfg.tmpl b/selftest/template_test/osmo-nitb.cfg.tmpl
new file mode 100644
index 0000000..3404b7f
--- /dev/null
+++ b/selftest/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/selftest/trial_test.err b/selftest/trial_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/selftest/trial_test.err
diff --git a/selftest/trial_test.ok b/selftest/trial_test.ok
new file mode 100644
index 0000000..0b3e31a
--- /dev/null
+++ b/selftest/trial_test.ok
@@ -0,0 +1,16 @@
+- make a few trials dirs
+[TMP]/first
+[TMP]/second
+[TMP]/third
+- fetch trial dirs in order
+[TMP]/first
+['taken']
+[TMP]/second
+[TMP]/third
+- no more trial dirs left
+None
+- test checksum verification
+- detect wrong checksum
+ok, got RuntimeError("Checksum mismatch for 'trial_test/invalid_checksum/file2' vs. 'trial_test/invalid_checksum/checksums.md5' line 2",)
+- detect missing file
+ok, got RuntimeError("File listed in checksums file but missing in trials dir: 'trial_test/missing_file/file2' vs. 'trial_test/missing_file/checksums.md5' line 2",)
diff --git a/selftest/trial_test.ok.ign b/selftest/trial_test.ok.ign
new file mode 100644
index 0000000..38a82ce
--- /dev/null
+++ b/selftest/trial_test.ok.ign
@@ -0,0 +1,2 @@
+/tmp/[^/]* [TMP]
+....-..-.._..-..-.. [TIMESTAMP]
diff --git a/selftest/trial_test.py b/selftest/trial_test.py
new file mode 100755
index 0000000..ba3f01b
--- /dev/null
+++ b/selftest/trial_test.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+
+import time
+import _prep
+import os
+from osmo_gsm_tester import util
+from osmo_gsm_tester.trial import Trial
+
+workdir = util.get_tempdir()
+
+trials_dir = util.Dir(workdir)
+
+print('- make a few trials dirs')
+print(trials_dir.mkdir('first'))
+time.sleep(1)
+print(trials_dir.mkdir('second'))
+time.sleep(1)
+print(trials_dir.mkdir('third'))
+
+print('- fetch trial dirs in order')
+t = Trial.next(trials_dir)
+print(t)
+print(repr(sorted(t.dir.children())))
+print(Trial.next(trials_dir))
+print(Trial.next(trials_dir))
+
+print('- no more trial dirs left')
+print(repr(Trial.next(trials_dir)))
+
+print('- test checksum verification')
+d = util.Dir('trial_test')
+t = Trial(d.child('valid_checksums'))
+t.verify()
+
+print('- detect wrong checksum')
+t = Trial(d.child('invalid_checksum'))
+try:
+ t.verify()
+except RuntimeError as e:
+ print('ok, got %r' % e)
+
+print('- detect missing file')
+t = Trial(d.child('missing_file'))
+try:
+ t.verify()
+except RuntimeError as e:
+ print('ok, got %r' % e)
+
+# vim: expandtab tabstop=4 shiftwidth=4
diff --git a/selftest/trial_test/invalid_checksum/checksums.md5 b/selftest/trial_test/invalid_checksum/checksums.md5
new file mode 100644
index 0000000..90d3547
--- /dev/null
+++ b/selftest/trial_test/invalid_checksum/checksums.md5
@@ -0,0 +1,3 @@
+5149d403009a139c7e085405ef762e1a file1
+3d709e89c8ce201e3c928eb917989aef file2
+60b91f1875424d3b4322b0fdd0529d5d file3
diff --git a/selftest/trial_test/invalid_checksum/file1 b/selftest/trial_test/invalid_checksum/file1
new file mode 100644
index 0000000..e212970
--- /dev/null
+++ b/selftest/trial_test/invalid_checksum/file1
@@ -0,0 +1 @@
+file1
diff --git a/selftest/trial_test/invalid_checksum/file2 b/selftest/trial_test/invalid_checksum/file2
new file mode 100644
index 0000000..34ccdac
--- /dev/null
+++ b/selftest/trial_test/invalid_checksum/file2
@@ -0,0 +1 @@
+no no no
diff --git a/selftest/trial_test/invalid_checksum/file3 b/selftest/trial_test/invalid_checksum/file3
new file mode 100644
index 0000000..7c8ac2f
--- /dev/null
+++ b/selftest/trial_test/invalid_checksum/file3
@@ -0,0 +1 @@
+file3
diff --git a/selftest/trial_test/missing_file/checksums.md5 b/selftest/trial_test/missing_file/checksums.md5
new file mode 100644
index 0000000..90d3547
--- /dev/null
+++ b/selftest/trial_test/missing_file/checksums.md5
@@ -0,0 +1,3 @@
+5149d403009a139c7e085405ef762e1a file1
+3d709e89c8ce201e3c928eb917989aef file2
+60b91f1875424d3b4322b0fdd0529d5d file3
diff --git a/selftest/trial_test/missing_file/file1 b/selftest/trial_test/missing_file/file1
new file mode 100644
index 0000000..e212970
--- /dev/null
+++ b/selftest/trial_test/missing_file/file1
@@ -0,0 +1 @@
+file1
diff --git a/selftest/trial_test/missing_file/file3 b/selftest/trial_test/missing_file/file3
new file mode 100644
index 0000000..7c8ac2f
--- /dev/null
+++ b/selftest/trial_test/missing_file/file3
@@ -0,0 +1 @@
+file3
diff --git a/selftest/trial_test/valid_checksums/checksums.md5 b/selftest/trial_test/valid_checksums/checksums.md5
new file mode 100644
index 0000000..90d3547
--- /dev/null
+++ b/selftest/trial_test/valid_checksums/checksums.md5
@@ -0,0 +1,3 @@
+5149d403009a139c7e085405ef762e1a file1
+3d709e89c8ce201e3c928eb917989aef file2
+60b91f1875424d3b4322b0fdd0529d5d file3
diff --git a/selftest/trial_test/valid_checksums/file1 b/selftest/trial_test/valid_checksums/file1
new file mode 100644
index 0000000..e212970
--- /dev/null
+++ b/selftest/trial_test/valid_checksums/file1
@@ -0,0 +1 @@
+file1
diff --git a/selftest/trial_test/valid_checksums/file2 b/selftest/trial_test/valid_checksums/file2
new file mode 100644
index 0000000..6c493ff
--- /dev/null
+++ b/selftest/trial_test/valid_checksums/file2
@@ -0,0 +1 @@
+file2
diff --git a/selftest/trial_test/valid_checksums/file3 b/selftest/trial_test/valid_checksums/file3
new file mode 100644
index 0000000..7c8ac2f
--- /dev/null
+++ b/selftest/trial_test/valid_checksums/file3
@@ -0,0 +1 @@
+file3
diff --git a/selftest/util_test.err b/selftest/util_test.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/selftest/util_test.err
diff --git a/selftest/util_test.ok b/selftest/util_test.ok
new file mode 100644
index 0000000..c2c5f87
--- /dev/null
+++ b/selftest/util_test.ok
@@ -0,0 +1,5 @@
+- expect the same hashes on every test run
+a9993e364706816aba3e25717850c26c9cd0d89d
+356a192b7913b04c54574d18c28d46e6395428ab
+40bd001563085fc35165329ea1ff5c5ecbdbbeef
+c129b324aee662b04eccf68babba85851346dff9
diff --git a/selftest/util_test.py b/selftest/util_test.py
new file mode 100755
index 0000000..c517655
--- /dev/null
+++ b/selftest/util_test.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python3
+import _prep
+
+from osmo_gsm_tester.util import hash_obj
+
+print('- expect the same hashes on every test run')
+print(hash_obj('abc'))
+print(hash_obj(1))
+print(hash_obj([1, 2, 3]))
+print(hash_obj({ 'k': [ {'a': 1, 'b': 2}, {'a': 3, 'b': 4}, ],
+ 'i': [ {'c': 1, 'd': 2}, {'c': 3, 'd': 4}, ] }))
+