Source code for tests.MulticastUDPClient

#! /usr/bin/env python
# -*- coding: utf-8 -*-

# Multicast Python Module (Testing)
# ..................................
# Copyright (c) 2017-2025, Mr. Walls
# ..................................
# Licensed under MIT (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# ..........................................
# https://www.github.com/reactive-firewall/multicast/LICENSE.md
# ..........................................
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# THIS FILE IS A TEST FILE ONLY.

# Disclaimer of Warranties.
# A. YOU EXPRESSLY ACKNOWLEDGE AND AGREE THAT, TO THE EXTENT PERMITTED BY
#    APPLICABLE LAW, USE OF THIS SHELL SCRIPT AND ANY SERVICES PERFORMED
#    BY OR ACCESSED THROUGH THIS SHELL SCRIPT IS AT YOUR SOLE RISK AND
#    THAT THE ENTIRE RISK AS TO SATISFACTORY QUALITY, PERFORMANCE, ACCURACY AND
#    EFFORT IS WITH YOU.
#
# B. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THIS SHELL SCRIPT
#    AND SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE", WITH ALL FAULTS AND
#    WITHOUT WARRANTY OF ANY KIND, AND THE AUTHOR OF THIS SHELL SCRIPT'S LICENSORS
#    (COLLECTIVELY REFERRED TO AS "THE AUTHOR" FOR THE PURPOSES OF THIS DISCLAIMER)
#    HEREBY DISCLAIM ALL WARRANTIES AND CONDITIONS WITH RESPECT TO THIS SHELL SCRIPT
#    SOFTWARE AND SERVICES, EITHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING, BUT
#    NOT LIMITED TO, THE IMPLIED WARRANTIES AND/OR CONDITIONS OF
#    MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE,
#    ACCURACY, QUIET ENJOYMENT, AND NON-INFRINGEMENT OF THIRD PARTY RIGHTS.
#
# C. THE AUTHOR DOES NOT WARRANT AGAINST INTERFERENCE WITH YOUR ENJOYMENT OF THE
#    THE AUTHOR's SOFTWARE AND SERVICES, THAT THE FUNCTIONS CONTAINED IN, OR
#    SERVICES PERFORMED OR PROVIDED BY, THIS SHELL SCRIPT WILL MEET YOUR
#    REQUIREMENTS, THAT THE OPERATION OF THIS SHELL SCRIPT OR SERVICES WILL
#    BE UNINTERRUPTED OR ERROR-FREE, THAT ANY SERVICES WILL CONTINUE TO BE MADE
#    AVAILABLE, THAT THIS SHELL SCRIPT OR SERVICES WILL BE COMPATIBLE OR
#    WORK WITH ANY THIRD PARTY SOFTWARE, APPLICATIONS OR THIRD PARTY SERVICES,
#    OR THAT DEFECTS IN THIS SHELL SCRIPT OR SERVICES WILL BE CORRECTED.
#    INSTALLATION OF THIS THE AUTHOR SOFTWARE MAY AFFECT THE USABILITY OF THIRD
#    PARTY SOFTWARE, APPLICATIONS OR THIRD PARTY SERVICES.
#
# D. YOU FURTHER ACKNOWLEDGE THAT THIS SHELL SCRIPT AND SERVICES ARE NOT
#    INTENDED OR SUITABLE FOR USE IN SITUATIONS OR ENVIRONMENTS WHERE THE FAILURE
#    OR TIME DELAYS OF, OR ERRORS OR INACCURACIES IN, THE CONTENT, DATA OR
#    INFORMATION PROVIDED BY THIS SHELL SCRIPT OR SERVICES COULD LEAD TO
#    DEATH, PERSONAL INJURY, OR SEVERE PHYSICAL OR ENVIRONMENTAL DAMAGE,
#    INCLUDING WITHOUT LIMITATION THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT
#    NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, LIFE SUPPORT OR
#    WEAPONS SYSTEMS.
#
# E. NO ORAL OR WRITTEN INFORMATION OR ADVICE GIVEN BY THE AUTHOR
#    SHALL CREATE A WARRANTY. SHOULD THIS SHELL SCRIPT OR SERVICES PROVE DEFECTIVE,
#    YOU ASSUME THE ENTIRE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
#
#    Limitation of Liability.
# F. TO THE EXTENT NOT PROHIBITED BY APPLICABLE LAW, IN NO EVENT SHALL THE AUTHOR
#    BE LIABLE FOR PERSONAL INJURY, OR ANY INCIDENTAL, SPECIAL, INDIRECT OR
#    CONSEQUENTIAL DAMAGES WHATSOEVER, INCLUDING, WITHOUT LIMITATION, DAMAGES
#    FOR LOSS OF PROFITS, CORRUPTION OR LOSS OF DATA, FAILURE TO TRANSMIT OR
#    RECEIVE ANY DATA OR INFORMATION, BUSINESS INTERRUPTION OR ANY OTHER
#    COMMERCIAL DAMAGES OR LOSSES, ARISING OUT OF OR RELATED TO YOUR USE OR
#    INABILITY TO USE THIS SHELL SCRIPT OR SERVICES OR ANY THIRD PARTY
#    SOFTWARE OR APPLICATIONS IN CONJUNCTION WITH THIS SHELL SCRIPT OR
#    SERVICES, HOWEVER CAUSED, REGARDLESS OF THE THEORY OF LIABILITY (CONTRACT,
#    TORT OR OTHERWISE) AND EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
#    POSSIBILITY OF SUCH DAMAGES. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
#    OR LIMITATION OF LIABILITY FOR PERSONAL INJURY, OR OF INCIDENTAL OR
#    CONSEQUENTIAL DAMAGES, SO THIS LIMITATION MAY NOT APPLY TO YOU. In no event
#    shall THE AUTHOR's total liability to you for all damages (other than as may
#    be required by applicable law in cases involving personal injury) exceed
#    the amount of five dollars ($5.00). The foregoing limitations will apply
#    even if the above stated remedy fails of its essential purpose.
################################################################################

__module__ = "tests"
"""This is a testing related stand-alone utilities module.

This module provides test fixtures and utilities for testing multicast communication.
It includes a basic UDP client implementation and handler for testing multicast
functionality.

Classes:
	MCastClient: Test fixture for multicast client operations.
	MyUDPHandler: UDP request handler for echo functionality.

Functions:
	main: Entry point for test operations.

Example:
	>>> from tests.MulticastUDPClient import MCastClient
	>>> client = MCastClient(grp_addr='224.0.0.1', src_port=59259)
	>>> isinstance(client._source_port, int)
	True
	>>>

"""

__name__ = "tests.MulticastUDPClient"  # skipcq: PYL-W0622

try:
	import sys
	if not hasattr(sys, 'modules') or not sys.modules:  # pragma: no branch
		raise ModuleNotFoundError(
			"[CWE-440] OMG! sys.modules is not available or empty."
		) from None
except ImportError as _cause:  # pragma: no branch
	raise ImportError("[CWE-440] Unable to import sys module.") from _cause

try:
	import socket
	import socketserver
except ImportError as _cause:  # pragma: no branch
	raise ImportError("[CWE-758] Test module failed completely.") from _cause

try:
	import random
except ImportError as _cause:  # pragma: no branch
	raise ModuleNotFoundError("[CWE-758] Test module failed to randomize.") from _cause


class MCastClient(object):  # skipcq: PYL-R0205
	"""
	For use as a test fixture.

	A trivial implementation of a socket-based object with a function
	named say. The say function of this class performs a send and recv on a given socket and
	then prints out simple diognostics about the content sent and any response received.

	Testing:

		First some test fixtures:

		>>> import socket as socket
		>>> import random as random
		>>>

	Testcase 0: test the class MCastClient is.
		A: Test that the MulticastUDPClient component is importable.
		B: Test that the MCastClient class is importable.

		>>> import tests.MulticastUDPClient
		>>> from MulticastUDPClient import MCastClient as MCastClient
		>>> MCastClient is not None
		True
		>>>

	Testcase 1: Test the class MCastClient has a say function.
		A: Test that the MulticastUDPClient component is importable.
		B: Test that the MCastClient class is importable.
		C: Test that the MCastClient class has the function named say.

		>>> import tests.MulticastUDPClient
		>>> from MulticastUDPClient import MCastClient as MCastClient
		>>> MCastClient is not None
		True
		>>> MCastClient.say is not None
		True
		>>> type(MCastClient.say)
		<class 'function'>
		>>>


	"""

	__module__ = "tests.MulticastUDPClient.MCastClient"

	_group_addr = None
	"""The multicast group address."""

	_source_port = None
	"""The source port for the client."""

	# skipcq: TCV-002
	def __init__(self, *args, **kwargs) -> None:  # pragma: no cover
		"""
		Initialize a MCastClient object with optional group address and source port.

		The client can be initialized with or without specifying a group address and source port.
		If no source port is provided, a random port between 50000 and 59999 is generated.

		Args:
			*args: Variable length argument list (Unused).
			**kwargs: Arbitrary keyword arguments.
				- grp_addr (str): The multicast group address.
				- src_port (int): The source port for the client.

		Meta Testing:

			First set up test fixtures by importing test context.

				>>> import tests.MulticastUDPClient as MulticastUDPClient
				>>> from MulticastUDPClient import MCastClient as MCastClient
				>>>

			Testcase 1: Initialization without any arguments.

				>>> client = MCastClient()
				>>> 50000 <= client._source_port <= 59999
				True
				>>> client._group_addr is None
				True
				>>>

			Testcase 2: Initialization with only group address.

				>>> tst_args = {}
				>>> client = MCastClient(grp_addr="224.0.0.1")
				>>> client._group_addr
				'224.0.0.1'
				>>> 50000 <= client._source_port <= 59999
				True
				>>>

			Testcase 3: Initialization with only source port.

				>>> client = MCastClient(src_port=55555)
				>>> client._source_port
				55555
				>>> client._group_addr is None
				True
				>>>

			Testcase 4: Initialization with both group address and source port.

				>>> client = MCastClient(grp_addr="224.0.0.2", src_port=55556)
				>>> client._group_addr
				'224.0.0.2'
				>>> client._source_port
				55556
				>>>


		"""
		# skipcq: TCV-002
		if "grp_addr" in kwargs:  # pragma: no branch
			self._group_addr = kwargs.get("grp_addr", None)  # skipcq: PTC-W0039 - ensure None
		if "src_port" in kwargs:  # pragma: no branch
			self._source_port = kwargs.get("src_port", 0)
		else:  # pragma: no branch
			self._source_port = int(
				50000 + (
					int(random.SystemRandom().randbytes(int(60000).__sizeof__()).hex(), 16) % 9999
				)
			)

	# skipcq: TCV-002
[docs] @staticmethod def say(address: str, port: int, sock: socket.socket, msg: str) -> None: # pragma: no cover """ Send a message to a specified multicast address and port, then receive and print it. This function sends a UTF-8 encoded message to the specified multicast address and port using the provided connection. It then waits for a response, decodes it, and prints both the sent and received messages. Args: address (str): The multicast group address to send the message to. port (int): The port number to send the message to. sock (socket.socket): The socket connection to use for sending and receiving. msg (str): The message to be sent. Returns: None Prints: The sent message and the received response. Meta Testing: First, set up test fixtures: >>> import unittest.mock >>> from MulticastUDPClient import MCastClient >>> Testcase 1: Test sending and receiving a message. >>> mock_socket = unittest.mock.Mock() >>> mock_socket.recv.return_value = b"Response received" >>> client = MCastClient() >>> client.say("224.0.0.1", 59991, mock_socket, "Test message") Sent: Test message Received: Response received >>> Testcase 2: Test sending a 'STOP' message. >>> mock_socket.recv.return_value = b"Stopped" >>> client.say("224.0.0.1", 59991, mock_socket, "STOP") Sent: STOP Received: Stopped >>> Note: This function assumes that the connection is already properly configured for multicast communication. """ # skipcq: TCV-002 sock.sendto(bytes(msg + "\n", "utf-8"), (address, port)) # pragma: no cover received = str(sock.recv(1024), "utf-8") # pragma: no cover sp = " " * 4 # pragma: no cover if (sys.stdout.isatty()): # pragma: no cover print(f"Sent: {sp}{msg}") # skipcq: PYL-C0209 - must remain compatible print(f"Received: {received}") # skipcq: PYL-C0209 - must remain compatible
class MyUDPHandler(socketserver.BaseRequestHandler): """ Subclasses socketserver.BaseRequestHandler to handle echo functionality. Simplifies testing by echoing back the received string data in uppercase, after printing the sender's IP address. Meta Testing: First set up test fixtures by importing test context. >>> import tests.MulticastUDPClient as MulticastUDPClient >>> from MulticastUDPClient import MyUDPHandler as MyUDPHandler >>> Testcase 1: MyUDPHandler should be automatically imported. >>> MyUDPHandler.__name__ is not None True >>> """ __module__ = "tests.MulticastUDPClient.MyUDPHandler" # skipcq: TCV-002
[docs] def handle(self) -> None: # pragma: no cover """ Handle incoming UDP requests. This method overrides the `handle` method from `socketserver.BaseRequestHandler` to process incoming UDP messages. It receives a message from a client, echoes it back in uppercase, and prints diagnostic information. Meta Testing: First set up test fixtures by importing test context. >>> import socket >>> import threading >>> from tests.MulticastUDPClient import MyUDPHandler >>> Testcase 1: Test handling a simple message. >>> import socketserver >>> server = socketserver.UDPServer(('localhost', 0), MyUDPHandler) >>> threading.Thread(target=server.serve_forever, daemon=True).start() >>> client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) >>> client_socket.sendto("hello world\n", server.server_address) >>> data, _ = client_socket.recvfrom(1024) >>> data b'HELLO WORLD\n' >>> server.shutdown() >>> Testcase 2: Test handling an empty message. >>> import socketserver >>> server = socketserver.UDPServer(('localhost', 0), MyUDPHandler) >>> threading.Thread(target=server.serve_forever, daemon=True).start() >>> client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) >>> client_socket.sendto("\n", server.server_address) >>> data, _ = client_socket.recvfrom(1024) >>> data b'\n' >>> server.shutdown() >>> Note: This method assumes that the incoming request tuple contains a string and a socket, as per `socketserver.BaseRequestHandler` for datagram services. """ # skipcq: TCV-002 data = self.request[0].strip() # pragma: no cover sock = self.request[1] # pragma: no cover print(f"{self.client_address[0]} wrote: ") # pragma: no cover print(data) # pragma: no cover sock.sendto(data.upper(), self.client_address) # pragma: no cover
# skipcq: TCV-002
[docs] def main() -> None: # pragma: no cover """ The main test operations. Testing: First some test fixtures: >>> import socket as socket >>> import random as random >>> Testcase 0: test the function main is. A: Test that the MulticastUDPClient component is importable. B: Test that the MulticastUDPClient has a main function. >>> import tests.MulticastUDPClient >>> tests.MulticastUDPClient.main is not None True >>> type(tests.MulticastUDPClient.main) <class 'function'> >>> """ # skipcq: TCV-002 HOST, PORT = "224.0.0.1", 59991 # pragma: no cover data = "TEST This is a test" # pragma: no cover sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # pragma: no cover tsts_fxr = MCastClient() # pragma: no cover print(str((HOST, PORT))) # pragma: no cover tsts_fxr.say(HOST, PORT, sock, data) # pragma: no cover tsts_fxr.say(HOST, PORT, sock, "STOP") # pragma: no cover
if __name__ == "__main__": # pragma: no branch main() # skipcq: TCV-002 # skipcq: PYL-R1722 exit(0) # skipcq: PYL-R1722 -- intentionally allow overwriteing exit for testing.