1
0
mirror of https://github.com/craigerl/aprsd.git synced 2024-09-27 07:36:40 -04:00

Added threads functions to APRSDPluginBase

This patch updates the APRSDPluginBase class to include
standard methods for allowing plugins to create, start, stop
threads that the plugin might need/use.  Also update the aprsd-dev
to correctly start the threads and stop them for testing plugin
functionality.
Also added more unit tests and fake objects for unit tests.
This commit is contained in:
Hemna 2021-08-20 15:21:47 -04:00
parent 5f4cf89733
commit 86777d838c
5 changed files with 242 additions and 47 deletions

View File

@ -179,7 +179,6 @@ def test_plugin(
"""APRSD Plugin test app.""" """APRSD Plugin test app."""
config = utils.parse_config(config_file) config = utils.parse_config(config_file)
email.CONFIG = config
setup_logging(config, loglevel, False) setup_logging(config, loglevel, False)
LOG.info(f"Test APRSD PLugin version: {aprsd.__version__}") LOG.info(f"Test APRSD PLugin version: {aprsd.__version__}")
@ -189,12 +188,19 @@ def test_plugin(
client.Client(config) client.Client(config)
pm = plugin.PluginManager(config) pm = plugin.PluginManager(config)
obj = pm._create_class(plugin_path, plugin.APRSDMessagePluginBase, config=config) obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
packet = {"from": fromcall, "message_text": message, "msgNo": 1} packet = {"from": fromcall, "message_text": message, "msgNo": 1}
<<<<<<< HEAD
reply = obj.run(packet) reply = obj.run(packet)
LOG.info(f"Result = '{reply}'") LOG.info(f"Result = '{reply}'")
=======
reply = obj.filter(packet)
# Plugin might have threads, so lets stop them so we can exit.
obj.stop_threads()
LOG.info("Result = '{}'".format(reply))
>>>>>>> f8f84c4 (Added threads functions to APRSDPluginBase)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -8,7 +8,7 @@ import os
import re import re
import threading import threading
from aprsd import messaging, packets from aprsd import messaging, packets, threads
import pluggy import pluggy
from thesmuggler import smuggle from thesmuggler import smuggle
@ -51,10 +51,40 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
message_counter = 0 message_counter = 0
version = "1.0" version = "1.0"
# Holds the list of APRSDThreads that the plugin creates
threads = []
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
self.message_counter = 0 self.message_counter = 0
self.setup() self.setup()
self.threads = self.create_threads()
if self.threads:
self.start_threads()
def start_threads(self):
if self.threads:
if not isinstance(self.threads, list):
self.threads = [self.threads]
try:
for thread in self.threads:
if isinstance(thread, threads.APRSDThread):
thread.start()
else:
LOG.error(
"Can't start thread {}:{}, Must be a child "
"of aprsd.threads.APRSDThread".format(
self,
thread,
),
)
except Exception:
LOG.error(
"Failed to start threads for plugin {}".format(
self,
),
)
@property @property
def message_count(self): def message_count(self):
@ -69,6 +99,16 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
"""Do any plugin setup here.""" """Do any plugin setup here."""
pass pass
def create_threads(self):
"""Gives the plugin writer the ability start a background thread."""
return []
def stop_threads(self):
"""Stop any threads this plugin might have created."""
for thread in self.threads:
if isinstance(thread, threads.APRSDThread):
thread.stop()
@hookimpl @hookimpl
@abc.abstractmethod @abc.abstractmethod
def filter(self, packet): def filter(self, packet):
@ -129,11 +169,6 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
"""The regex to match from the caller""" """The regex to match from the caller"""
raise NotImplementedError raise NotImplementedError
@property
def version(self):
"""Version"""
raise NotImplementedError
@hookimpl @hookimpl
def filter(self, packet): def filter(self, packet):
result = None result = None
@ -225,7 +260,7 @@ class PluginManager:
): ):
""" """
Method to create a class from a fqn python string. Method to create a class from a fqn python string.
:param module_class_string: full name of the class to create an object of :param module_class_string: full name of the class to create an object
:param super_cls: expected super class for validity, None if bypass :param super_cls: expected super class for validity, None if bypass
:param kwargs: parameters to pass :param kwargs: parameters to pass
:return: :return:

View File

@ -46,14 +46,16 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
LOG.error("EmailPlugin DISABLED!!!!") LOG.error("EmailPlugin DISABLED!!!!")
else: else:
self.enabled = True self.enabled = True
email_thread = APRSDEmailThread(
msg_queues=threads.msg_queues,
config=self.config,
)
email_thread.start()
else: else:
LOG.info("Email services not enabled.") LOG.info("Email services not enabled.")
def create_threads(self):
if self.enabled:
return APRSDEmailThread(
msg_queues=threads.msg_queues,
config=self.config,
)
@trace.trace @trace.trace
def process(self, packet): def process(self, packet):
LOG.info("Email COMMAND") LOG.info("Email COMMAND")

66
tests/fake.py Normal file
View File

@ -0,0 +1,66 @@
from aprsd import packets, plugin, threads
FAKE_MESSAGE_TEXT = "fake MeSSage"
FAKE_FROM_CALLSIGN = "KFART"
FAKE_TO_CALLSIGN = "KMINE"
def fake_packet(
fromcall=FAKE_FROM_CALLSIGN,
tocall=FAKE_TO_CALLSIGN,
message=None,
msg_number=None,
message_format=packets.PACKET_TYPE_MESSAGE,
):
packet = {
"from": fromcall,
"addresse": tocall,
"format": message_format,
}
if message:
packet["message_text"] = message
if msg_number:
packet["msgNo"] = msg_number
return packet
class FakeBaseNoThreadsPlugin(plugin.APRSDPluginBase):
version = "1.0"
def filter(self, packet):
return None
def process(self, packet):
return "process"
class FakeThread(threads.APRSDThread):
def __init__(self):
super().__init__("FakeThread")
def loop(self):
return True
class FakeBaseThreadsPlugin(plugin.APRSDPluginBase):
version = "1.0"
def filter(self, packet):
return None
def process(self, packet):
return "process"
def create_threads(self):
return FakeThread()
class FakeRegexCommandPlugin(plugin.APRSDRegexCommandPluginBase):
version = "1.0"
command_regex = "^[fF]"
command_name = "fake"
def process(self, packet):
return FAKE_MESSAGE_TEXT

View File

@ -4,7 +4,7 @@ from unittest import mock
import pytz import pytz
import aprsd import aprsd
from aprsd import messaging, stats, utils from aprsd import messaging, packets, stats, utils
from aprsd.fuzzyclock import fuzzy from aprsd.fuzzyclock import fuzzy
from aprsd.plugins import fortune as fortune_plugin from aprsd.plugins import fortune as fortune_plugin
from aprsd.plugins import ping as ping_plugin from aprsd.plugins import ping as ping_plugin
@ -12,36 +12,122 @@ from aprsd.plugins import query as query_plugin
from aprsd.plugins import time as time_plugin from aprsd.plugins import time as time_plugin
from aprsd.plugins import version as version_plugin from aprsd.plugins import version as version_plugin
from . import fake
class TestPlugin(unittest.TestCase): class TestPlugin(unittest.TestCase):
def setUp(self): def setUp(self):
self.fromcall = "KFART" self.fromcall = fake.FAKE_FROM_CALLSIGN
self.ack = 1 self.ack = 1
self.config = utils.DEFAULT_CONFIG_DICT self.config = utils.DEFAULT_CONFIG_DICT
self.config["ham"]["callsign"] = self.fromcall self.config["ham"]["callsign"] = self.fromcall
self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN
# Inintialize the stats object with the config # Inintialize the stats object with the config
stats.APRSDStats(self.config) stats.APRSDStats(self.config)
def fake_packet(self, fromcall="KFART", message=None, msg_number=None): @mock.patch.object(fake.FakeBaseNoThreadsPlugin, "process")
packet = { def test_base_plugin_no_threads(self, mock_process):
"from": fromcall, p = fake.FakeBaseNoThreadsPlugin(self.config)
"addresse": self.config["aprs"]["login"],
"format": "message",
}
if message:
packet["message_text"] = message
if msg_number: expected = []
packet["msgNo"] = msg_number actual = p.create_threads()
self.assertEqual(expected, actual)
return packet expected = "1.0"
actual = p.version
self.assertEqual(expected, actual)
expected = 0
actual = p.message_counter
self.assertEqual(expected, actual)
expected = None
actual = p.filter(fake.fake_packet())
self.assertEqual(expected, actual)
mock_process.assert_not_called()
@mock.patch.object(fake.FakeBaseThreadsPlugin, "create_threads")
def test_base_plugin_threads_created(self, mock_create):
fake.FakeBaseThreadsPlugin(self.config)
mock_create.assert_called_once()
def test_base_plugin_threads(self):
p = fake.FakeBaseThreadsPlugin(self.config)
actual = p.create_threads()
self.assertTrue(isinstance(actual, fake.FakeThread))
p.stop_threads()
@mock.patch.object(fake.FakeRegexCommandPlugin, "process")
def test_regex_base_not_called(self, mock_process):
p = fake.FakeRegexCommandPlugin(self.config)
packet = fake.fake_packet(message="a")
expected = None
actual = p.filter(packet)
self.assertEqual(expected, actual)
mock_process.assert_not_called()
packet = fake.fake_packet(tocall="notMe", message="f")
expected = None
actual = p.filter(packet)
self.assertEqual(expected, actual)
mock_process.assert_not_called()
packet = fake.fake_packet(
message="F",
message_format=packets.PACKET_TYPE_MICE,
)
expected = None
actual = p.filter(packet)
self.assertEqual(expected, actual)
mock_process.assert_not_called()
packet = fake.fake_packet(
message="f",
message_format=packets.PACKET_TYPE_ACK,
)
expected = None
actual = p.filter(packet)
self.assertEqual(expected, actual)
mock_process.assert_not_called()
@mock.patch.object(fake.FakeRegexCommandPlugin, "process")
def test_regex_base_assert_called(self, mock_process):
p = fake.FakeRegexCommandPlugin(self.config)
packet = fake.fake_packet(message="f")
p.filter(packet)
mock_process.assert_called_once()
def test_regex_base_process_called(self):
p = fake.FakeRegexCommandPlugin(self.config)
packet = fake.fake_packet(message="f")
expected = fake.FAKE_MESSAGE_TEXT
actual = p.filter(packet)
self.assertEqual(expected, actual)
packet = fake.fake_packet(message="F")
expected = fake.FAKE_MESSAGE_TEXT
actual = p.filter(packet)
self.assertEqual(expected, actual)
packet = fake.fake_packet(message="fake")
expected = fake.FAKE_MESSAGE_TEXT
actual = p.filter(packet)
self.assertEqual(expected, actual)
packet = fake.fake_packet(message="FAKE")
expected = fake.FAKE_MESSAGE_TEXT
actual = p.filter(packet)
self.assertEqual(expected, actual)
class TestFortunePlugin(TestPlugin):
@mock.patch("shutil.which") @mock.patch("shutil.which")
def test_fortune_fail(self, mock_which): def test_fortune_fail(self, mock_which):
fortune = fortune_plugin.FortunePlugin(self.config) fortune = fortune_plugin.FortunePlugin(self.config)
mock_which.return_value = None mock_which.return_value = None
expected = "Fortune command not installed" expected = "Fortune command not installed"
packet = self.fake_packet(message="fortune") packet = fake.fake_packet(message="fortune")
actual = fortune.filter(packet) actual = fortune.filter(packet)
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
@ -54,13 +140,15 @@ class TestPlugin(unittest.TestCase):
mock_output.return_value = "Funny fortune" mock_output.return_value = "Funny fortune"
expected = "Funny fortune" expected = "Funny fortune"
packet = self.fake_packet(message="fortune") packet = fake.fake_packet(message="fortune")
actual = fortune.filter(packet) actual = fortune.filter(packet)
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
class TestQueryPlugin(TestPlugin):
@mock.patch("aprsd.messaging.MsgTrack.flush") @mock.patch("aprsd.messaging.MsgTrack.flush")
def test_query_flush(self, mock_flush): def test_query_flush(self, mock_flush):
packet = self.fake_packet(message="!delete") packet = fake.fake_packet(message="!delete")
query = query_plugin.QueryPlugin(self.config) query = query_plugin.QueryPlugin(self.config)
expected = "Deleted ALL pending msgs." expected = "Deleted ALL pending msgs."
@ -72,7 +160,7 @@ class TestPlugin(unittest.TestCase):
def test_query_restart_delayed(self, mock_restart): def test_query_restart_delayed(self, mock_restart):
track = messaging.MsgTrack() track = messaging.MsgTrack()
track.track = {} track.track = {}
packet = self.fake_packet(message="!4") packet = fake.fake_packet(message="!4")
query = query_plugin.QueryPlugin(self.config) query = query_plugin.QueryPlugin(self.config)
expected = "No pending msgs to resend" expected = "No pending msgs to resend"
@ -87,6 +175,8 @@ class TestPlugin(unittest.TestCase):
actual = query.filter(packet) actual = query.filter(packet)
mock_restart.assert_called_once() mock_restart.assert_called_once()
class TestTimePlugins(TestPlugin):
@mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz") @mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz")
@mock.patch("aprsd.plugins.time.TimePlugin._get_utcnow") @mock.patch("aprsd.plugins.time.TimePlugin._get_utcnow")
def test_time(self, mock_utcnow, mock_localtz): def test_time(self, mock_utcnow, mock_localtz):
@ -104,8 +194,7 @@ class TestPlugin(unittest.TestCase):
fake_time.tm_sec = 13 fake_time.tm_sec = 13
time = time_plugin.TimePlugin(self.config) time = time_plugin.TimePlugin(self.config)
packet = self.fake_packet( packet = fake.fake_packet(
fromcall="KFART",
message="location", message="location",
msg_number=1, msg_number=1,
) )
@ -115,8 +204,7 @@ class TestPlugin(unittest.TestCase):
cur_time = fuzzy(h, m, 1) cur_time = fuzzy(h, m, 1)
packet = self.fake_packet( packet = fake.fake_packet(
fromcall="KFART",
message="time", message="time",
msg_number=1, msg_number=1,
) )
@ -128,6 +216,8 @@ class TestPlugin(unittest.TestCase):
actual = time.filter(packet) actual = time.filter(packet)
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
class TestPingPlugin(TestPlugin):
@mock.patch("time.localtime") @mock.patch("time.localtime")
def test_ping(self, mock_time): def test_ping(self, mock_time):
fake_time = mock.MagicMock() fake_time = mock.MagicMock()
@ -138,8 +228,7 @@ class TestPlugin(unittest.TestCase):
ping = ping_plugin.PingPlugin(self.config) ping = ping_plugin.PingPlugin(self.config)
packet = self.fake_packet( packet = fake.fake_packet(
fromcall="KFART",
message="location", message="location",
msg_number=1, msg_number=1,
) )
@ -157,8 +246,7 @@ class TestPlugin(unittest.TestCase):
+ str(s).zfill(2) + str(s).zfill(2)
) )
packet = self.fake_packet( packet = fake.fake_packet(
fromcall="KFART",
message="Ping", message="Ping",
msg_number=1, msg_number=1,
) )
@ -166,21 +254,21 @@ class TestPlugin(unittest.TestCase):
expected = ping_str(h, m, s) expected = ping_str(h, m, s)
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
packet = self.fake_packet( packet = fake.fake_packet(
fromcall="KFART",
message="ping", message="ping",
msg_number=1, msg_number=1,
) )
actual = ping.filter(packet) actual = ping.filter(packet)
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
class TestVersionPlugin(TestPlugin):
@mock.patch("aprsd.plugin.PluginManager.get_plugins") @mock.patch("aprsd.plugin.PluginManager.get_plugins")
def test_version(self, mock_get_plugins): def test_version(self, mock_get_plugins):
expected = f"APRSD ver:{aprsd.__version__} uptime:0:0:0" expected = f"APRSD ver:{aprsd.__version__} uptime:0:0:0"
version = version_plugin.VersionPlugin(self.config) version = version_plugin.VersionPlugin(self.config)
packet = self.fake_packet( packet = fake.fake_packet(
fromcall="KFART",
message="No", message="No",
msg_number=1, msg_number=1,
) )
@ -188,16 +276,14 @@ class TestPlugin(unittest.TestCase):
actual = version.filter(packet) actual = version.filter(packet)
self.assertEqual(None, actual) self.assertEqual(None, actual)
packet = self.fake_packet( packet = fake.fake_packet(
fromcall="KFART",
message="version", message="version",
msg_number=1, msg_number=1,
) )
actual = version.filter(packet) actual = version.filter(packet)
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
packet = self.fake_packet( packet = fake.fake_packet(
fromcall="KFART",
message="Version", message="Version",
msg_number=1, msg_number=1,
) )