Skip to content

Commit

Permalink
Fix/issues related to driver dependency (#1109)
Browse files Browse the repository at this point in the history
* fix two bugs

- fix exception handling within pre_resource steps
- fix driver flag setting when driver dependency presents

* have post_run_checks warning written to log file

* address review comments; add support of dependencies=None

* really address review comments; bugfix
  • Loading branch information
zhenyu-ms authored Jul 16, 2024
1 parent f072070 commit 2adf385
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 12 deletions.
5 changes: 3 additions & 2 deletions doc/en/multitest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ A MultiTest instance can be constructed from the following parameters:
starting concurrently based on the dependencies. Drivers included in
``environment`` while not presented in ``dependencies`` will be started
concurrently at the very beginning. Using empty dict here to instruct all
drivers to start concurrently. Click :ref:`here <multitest_drivers>` for more
information.
drivers to start concurrently. Using ``None`` here to specify the legacy
scheduling with ``async_start`` flags being set. Click :ref:`here <multitest_drivers>`
for more information.

* **Runtime Information**: The environment always contains a member called
``runtime_info`` which contains information about the current state of the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix some runtime issues when driver dependency is set. ``dependencies`` argument of ``MultiTest`` now supports ``None``.
3 changes: 2 additions & 1 deletion testplan/common/report/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def __exit__(self, exc_type, exc_value, tb):
str(exc_value),
)
self.report.status_override = Status.SKIPPED
return True
elif issubclass(exc_type, self.exception_classes):
# Custom exception message with extra args
exc_msg = "".join(
Expand All @@ -79,7 +80,7 @@ def __exit__(self, exc_type, exc_value, tb):

if self.fail:
self.report.status_override = Status.ERROR
return True
return True


@total_ordering
Expand Down
5 changes: 4 additions & 1 deletion testplan/runnable/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,6 @@ def add_post_resource_steps(self):
self._add_step(self._invoke_exporters)
self._add_step(self._post_exporters)
self._add_step(self._stop_remote_services)
self._add_step(self._close_file_logger)
super(TestRunner, self).add_post_resource_steps()
self._add_step(self._stop_resource_monitor)

Expand Down Expand Up @@ -1126,6 +1125,10 @@ def _wait_ongoing(self):
break
time.sleep(self.cfg.active_loop_sleep)

def _post_run_checks(self, start_threads, start_procs):
super()._post_run_checks(start_threads, start_procs)
self._close_file_logger()

def _create_result(self):
"""Fetch task result from executors and create a full test result."""
step_result = True
Expand Down
4 changes: 2 additions & 2 deletions testplan/testing/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def get_options(cls):
[Or(Resource, RemoteDriver)], validate_func()
),
ConfigOption("dependencies", default=None): Or(
Use(parse_dependency), validate_func()
None, Use(parse_dependency), validate_func()
),
ConfigOption("initial_context", default={}): Or(
dict, validate_func()
Expand Down Expand Up @@ -430,11 +430,11 @@ def _build_environment(self) -> None:
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)

self._env_built = True

def _set_dependencies(self) -> None:
Expand Down
11 changes: 6 additions & 5 deletions testplan/testing/environment/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import copy
import time
from contextlib import contextmanager
from dataclasses import dataclass
from typing import TYPE_CHECKING, Dict, Optional
from typing import TYPE_CHECKING, Optional

from testplan.common.config import UNSET
from testplan.common.entity.base import Environment
Expand Down Expand Up @@ -54,9 +55,9 @@ 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__["_orig_dependency"] = None # Optional[DriverDepGraph]
self.__dict__["_rt_dependency"] = None # Optional[DriverDepGraph]
self.__dict__["_pocketwatches"] = {} # Dict[str, DriverPocketwatch]

def set_dependency(self, dependency: Optional[DriverDepGraph]):
if dependency is None:
Expand All @@ -72,7 +73,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."
Expand Down
8 changes: 8 additions & 0 deletions testplan/testing/multitest/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,14 @@ def skip_step(self, step) -> bool:
or self.resources.stop_exceptions
or self._get_error_logs()
)
elif "_start_resource" not in self.result.step_results and any(
map(
lambda x: isinstance(x, Exception),
self.result.step_results.values(),
)
):
# exc before _start_resource
return True
elif step in (
self._start_resource,
self._stop_resource,
Expand Down
51 changes: 51 additions & 0 deletions tests/functional/testplan/runnable/interactive/test_interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
12 changes: 11 additions & 1 deletion tests/unit/testplan/common/report/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def test_exception_logger_suppression():
@disable_log_propagation(LOGGER)
def test_exception_logger_reraise():
"""
ExceptionLoggerBase should raise the exception without logging
ExceptionLogger.* should raise the exception without logging
if it doesn't match `exception_classes`.
"""
rep = DummyReport()
Expand All @@ -50,6 +50,16 @@ def test_exception_logger_reraise():
with rep.logged_exceptions(IndexError):
raise KeyError("bar") # raised

rep = DummyReportGroup()

with pytest.raises(KeyError):

with rep.logged_exceptions(IndexError):
raise IndexError("foo") # suppressed

with rep.logged_exceptions(IndexError):
raise KeyError("bar") # raised


class TestReport:
def test_equality(self):
Expand Down
17 changes: 17 additions & 0 deletions tests/unit/testplan/testing/environment/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,23 @@ def test_legacy_driver_scheduling(mocker):
env.stop()


def test_set_dependency_none(mocker):
m = mocker.Mock()
env = TestEnvironment()
env.add(MockDriver("a", m, async_start=True))
env.add(MockDriver("b", m))
env.set_dependency(None)
env.start()
m.assert_has_calls(
[
mocker.call.pre("a"),
mocker.call.pre("b"),
mocker.call.post("b"),
mocker.call.post("a"),
]
)


def test_bad_config_exception():
env = TestEnvironment()
env.add(MockDriver("a", async_start=True))
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/testplan/testing/multitest/test_multitest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -597,3 +598,23 @@ 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 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

0 comments on commit 2adf385

Please sign in to comment.