<p>pespin <strong>submitted</strong> this change.</p><p><a href="https://gerrit.osmocom.org/c/osmo-gsm-tester/+/18799">View Change</a></p><div style="white-space:pre-wrap">Approvals:
  pespin: Looks good to me, approved; Verified

</div><pre style="font-family: monospace,monospace; white-space: pre-wrap;">Implement per-test timeout guard<br><br>Timeout value can be specified by test in suite.conf:<br><br>config:<br>  suite:<br>    <suite_name>:<br>      <test_name>:<br>        timeout: 2 # 2 seconds timeout<br><br>Change-Id: I522f51f77f8be64ebfdb5d5e07ba92baf82d7706<br>---<br>M doc/manuals/chapters/config.adoc<br>M selftest/suite_test/suite_test.ok<br>M selftest/suite_test/suite_test.py<br>M selftest/suite_test/suitedirA/test_suite/suite.conf<br>A selftest/suite_test/suitedirA/test_suite/test_timeout.py<br>M src/osmo_gsm_tester/core/suite.py<br>M src/osmo_gsm_tester/core/test.py<br>M src/osmo_gsm_tester/testenv.py<br>8 files changed, 78 insertions(+), 20 deletions(-)<br><br></pre><pre style="font-family: monospace,monospace; white-space: pre-wrap;"><span>diff --git a/doc/manuals/chapters/config.adoc b/doc/manuals/chapters/config.adoc</span><br><span>index 118f056..f4e08b3 100644</span><br><span>--- a/doc/manuals/chapters/config.adoc</span><br><span>+++ b/doc/manuals/chapters/config.adoc</span><br><span>@@ -190,7 +190,18 @@</span><br><span>   a_suite_test_foo:</span><br><span>     one_test_parameter_for_test_foo: 'str'</span><br><span>     another_test_parameter_for_test_foo: ['bool_str']</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+config:</span><br><span style="color: hsl(120, 100%, 40%);">+  suite:</span><br><span style="color: hsl(120, 100%, 40%);">+    <suite_name>:</span><br><span style="color: hsl(120, 100%, 40%);">+      some_suite_parameter: 3</span><br><span style="color: hsl(120, 100%, 40%);">+      a_suite_test_foo:</span><br><span style="color: hsl(120, 100%, 40%);">+        one_test_parameter_for_test_foo: 'hello'</span><br><span style="color: hsl(120, 100%, 40%);">+        timeout: 30 <1></span><br><span> ----</span><br><span style="color: hsl(120, 100%, 40%);">+<1> The per-test _timeout_ attribute is implicitly defined for all tests with</span><br><span style="color: hsl(120, 100%, 40%);">+type _duration_, and will trigger a timeout if test doesn't finish in time</span><br><span style="color: hsl(120, 100%, 40%);">+specified.</span><br><span> </span><br><span> [[scenarios_dir]]</span><br><span> ==== 'scenarios_dir'</span><br><span>diff --git a/selftest/suite_test/suite_test.ok b/selftest/suite_test/suite_test.ok</span><br><span>index c7c76d7..58593fd 100644</span><br><span>--- a/selftest/suite_test/suite_test.ok</span><br><span>+++ b/selftest/suite_test/suite_test.ok</span><br><span>@@ -15,6 +15,11 @@</span><br><span> cnf [PATH]/selftest/suite_test/suitedirA/empty_dir/suite.conf: ERR: FileNotFoundError: [Errno 2] No such file or directory: '[PATH]/selftest/suite_test/suitedirA/empty_dir/suite.conf'  [empty_dir↪[PATH]/selftest/suite_test/suitedirA/empty_dir/suite.conf]</span><br><span> - valid suite dir</span><br><span> cnf test_suite: DBG: reading suite.conf</span><br><span style="color: hsl(120, 100%, 40%);">+config:</span><br><span style="color: hsl(120, 100%, 40%);">+  suite:</span><br><span style="color: hsl(120, 100%, 40%);">+    test_suite:</span><br><span style="color: hsl(120, 100%, 40%);">+      test_timeout:</span><br><span style="color: hsl(120, 100%, 40%);">+        timeout: '1'</span><br><span> resources:</span><br><span>   bts:</span><br><span>   - label: sysmoCell 5000</span><br><span>@@ -28,7 +33,7 @@</span><br><span> </span><br><span> - run hello world test</span><br><span> tst test_suite: DBG: {combining='config'}</span><br><span style="color: hsl(0, 100%, 40%);">-tst {combining_scenarios='config'}: DBG: {definition_conf={}}  [test_suite↪{combining_scenarios='config'}]</span><br><span style="color: hsl(120, 100%, 40%);">+tst {combining_scenarios='config'}: DBG: {definition_conf={suite={test_suite={test_timeout={timeout='1'}}}}}  [test_suite↪{combining_scenarios='config'}]</span><br><span> </span><br><span> ---------------------------------------------------------------------</span><br><span> trial test_suite</span><br><span>@@ -101,7 +106,7 @@</span><br><span> ---------------------------------------------------------------------</span><br><span> trial test_suite PASS</span><br><span> ---------------------------------------------------------------------</span><br><span style="color: hsl(0, 100%, 40%);">-PASS: test_suite (pass: 1, skip: 6)</span><br><span style="color: hsl(120, 100%, 40%);">+PASS: test_suite (pass: 1, skip: 7)</span><br><span>     pass: hello_world.py (N.N sec)</span><br><span>     skip: mo_mt_sms.py</span><br><span>     skip: mo_sms.py</span><br><span>@@ -109,6 +114,7 @@</span><br><span>     skip: test_fail.py</span><br><span>     skip: test_fail_raise.py</span><br><span>     skip: test_suite_params.py</span><br><span style="color: hsl(120, 100%, 40%);">+    skip: test_timeout.py</span><br><span> </span><br><span> - a test with an error</span><br><span> </span><br><span>@@ -125,7 +131,7 @@</span><br><span> ---------------------------------------------------------------------</span><br><span> trial test_suite FAIL</span><br><span> ---------------------------------------------------------------------</span><br><span style="color: hsl(0, 100%, 40%);">-FAIL: test_suite (fail: 1, skip: 6)</span><br><span style="color: hsl(120, 100%, 40%);">+FAIL: test_suite (fail: 1, skip: 7)</span><br><span>     skip: hello_world.py (N.N sec)</span><br><span>     skip: mo_mt_sms.py</span><br><span>     skip: mo_sms.py</span><br><span>@@ -133,6 +139,7 @@</span><br><span>     skip: test_fail.py</span><br><span>     skip: test_fail_raise.py</span><br><span>     skip: test_suite_params.py</span><br><span style="color: hsl(120, 100%, 40%);">+    skip: test_timeout.py</span><br><span> </span><br><span> - a test with a failure</span><br><span> </span><br><span>@@ -149,7 +156,7 @@</span><br><span> ---------------------------------------------------------------------</span><br><span> trial test_suite FAIL</span><br><span> ---------------------------------------------------------------------</span><br><span style="color: hsl(0, 100%, 40%);">-FAIL: test_suite (fail: 1, skip: 6)</span><br><span style="color: hsl(120, 100%, 40%);">+FAIL: test_suite (fail: 1, skip: 7)</span><br><span>     skip: hello_world.py (N.N sec)</span><br><span>     skip: mo_mt_sms.py</span><br><span>     skip: mo_sms.py</span><br><span>@@ -157,6 +164,7 @@</span><br><span>     FAIL: test_fail.py (N.N sec) EpicFail: This failure is expected</span><br><span>     skip: test_fail_raise.py</span><br><span>     skip: test_suite_params.py</span><br><span style="color: hsl(120, 100%, 40%);">+    skip: test_timeout.py</span><br><span> </span><br><span> - a test with a raised failure</span><br><span> </span><br><span>@@ -172,7 +180,7 @@</span><br><span> ---------------------------------------------------------------------</span><br><span> trial test_suite FAIL</span><br><span> ---------------------------------------------------------------------</span><br><span style="color: hsl(0, 100%, 40%);">-FAIL: test_suite (fail: 1, skip: 6)</span><br><span style="color: hsl(120, 100%, 40%);">+FAIL: test_suite (fail: 1, skip: 7)</span><br><span>     skip: hello_world.py (N.N sec)</span><br><span>     skip: mo_mt_sms.py</span><br><span>     skip: mo_sms.py</span><br><span>@@ -180,9 +188,10 @@</span><br><span>     skip: test_fail.py (N.N sec)</span><br><span>     FAIL: test_fail_raise.py (N.N sec) ExpectedFail: This failure is expected</span><br><span>     skip: test_suite_params.py</span><br><span style="color: hsl(120, 100%, 40%);">+    skip: test_timeout.py</span><br><span> - test with half empty scenario</span><br><span> tst test_suite: DBG: {combining='config'}  [suite.py:[LINENR]]</span><br><span style="color: hsl(0, 100%, 40%);">-tst {combining_scenarios='config'}: DBG: {definition_conf={}}  [test_suite↪{combining_scenarios='config'}]  [suite.py:[LINENR]]</span><br><span style="color: hsl(120, 100%, 40%);">+tst {combining_scenarios='config'}: DBG: {definition_conf={suite={test_suite={test_timeout={timeout='1'}}}}}  [test_suite↪{combining_scenarios='config'}]  [suite.py:[LINENR]]</span><br><span> tst {combining_scenarios='config', scenario='foo'}: DBG: {conf={}, scenario='foo'}  [test_suite↪{combining_scenarios='config', scenario='foo'}]  [suite.py:[LINENR]]</span><br><span> </span><br><span> ---------------------------------------------------------------------</span><br><span>@@ -261,7 +270,7 @@</span><br><span> ---------------------------------------------------------------------</span><br><span> trial test_suite PASS</span><br><span> ---------------------------------------------------------------------</span><br><span style="color: hsl(0, 100%, 40%);">-PASS: test_suite (pass: 1, skip: 6)</span><br><span style="color: hsl(120, 100%, 40%);">+PASS: test_suite (pass: 1, skip: 7)</span><br><span>     pass: hello_world.py (N.N sec)</span><br><span>     skip: mo_mt_sms.py</span><br><span>     skip: mo_sms.py</span><br><span>@@ -269,9 +278,10 @@</span><br><span>     skip: test_fail.py</span><br><span>     skip: test_fail_raise.py</span><br><span>     skip: test_suite_params.py</span><br><span style="color: hsl(120, 100%, 40%);">+    skip: test_timeout.py</span><br><span> - test with scenario</span><br><span> tst test_suite: DBG: {combining='config'}  [suite.py:[LINENR]]</span><br><span style="color: hsl(0, 100%, 40%);">-tst {combining_scenarios='config'}: DBG: {definition_conf={}}  [test_suite↪{combining_scenarios='config'}]  [suite.py:[LINENR]]</span><br><span style="color: hsl(120, 100%, 40%);">+tst {combining_scenarios='config'}: DBG: {definition_conf={suite={test_suite={test_timeout={timeout='1'}}}}}  [test_suite↪{combining_scenarios='config'}]  [suite.py:[LINENR]]</span><br><span> tst {combining_scenarios='config', scenario='foo'}: DBG: {conf={}, scenario='foo'}  [test_suite↪{combining_scenarios='config', scenario='foo'}]  [suite.py:[LINENR]]</span><br><span> </span><br><span> ---------------------------------------------------------------------</span><br><span>@@ -350,7 +360,7 @@</span><br><span> ---------------------------------------------------------------------</span><br><span> trial test_suite PASS</span><br><span> ---------------------------------------------------------------------</span><br><span style="color: hsl(0, 100%, 40%);">-PASS: test_suite (pass: 1, skip: 6)</span><br><span style="color: hsl(120, 100%, 40%);">+PASS: test_suite (pass: 1, skip: 7)</span><br><span>     pass: hello_world.py (N.N sec)</span><br><span>     skip: mo_mt_sms.py</span><br><span>     skip: mo_sms.py</span><br><span>@@ -358,9 +368,10 @@</span><br><span>     skip: test_fail.py</span><br><span>     skip: test_fail_raise.py</span><br><span>     skip: test_suite_params.py</span><br><span style="color: hsl(120, 100%, 40%);">+    skip: test_timeout.py</span><br><span> - test with scenario and modifiers</span><br><span> tst test_suite: DBG: {combining='config'}  [suite.py:[LINENR]]</span><br><span style="color: hsl(0, 100%, 40%);">-tst {combining_scenarios='config'}: DBG: {definition_conf={}}  [test_suite↪{combining_scenarios='config'}]  [suite.py:[LINENR]]</span><br><span style="color: hsl(120, 100%, 40%);">+tst {combining_scenarios='config'}: DBG: {definition_conf={suite={test_suite={test_timeout={timeout='1'}}}}}  [test_suite↪{combining_scenarios='config'}]  [suite.py:[LINENR]]</span><br><span> tst {combining_scenarios='config', scenario='foo'}: DBG: {conf={}, scenario='foo'}  [test_suite↪{combining_scenarios='config', scenario='foo'}]  [suite.py:[LINENR]]</span><br><span> tst test_suite: reserving resources in [PATH]/selftest/suite_test/test_work/state_dir ...  [suite.py:[LINENR]]</span><br><span> tst test_suite: DBG: {combining='resources'}  [suite.py:[LINENR]]</span><br><span>@@ -485,7 +496,7 @@</span><br><span> ---------------------------------------------------------------------</span><br><span> trial test_suite PASS</span><br><span> ---------------------------------------------------------------------</span><br><span style="color: hsl(0, 100%, 40%);">-PASS: test_suite (pass: 1, skip: 6)</span><br><span style="color: hsl(120, 100%, 40%);">+PASS: test_suite (pass: 1, skip: 7)</span><br><span>     pass: hello_world.py (N.N sec)</span><br><span>     skip: mo_mt_sms.py</span><br><span>     skip: mo_sms.py</span><br><span>@@ -493,9 +504,10 @@</span><br><span>     skip: test_fail.py</span><br><span>     skip: test_fail_raise.py</span><br><span>     skip: test_suite_params.py</span><br><span style="color: hsl(120, 100%, 40%);">+    skip: test_timeout.py</span><br><span> - test with suite-specific config</span><br><span> tst test_suite: DBG: {combining='config'}  [suite.py:[LINENR]]</span><br><span style="color: hsl(0, 100%, 40%);">-tst {combining_scenarios='config'}: DBG: {definition_conf={}}  [test_suite↪{combining_scenarios='config'}]  [suite.py:[LINENR]]</span><br><span style="color: hsl(120, 100%, 40%);">+tst {combining_scenarios='config'}: DBG: {definition_conf={suite={test_suite={test_timeout={timeout='1'}}}}}  [test_suite↪{combining_scenarios='config'}]  [suite.py:[LINENR]]</span><br><span> tst {combining_scenarios='config', scenario='foo'}: DBG: {conf={suite={test_suite={some_suite_global_param='heyho', test_suite_params={one_bool_parameter='true', second_list_parameter=['23', '45']}}}}, scenario='foo'}  [test_suite↪{combining_scenarios='config', scenario='foo'}]  [suite.py:[LINENR]]</span><br><span> tst test_suite: reserving resources in [PATH]/selftest/suite_test/test_work/state_dir ...  [suite.py:[LINENR]]</span><br><span> tst test_suite: DBG: {combining='resources'}  [suite.py:[LINENR]]</span><br><span>@@ -614,13 +626,21 @@</span><br><span> tst test_suite_params.py:[LINENR]: starting test  [test_suite↪test_suite_params.py:[LINENR]]  [test_suite_params.py:[LINENR]]</span><br><span> tst test_suite_params.py:[LINENR]: SPECIFIC SUITE CONFIG: {'some_suite_global_param': 'heyho',  [test_suite↪test_suite_params.py:[LINENR]]  [test_suite_params.py:[LINENR]]</span><br><span> tst test_suite_params.py:[LINENR]:  'test_suite_params': {'one_bool_parameter': 'true',  [test_suite↪test_suite_params.py:[LINENR]]  [test_suite_params.py:[LINENR]]</span><br><span style="color: hsl(0, 100%, 40%);">-tst test_suite_params.py:[LINENR]:                        'second_list_parameter': ['23', '45']}}  [test_suite↪test_suite_params.py:[LINENR]]  [test_suite_params.py:[LINENR]]</span><br><span style="color: hsl(120, 100%, 40%);">+tst test_suite_params.py:[LINENR]:                        'second_list_parameter': ['23', '45']},  [test_suite↪test_suite_params.py:[LINENR]]  [test_suite_params.py:[LINENR]]</span><br><span style="color: hsl(120, 100%, 40%);">+tst test_suite_params.py:[LINENR]:  'test_timeout': {'timeout': '1'}}  [test_suite↪test_suite_params.py:[LINENR]]  [test_suite_params.py:[LINENR]]</span><br><span> tst test_suite_params.py:[LINENR]: SPECIFIC TEST CONFIG: {'one_bool_parameter': 'true', 'second_list_parameter': ['23', '45']}  [test_suite↪test_suite_params.py:[LINENR]]  [test_suite_params.py:[LINENR]]</span><br><span> tst test_suite_params.py:[LINENR] Test passed (N.N sec)  [test_suite↪test_suite_params.py]  [test.py:[LINENR]]</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+----------------------------------------------</span><br><span style="color: hsl(120, 100%, 40%);">+trial test_suite test_timeout.py</span><br><span style="color: hsl(120, 100%, 40%);">+----------------------------------------------</span><br><span style="color: hsl(120, 100%, 40%);">+tst test_timeout.py:[LINENR]: starting test and waiting to receive Timeout after 1 seconds  [test_suite↪test_timeout.py:[LINENR]]  [test_timeout.py:[LINENR]]</span><br><span style="color: hsl(120, 100%, 40%);">+tst test_timeout.py:[LINENR]: ERR: Error: test_timeout.py:[LINENR] Test Timeout triggered: 1 seconds elapsed [test_suite↪test_timeout.py:[LINENR]↪test_timeout.py]  [test_suite↪test_timeout.py:[LINENR]]  [testenv.py:[LINENR]: raise log_module.Error('Test Timeout triggered: %d seconds elapsed' % self._test.elapsed_time())]</span><br><span style="color: hsl(120, 100%, 40%);">+tst test_timeout.py:[LINENR]: Test FAILED (N.N sec)  [test_suite↪test_timeout.py:[LINENR]]  [test.py:[LINENR]]</span><br><span> ---------------------------------------------------------------------</span><br><span style="color: hsl(0, 100%, 40%);">-trial test_suite PASS</span><br><span style="color: hsl(120, 100%, 40%);">+trial test_suite FAIL</span><br><span> ---------------------------------------------------------------------</span><br><span style="color: hsl(0, 100%, 40%);">-PASS: test_suite (pass: 1, skip: 6)</span><br><span style="color: hsl(120, 100%, 40%);">+FAIL: test_suite (fail: 1, pass: 1, skip: 6)</span><br><span>     skip: hello_world.py</span><br><span>     skip: mo_mt_sms.py</span><br><span>     skip: mo_sms.py</span><br><span>@@ -628,6 +648,7 @@</span><br><span>     skip: test_fail.py</span><br><span>     skip: test_fail_raise.py</span><br><span>     pass: test_suite_params.py (N.N sec)</span><br><span style="color: hsl(120, 100%, 40%);">+    FAIL: test_timeout.py (N.N sec) Error: test_timeout.py:[LINENR] Test Timeout triggered: 1 seconds elapsed [test_suite↪test_timeout.py:[LINENR]↪test_timeout.py]</span><br><span> - test with template overlay</span><br><span> cnf suiteC: DBG: reading suite.conf  [suite.py:[LINENR]]</span><br><span> tst suiteC: DBG: {combining='config'}  [suite.py:[LINENR]]</span><br><span>diff --git a/selftest/suite_test/suite_test.py b/selftest/suite_test/suite_test.py</span><br><span>index 260b9c4..9708037 100755</span><br><span>--- a/selftest/suite_test/suite_test.py</span><br><span>+++ b/selftest/suite_test/suite_test.py</span><br><span>@@ -102,7 +102,7 @@</span><br><span> s = suite.SuiteRun(trial, 'test_suite', s_def, [sc])</span><br><span> s.reserve_resources()</span><br><span> print(repr(s.reserved_resources))</span><br><span style="color: hsl(0, 100%, 40%);">-results = s.run_tests('test_suite_params.py')</span><br><span style="color: hsl(120, 100%, 40%);">+results = s.run_tests(['test_suite_params.py', 'test_timeout.py'])</span><br><span> print(report.suite_to_text(s))</span><br><span> </span><br><span> print('- test with template overlay')</span><br><span>diff --git a/selftest/suite_test/suitedirA/test_suite/suite.conf b/selftest/suite_test/suitedirA/test_suite/suite.conf</span><br><span>index ff4899a..0426ea7 100644</span><br><span>--- a/selftest/suite_test/suitedirA/test_suite/suite.conf</span><br><span>+++ b/selftest/suite_test/suitedirA/test_suite/suite.conf</span><br><span>@@ -15,3 +15,9 @@</span><br><span>       one_bool_parameter: 'bool_str'</span><br><span>       second_list_parameter: ['uint']</span><br><span> </span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+config:</span><br><span style="color: hsl(120, 100%, 40%);">+  suite:</span><br><span style="color: hsl(120, 100%, 40%);">+    test_suite:</span><br><span style="color: hsl(120, 100%, 40%);">+      test_timeout:</span><br><span style="color: hsl(120, 100%, 40%);">+        timeout: 1 # timeout in 1 second</span><br><span>diff --git a/selftest/suite_test/suitedirA/test_suite/test_timeout.py b/selftest/suite_test/suitedirA/test_suite/test_timeout.py</span><br><span>new file mode 100644</span><br><span>index 0000000..eeddb70</span><br><span>--- /dev/null</span><br><span>+++ b/selftest/suite_test/suitedirA/test_suite/test_timeout.py</span><br><span>@@ -0,0 +1,6 @@</span><br><span style="color: hsl(120, 100%, 40%);">+from osmo_gsm_tester.testenv import *</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+timeout = int(tenv.config_test_specific()['timeout'])</span><br><span style="color: hsl(120, 100%, 40%);">+print('starting test and waiting to receive Timeout after %d seconds' % timeout)</span><br><span style="color: hsl(120, 100%, 40%);">+sleep(10)</span><br><span style="color: hsl(120, 100%, 40%);">+print('test failed, we expected timeout after %d seconds' % timeout)</span><br><span>diff --git a/src/osmo_gsm_tester/core/suite.py b/src/osmo_gsm_tester/core/suite.py</span><br><span>index 9b9062d..938471c 100644</span><br><span>--- a/src/osmo_gsm_tester/core/suite.py</span><br><span>+++ b/src/osmo_gsm_tester/core/suite.py</span><br><span>@@ -44,6 +44,8 @@</span><br><span>         self.suite_dir = suite_dir</span><br><span>         self.conf = None</span><br><span>         self._schema = None</span><br><span style="color: hsl(120, 100%, 40%);">+        self.test_basenames = []</span><br><span style="color: hsl(120, 100%, 40%);">+        self.load_test_basenames()</span><br><span>         self.read_conf()</span><br><span> </span><br><span>     def read_conf(self):</span><br><span>@@ -54,13 +56,16 @@</span><br><span>                                              SuiteDefinition.CONF_FILENAME))</span><br><span>         # Drop schema part since it's dynamically defining content, makes no sense to validate it.</span><br><span>         self._schema = self.conf.pop('schema', {})</span><br><span style="color: hsl(120, 100%, 40%);">+        # Add per-test 'timeout' attribute:</span><br><span style="color: hsl(120, 100%, 40%);">+        d = {t.rstrip('.py'):{'timeout': schema.DURATION} for t in self.test_basenames}</span><br><span style="color: hsl(120, 100%, 40%);">+        schema.combine(self._schema, d)</span><br><span style="color: hsl(120, 100%, 40%);">+        # Convert config file format to proper schema format and register it:</span><br><span>         sdef = schema.config_to_schema_def(self._schema, "%s." % self._suite_name)</span><br><span>         schema.register_config_schema('suite', sdef)</span><br><span style="color: hsl(120, 100%, 40%);">+        # Finally validate the file:</span><br><span>         schema.validate(self.conf, schema.get_all_schema())</span><br><span style="color: hsl(0, 100%, 40%);">-        self.load_test_basenames()</span><br><span> </span><br><span>     def load_test_basenames(self):</span><br><span style="color: hsl(0, 100%, 40%);">-        self.test_basenames = []</span><br><span>         for basename in sorted(os.listdir(self.suite_dir)):</span><br><span>             if not basename.endswith('.py'):</span><br><span>                 continue</span><br><span>diff --git a/src/osmo_gsm_tester/core/test.py b/src/osmo_gsm_tester/core/test.py</span><br><span>index 45dfd41..c6d88e6 100644</span><br><span>--- a/src/osmo_gsm_tester/core/test.py</span><br><span>+++ b/src/osmo_gsm_tester/core/test.py</span><br><span>@@ -35,12 +35,12 @@</span><br><span>     PASS = 'pass'</span><br><span>     FAIL = 'FAIL'</span><br><span> </span><br><span style="color: hsl(0, 100%, 40%);">-    def __init__(self, suite_run, test_basename, test_specific_config):</span><br><span style="color: hsl(120, 100%, 40%);">+    def __init__(self, suite_run, test_basename, config_test_specific):</span><br><span>         self.basename = test_basename</span><br><span>         super().__init__(log.C_TST, self.basename)</span><br><span>         self._run_dir = None</span><br><span>         self.suite_run = suite_run</span><br><span style="color: hsl(0, 100%, 40%);">-        self._config_test_specific = test_specific_config</span><br><span style="color: hsl(120, 100%, 40%);">+        self._config_test_specific = config_test_specific</span><br><span>         self.path = os.path.join(self.suite_run.definition.suite_dir, self.basename)</span><br><span>         self.status = Test.UNKNOWN</span><br><span>         self.start_timestamp = 0</span><br><span>@@ -49,6 +49,7 @@</span><br><span>         self.fail_message = None</span><br><span>         self.log_targets = []</span><br><span>         self._report_stdout = None</span><br><span style="color: hsl(120, 100%, 40%);">+        self.timeout = int(config_test_specific['timeout']) if 'timeout' in config_test_specific else None</span><br><span> </span><br><span>     def module_name(self):</span><br><span>         'Return test name without trailing .py'</span><br><span>diff --git a/src/osmo_gsm_tester/testenv.py b/src/osmo_gsm_tester/testenv.py</span><br><span>index 11199c2..77d844a 100644</span><br><span>--- a/src/osmo_gsm_tester/testenv.py</span><br><span>+++ b/src/osmo_gsm_tester/testenv.py</span><br><span>@@ -55,6 +55,8 @@</span><br><span>         self.test_import_modules_to_clean_up = []</span><br><span>         self.objects_to_clean_up = None</span><br><span>         MainLoop.register_poll_func(self.poll)</span><br><span style="color: hsl(120, 100%, 40%);">+        if self._test.timeout is not None: # aimed at firing once</span><br><span style="color: hsl(120, 100%, 40%);">+            MainLoop.register_poll_func(self.timeout_expired, timestep=self._test.timeout)</span><br><span> </span><br><span>     def test(self):</span><br><span>         return self._test</span><br><span>@@ -120,6 +122,11 @@</span><br><span>             except Exception:</span><br><span>                 log_module.log_exn()</span><br><span> </span><br><span style="color: hsl(120, 100%, 40%);">+    def timeout_expired(self):</span><br><span style="color: hsl(120, 100%, 40%);">+        # Avoid timeout being called several times:</span><br><span style="color: hsl(120, 100%, 40%);">+        MainLoop.unregister_poll_func(self.timeout_expired)</span><br><span style="color: hsl(120, 100%, 40%);">+        raise log_module.Error('Test Timeout triggered: %d seconds elapsed' % self._test.elapsed_time())</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span>     def poll(self):</span><br><span>         for proc, respawn in self._processes:</span><br><span>             if proc.terminated():</span><br><span>@@ -139,6 +146,7 @@</span><br><span>         self.objects_cleanup()</span><br><span>         self.suite_run.reserved_resources.put_all()</span><br><span>         MainLoop.unregister_poll_func(self.poll)</span><br><span style="color: hsl(120, 100%, 40%);">+        MainLoop.unregister_poll_func(self.timeout_expired)</span><br><span>         self.test_import_modules_cleanup()</span><br><span>         self.set_overlay_template_dir(None)</span><br><span> </span><br><span></span><br></pre><p>To view, visit <a href="https://gerrit.osmocom.org/c/osmo-gsm-tester/+/18799">change 18799</a>. To unsubscribe, or for help writing mail filters, visit <a href="https://gerrit.osmocom.org/settings">settings</a>.</p><div itemscope itemtype="http://schema.org/EmailMessage"><div itemscope itemprop="action" itemtype="http://schema.org/ViewAction"><link itemprop="url" href="https://gerrit.osmocom.org/c/osmo-gsm-tester/+/18799"/><meta itemprop="name" content="View Change"/></div></div>

<div style="display:none"> Gerrit-Project: osmo-gsm-tester </div>
<div style="display:none"> Gerrit-Branch: master </div>
<div style="display:none"> Gerrit-Change-Id: I522f51f77f8be64ebfdb5d5e07ba92baf82d7706 </div>
<div style="display:none"> Gerrit-Change-Number: 18799 </div>
<div style="display:none"> Gerrit-PatchSet: 5 </div>
<div style="display:none"> Gerrit-Owner: pespin <pespin@sysmocom.de> </div>
<div style="display:none"> Gerrit-Reviewer: Jenkins Builder </div>
<div style="display:none"> Gerrit-Reviewer: laforge <laforge@osmocom.org> </div>
<div style="display:none"> Gerrit-Reviewer: neels <nhofmeyr@sysmocom.de> </div>
<div style="display:none"> Gerrit-Reviewer: pespin <pespin@sysmocom.de> </div>
<div style="display:none"> Gerrit-MessageType: merged </div>