fix octphy, fix conf, improve logging

Clearly separate the kinds of BTS hardware the GSM tester knows ('type') from
the NITB's bts/type config item ('osmobsc_bts_type' -- not 'osmonitb_...' to
stay in tune with future developments: it is the libbsc that needs this).

For BTS hardware kinds, use the full name of the binary for osmo driven models:
osmo-bts-sysmo, osmo-bts-trx, osmo-bts-octphy.

Change-Id: I1aa9b48e74013a93f9db1a34730f17717fb3b36c
diff --git a/selftest/real_suite/default.conf b/selftest/real_suite/default.conf
index 8c7d1e5..626da4b 100644
--- a/selftest/real_suite/default.conf
+++ b/selftest/real_suite/default.conf
@@ -11,6 +11,7 @@
   location_area_code: 23
   base_station_id_code: 63
   stream_id: 255
+  osmobsc_bts_type: sysmobts
   trx_list:
   - max_power_red: 22
     arfcn: 868
diff --git a/selftest/real_suite/resources.conf b/selftest/real_suite/resources.conf
index 3a5b129..c6573ee 100644
--- a/selftest/real_suite/resources.conf
+++ b/selftest/real_suite/resources.conf
@@ -7,22 +7,22 @@
 
 bts:
 - label: sysmoBTS 1002
-  type: sysmo
+  type: osmo-bts-sysmo
   ipa_unit_id: 1
   addr: 10.42.42.114
   band: GSM-1800
 
 - label: octBTS 3000
-  type: oct
+  type: osmo-bts-octphy
   ipa_unit_id: 5
   addr: 10.42.42.115
   band: GSM-1800
-  trx:
+  trx_list:
   - hw_addr: 00:0c:90:32:b5:8a
     net_device: eth0.2342
 
 - label: Ettus B210
-  type: osmotrx
+  type: osmo-bts-trx
   ipa_unit_id: 6
   addr: 10.42.42.116
   band: GSM-1800
@@ -32,7 +32,7 @@
   ipa_unit_id: 1902
   addr: 10.42.42.190
   band: GSM-1900
-  trx:
+  trx_list:
   - hw_addr: 00:02:95:00:41:b3
 
 arfcn:
diff --git a/selftest/real_suite/scenarios/octphy.conf b/selftest/real_suite/scenarios/octphy.conf
index 02eb48b..3a419e8 100644
--- a/selftest/real_suite/scenarios/octphy.conf
+++ b/selftest/real_suite/scenarios/octphy.conf
@@ -1,3 +1,3 @@
 resources:
   bts:
-  - type: octphy
+  - type: osmo-bts-octphy
diff --git a/selftest/real_suite/scenarios/sysmo.conf b/selftest/real_suite/scenarios/sysmo.conf
index 5980c1a..624758b 100644
--- a/selftest/real_suite/scenarios/sysmo.conf
+++ b/selftest/real_suite/scenarios/sysmo.conf
@@ -1,3 +1,3 @@
 resources:
   bts:
-  - type: sysmo
+  - type: osmo-bts-sysmo
diff --git a/selftest/real_suite/scenarios/trx.conf b/selftest/real_suite/scenarios/trx.conf
index 98065aa..f1d6d13 100644
--- a/selftest/real_suite/scenarios/trx.conf
+++ b/selftest/real_suite/scenarios/trx.conf
@@ -1,3 +1,3 @@
 resources:
   bts:
-  - type: osmotrx
+  - type: osmo-bts-trx
diff --git a/src/osmo-gsm-tester.py b/src/osmo-gsm-tester.py
index 0137f25..17b16b7 100755
--- a/src/osmo-gsm-tester.py
+++ b/src/osmo-gsm-tester.py
@@ -143,38 +143,50 @@
     trials = []
     for trial_package in args.trial_package:
         t = trial.Trial(trial_package)
-        t.verify()
-        trials.append(t)
+        try:
+            t.verify()
+            trials.append(t)
+        except:
+            t.log_exn()
 
     trials_passed = []
     trials_failed = []
 
     for current_trial in trials:
-        with current_trial:
-            suites_passed = 0
-            suites_failed = 0
-            for suite_def, scenarios in suite_scenarios:
-                suite_run = suite.SuiteRun(current_trial, suite_def, scenarios)
-                result = suite_run.run_tests(test_names)
-                if result.all_passed:
-                    suites_passed += 1
-                    suite_run.log('PASS')
+        try:
+            with current_trial:
+                suites_passed = []
+                suites_failed = []
+                for suite_scenario_str, suite_def, scenarios in suite_scenarios:
+                    log.large_separator(current_trial.name(), suite_scenario_str)
+                    suite_run = suite.SuiteRun(current_trial, suite_scenario_str, suite_def, scenarios)
+                    result = suite_run.run_tests(test_names)
+                    if result.all_passed:
+                        suites_passed.append(suite_scenario_str)
+                        suite_run.log('PASS')
+                    else:
+                        suites_failed.append(suite_scenario_str)
+                        suite_run.err('FAIL')
+                if not suites_failed:
+                    current_trial.log('PASS')
+                    trials_passed.append(current_trial.name())
                 else:
-                    suites_failed += 1
-                    suite_run.err('FAIL')
-            if not suites_failed:
-                current_trial.log('PASS')
-                trials_passed.append(current_trial.name())
-            else:
-                current_trial.err('FAIL')
-                trials_failed.append(current_trial.name())
+                    current_trial.err('FAIL')
+                    trials_failed.append((current_trial.name(), suites_passed, suites_failed))
+        except:
+            current_trial.log_exn()
 
     sys.stderr.flush()
     sys.stdout.flush()
+    log.large_separator()
     if trials_passed:
         print('Trials passed:\n  ' + ('\n  '.join(trials_passed)))
     if trials_failed:
-        print('Trials failed:\n  ' + ('\n  '.join(trials_failed)))
+        print('Trials failed:')
+        for trial_name, suites_passed, suites_failed in trials_failed:
+            print('  %s (%d of %d suite runs failed)' % (trial_name, len(suites_failed), len(suites_failed) + len(suites_passed)))
+            for suite in suites_failed:
+                print('    FAIL:', suite)
         exit(1)
 
 # vim: expandtab tabstop=4 shiftwidth=4
diff --git a/src/osmo_gsm_tester/bts_octphy.py b/src/osmo_gsm_tester/bts_octphy.py
index 4396108..1f36a79 100644
--- a/src/osmo_gsm_tester/bts_octphy.py
+++ b/src/osmo_gsm_tester/bts_octphy.py
@@ -52,7 +52,7 @@
             raise RuntimeError('No lib/ in %r' % self.inst)
         self.env = { 'LD_LIBRARY_PATH': lib }
 
-        self.launch_process(OsmoBtsTrx.BIN_BTS_OCTPHY, '-r', '1', '-c', os.path.abspath(self.config_file))
+        self.launch_process(OsmoBtsOctphy.BIN_BTS_OCTPHY, '-r', '1', '-c', os.path.abspath(self.config_file))
         self.suite_run.poll()
 
     def launch_process(self, binary_name, *args):
@@ -87,8 +87,6 @@
         values = config.get_defaults('nitb_bts')
         config.overlay(values, config.get_defaults('osmo_bts_octphy'))
         config.overlay(values, self.conf)
-        # using type 'sysmobts' for osmo-bts-octphy
-        config.overlay(values, { 'type': 'sysmobts' })
         self.dbg(conf=values)
         return values
 
diff --git a/src/osmo_gsm_tester/bts_osmotrx.py b/src/osmo_gsm_tester/bts_osmotrx.py
index 417fbf2..71cdd48 100644
--- a/src/osmo_gsm_tester/bts_osmotrx.py
+++ b/src/osmo_gsm_tester/bts_osmotrx.py
@@ -92,8 +92,6 @@
         values = config.get_defaults('nitb_bts')
         config.overlay(values, config.get_defaults('osmo_bts_trx'))
         config.overlay(values, self.conf)
-        # using type 'sysmobts' for osmo-bts-trx
-        config.overlay(values, { 'type': 'sysmobts' })
         self.dbg(conf=values)
         return values
 
diff --git a/src/osmo_gsm_tester/bts_sysmo.py b/src/osmo_gsm_tester/bts_sysmo.py
index 64fa7f4..dd396ff 100644
--- a/src/osmo_gsm_tester/bts_sysmo.py
+++ b/src/osmo_gsm_tester/bts_sysmo.py
@@ -127,7 +127,6 @@
         values = config.get_defaults('nitb_bts')
         config.overlay(values, config.get_defaults('osmo_bts_sysmo'))
         config.overlay(values, self.conf)
-        config.overlay(values, { 'type': 'sysmobts' })
         self.dbg(conf=values)
         return values
 
diff --git a/src/osmo_gsm_tester/config.py b/src/osmo_gsm_tester/config.py
index cbbfa6f..68bbd13 100644
--- a/src/osmo_gsm_tester/config.py
+++ b/src/osmo_gsm_tester/config.py
@@ -177,6 +177,12 @@
     defaults = read_config_file('default.conf', if_missing_return={})
     return defaults.get(for_kind, {})
 
+class Scenario(log.Origin, dict):
+    def __init__(self, name, path):
+        self.set_name(name)
+        self.set_log_category(log.C_TST)
+        self.path = path
+
 def get_scenario(name, validation_schema=None):
     scenarios_dir = get_scenarios_dir()
     if not name.endswith('.conf'):
@@ -184,7 +190,9 @@
     path = scenarios_dir.child(name)
     if not os.path.isfile(path):
         raise RuntimeError('No such scenario file: %r' % path)
-    return read(path, validation_schema=validation_schema)
+    sc = Scenario(name, path)
+    sc.update(read(path, validation_schema=validation_schema))
+    return sc
 
 def add(dest, src):
     if is_dict(dest):
diff --git a/src/osmo_gsm_tester/log.py b/src/osmo_gsm_tester/log.py
index f56d2c9..3e96999 100644
--- a/src/osmo_gsm_tester/log.py
+++ b/src/osmo_gsm_tester/log.py
@@ -186,6 +186,14 @@
             log_str = log_str + '\n'
         self.log_sink(log_str)
 
+    def large_separator(self, *msgs):
+        msg = ' '.join(msgs)
+        if not msg:
+            msg = '------------------------------------------'
+        self.log_sink('------------------------------------------\n'
+                      '%s\n'
+                      '------------------------------------------\n' % msg)
+
 targets = [ LogTarget() ]
 
 def level_str(level):
@@ -207,6 +215,10 @@
     for target in targets:
         target.log(origin, category, level, src, messages, named_items)
 
+def large_separator(*msgs):
+    for target in targets:
+        target.large_separator(*msgs)
+
 def get_src_from_caller(levels_up=1):
     caller = getframeinfo(stack()[levels_up][0])
     return '%s:%d' % (os.path.basename(caller.filename), caller.lineno)
diff --git a/src/osmo_gsm_tester/resource.py b/src/osmo_gsm_tester/resource.py
index 5cfbeaf..b842d98 100644
--- a/src/osmo_gsm_tester/resource.py
+++ b/src/osmo_gsm_tester/resource.py
@@ -29,7 +29,7 @@
 from . import schema
 from . import ofono_client
 from . import osmo_nitb
-from . import bts_sysmo, bts_osmotrx
+from . import bts_sysmo, bts_osmotrx, bts_octphy
 
 from .util import is_dict, is_list
 
@@ -54,7 +54,8 @@
         'bts[].ipa_unit_id': schema.INT,
         'bts[].addr': schema.IPV4,
         'bts[].band': schema.BAND,
-        'bts[].trx[].hw_addr': schema.HWADDR,
+        'bts[].trx_list[].hw_addr': schema.HWADDR,
+        'bts[].trx_list[].net_device': schema.STR,
         'arfcn[].arfcn': schema.INT,
         'arfcn[].band': schema.BAND,
         'modem[].label': schema.STR,
@@ -68,8 +69,9 @@
     RESOURCES_SCHEMA)
 
 KNOWN_BTS_TYPES = {
-        'sysmo': bts_sysmo.SysmoBts,
-        'osmotrx': bts_osmotrx.OsmoBtsTrx,
+        'osmo-bts-sysmo': bts_sysmo.SysmoBts,
+        'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
+        'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
     }
 
 def register_bts_type(name, clazz):
diff --git a/src/osmo_gsm_tester/suite.py b/src/osmo_gsm_tester/suite.py
index 08965b5..74c8b28 100644
--- a/src/osmo_gsm_tester/suite.py
+++ b/src/osmo_gsm_tester/suite.py
@@ -20,6 +20,7 @@
 import os
 import sys
 import time
+import copy
 from . import config, log, template, util, resource, schema, ofono_client, osmo_nitb
 from . import test
 
@@ -56,7 +57,6 @@
                                     SuiteDefinition.CONF_SCHEMA)
             self.load_tests()
 
-
     def load_tests(self):
         with self:
             self.tests = []
@@ -122,22 +122,27 @@
     _config = None
     _processes = None
 
-    def __init__(self, current_trial, suite_definition, scenarios=[]):
+    def __init__(self, current_trial, suite_scenario_str, suite_definition, scenarios=[]):
         self.trial = current_trial
         self.definition = suite_definition
         self.scenarios = scenarios
-        self.set_name(suite_definition.name())
+        self.set_name(suite_scenario_str)
         self.set_log_category(log.C_TST)
         self.resources_pool = resource.ResourcesPool()
 
     def combined(self, conf_name):
-        combination = self.definition.conf.get(conf_name) or {}
-        for scenario in self.scenarios:
-            c = scenario.get(conf_name)
-            if c is None:
-                continue
-            config.combine(combination, c)
-        return combination
+        self.dbg(combining=conf_name)
+        with log.Origin(combining_scenarios=conf_name):
+            combination = copy.deepcopy(self.definition.conf.get(conf_name) or {})
+            self.dbg(definition_conf=combination)
+            for scenario in self.scenarios:
+                with scenario:
+                    c = scenario.get(conf_name)
+                    self.dbg(scenario=scenario.name(), conf=c)
+                    if c is None:
+                        continue
+                    config.combine(combination, c)
+            return combination
 
     def resource_requirements(self):
         if self._resource_requirements is None:
@@ -183,6 +188,7 @@
             self.reserved_resources = self.resources_pool.reserve(self, self.resource_requirements())
 
     def run_tests(self, names=None):
+        self.log('Suite run start')
         if not self.reserved_resources:
             self.reserve_resources()
         results = SuiteRun.Results()
@@ -281,7 +287,6 @@
         self.log('prompt entered:', entered)
         return entered
 
-
 loaded_suite_definitions = {}
 
 def load(suite_name):
@@ -319,7 +324,7 @@
     suite_name, scenario_names = parse_suite_scenario_str(suite_scenario_str)
     suite = load(suite_name)
     scenarios = [config.get_scenario(scenario_name) for scenario_name in scenario_names]
-    return (suite, scenarios)
+    return (suite_scenario_str, suite, scenarios)
 
 def bts_obj(suite_run, conf):
     bts_type = conf.get('type')
diff --git a/src/osmo_gsm_tester/templates/osmo-bts-octphy.cfg.tmpl b/src/osmo_gsm_tester/templates/osmo-bts-octphy.cfg.tmpl
index 90d6092..bf6adf9 100644
--- a/src/osmo_gsm_tester/templates/osmo-bts-octphy.cfg.tmpl
+++ b/src/osmo_gsm_tester/templates/osmo-bts-octphy.cfg.tmpl
@@ -20,13 +20,17 @@
 line vty
  no login
 !
-phy 0
- octphy hw-addr ${osmo_bts_octphy.hw_addr}
- octphy net-device ${osmo_bts_octphy.net_device}
+%for trx in osmo_bts_octphy.trx_list:
+phy ${loop.index}
+ octphy hw-addr ${trx.hw_addr}
+ octphy net-device ${trx.net_device}
  instance 0
+%endfor
 bts 0
  band ${osmo_bts_octphy.band}
  ipa unit-id ${osmo_bts_octphy.ipa_unit_id} 0
  oml remote-ip ${osmo_bts_octphy.oml_remote_ip}
- trx 0
-  phy 0 instance 0
+%for trx in osmo_bts_octphy.trx_list:
+ trx ${loop.index}
+  phy ${loop.index} instance 0
+%endfor
diff --git a/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl b/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl
index e7dc119..28cf61c 100644
--- a/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl
+++ b/src/osmo_gsm_tester/templates/osmo-nitb.cfg.tmpl
@@ -46,7 +46,7 @@
  timer t3141 0
 %for bts in nitb.net.bts_list:
  bts ${loop.index}
-  type ${bts.type}
+  type ${bts.osmobsc_bts_type}
   band ${bts.band}
   cell_identity 0
   location_area_code ${bts.location_area_code}
diff --git a/src/osmo_gsm_tester/util.py b/src/osmo_gsm_tester/util.py
index 61d0f6e..e132e21 100644
--- a/src/osmo_gsm_tester/util.py
+++ b/src/osmo_gsm_tester/util.py
@@ -74,6 +74,7 @@
         return dict2obj(self.obj[key])
 
     def __getattr__(self, key):
+        'provide error information to know which template item was missing'
         try:
             return dict2obj(getattr(self.obj, key))
         except AttributeError:
@@ -82,19 +83,9 @@
             except KeyError:
                 raise AttributeError(key)
 
-class ListProxy:
-    'allow nesting for DictProxy'
-    def __init__(self, obj):
-        self.obj = obj
-
-    def __getitem__(self, key):
-        return dict2obj(self.obj[key])
-
 def dict2obj(value):
-    if isinstance(value, dict):
+    if is_list(value) or is_dict(value):
         return DictProxy(value)
-    if isinstance(value, (tuple, list)):
-        return ListProxy(value)
     return value