Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/dependencies accept remote driver #1144

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support :py:class:`RemoteDriver <testplan.common.remote.remote_driver.RemoteDriver>` in dependency graph of test environment (the ``dependencies`` parameter).
5 changes: 5 additions & 0 deletions examples/Assertions/Basic/test_plan_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def test_table_namespace(self, env, result):
["Rick", 67],
]

tuple_of_tuples = tuple(tuple(r) for r in list_of_lists)

sample_table = [
["symbol", "amount"],
["AAPL", 12],
Expand All @@ -61,6 +63,9 @@ def test_table_namespace(self, env, result):
display_index=True,
description="Table Log: list of lists",
)
result.table.log(
tuple_of_tuples, description="Table Log: tuple of tuples"
)
result.table.log(list_of_lists[:1], description="Empty table")
result.table.log(
[{"name": "Bob", "age": 32}, {"name": "Susan"}],
Expand Down
12 changes: 7 additions & 5 deletions examples/RemoteDriver/Basic/test_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,22 @@ def main(plan):
driver_cls=TCPServer, # what type of driver
**tcp_server_args, # args to driver class
)
local_tcp_client = TCPClient(
name="client",
host=context("server", "{{host}}"),
port=context("server", "{{port}}"),
)

plan.add(
MultiTest(
name="Remote TCP Server Test",
suites=TCPTestsuite(),
description="Running a TCP Server on remote host",
environment=[
local_tcp_client,
remote_tcp_server,
TCPClient(
name="client",
host=context("server", "{{host}}"),
port=context("server", "{{port}}"),
),
],
dependencies={remote_tcp_server: local_tcp_client},
)
)

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"validators<=0.20.0",
"Pillow",
"plotly",
"pandas", # required by plotly.express
"rpyc",
"matplotlib",
"memoization",
Expand Down
2 changes: 2 additions & 0 deletions testplan/common/utils/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def add_vertex(self, rep: T, val: U) -> bool:
"""
if rep in self.vertices:
return False

self.vertices[rep] = val
self.edges[rep] = dict()
self.indegrees[rep] = 0
Expand All @@ -111,6 +112,7 @@ def add_edge(self, src: T, dst: T, val: V) -> bool:
or dst in self.edges[src]
):
return False

self.edges[src][dst] = val
self.indegrees[dst] += 1
self.outdegrees[src] += 1
Expand Down
6 changes: 3 additions & 3 deletions testplan/common/utils/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ class TableEntry:
"""
Represents a table. Internally represented either
as a ``list`` of ``list`` or a ``list`` of ``dict``.
---
Note tuples are counted as lists here, callers should be able to handle
them.
"""

def __init__(self, source, placeholder=None):
Expand All @@ -15,9 +18,6 @@ def __init__(self, source, placeholder=None):

self._validate_input()

if self._source and isinstance(self._source[0], (list, tuple)):
self._columns = self._source[0]

@property
def columns(self):
"""Get column names."""
Expand Down
6 changes: 2 additions & 4 deletions testplan/runnable/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ def __init__(self, **options):
# when executing unit/functional tests or running in interactive mode.
self._reset_report_uid = not self._is_interactive_run()
self.scheduled_modules = [] # For interactive reload
self.remote_services = {}
self.remote_services: Dict[str, RemoteService] = {}
self.runid_filename = uuid.uuid4().hex
self.define_runpath()
self._runnable_uids = set()
Expand Down Expand Up @@ -573,9 +573,7 @@ def add_remote_service(self, remote_service: RemoteService):
remote_service.start()

def _stop_remote_services(self):

for name, rmt_svc in self.remote_services.items():
self.logger.info("Stopping Remote Server %s", name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why remove this logging

for _, rmt_svc in self.remote_services.items():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.remote_services.values() ?

rmt_svc.stop()

def _clone_task_for_part(self, task_info, _task_arguments, part):
Expand Down
33 changes: 19 additions & 14 deletions testplan/testing/environment/graph.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from copy import copy
from dataclasses import dataclass, field
from itertools import product
from typing import TYPE_CHECKING, Dict, Iterable, List, Set
from typing import TYPE_CHECKING, Iterable, List, Set, Union
from typing_extensions import TypeAlias

from testplan.common.utils.graph import DirectedGraph

if TYPE_CHECKING:
from testplan.common.remote.remote_driver import RemoteDriver
from testplan.testing.multitest.driver import Driver

D: TypeAlias = Union["RemoteDriver", "Driver"]


def _type_err(msg: str) -> TypeError:
return TypeError(
Expand All @@ -16,13 +20,13 @@ def _type_err(msg: str) -> TypeError:


@dataclass
class DriverDepGraph(DirectedGraph[str, "Driver", bool]):
class DriverDepGraph(DirectedGraph[str, D, bool]):
"""
An always acyclic directed graph, also bookkeeping driver starting status.
"""

processing: Set["Driver"] = field(default_factory=set)
processed: List["Driver"] = field(default_factory=list)
processing: Set[D] = field(default_factory=set)
processed: List[D] = field(default_factory=list)

@classmethod
def from_directed_graph(cls, g: DirectedGraph) -> "DriverDepGraph":
Expand All @@ -35,26 +39,26 @@ def from_directed_graph(cls, g: DirectedGraph) -> "DriverDepGraph":
g_ = copy(g)
return cls(g_.vertices, g_.edges, g_.indegrees, g_.outdegrees)

def mark_processing(self, driver: "Driver"):
def mark_processing(self, driver: D):
self.processing.add(driver.uid())

def mark_processed(self, driver: "Driver"):
def mark_processed(self, driver: D):
self.processing.remove(driver.uid())
self.remove_vertex(driver.uid())
self.processed.append(driver.uid())

def mark_failed_to_process(self, driver: "Driver"):
def mark_failed_to_process(self, driver: D):
self.processing.remove(driver.uid())
self.remove_vertex(driver.uid())

def drivers_to_process(self) -> List["Driver"]:
def drivers_to_process(self) -> List[D]:
return [
self.vertices[d]
for d in self.zero_indegrees()
if d not in self.processing
]

def drivers_processing(self) -> List["Driver"]:
def drivers_processing(self) -> List[D]:
return [self.vertices[d] for d in self.processing]

def all_drivers_processed(self) -> bool:
Expand Down Expand Up @@ -102,6 +106,7 @@ def parse_dependency(input: dict) -> DriverDepGraph:
}
"""

from testplan.common.remote.remote_driver import RemoteDriver
from testplan.testing.multitest.driver import Driver

if not isinstance(input, dict):
Expand All @@ -111,7 +116,7 @@ def parse_dependency(input: dict) -> DriverDepGraph:

for k, v in input.items():
if not (
isinstance(k, Driver)
isinstance(k, (Driver, RemoteDriver))
or (
isinstance(k, Iterable)
and all(isinstance(x, Driver) for x in k)
Expand All @@ -122,19 +127,19 @@ def parse_dependency(input: dict) -> DriverDepGraph:
)

if not (
isinstance(v, Driver)
isinstance(v, (Driver, RemoteDriver))
or (
isinstance(v, Iterable)
and all(isinstance(x, Driver) for x in v)
and all(isinstance(x, (Driver, RemoteDriver)) for x in v)
)
):
raise _type_err(
"Driver or flat collection of Driver expected for dict values."
)

if isinstance(k, Driver):
if isinstance(k, (Driver, RemoteDriver)):
k = (k,)
if isinstance(v, Driver):
if isinstance(v, (Driver, RemoteDriver)):
v = (v,)

for s, e in product(k, v):
Expand Down
2 changes: 1 addition & 1 deletion testplan/testing/multitest/entries/stdout/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def get_details(self, entry: base.TableLog) -> str:
if isinstance(cell, bytes):
entry.table[j][i] = str(cell)

return AsciiTable([entry.columns] + entry.table).table
return AsciiTable([entry.columns, *entry.table]).table


@registry.bind(base.DictLog, base.FixLog)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def make_app(app_args, driver_args, name="app"):
),
(
["--mask-sigterm", "parent", "--sleep-time", "5"],
{"stop_timeout": 1, "stop_signal": signal.SIGSEGV},
{"stop_timeout": 1, "stop_signal": signal.SIGKILL},
GoodSuite,
False,
),
Expand Down