diff --git a/doc/newsfragments/3005_changed.pre_resources_exception_handling.rst b/doc/newsfragments/3005_changed.pre_resources_exception_handling.rst new file mode 100644 index 000000000..4d009fd15 --- /dev/null +++ b/doc/newsfragments/3005_changed.pre_resources_exception_handling.rst @@ -0,0 +1 @@ +Fix two runtime issues related to driver dependency setting. \ No newline at end of file diff --git a/testplan/testing/base.py b/testplan/testing/base.py index aa1d944ab..4352d5db6 100644 --- a/testplan/testing/base.py +++ b/testplan/testing/base.py @@ -416,33 +416,45 @@ def propagate_tag_indices(self) -> None: self.report.propagate_tag_indices() def _init_context(self) -> None: - if callable(self.cfg.initial_context): - self.resources._initial_context = self.cfg.initial_context() - else: - self.resources._initial_context = self.cfg.initial_context + try: + if callable(self.cfg.initial_context): + self.resources._initial_context = self.cfg.initial_context() + else: + self.resources._initial_context = self.cfg.initial_context + except Exception as e: + self.resources.self_exception = e + raise def _build_environment(self) -> None: # build environment only once in interactive mode if self._env_built: return - if callable(self.cfg.environment): - drivers = self.cfg.environment() - else: - drivers = self.cfg.environment + try: + if callable(self.cfg.environment): + drivers = self.cfg.environment() + else: + drivers = self.cfg.environment + for driver in drivers: + driver.parent = self + driver.cfg.parent = self.cfg + self.resources.add(driver) + except Exception as e: + self.resources.self_exception = e + raise - for driver in drivers: - driver.parent = self - driver.cfg.parent = self.cfg - self.resources.add(driver) self._env_built = True def _set_dependencies(self) -> None: - if callable(self.cfg.dependencies): - deps = parse_dependency(self.cfg.dependencies()) - else: - deps = self.cfg.dependencies - self.resources.set_dependency(deps) + try: + if callable(self.cfg.dependencies): + deps = parse_dependency(self.cfg.dependencies()) + else: + deps = self.cfg.dependencies + self.resources.set_dependency(deps) + except Exception as e: + self.resources.self_exception = e + raise def _start_resource(self) -> None: if len(self.resources) == 0: diff --git a/testplan/testing/environment/base.py b/testplan/testing/environment/base.py index fa1cff576..76dd2f36c 100644 --- a/testplan/testing/environment/base.py +++ b/testplan/testing/environment/base.py @@ -54,9 +54,12 @@ class TestEnvironment(Environment): def __init__(self, parent: Optional["Test"] = None): super().__init__(parent) - self.__dict__["_orig_dependency"]: Optional[DriverDepGraph] = None - self.__dict__["_rt_dependency"]: Optional[DriverDepGraph] = None - self.__dict__["_pocketwatches"]: Dict[str, DriverPocketwatch] = dict() + self.__dict__["self_exception"] = None # Optional[Exception] + self.__dict__["_orig_dependency"] = None # Optional[DriverDepGraph] + self.__dict__["_rt_dependency"] = None # Optional[DriverDepGraph] + self.__dict__[ + "_pocketwatches" + ] = dict() # Dict[str, DriverPocketwatch] def set_dependency(self, dependency: Optional[DriverDepGraph]): if dependency is None: @@ -72,7 +75,7 @@ def set_dependency(self, dependency: Optional[DriverDepGraph]): "while not being declared in `environment` parameter." ) for d in self._resources.values(): - if d.async_start != UNSET: + if d.cfg.async_start != UNSET: raise ValueError( f"`async_start` parameter of driver {d} should not " "be set if driver dependency is specified." diff --git a/testplan/testing/multitest/base.py b/testplan/testing/multitest/base.py index 05c041f7f..e8ed7f586 100644 --- a/testplan/testing/multitest/base.py +++ b/testplan/testing/multitest/base.py @@ -587,10 +587,14 @@ def skip_step(self, step) -> bool: """Check if a step should be skipped.""" if step == self._run_error_handler: return not ( - self.resources.start_exceptions + self.resources.self_exception is not None + or self.resources.start_exceptions or self.resources.stop_exceptions or self._get_error_logs() ) + elif self.resources.self_exception is not None: + # self of status ERROR + return True elif step in ( self._start_resource, self._stop_resource, diff --git a/tests/functional/testplan/runnable/interactive/test_interactive.py b/tests/functional/testplan/runnable/interactive/test_interactive.py index 94ec90ce1..41d1cc51a 100644 --- a/tests/functional/testplan/runnable/interactive/test_interactive.py +++ b/tests/functional/testplan/runnable/interactive/test_interactive.py @@ -430,3 +430,54 @@ def test_abort_handler(): timeout=5, raise_on_timeout=True, ) + + +def test_restart_multitest_w_dependencies(): + s = TCPServer(name="server") + c = TCPClient( + name="client", + host=context("server", "{{host}}"), + port=context("server", "{{port}}"), + ) + mt = MultiTest( + name="Test", + suites=[TCPSuite(0)], + environment=[s, c], + dependencies={s: c}, + after_start=lambda env: env.server.accept_connection(), + ) + + with InteractivePlan( + name="InteractivePlan", + interactive_port=0, + interactive_block=False, + parse_cmdline=False, + logger_level=USER_INFO, + ) as plan: + plan.add(mt) + plan.run() + wait_for_interactive_start(plan) + + plan.interactive.start_test_resources("Test") + plan.interactive.run_test("Test") + assert plan.interactive.test( + "Test" + ).run_result(), "no exceptions in steps" + assert plan.interactive.report[ + "Test" + ].unknown, "env stop remain unknown" + plan.interactive.stop_test_resources("Test") + assert plan.interactive.report["Test"].passed, "env stop succeeded" + + plan.interactive.reset_all_tests() + + plan.interactive.start_test_resources("Test") + plan.interactive.run_test("Test") + assert plan.interactive.test( + "Test" + ).run_result(), "no exceptions in steps" + assert plan.interactive.report[ + "Test" + ].unknown, "env stop remain unknown" + plan.interactive.stop_test_resources("Test") + assert plan.interactive.report["Test"].passed, "env stop succeeded" diff --git a/tests/unit/testplan/testing/multitest/test_multitest.py b/tests/unit/testplan/testing/multitest/test_multitest.py index 5a26a6e61..05733603c 100644 --- a/tests/unit/testplan/testing/multitest/test_multitest.py +++ b/tests/unit/testplan/testing/multitest/test_multitest.py @@ -11,6 +11,7 @@ from testplan.common.utils.thread import Barrier from testplan.testing import common, filtering, multitest, ordering from testplan.testing.multitest import base +from testplan.testing.multitest.driver import Driver MTEST_DEFAULT_PARAMS = { "test_filter": filtering.Filter(), @@ -597,3 +598,26 @@ def test_skip_strategy(skip_strategy, case_count): ) ret = mt.run() assert len(ret.report.entries[0]) == case_count + + +def test_skip_steps(): + s = Driver(name="server") + c = Driver( + name="client", + ) + mt = multitest.MultiTest( + name="Test", + suites=[Suite()], + environment=lambda: [s, c], + dependencies=lambda: {s: c, c: s}, + ) + + mt.run() + assert mt.resources.self_exception is not None + assert all( + map( + lambda x: x in mt.result.step_results, + ["_init_context", "_build_environment", "_set_dependencies"], + ) + ) + assert "_start_resource" not in mt.result.step_results