Skip to content

Commit 6db2263

Browse files
Release Candidate for v2.0.0
* patch-specialize-exception-hear-154: [PATCH] added more doctests (- WIP #179 -) [CI] Minor tweak to tox.ini [PATCH] Applied changes as pre review (- WIP #179 -) [STYLE] Style fix to align docstring with CEP-7 (- WIP PR #179 -) [STYLE] Minor comment space style fix (- WIP #154 -) [DOCUMENTATION] Additional docstrings (- WIP #154 -) [FEATURE] Implemented Custom Shutdown Exception (- WIP #154 -) * HOTFIX-fix-flake8-regression-181: [STYLE] Aligned '.flake8.ini' with CEP-8 and CEP-7 (- WIP #181 -) * testing-coverage: [REGRESSION] minor regression fix in new tests (- WIP #53 & PR #178 -) [CI] Configuration tweak for enhanced TOX in CI (- WIP PR #178 -) [STYLE] Apply suggestions from code review (- WIP PR #178 -) [TESTING] improved coverage slightly (- #53 -)
3 parents 9e670a0 + 65aca58 + 4f6c61f commit 6db2263

File tree

6 files changed

+217
-15
lines changed

6 files changed

+217
-15
lines changed

.flake8.ini

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
[flake8]
2-
select = C,E,F,W,B,B950
2+
select = C,D,E,F,W,B,B950
33
# Ignore specific warnings and errors according to CEP-8 style
4-
extend-ignore =
5-
W191, # Indentation contains tabs
6-
W391, # Blank line at end of file
7-
E117, # Over-indented
8-
D208, # Docstring is over-indented
9-
D203, # 1 blank line required before class docstring - CEP-7
10-
D212, # Multi-line docstring summary should start at the first line - CEP-7
4+
extend-ignore = E117,D203,D208,D212,W191,W391
5+
# CEP-8 Custom Exceptions:
6+
# W191, # Indentation contains tabs
7+
# W391, # Blank line at end of file
8+
# E117, # Over-indented
9+
# D208, # Docstring is over-indented
10+
# D203, # 1 blank line required before class docstring - CEP-7
11+
# D212, # Multi-line docstring summary should start at the first line - CEP-7
1112
# Ignore long lines as specified in CEP-8
1213
max-line-length = 100
1314
extend-exclude =
@@ -20,7 +21,7 @@ extend-exclude =
2021
# There's no value in checking tox directories
2122
.tox,
2223
# This contains our built stuff
23-
build
24+
build,
2425
# Nothing to find in node_modules, ignore them
2526
node_modules,
2627
# This contains our built package for PyPi

.github/workflows/Tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ jobs:
674674
- name: Prep Testing Tox
675675
id: prep-tox
676676
run: |
677-
if [ "$OS" == "ubuntu-latest" ] ; then { apt-get update || true ;} ; wait ; { apt-get install --assume-yes python3.10 python3.11 || echo "::warning file=.github/workflows/Tests.yml,line=621,endLine=624,title=SKIPPED::SKIP Enhanced TOX Tests." ;} ; wait ; fi
677+
if [ "$OS" == "ubuntu-latest" ] ; then { sudo apt-get update || true ;} ; wait ; { sudo apt-get install --assume-yes python3.10 python3.11 || echo "::warning file=.github/workflows/Tests.yml,line=677,endLine=677,title=SKIPPED::SKIP Enhanced TOX Tests." ;} ; wait ; fi
678678
- name: Install dependencies for Tox
679679
run: |
680680
pip install --upgrade "pip>=22.0" "setuptools>=75.0" "wheel>=0.44" "build>=1.2.1";

multicast/hear.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,13 +414,21 @@ def handle(self):
414414
True
415415
>>>
416416
417+
Testcase 3: `handle` requires valid requests or ignores input.
418+
419+
>>> handler.request = ("The Test", multicast.genSocket())
420+
>>> handler.client_address = ("224.0.1.2", 51234)
421+
>>> handler.handle() is None
422+
True
423+
>>>
417424
"""
418425
(data, sock) = self.request
419426
if data is None or not sock:
420427
return # nothing to do -- fail fast.
421428
else:
422-
data = str(data, encoding='utf8') # needed b/c some implementations decode some do not.
423-
print(f"{self.client_address[0]} SAYS: {data.strip()} to ALL")
429+
data = data.decode('utf8') if isinstance(data, bytes) else str(data)
430+
if (_sys.stdout.isatty()): # pragma: no cover
431+
print(f"{self.client_address[0]} SAYS: {data.strip()} to ALL")
424432
if data is not None:
425433
myID = str(sock.getsockname()[0])
426434
if (_sys.stdout.isatty()): # pragma: no cover

tests/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
from tests import test_manifest
106106
from tests import test_build
107107
from tests import test_usage
108+
from tests import test_hear_server
108109
from tests import test_hear_server_activate
109110
from tests import test_hear_cleanup
110111
from tests import test_hear_data_processing
@@ -114,7 +115,8 @@
114115
depends = [
115116
profiling, test_basic, test_deps, test_install_requires, test_build, test_manifest,
116117
test_usage, test_hear_server_activate, test_hear_cleanup, test_fuzz,
117-
test_hear_data_processing, test_exceptions, test_hear_keyboard_interrupt
118+
test_hear_data_processing, test_exceptions, test_hear_keyboard_interrupt,
119+
test_hear_server
118120
]
119121
for unit_test in depends:
120122
try:
@@ -150,6 +152,7 @@
150152
test_install_requires.TestParseRequirements, test_usage.MulticastTestSuite,
151153
test_usage.BasicIntegrationTestSuite, test_hear_server_activate.McastServerActivateTestSuite,
152154
test_hear_cleanup.HearCleanupTestSuite, test_hear_data_processing.RecvDataProcessingTestSuite,
155+
test_hear_server.McastServerTestSuite, test_hear_server.HearUDPHandlerTestSuite,
153156
test_hear_data_processing.HearHandleNoneDataTestSuite,
154157
test_hear_keyboard_interrupt.TestHearKeyboardInterrupt
155158
)

tests/test_fuzz.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def test_multicast_sender_with_random_data(self, data):
126126
self.assertTrue(theResult, fail_fixture)
127127

128128
@given(st.text(alphabet=string.ascii_letters + string.digits, min_size=3, max_size=15))
129-
@settings(deadline=None)
129+
@settings(deadline=300)
130130
def test_invalid_Error_WHEN_cli_called_GIVEN_invalid_fuzz_input(self, text):
131131
"""
132132
Test the multicast CLI's response to invalid fuzzed input.
@@ -169,7 +169,7 @@ def test_invalid_Error_WHEN_cli_called_GIVEN_invalid_fuzz_input(self, text):
169169
self.assertTrue(theResult, fail_fixture)
170170

171171
@given(st.text(alphabet=string.ascii_letters + string.digits, min_size=56, max_size=2048))
172-
@settings(deadline=None)
172+
@settings(deadline=2222)
173173
def test_say_works_WHEN_using_stdin_GIVEN_alnum_of_any_size_fuzz_input(self, text):
174174
"""
175175
Test the multicast send response to valid alnum input.

tests/test_hear_server.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#! /usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
# Python Test Repo Template
5+
# ..................................
6+
# Copyright (c) 2017-2024, Mr. Walls
7+
# ..................................
8+
# Licensed under MIT (the "License");
9+
# you may not use this file except in compliance with the License.
10+
# You may obtain a copy of the License at
11+
# ..........................................
12+
# http://www.github.com/reactive-firewall/python-repo/LICENSE.md
13+
# ..........................................
14+
# Unless required by applicable law or agreed to in writing, software
15+
# distributed under the License is distributed on an "AS IS" BASIS,
16+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
# See the License for the specific language governing permissions and
18+
# limitations under the License.
19+
20+
__module__ = """tests"""
21+
22+
23+
try:
24+
try:
25+
import context
26+
except Exception as _: # pragma: no branch
27+
del _ # skipcq - cleanup any error vars early
28+
from . import context
29+
if context.__name__ is None:
30+
raise ModuleNotFoundError("[CWE-758] Failed to import context") from None
31+
else:
32+
import socket
33+
from context import multicast # pylint: disable=cyclic-import - skipcq: PYL-R0401
34+
from context import unittest
35+
except Exception as err:
36+
raise ImportError("[CWE-758] Failed to import test context") from err
37+
38+
39+
class McastHearTestSuite(context.BasicUsageTestSuite):
40+
41+
__module__ = """tests.test_hear_server"""
42+
43+
__name__ = """tests.test_hear_server.McastHearTestSuite"""
44+
45+
@staticmethod
46+
def get_default_ip():
47+
"""Get the default IP address of the machine.
48+
49+
Returns:
50+
str: The IP address of the default network interface.
51+
52+
Note:
53+
Uses 203.0.113.1 (TEST-NET-3) for RFC 5737 compliance.
54+
Port 59095 is chosen as an arbitrary high port number.
55+
"""
56+
try:
57+
# Create a socket connection to an external address
58+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
59+
# Connect to a public non-routable IP
60+
s.connect(("203.0.113.1", 59095))
61+
# Get the IP address of the default interface
62+
ip = s.getsockname()[0]
63+
except socket.error as e:
64+
raise multicast.exceptions.CommandExecutionError("Failed to determine IP", 69) from e
65+
finally:
66+
s.close()
67+
return ip
68+
69+
70+
class McastServerTestSuite(McastHearTestSuite):
71+
72+
__module__ = """tests.test_hear_server"""
73+
74+
__name__ = """tests.test_hear_server.McastServerTestSuite"""
75+
76+
def test_handle_error_without_stop_in_request(self):
77+
"""
78+
Test McastServer.handle_error with a non-STOP request.
79+
80+
Verifies that the server properly handles requests without
81+
the STOP command and cleans up resources.
82+
"""
83+
theResult = False
84+
fail_fixture = str("""Mock(BLAH) --> Handler-HEAR == error""")
85+
_fixture_port_num = self._the_test_port
86+
try:
87+
self.assertIsNotNone(_fixture_port_num)
88+
self.assertIsInstance(_fixture_port_num, int)
89+
# Create an instance of McastServer
90+
server_address = ('224.0.0.1', _fixture_port_num)
91+
server = multicast.hear.McastServer(server_address, multicast.hear.HearUDPHandler)
92+
client_address = (self.get_default_ip(), _fixture_port_num)
93+
# Mock a request not containing "STOP"
94+
request = (str("Regular message"), multicast.genSocket())
95+
try:
96+
server.handle_error(request, client_address)
97+
finally:
98+
# Clean up
99+
server.server_close()
100+
theResult = (multicast.endSocket(request[1]) is None)
101+
self.assertTrue(theResult, "RESOURCE LEAK")
102+
except Exception as err:
103+
context.debugtestError(err)
104+
self.fail(fail_fixture)
105+
self.assertTrue(theResult, fail_fixture)
106+
107+
def test_handle_error_with_none_request(self):
108+
theResult = False
109+
fail_fixture = str("""Mock(EMPTY) --X Handler-HEAR != Safe""")
110+
_fixture_port_num = self._the_test_port
111+
try:
112+
self.assertIsNotNone(_fixture_port_num)
113+
self.assertIsInstance(_fixture_port_num, int)
114+
# Create an instance of McastServer
115+
server_address = ('224.0.0.1', _fixture_port_num)
116+
server = multicast.hear.McastServer(server_address, multicast.hear.HearUDPHandler)
117+
client_address = (self.get_default_ip(), _fixture_port_num)
118+
# Mock None as a request
119+
request = None
120+
self.assertIsNone(request, "RESOURCE LEAK")
121+
try:
122+
server.handle_error(request, client_address)
123+
finally:
124+
# Clean up
125+
server.server_close()
126+
theResult = (request is None)
127+
except Exception as err:
128+
context.debugtestError(err)
129+
self.fail(fail_fixture)
130+
self.assertTrue(theResult, fail_fixture)
131+
132+
133+
class HearUDPHandlerTestSuite(McastHearTestSuite):
134+
135+
__module__ = """tests.test_hear_server"""
136+
137+
__name__ = """tests.test_hear_server.HearUDPHandlerTestSuite"""
138+
139+
def test_handle_with_none_data_and_sock(self):
140+
fail_fixture = str("""Handler(None, None) --> HEAR == error""")
141+
_fixture_port_num = self._the_test_port
142+
self.assertIsNotNone(_fixture_port_num)
143+
self.assertIsInstance(_fixture_port_num, int)
144+
handler = multicast.hear.HearUDPHandler(
145+
request=(None, None),
146+
client_address=(self.get_default_ip(), _fixture_port_num),
147+
server=None
148+
)
149+
# Should return early without processing
150+
result = handler.handle()
151+
self.assertIsNone(result, fail_fixture)
152+
153+
def test_handle_with_data_none_sock(self):
154+
fail_fixture = str("""Handler(None, None) --> HEAR == error""")
155+
_fixture_port_num = self._the_test_port
156+
self.assertIsNotNone(_fixture_port_num)
157+
self.assertIsInstance(_fixture_port_num, int)
158+
handler = multicast.hear.HearUDPHandler(
159+
request=(b"No-Op", None),
160+
client_address=(self.get_default_ip(), _fixture_port_num),
161+
server=None
162+
)
163+
# Should return early without processing
164+
result = handler.handle()
165+
self.assertIsNone(result, fail_fixture)
166+
167+
def test_handle_with_valid_data_and_sock(self):
168+
sock = multicast.genSocket()
169+
fail_fixture = str("""Handler("The Test", sock) --> HEAR == error""")
170+
_fixture_port_num = self._the_test_port
171+
try:
172+
self.assertIsNotNone(_fixture_port_num)
173+
self.assertIsInstance(_fixture_port_num, int)
174+
handler = multicast.hear.HearUDPHandler(
175+
request=(b"The Test", sock),
176+
client_address=(self.get_default_ip(), _fixture_port_num),
177+
server=None
178+
)
179+
# Should process the message
180+
result = handler.handle()
181+
# Clean up socket
182+
self.assertIsNone(multicast.endSocket(sock), "RESOURCE LEAK")
183+
except Exception as err:
184+
context.debugtestError(err)
185+
self.fail(fail_fixture)
186+
self.assertIsNone(result, fail_fixture)
187+
188+
189+
if __name__ == '__main__':
190+
unittest.main()

0 commit comments

Comments
 (0)