Driver
Simultaneous Start-up
- Required files:
test_plan.py
#!/usr/bin/env python
"""
This example is to demonstrate driver scheduling following dependencies.
"""
import sys
from testplan import test_plan
from testplan.common.utils.context import context
from testplan.report.testing.styles import Style, StyleEnum
from testplan.testing.multitest import MultiTest
from testplan.testing.multitest.driver.tcp import TCPServer, TCPClient
from suites import MultiTCPSuite
OUTPUT_STYLE = Style(StyleEnum.ASSERTION_DETAIL, StyleEnum.ASSERTION_DETAIL)
# Hard-coding `pdf_path`, 'stdout_style' and 'pdf_style' so that the
# downloadable example gives meaningful and presentable output.
# NOTE: this programmatic arguments passing approach will cause Testplan
# to ignore any command line arguments related to that functionality.
@test_plan(
name="TCPConnections",
pdf_path="report.pdf",
stdout_style=OUTPUT_STYLE,
pdf_style=OUTPUT_STYLE,
)
def main(plan):
"""
Testplan decorated main function to add and execute 2 MultiTests.
:return: Testplan result object.
:rtype: ``testplan.base.TestplanResult``
"""
server_1 = TCPServer(name="server_1")
client_1 = TCPClient(
name="client_1",
host=context("server_1", "{{host}}"),
port=context("server_1", "{{port}}"),
)
server_2 = TCPServer(name="server_2")
client_2 = TCPClient(
name="client_2",
host=context("server_2", "{{host}}"),
port=context("server_2", "{{port}}"),
)
client_3 = TCPClient(
name="client_3",
host=context("server_2", "{{host}}"),
port=context("server_2", "{{port}}"),
)
# If driver A is a dependency for driver B to start, we put driver A in the key
# of dependencies dictionary and driver B as its corresponding value, so
# visually driver A appears before driver B.
# Here server_1 and server_2 will be started simutaneously to reduce the
# overall test running time while not violating the dependencies.
plan.add(
MultiTest(
name="MultiTCPDrivers",
suites=[MultiTCPSuite()],
environment=[
server_1,
server_2,
client_1,
client_2,
client_3,
],
dependencies={
server_1: client_1,
server_2: (client_2, client_3),
},
)
)
if __name__ == "__main__":
res = main()
print("Exiting code: {}".format(res.exit_code))
sys.exit(res.exit_code)
suites.py
"""Tests TCP communication among multiple servers and multiple clients."""
from testplan.testing.multitest import testsuite, testcase
@testsuite
class MultiTCPSuite:
"""TCP tests for multiple servers and multiple clients."""
@testcase
def test_send_and_receive_msg(self, env, result):
"""
Client sends a message, server received and responds back.
"""
env.client_1.send_text("zhengshan xiaozhong")
env.server_1.accept_connection()
result.equal("zhengshan xiaozhong", env.server_1.receive_text())
env.server_1.send_text("good black tea")
result.equal("good black tea", env.client_1.receive_text())
env.client_2.send_text("whisky")
conn_2 = env.server_2.accept_connection()
env.client_3.send_text("soda water")
conn_3 = env.server_2.accept_connection()
result.equal("whisky", env.server_2.receive_text(conn_idx=conn_2))
result.equal("soda water", env.server_2.receive_text(conn_idx=conn_3))
env.server_2.send_text("good highball", conn_idx=conn_2)
env.server_2.send_text("good highball", conn_idx=conn_3)
result.equal("good highball", env.client_2.receive_text())
result.equal("good highball", env.client_3.receive_text())
Environment Builder
- Required files:
test_plan.py
#!/usr/bin/env python
"""
This example demonstrates usage of callable object to construct environment,
intial_context and dependencies for a multitest at runtime.
"""
import sys
from testplan import test_plan
from testplan.testing.multitest import MultiTest
from env_builder import EnvBuilder
from suites import TestOneClient, TestTwoClients
@test_plan(name="EnvBuilderExample")
def main(plan):
env_builder1 = EnvBuilder("One Client", ["client1", "server1"])
env_builder2 = EnvBuilder("Two Clients", ["client1", "client2", "server1"])
plan.add(
MultiTest(
name="TestOneClient",
suites=[TestOneClient()],
environment=env_builder1.build_env,
dependencies=env_builder1.build_deps,
initial_context=env_builder1.init_ctx,
)
)
plan.add(
MultiTest(
name="TestTwoClients",
suites=[TestTwoClients()],
environment=env_builder2.build_env,
dependencies=env_builder2.build_deps,
initial_context=env_builder2.init_ctx,
)
)
if __name__ == "__main__":
res = main()
print("Exiting code: {}".format(res.exit_code))
sys.exit(res.exit_code)
suites.py
from testplan.testing.multitest import testsuite, testcase
@testsuite
class TestOneClient:
def setup(self, env, result):
result.log(f"Testing with [{env.env_name}] env")
env.server1.accept_connection()
@testcase
def test_send_and_receive_msg(self, env, result):
env.client1.send_text("hi server")
result.equal("hi server", env.server1.receive_text())
env.server1.send_text("hi client")
result.equal("hi client", env.client1.receive_text())
@testsuite
class TestTwoClients:
def setup(self, env, result):
result.log(f"Testing with [{env.env_name}] env")
env.client1.connect()
self.conn1 = env.server1.accept_connection()
env.client2.connect()
self.conn2 = env.server1.accept_connection()
@testcase
def test_send_and_receive_msg(self, env, result):
env.client1.send_text("hi server from client1")
env.client2.send_text("hi server from client2")
result.equal(
"hi server from client1",
env.server1.receive_text(conn_idx=self.conn1),
)
result.equal(
"hi server from client2",
env.server1.receive_text(conn_idx=self.conn2),
)
env.server1.send_text("hi client1", conn_idx=self.conn1)
env.server1.send_text("hi client2", conn_idx=self.conn2)
result.equal("hi client1", env.client1.receive_text())
result.equal("hi client2", env.client2.receive_text())
env_builder.py
"""
This demonstrates one possible way of implementing of EnvBuilder class.
"""
from testplan.common.utils.context import context
from testplan.testing.multitest.driver.tcp import TCPClient, TCPServer
class EnvBuilder:
def __init__(self, env_name: str, drivers: list):
"""
:param env_name: name of this env builder
:param drivers: list of drivers to be created
"""
self.env_name = env_name
self.drivers = drivers
self.client_auto_connect = False if len(self.drivers) == 3 else True
self._client1 = None
self._client2 = None
self._server1 = None
def build_env(self):
return [getattr(self, driver_name) for driver_name in self.drivers]
def init_ctx(self):
return {"env_name": self.env_name}
def build_deps(self):
if len(self.drivers) == 2:
return {self.server1: self.client1}
elif len(self.drivers) == 3:
return {self.server1: (self.client1, self.client2)}
@property
def client1(self):
if not self._client1:
self._client1 = TCPClient(
name="client1",
host=context("server1", "{{host}}"),
port=context("server1", "{{port}}"),
connect_at_start=self.client_auto_connect,
)
return self._client1
@property
def client2(self):
if not self._client2:
self._client2 = TCPClient(
name="client2",
host=context("server1", "{{host}}"),
port=context("server1", "{{port}}"),
connect_at_start=self.client_auto_connect,
)
return self._client2
@property
def server1(self):
if not self._server1:
self._server1 = TCPServer(name="server1")
return self._server1
Driver Connection
- Required files:
test_plan.py
#!/usr/bin/env python
"""
This example is to showcase the driver connection functionality.
"""
import os
import re
import sys
from drivers import (
CustomTCPClient,
WritingDriver,
ReadingDriver,
UnconnectedDriver,
)
from testplan import test_plan
from testplan.common.utils.context import context
from testplan.testing.multitest import MultiTest
from testplan.testing.multitest.driver.tcp import TCPServer
def environment():
"""
MultiTest environment that will be made available within the testcases.
"""
server = TCPServer(name="server")
client = CustomTCPClient(
name="client",
host=context("server", "{{host}}"),
port=context("server", "{{port}}"),
)
writer_name = "WritingDriver"
writer_regexps = [
re.compile(r".*Writing to file: (?P<file_path>.*)"),
]
writer = WritingDriver(
name=writer_name,
pre_args=[sys.executable],
binary=os.path.join(
os.path.dirname(os.path.abspath(__file__)), "writer.py"
),
log_regexps=writer_regexps,
)
reader_regexps = [
re.compile(r".*Reading from file: (?P<file_path>.*)"),
]
reader = ReadingDriver(
name="ReadingDriver",
pre_args=[sys.executable],
binary=os.path.join(
os.path.dirname(os.path.abspath(__file__)), "reader.py"
),
args=[context(writer_name, "{{file_path}}")],
log_regexps=reader_regexps,
)
unconnected = UnconnectedDriver("unconnected_driver")
return [server, client, writer, reader, unconnected]
@test_plan(name="DriverConnectionExample", driver_info=True)
def main(plan):
"""
Testplan decorated main function to add and execute MultiTests.
:return: Testplan result object.
:rtype: ``testplan.base.TestplanResult``
"""
test = MultiTest(
name="DriverConnectionExample",
suites=[],
environment=environment(),
)
plan.add(test)
if __name__ == "__main__":
res = main()
print("Exiting code: {}".format(res.exit_code))
sys.exit(res.exit_code)
custom_connection.py
"""
Example for creating new custom connections
"""
from typing import List
from testplan.testing.multitest.driver.connection import (
BaseConnectionInfo,
BaseDriverConnectionGroup,
BaseConnectionExtractor,
Direction,
)
class CustomConnectionInfo(BaseConnectionInfo):
@property
def connection_rep(self):
# this is the value by which drivers will be matched
# by default, this is f"self.protocol}//{self.identifier}
return f"custom://{self.identifier}"
def promote_to_connection(self):
return CustomDriverConnectionGroup.from_connection_info(self)
class CustomDriverConnectionGroup(BaseDriverConnectionGroup):
@classmethod
def from_connection_info(
cls, driver_connection_info: CustomConnectionInfo
):
# Add any custom logic here
# For example, to add a dummy driver
conn = super(CustomDriverConnectionGroup, cls).from_connection_info(
driver_connection_info
)
conn.in_drivers["Dummy Driver"].add("")
return conn
def add_driver_if_in_connection(
self, driver_name: str, driver_connection_info: CustomConnectionInfo
):
# Define logic on how to match drivers here
# Default behavior for predefined connections is to match based on the connection_rep
if self.connection_rep == driver_connection_info.connection_rep:
# Append the identifier
# For port based connection, ports are the identifier
# For file based connections, read/write are the identifier
self.out_drivers[driver_name].add("Read")
return True
return False
class CustomConnectionExtractor(BaseConnectionExtractor):
def extract_connection(self, driver) -> List[CustomConnectionInfo]:
return [
CustomConnectionInfo(
protocol="custom",
identifier=driver.name,
direction=Direction.CONNECTING,
)
]
writer.py
"""
Sample python script that writes to a file
"""
import os
import sys
import logging
logging.basicConfig(stream=sys.stdout, format="%(message)s")
class Writer:
def __init__(self):
self._logger = logging.getLogger()
self._logger.setLevel(logging.INFO)
self.file = os.path.join(os.getcwd(), "test.txt")
def loop(self):
with open(self.file, "w"):
self._logger.info("Writing to file: %s", self.file)
while True:
continue
if __name__ == "__main__":
writer = Writer()
writer.loop()
reader.py
"""
Sample python script that reads from a file
"""
import sys
import logging
logging.basicConfig(stream=sys.stdout, format="%(message)s")
class Reader:
def __init__(self, file):
self._logger = logging.getLogger()
self._logger.setLevel(logging.INFO)
self.file = file
def loop(self):
with open(self.file, "r"):
self._logger.info("Reading from file: %s", self.file)
while True:
continue
if __name__ == "__main__":
sys.stderr.flush()
_, file_path = sys.argv
reader = Reader(file_path)
reader.loop()
drivers.py
from testplan.testing.multitest.driver.base import Driver
from testplan.testing.multitest.driver.app import App
from testplan.testing.multitest.driver.tcp import TCPClient
from testplan.testing.multitest.driver.connection import (
Direction,
Protocol,
ConnectionExtractor,
)
from custom_connection import CustomConnectionExtractor
class CustomTCPClient(TCPClient):
# override EXTRACTORS
EXTRACTORS = [
ConnectionExtractor(Protocol.TCP, Direction.CONNECTING),
CustomConnectionExtractor(),
]
class WritingDriver(App):
"""
Inherits the generic ``testplan.testing.multitest.driver.app.App`` driver
and expose file path read from log extracts.
"""
def __init__(self, **options):
super(WritingDriver, self).__init__(**options)
self.file_path = None
def post_start(self):
"""
Store file_path to be made available in its context
so that reading driver can connect to it.
"""
super(WritingDriver, self).post_start()
self.file_path = self.extracts["file_path"]
class ReadingDriver(App):
"""
Inherits the generic ``testplan.testing.multitest.driver.app.App`` driver
"""
pass
class UnconnectedDriver(Driver):
pass