Skip to content

Commit 858948d

Browse files
committed
fix duplication for testmsg cog
1 parent f73f121 commit 858948d

File tree

7 files changed

+144
-33
lines changed

7 files changed

+144
-33
lines changed

.vscode/settings.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,25 @@
33
"tests"
44
],
55
"python.testing.unittestEnabled": false,
6-
"python.testing.pytestEnabled": true
6+
"python.testing.pytestEnabled": true,
7+
"workbench.colorCustomizations": {
8+
"activityBar.activeBackground": "#1e593f",
9+
"activityBar.background": "#1e593f",
10+
"activityBar.foreground": "#e7e7e7",
11+
"activityBar.inactiveForeground": "#e7e7e799",
12+
"activityBarBadge.background": "#221030",
13+
"activityBarBadge.foreground": "#e7e7e7",
14+
"commandCenter.border": "#e7e7e799",
15+
"sash.hoverBorder": "#1e593f",
16+
"statusBar.background": "#113324",
17+
"statusBar.foreground": "#e7e7e7",
18+
"statusBarItem.hoverBackground": "#1e593f",
19+
"statusBarItem.remoteBackground": "#113324",
20+
"statusBarItem.remoteForeground": "#e7e7e7",
21+
"titleBar.activeBackground": "#113324",
22+
"titleBar.activeForeground": "#e7e7e7",
23+
"titleBar.inactiveBackground": "#11332499",
24+
"titleBar.inactiveForeground": "#e7e7e799"
25+
},
26+
"peacock.color": "#113324"
727
}

bridger/cogs/testmsg.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@
2727

2828

2929
class TestMsg(commands.GroupCog, name="testmsg"):
30+
__test__ = False # Disable pytest discovery for this cog
3031
queue = SimpleMemoryCache()
3132

3233
def __init__(self, bot: commands.Bot, discord_channel_id: int, influx_reader: InfluxReader):
3334
self.bot = bot
3435
self.discord_channel_id = discord_channel_id
3536
self.discord_channel = None
3637
self.influx_reader = influx_reader
37-
self.deduplicator = PacketDeduplicator(maxlen=100)
38+
self.deduplicator = PacketDeduplicator(maxlen=100, use_gateway_id=True)
3839

3940
@commands.Cog.listener(name="on_ready")
4041
async def on_ready(self):

bridger/deduplication.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@
66

77

88
class PacketDeduplicator:
9-
def __init__(self, maxlen: int = 100):
9+
def __init__(self, maxlen: int = 100, use_gateway_id: bool = False):
1010
self.message_queue = deque(maxlen=maxlen)
11+
self.use_gateway_id = use_gateway_id
1112

1213
def is_duplicate(self, service_envelope: ServiceEnvelope) -> bool:
1314
packet_id = service_envelope.packet.id
1415
gateway_id = service_envelope.gateway_id
1516

16-
if packet_id in self.message_queue:
17+
if self.use_gateway_id:
18+
key = (gateway_id, packet_id)
19+
else:
20+
key = packet_id
21+
22+
if key in self.message_queue:
1723
logger.bind(envelope_id=packet_id).opt(colors=True).debug(
1824
f"Packet <yellow>{packet_id}</yellow> from <green>{gateway_id}</green> already in queue"
1925
)
@@ -23,7 +29,14 @@ def is_duplicate(self, service_envelope: ServiceEnvelope) -> bool:
2329

2430
def mark_processed(self, service_envelope: ServiceEnvelope) -> None:
2531
packet_id = service_envelope.packet.id
26-
self.message_queue.append(packet_id)
32+
gateway_id = service_envelope.gateway_id
33+
34+
if self.use_gateway_id:
35+
key = (gateway_id, packet_id)
36+
else:
37+
key = packet_id
38+
39+
self.message_queue.append(key)
2740

2841
def should_process(self, service_envelope: ServiceEnvelope) -> bool:
2942
if self.is_duplicate(service_envelope):

tests/test_dataclasses.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66

77

88
@dataclass
9-
class TestNodeWithNodeId(NodeMixin):
9+
class NodeWithNodeId(NodeMixin):
1010
"""Test class with node_id attribute"""
1111

1212
node_id: int
1313

1414

1515
@dataclass
16-
class TestNodeWithFrom(NodeMixin):
16+
class NodeWithFrom(NodeMixin):
1717
"""Test class with _from attribute (like TelemetryPoint)"""
1818

1919
_from: int
@@ -24,38 +24,38 @@ class TestNodeMixin:
2424

2525
def test_node_hex_id_with_bang_from_node_id(self):
2626
"""Test hex ID conversion from node_id attribute"""
27-
node = TestNodeWithNodeId(node_id=439041101) # 0x1a2b3c4d
27+
node = NodeWithNodeId(node_id=439041101) # 0x1a2b3c4d
2828
assert node.node_hex_id_with_bang == "!1a2b3c4d"
2929
assert node.node_hex_id_without_bang == "1a2b3c4d"
3030
assert node.color == "2b3c4d"
3131

3232
def test_node_hex_id_with_bang_from_from_attribute(self):
3333
"""Test hex ID conversion from _from attribute"""
34-
node = TestNodeWithFrom(_from=439041101) # 0x1a2b3c4d
34+
node = NodeWithFrom(_from=439041101) # 0x1a2b3c4d
3535
assert node.node_hex_id_with_bang == "!1a2b3c4d"
3636
assert node.node_hex_id_without_bang == "1a2b3c4d"
3737
assert node.color == "2b3c4d"
3838

3939
def test_node_hex_id_padding(self):
4040
"""Test that hex IDs are properly zero-padded to 8 characters"""
41-
node = TestNodeWithNodeId(node_id=255) # 0xff
41+
node = NodeWithNodeId(node_id=255) # 0xff
4242
assert node.node_hex_id_with_bang == "!000000ff"
4343
assert node.node_hex_id_without_bang == "000000ff"
4444

4545
def test_node_hex_id_large_numbers(self):
4646
"""Test hex ID conversion with large numbers"""
47-
node = TestNodeWithNodeId(node_id=4294967295) # 0xffffffff
47+
node = NodeWithNodeId(node_id=4294967295) # 0xffffffff
4848
assert node.node_hex_id_with_bang == "!ffffffff"
4949
assert node.node_hex_id_without_bang == "ffffffff"
5050

5151
def test_node_hex_id_no_attributes_raises_error(self):
5252
"""Test that missing required attributes raises AttributeError"""
5353

5454
@dataclass
55-
class TestNodeEmpty(NodeMixin):
55+
class NodeEmpty(NodeMixin):
5656
other_field: str
5757

58-
node = TestNodeEmpty(other_field="test")
58+
node = NodeEmpty(other_field="test")
5959

6060
with pytest.raises(AttributeError) as exc_info:
6161
_ = node.node_hex_id_with_bang
@@ -66,25 +66,25 @@ def test_priority_from_attribute_over_node_id(self):
6666
"""Test that _from attribute takes priority over node_id"""
6767

6868
@dataclass
69-
class TestNodeBoth(NodeMixin):
69+
class NodeBoth(NodeMixin):
7070
_from: int
7171
node_id: int
7272

73-
node = TestNodeBoth(_from=439041101, node_id=123456) # Should use _from
73+
node = NodeBoth(_from=439041101, node_id=123456) # Should use _from
7474
assert node.node_hex_id_with_bang == "!1a2b3c4d"
7575

7676
def test_color_property_various_scenarios(self):
7777
"""Test color property extraction in various scenarios"""
7878
# Test with short hex (should take last 6 chars after padding)
79-
node1 = TestNodeWithNodeId(node_id=255) # 0x000000ff
79+
node1 = NodeWithNodeId(node_id=255) # 0x000000ff
8080
assert node1.color == "0000ff"
8181

8282
# Test with full hex
83-
node2 = TestNodeWithNodeId(node_id=0xFFA2B3C4)
83+
node2 = NodeWithNodeId(node_id=0xFFA2B3C4)
8484
assert node2.color == "a2b3c4"
8585

8686
# Test with direct node_id input
87-
node3 = TestNodeWithNodeId(node_id=0x12345678)
87+
node3 = NodeWithNodeId(node_id=0x12345678)
8888
assert node3.color == "345678"
8989

9090

tests/test_deduplication.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ def deduplicator():
1010
return PacketDeduplicator(maxlen=3)
1111

1212

13+
@pytest.fixture
14+
def deduplicator_with_gateway_id():
15+
return PacketDeduplicator(maxlen=3, use_gateway_id=True)
16+
17+
1318
@pytest.fixture
1419
def service_envelope():
1520
envelope = MagicMock()
@@ -130,3 +135,58 @@ def test_bounded_deque_allows_reprocessing_evicted_packets(self, deduplicator):
130135
assert deduplicator.should_process(envelopes[0])
131136
# Packet 4 should still be in queue and be duplicate
132137
assert not deduplicator.should_process(envelopes[4])
138+
139+
140+
class TestPacketDeduplicatorWithGatewayId:
141+
def test_init_with_gateway_id(self, deduplicator_with_gateway_id):
142+
assert deduplicator_with_gateway_id.use_gateway_id is True
143+
assert deduplicator_with_gateway_id.message_queue.maxlen == 3
144+
145+
def test_mark_processed_adds_to_queue_with_gateway_id(self, deduplicator_with_gateway_id, service_envelope):
146+
deduplicator_with_gateway_id.mark_processed(service_envelope)
147+
assert ("!1a2b3c4d", 12345) in deduplicator_with_gateway_id.message_queue
148+
149+
def test_is_duplicate_with_gateway_id_not_in_queue(self, deduplicator_with_gateway_id, service_envelope):
150+
deduplicator_with_gateway_id.message_queue.append(("!different", 99999))
151+
assert not deduplicator_with_gateway_id.is_duplicate(service_envelope)
152+
153+
def test_is_duplicate_with_gateway_id_in_queue(self, deduplicator_with_gateway_id, service_envelope):
154+
deduplicator_with_gateway_id.message_queue.append(("!1a2b3c4d", 12345))
155+
assert deduplicator_with_gateway_id.is_duplicate(service_envelope)
156+
157+
def test_should_process_same_packet_different_gateway_with_gateway_id(
158+
self, deduplicator_with_gateway_id, service_envelope, service_envelope_different_gateway
159+
):
160+
assert deduplicator_with_gateway_id.should_process(service_envelope)
161+
assert deduplicator_with_gateway_id.should_process(service_envelope_different_gateway)
162+
163+
def test_should_process_duplicate_packet_same_gateway_with_gateway_id(
164+
self, deduplicator_with_gateway_id, service_envelope
165+
):
166+
deduplicator_with_gateway_id.mark_processed(service_envelope)
167+
assert not deduplicator_with_gateway_id.should_process(service_envelope)
168+
169+
def test_bounded_deque_behavior_with_gateway_id(self, deduplicator_with_gateway_id):
170+
# Fill queue to capacity (maxlen=3)
171+
for i in range(3):
172+
envelope = MagicMock()
173+
envelope.packet.id = i
174+
envelope.gateway_id = "!test"
175+
deduplicator_with_gateway_id.mark_processed(envelope)
176+
177+
assert len(deduplicator_with_gateway_id.message_queue) == 3
178+
assert ("!test", 0) in deduplicator_with_gateway_id.message_queue
179+
assert ("!test", 1) in deduplicator_with_gateway_id.message_queue
180+
assert ("!test", 2) in deduplicator_with_gateway_id.message_queue
181+
182+
# Add one more, should evict the oldest (0)
183+
envelope = MagicMock()
184+
envelope.packet.id = 3
185+
envelope.gateway_id = "!test"
186+
deduplicator_with_gateway_id.mark_processed(envelope)
187+
188+
assert len(deduplicator_with_gateway_id.message_queue) == 3
189+
assert ("!test", 0) not in deduplicator_with_gateway_id.message_queue
190+
assert ("!test", 1) in deduplicator_with_gateway_id.message_queue
191+
assert ("!test", 2) in deduplicator_with_gateway_id.message_queue
192+
assert ("!test", 3) in deduplicator_with_gateway_id.message_queue

tests/test_http.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ async def test_client(aiohttp_client):
1212
mock_instance = AsyncMock()
1313
mock_device.return_value = mock_instance
1414
app = create_app()
15+
app["device"] = mock_instance # Set state before starting client to avoid deprecation warning
1516
client = await aiohttp_client(app)
16-
app["device"] = mock_instance
1717
yield client
1818

1919

tests/test_testmsg.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,21 @@ def test_init_creates_deduplicator(self, testmsg_cog):
5858

5959
def test_deduplicator_processes_unique_message(self, testmsg_cog, mock_service_envelope):
6060
assert testmsg_cog.deduplicator.should_process(mock_service_envelope)
61-
assert 12345 in testmsg_cog.deduplicator.message_queue
61+
assert ("!1a2b3c4d", 12345) in testmsg_cog.deduplicator.message_queue
6262

63-
def test_deduplicator_skips_duplicate_message(self, testmsg_cog, mock_service_envelope, mock_service_envelope_duplicate):
63+
def test_deduplicator_processes_same_packet_different_gateway(
64+
self, testmsg_cog, mock_service_envelope, mock_service_envelope_duplicate
65+
):
6466
assert testmsg_cog.deduplicator.should_process(mock_service_envelope)
65-
assert not testmsg_cog.deduplicator.should_process(mock_service_envelope_duplicate)
67+
assert testmsg_cog.deduplicator.should_process(mock_service_envelope_duplicate)
6668

6769
def test_deduplicator_processes_different_messages(
6870
self, testmsg_cog, mock_service_envelope, mock_service_envelope_different
6971
):
7072
assert testmsg_cog.deduplicator.should_process(mock_service_envelope)
7173
assert testmsg_cog.deduplicator.should_process(mock_service_envelope_different)
72-
assert 12345 in testmsg_cog.deduplicator.message_queue
73-
assert 67890 in testmsg_cog.deduplicator.message_queue
74+
assert ("!1a2b3c4d", 12345) in testmsg_cog.deduplicator.message_queue
75+
assert ("!1a2b3c4d", 67890) in testmsg_cog.deduplicator.message_queue
7476

7577
@patch("bridger.cogs.testmsg.PBPacketProcessor")
7678
def test_mqtt_processing_skips_duplicate_packets(self, mock_processor_class, testmsg_cog, mock_service_envelope):
@@ -108,7 +110,7 @@ def test_mqtt_processing_handles_unique_packets(self, mock_processor_class, test
108110
assert processor.data.text == "!test message"
109111

110112
def test_deduplicator_bounded_queue_behavior(self, testmsg_cog):
111-
testmsg_cog.deduplicator = PacketDeduplicator(maxlen=3)
113+
testmsg_cog.deduplicator = PacketDeduplicator(maxlen=3, use_gateway_id=True)
112114

113115
for i in range(3):
114116
envelope = MagicMock()
@@ -117,28 +119,28 @@ def test_deduplicator_bounded_queue_behavior(self, testmsg_cog):
117119
testmsg_cog.deduplicator.mark_processed(envelope)
118120

119121
assert len(testmsg_cog.deduplicator.message_queue) == 3
120-
assert 0 in testmsg_cog.deduplicator.message_queue
121-
assert 1 in testmsg_cog.deduplicator.message_queue
122-
assert 2 in testmsg_cog.deduplicator.message_queue
122+
assert ("!test", 0) in testmsg_cog.deduplicator.message_queue
123+
assert ("!test", 1) in testmsg_cog.deduplicator.message_queue
124+
assert ("!test", 2) in testmsg_cog.deduplicator.message_queue
123125

124126
envelope = MagicMock()
125127
envelope.packet.id = 3
126128
envelope.gateway_id = "!test"
127129
testmsg_cog.deduplicator.mark_processed(envelope)
128130

129131
assert len(testmsg_cog.deduplicator.message_queue) == 3
130-
assert 0 not in testmsg_cog.deduplicator.message_queue
131-
assert 1 in testmsg_cog.deduplicator.message_queue
132-
assert 2 in testmsg_cog.deduplicator.message_queue
133-
assert 3 in testmsg_cog.deduplicator.message_queue
132+
assert ("!test", 0) not in testmsg_cog.deduplicator.message_queue
133+
assert ("!test", 1) in testmsg_cog.deduplicator.message_queue
134+
assert ("!test", 2) in testmsg_cog.deduplicator.message_queue
135+
assert ("!test", 3) in testmsg_cog.deduplicator.message_queue
134136

135137
@patch("bridger.deduplication.logger")
136138
def test_deduplicator_logs_duplicate_detection(self, mock_logger, testmsg_cog, mock_service_envelope):
137139
testmsg_cog.deduplicator.should_process(mock_service_envelope)
138140
testmsg_cog.deduplicator.is_duplicate(mock_service_envelope)
139141
mock_logger.bind.assert_called_with(envelope_id=12345)
140142

141-
def test_integration_deduplication_prevents_discord_spam(self, testmsg_cog):
143+
def test_integration_deduplication_processes_different_gateways(self, testmsg_cog):
142144
envelope1 = MagicMock()
143145
envelope1.packet.id = 99999
144146
envelope1.gateway_id = "!gateway1"
@@ -150,5 +152,20 @@ def test_integration_deduplication_prevents_discord_spam(self, testmsg_cog):
150152
should_process_first = testmsg_cog.deduplicator.should_process(envelope1)
151153
assert should_process_first
152154

155+
should_process_second = testmsg_cog.deduplicator.should_process(envelope2)
156+
assert should_process_second
157+
158+
def test_integration_deduplication_prevents_true_duplicates(self, testmsg_cog):
159+
envelope1 = MagicMock()
160+
envelope1.packet.id = 99999
161+
envelope1.gateway_id = "!gateway1"
162+
163+
envelope2 = MagicMock()
164+
envelope2.packet.id = 99999
165+
envelope2.gateway_id = "!gateway1"
166+
167+
should_process_first = testmsg_cog.deduplicator.should_process(envelope1)
168+
assert should_process_first
169+
153170
should_process_second = testmsg_cog.deduplicator.should_process(envelope2)
154171
assert not should_process_second

0 commit comments

Comments
 (0)