|
15 | 15 | from websockets.frames import CloseCode, Frame, Opcode |
16 | 16 | from websockets.protocol import CLIENT, SERVER, Protocol, State |
17 | 17 | from websockets.sync.connection import * |
| 18 | +from websockets.sync.connection import broadcast |
18 | 19 |
|
19 | 20 | from ..protocol import RecordingProtocol |
20 | 21 | from ..utils import MS |
@@ -54,6 +55,11 @@ def assertFrameSent(self, frame): |
54 | 55 | self.wait_for_remote_side() |
55 | 56 | self.assertEqual(self.remote_connection.protocol.get_frames_rcvd(), [frame]) |
56 | 57 |
|
| 58 | + def assertFramesSent(self, frames): |
| 59 | + """Check that several frames were sent.""" |
| 60 | + self.wait_for_remote_side() |
| 61 | + self.assertEqual(self.remote_connection.protocol.get_frames_rcvd(), frames) |
| 62 | + |
57 | 63 | def assertNoFrameSent(self): |
58 | 64 | """Check that no frame was sent.""" |
59 | 65 | self.wait_for_remote_side() |
@@ -992,6 +998,153 @@ def test_unexpected_failure_in_send_context(self, send_text): |
992 | 998 | self.connection.send("😀") |
993 | 999 | self.assertIsInstance(raised.exception.__cause__, AssertionError) |
994 | 1000 |
|
| 1001 | + # Test broadcast. |
| 1002 | + |
| 1003 | + def test_broadcast_text(self): |
| 1004 | + """broadcast broadcasts a text message.""" |
| 1005 | + broadcast([self.connection], "😀") |
| 1006 | + self.assertFrameSent(Frame(Opcode.TEXT, "😀".encode())) |
| 1007 | + |
| 1008 | + def test_broadcast_text_reports_no_errors(self): |
| 1009 | + """broadcast broadcasts a text message without raising exceptions.""" |
| 1010 | + broadcast([self.connection], "😀", raise_exceptions=True) |
| 1011 | + self.assertFrameSent(Frame(Opcode.TEXT, "😀".encode())) |
| 1012 | + |
| 1013 | + def test_broadcast_binary(self): |
| 1014 | + """broadcast broadcasts a binary message.""" |
| 1015 | + broadcast([self.connection], b"\x01\x02\xfe\xff") |
| 1016 | + self.assertFrameSent(Frame(Opcode.BINARY, b"\x01\x02\xfe\xff")) |
| 1017 | + |
| 1018 | + def test_broadcast_binary_reports_no_errors(self): |
| 1019 | + """broadcast broadcasts a binary message without raising exceptions.""" |
| 1020 | + broadcast([self.connection], b"\x01\x02\xfe\xff", raise_exceptions=True) |
| 1021 | + self.assertFrameSent(Frame(Opcode.BINARY, b"\x01\x02\xfe\xff")) |
| 1022 | + |
| 1023 | + def test_broadcast_text_from_bytes(self): |
| 1024 | + """broadcast broadcasts a text message from bytes.""" |
| 1025 | + broadcast([self.connection], "😀".encode(), text=True) |
| 1026 | + self.assertFrameSent(Frame(Opcode.TEXT, "😀".encode())) |
| 1027 | + |
| 1028 | + def test_broadcast_binary_from_str(self): |
| 1029 | + """broadcast broadcasts a binary message from a str.""" |
| 1030 | + broadcast([self.connection], "😀", text=False) |
| 1031 | + self.assertFrameSent(Frame(Opcode.BINARY, "😀".encode())) |
| 1032 | + |
| 1033 | + def test_broadcast_no_clients(self): |
| 1034 | + """broadcast does nothing when called with an empty list of clients.""" |
| 1035 | + broadcast([], "😀") |
| 1036 | + self.assertNoFrameSent() |
| 1037 | + |
| 1038 | + def test_broadcast_two_clients(self): |
| 1039 | + """broadcast broadcasts a message to several clients.""" |
| 1040 | + broadcast([self.connection, self.connection], "😀") |
| 1041 | + self.assertFramesSent( |
| 1042 | + [ |
| 1043 | + Frame(Opcode.TEXT, "😀".encode()), |
| 1044 | + Frame(Opcode.TEXT, "😀".encode()), |
| 1045 | + ] |
| 1046 | + ) |
| 1047 | + |
| 1048 | + def test_broadcast_skips_closed_connection(self): |
| 1049 | + """broadcast ignores closed connections.""" |
| 1050 | + self.connection.close() |
| 1051 | + self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe8")) |
| 1052 | + |
| 1053 | + with self.assertNoLogs("websockets", logging.WARNING): |
| 1054 | + broadcast([self.connection], "😀") |
| 1055 | + self.assertNoFrameSent() |
| 1056 | + |
| 1057 | + def test_broadcast_skips_closing_connection(self): |
| 1058 | + """broadcast ignores closing connections.""" |
| 1059 | + |
| 1060 | + def closer(): |
| 1061 | + with self.delay_frames_rcvd(MS): |
| 1062 | + self.connection.close() |
| 1063 | + |
| 1064 | + with self.run_in_thread(closer): |
| 1065 | + self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe8")) |
| 1066 | + |
| 1067 | + with self.assertNoLogs("websockets", logging.WARNING): |
| 1068 | + broadcast([self.connection], "😀") |
| 1069 | + self.assertNoFrameSent() |
| 1070 | + |
| 1071 | + def test_broadcast_skips_connection_with_send_blocked(self): |
| 1072 | + """broadcast logs a warning when a connection is blocked in send.""" |
| 1073 | + gate = threading.Event() |
| 1074 | + |
| 1075 | + def fragments(): |
| 1076 | + yield "⏳" |
| 1077 | + gate.wait() |
| 1078 | + |
| 1079 | + with self.run_in_thread(self.connection.send, args=(fragments(),)): |
| 1080 | + self.assertFrameSent(Frame(Opcode.TEXT, "⏳".encode(), fin=False)) |
| 1081 | + |
| 1082 | + with self.assertLogs("websockets", logging.WARNING) as logs: |
| 1083 | + broadcast([self.connection], "😀") |
| 1084 | + |
| 1085 | + self.assertEqual( |
| 1086 | + [record.getMessage() for record in logs.records], |
| 1087 | + ["skipped broadcast: sending a fragmented message"], |
| 1088 | + ) |
| 1089 | + |
| 1090 | + gate.set() |
| 1091 | + |
| 1092 | + def test_broadcast_reports_connection_with_send_blocked(self): |
| 1093 | + """broadcast raises exceptions for connections blocked in send.""" |
| 1094 | + gate = threading.Event() |
| 1095 | + |
| 1096 | + def fragments(): |
| 1097 | + yield "⏳" |
| 1098 | + gate.wait() |
| 1099 | + |
| 1100 | + with self.run_in_thread(self.connection.send, args=(fragments(),)): |
| 1101 | + self.assertFrameSent(Frame(Opcode.TEXT, "⏳".encode(), fin=False)) |
| 1102 | + |
| 1103 | + with self.assertRaises(ExceptionGroup) as raised: |
| 1104 | + broadcast([self.connection], "😀", raise_exceptions=True) |
| 1105 | + |
| 1106 | + self.assertEqual( |
| 1107 | + str(raised.exception), "skipped broadcast (1 sub-exception)" |
| 1108 | + ) |
| 1109 | + exc = raised.exception.exceptions[0] |
| 1110 | + self.assertEqual(str(exc), "sending a fragmented message") |
| 1111 | + self.assertIsInstance(exc, ConcurrencyError) |
| 1112 | + |
| 1113 | + gate.set() |
| 1114 | + |
| 1115 | + @patch("socket.socket.sendall", side_effect=BrokenPipeError(32, "Broken pipe")) |
| 1116 | + def test_broadcast_skips_connection_failing_to_send(self, sendall): |
| 1117 | + """broadcast logs a warning when a connection fails to send.""" |
| 1118 | + with self.assertLogs("websockets", logging.WARNING) as logs: |
| 1119 | + broadcast([self.connection], "😀") |
| 1120 | + |
| 1121 | + self.assertEqual( |
| 1122 | + [record.getMessage() for record in logs.records], |
| 1123 | + [ |
| 1124 | + "skipped broadcast: failed to write message: " |
| 1125 | + "BrokenPipeError: [Errno 32] Broken pipe" |
| 1126 | + ], |
| 1127 | + ) |
| 1128 | + |
| 1129 | + @patch("socket.socket.sendall", side_effect=BrokenPipeError(32, "Broken pipe")) |
| 1130 | + def test_broadcast_reports_connection_failing_to_send(self, sendall): |
| 1131 | + """broadcast raises exceptions for connections failing to send.""" |
| 1132 | + with self.assertRaises(ExceptionGroup) as raised: |
| 1133 | + broadcast([self.connection], "😀", raise_exceptions=True) |
| 1134 | + |
| 1135 | + self.assertEqual(str(raised.exception), "skipped broadcast (1 sub-exception)") |
| 1136 | + exc = raised.exception.exceptions[0] |
| 1137 | + self.assertEqual(str(exc), "failed to write message") |
| 1138 | + self.assertIsInstance(exc, RuntimeError) |
| 1139 | + cause = exc.__cause__ |
| 1140 | + self.assertEqual(str(cause), "[Errno 32] Broken pipe") |
| 1141 | + self.assertIsInstance(cause, BrokenPipeError) |
| 1142 | + |
| 1143 | + def test_broadcast_type_error(self): |
| 1144 | + """broadcast raises TypeError when called with an unsupported type.""" |
| 1145 | + with self.assertRaises(TypeError): |
| 1146 | + broadcast([self.connection], ["⏳", "⌛️"]) |
| 1147 | + |
995 | 1148 |
|
996 | 1149 | class ServerConnectionTests(ClientConnectionTests): |
997 | 1150 | LOCAL = SERVER |
|
0 commit comments