Allow suites to dynamically register schemas so tests can receive parameters

Change-Id: Idbe99a35993d193cd97059feb980e61ff14c67ad
diff --git a/src/osmo_gsm_tester/core/schema.py b/src/osmo_gsm_tester/core/schema.py
index 0b21e70..9055c5b 100644
--- a/src/osmo_gsm_tester/core/schema.py
+++ b/src/osmo_gsm_tester/core/schema.py
@@ -325,6 +325,27 @@
 
     nest(None, config, schema)
 
+def config_to_schema_def(src, key_prefix):
+    'Converts a yaml parsed config into a schema dictionary used by validate()'
+    if util.is_dict(src):
+        out_dict = {}
+        for key, val in src.items():
+            list_token = ''
+            dict_token = ''
+            if util.is_list(val):
+                list_token = '[]'
+                assert len(val) == 1
+                val = val[0]
+            if util.is_dict(val):
+                dict_token = '.'
+            tmp_out = config_to_schema_def(val, "%s%s%s%s" %(key_prefix, key, list_token, dict_token))
+            out_dict = {**out_dict, **tmp_out}
+        return out_dict
+
+    # base case: string
+    return {key_prefix: str(src)}
+
+
 def generate_schemas():
     "Generate supported schemas dynamically from objects"
     obj_dir = '%s/../obj/' % os.path.dirname(os.path.abspath(__file__))
@@ -366,12 +387,13 @@
     """Register schema attributes to configure all instances of an object class.
        For instance: register_resource_schema_attributes('bsc', {'net.codec_list[]': schema.CODEC})
     """
-    global _CONFIG_SCHEMA
+    global _CONFIG_SCHEMA, _ALL_SCHEMA
     tmpdict = {}
     for key, val in obj_attr_dict.items():
         new_key = '%s.%s' % (obj_class_str, key)
         tmpdict[new_key] = val
     combine(_CONFIG_SCHEMA, tmpdict)
+    _ALL_SCHEMA = None # reset _ALL_SCHEMA so it is re-generated next time it's requested.
 
 def get_resources_schema():
     return _RESOURCES_SCHEMA;
diff --git a/src/osmo_gsm_tester/core/suite.py b/src/osmo_gsm_tester/core/suite.py
index 1bd6a63..81aab3e 100644
--- a/src/osmo_gsm_tester/core/suite.py
+++ b/src/osmo_gsm_tester/core/suite.py
@@ -38,8 +38,11 @@
     CONF_FILENAME = 'suite.conf'
 
     def __init__(self, suite_dir):
+        self._suite_name = os.path.basename(suite_dir)
+        super().__init__(log.C_CNF, self._suite_name)
         self.suite_dir = suite_dir
-        super().__init__(log.C_CNF, os.path.basename(self.suite_dir))
+        self.conf = None
+        self._schema = None
         self.read_conf()
 
     def read_conf(self):
@@ -47,8 +50,12 @@
         if not os.path.isdir(self.suite_dir):
             raise RuntimeError('No such directory: %r' % self.suite_dir)
         self.conf = config.read(os.path.join(self.suite_dir,
-                                             SuiteDefinition.CONF_FILENAME),
-                                schema.get_all_schema())
+                                             SuiteDefinition.CONF_FILENAME))
+        # Drop schema part since it's dynamically defining content, makes no sense to validate it.
+        self._schema = self.conf.pop('schema', {})
+        sdef = schema.config_to_schema_def(self._schema, "%s." % self._suite_name)
+        schema.register_config_schema('suite', sdef)
+        schema.validate(self.conf, schema.get_all_schema())
         self.load_test_basenames()
 
     def load_test_basenames(self):
@@ -58,6 +65,7 @@
                 continue
             self.test_basenames.append(basename)
 
+
 class SuiteRun(log.Origin):
     UNKNOWN = 'UNKNOWN'
     PASS = 'PASS'
@@ -79,6 +87,10 @@
         self.status = SuiteRun.UNKNOWN
         self.load_tests()
 
+    def suite_name(self):
+        'Return name of suite without scenarios'
+        return self.definition.name()
+
     def trial(self):
         return self._trial
 
@@ -130,6 +142,9 @@
             self._config = self.combined('config', False)
         return self._config
 
+    def config_suite_specific(self):
+        return self.config().get('suite', {}).get(self.suite_name(), {})
+
     def resource_pool(self):
         return self.resources_pool
 
diff --git a/src/osmo_gsm_tester/core/test.py b/src/osmo_gsm_tester/core/test.py
index 8ab124b..7e03b6c 100644
--- a/src/osmo_gsm_tester/core/test.py
+++ b/src/osmo_gsm_tester/core/test.py
@@ -49,6 +49,11 @@
         self.log_target = None
         self._report_stdout = None
 
+    def module_name(self):
+        'Return test name without trailing .py'
+        assert self.basename.endswith('.py')
+        return self.basename[:-3]
+
     def get_run_dir(self):
         if self._run_dir is None:
             self._run_dir = util.Dir(self.suite_run.get_run_dir().new_dir(self._name))
diff --git a/src/osmo_gsm_tester/testenv.py b/src/osmo_gsm_tester/testenv.py
index 42288aa..789e291 100644
--- a/src/osmo_gsm_tester/testenv.py
+++ b/src/osmo_gsm_tester/testenv.py
@@ -141,6 +141,12 @@
         MainLoop.unregister_poll_func(self.poll)
         self.test_import_modules_cleanup()
 
+    def config_suite_specific(self):
+        return self.suite_run.config_suite_specific()
+
+    def config_test_specific(self):
+        return self.suite_run.config_suite_specific().get(self._test.module_name(), {})
+
     def prompt(self, *msgs, **msg_details):
         'ask for user interaction. Do not use in tests that should run automatically!'
         if msg_details: