config: suites_dir and scenarios_dir are now a list of paths

This allows inheriting suites or scenarios from eg. sysmocom/ dir, while
still allowing to apply new suites and scenarios on top.

Change-Id: Icecdae32d400a6b6da2ebf167c1c795f7a74ae96
diff --git a/src/osmo_gsm_tester/core/config.py b/src/osmo_gsm_tester/core/config.py
index 9380cca..398e8ba 100644
--- a/src/osmo_gsm_tester/core/config.py
+++ b/src/osmo_gsm_tester/core/config.py
@@ -71,8 +71,8 @@
 CFG_RESOURCES_CONF = 'resource_conf_path'
 MAIN_CONFIG_SCHEMA = {
         CFG_STATE_DIR: schema.STR,
-        CFG_SUITES_DIR: schema.STR,
-        CFG_SCENARIOS_DIR: schema.STR,
+        CFG_SUITES_DIR + '[]': schema.STR,
+        CFG_SCENARIOS_DIR + '[]': schema.STR,
         CFG_TRIAL_DIR: schema.STR,
         CFG_DEFAULT_SUITES_CONF: schema.STR,
         CFG_DEFAULTS_CONF: schema.STR,
@@ -80,8 +80,8 @@
     }
 
 DF_CFG_STATE_DIR = '/var/tmp/osmo-gsm-tester/state/'
-DF_CFG_SUITES_DIR = './suites'
-DF_CFG_SCENARIOS_DIR = './scenarios'
+DF_CFG_SUITES_DIR = ['./suites']
+DF_CFG_SCENARIOS_DIR = ['./scenarios']
 DF_CFG_TRIAL_DIR = './trial'
 DF_CFG_DEFAULT_SUITES_CONF = './default-suites.conf'
 DF_CFG_DEFAULTS_CONF = './defaults.conf'
@@ -122,11 +122,16 @@
         MAIN_CONFIG_PATH = _find_main_config_path()
     return MAIN_CONFIG_PATH
 
-def main_config_path_to_abspath(path):
+def main_config_path_to_abspath(val):
     'Relative files in main config are relative towards the config file, not towards $CWD'
-    if not path.startswith(os.pathsep):
-        return os.path.realpath(os.path.join(os.path.dirname(_get_main_config_path()), path))
-    return path
+    # If val is a list of paths, recurse to translate its paths.
+    if isinstance(val, list):
+        for i in range(len(val)):
+            val[i] = main_config_path_to_abspath(val[i])
+        return val
+    if not val.startswith(os.pathsep):
+        return os.path.realpath(os.path.join(os.path.dirname(_get_main_config_path()), val))
+    return val
 
 def _get_main_config():
     global MAIN_CONFIG
@@ -169,11 +174,11 @@
 def get_state_dir():
     return Dir(get_main_config_value(CFG_STATE_DIR))
 
-def get_suites_dir():
-    return Dir(get_main_config_value(CFG_SUITES_DIR))
+def get_suites_dirs():
+    return [Dir(d) for d in get_main_config_value(CFG_SUITES_DIR)]
 
-def get_scenarios_dir():
-    return Dir(get_main_config_value(CFG_SCENARIOS_DIR))
+def get_scenarios_dirs():
+    return [Dir(d) for d in get_main_config_value(CFG_SCENARIOS_DIR)]
 
 DEFAULTS_CONF = None
 def get_defaults(for_kind):
diff --git a/src/osmo_gsm_tester/core/scenario.py b/src/osmo_gsm_tester/core/scenario.py
index efa045b..83ce490 100644
--- a/src/osmo_gsm_tester/core/scenario.py
+++ b/src/osmo_gsm_tester/core/scenario.py
@@ -88,25 +88,41 @@
         self.update(conf)
 
 def get_scenario(name, validation_schema=None):
-    scenarios_dir = config.get_scenarios_dir()
+    found = False
+    path = None
+    param_list = []
     if not name.endswith('.conf'):
         name = name + '.conf'
     is_parametrized_file = '@' in name
-    param_list = []
-    path = scenarios_dir.child(name)
     if not is_parametrized_file:
-        if not os.path.isfile(path):
-            raise RuntimeError('No such scenario file: %r' % path)
+        scenarios_dirs = config.get_scenarios_dirs()
+        for d in scenarios_dirs:
+            path = d.child(name)
+            if  os.path.isfile(path):
+                found = True
+                break
+        if not found:
+            raise RuntimeError('No such scenario file %s in %r' % (name, scenarios_dirs))
         sc = Scenario(name, path)
     else: # parametrized scenario:
         # Allow first matching complete matching names (eg: scenario@param1,param2.conf),
         # this allows setting specific content in different files for specific values.
-        if not os.path.isfile(path):
+        scenarios_dirs = config.get_scenarios_dirs()
+        for d in scenarios_dirs:
+            path = d.child(name)
+            if os.path.isfile(path):
+                found = True
+                break
+        if not found:
             # get "scenario@.conf" from "scenario@param1,param2.conf":
-            prefix_name = name[:name.index("@")+1] + '.conf'
-            path = scenarios_dir.child(prefix_name)
-            if not os.path.isfile(path):
-                raise RuntimeError('No such scenario file: %r (nor %s)' % (path, name))
+            for d in scenarios_dirs:
+                prefix_name = name[:name.index("@")+1] + '.conf'
+                path = d.child(prefix_name)
+                if os.path.isfile(path):
+                    found = True
+                    break
+        if not found:
+            raise RuntimeError('No such scenario file %r (nor %s) in %r' % (name, prefix_name, scenarios_dirs))
         # At this point, we have existing file path. Let's now scrap the parameter(s):
         # get param1,param2 str from scenario@param1,param2.conf
         param_list_str = name.split('@', 1)[1][:-len('.conf')]
diff --git a/src/osmo_gsm_tester/core/suite.py b/src/osmo_gsm_tester/core/suite.py
index a6eaca2..c55c5e9 100644
--- a/src/osmo_gsm_tester/core/suite.py
+++ b/src/osmo_gsm_tester/core/suite.py
@@ -232,12 +232,16 @@
     if suite is not None:
         return suite
 
-    suites_dir = config.get_suites_dir()
-    suite_dir = suites_dir.child(suite_name)
-    if not suites_dir.exists(suite_name):
-        raise RuntimeError('Suite not found: %r in %r' % (suite_name, suites_dir))
-    if not suites_dir.isdir(suite_name):
-        raise RuntimeError('Suite name found, but not a directory: %r' % (suite_dir))
+    suites_dirs = config.get_suites_dirs()
+    suite_dir = None
+    found = False
+    for d in suites_dirs:
+        suite_dir = d.child(suite_name)
+        if d.exists(suite_name) and d.isdir(suite_name):
+            found = True
+            break
+    if not found:
+        raise RuntimeError('Suite not found: %r in %r' % (suite_name, suites_dirs))
 
     suite_def = SuiteDefinition(suite_dir)
     loaded_suite_definitions[suite_name] = suite_def