# P2P Network Communication - Voice Chat Module Tests """ 语音聊天模块测试 测试音频采集、编码、通话控制和实时传输功能 """ import asyncio import pytest import struct import time from unittest.mock import Mock, MagicMock, patch, AsyncMock from datetime import datetime from client.voice_chat import ( VoiceChatModule, CallState, VoiceChatError, AudioDeviceError, CallError, AudioConfig, CallInfo, NetworkStats, JitterBuffer, AudioCapture, AudioPlayback, AudioEncoder, AudioDecoder, ) from shared.models import Message, MessageType, NetworkQuality class TestAudioConfig: """音频配置测试""" def test_default_config(self): """测试默认配置""" config = AudioConfig() assert config.sample_rate == 16000 assert config.channels == 1 assert config.chunk_duration == 0.02 assert config.bits_per_sample == 16 def test_chunk_size_calculation(self): """测试音频块大小计算""" config = AudioConfig(sample_rate=16000, chunk_duration=0.02) # 16000 * 0.02 = 320 samples assert config.chunk_size == 320 def test_bytes_per_chunk_calculation(self): """测试每块字节数计算""" config = AudioConfig( sample_rate=16000, channels=1, chunk_duration=0.02, bits_per_sample=16 ) # 320 samples * 1 channel * 2 bytes = 640 bytes assert config.bytes_per_chunk == 640 class TestCallInfo: """通话信息测试""" def test_call_info_creation(self): """测试通话信息创建""" info = CallInfo( peer_id="user123", peer_name="Test User", is_outgoing=True ) assert info.peer_id == "user123" assert info.peer_name == "Test User" assert info.is_outgoing is True assert info.start_time is None def test_call_duration_not_started(self): """测试未开始通话的时长""" info = CallInfo(peer_id="user123", peer_name="Test") assert info.duration == 0.0 def test_call_duration_started(self): """测试已开始通话的时长""" info = CallInfo(peer_id="user123", peer_name="Test") info.start_time = datetime.now() time.sleep(0.1) assert info.duration >= 0.1 class TestNetworkStats: """网络统计测试""" def test_default_stats(self): """测试默认统计值""" stats = NetworkStats() assert stats.packets_sent == 0 assert stats.packets_received == 0 assert stats.packets_lost == 0 assert stats.avg_latency == 0.0 def test_packet_loss_rate_no_packets(self): """测试无数据包时的丢包率""" stats = NetworkStats() assert stats.packet_loss_rate == 0.0 def test_packet_loss_rate_with_loss(self): """测试有丢包时的丢包率""" stats = NetworkStats( packets_sent=100, packets_received=90, packets_lost=10 ) # 10 / (100 + 90) ≈ 0.0526 assert 0.05 < stats.packet_loss_rate < 0.06 def test_network_quality_excellent(self): """测试优秀网络质量""" stats = NetworkStats(avg_latency=30) assert stats.get_quality() == NetworkQuality.EXCELLENT def test_network_quality_good(self): """测试良好网络质量""" stats = NetworkStats(avg_latency=75) assert stats.get_quality() == NetworkQuality.GOOD def test_network_quality_fair(self): """测试一般网络质量""" stats = NetworkStats(avg_latency=150) assert stats.get_quality() == NetworkQuality.FAIR def test_network_quality_poor(self): """测试较差网络质量""" stats = NetworkStats(avg_latency=250) assert stats.get_quality() == NetworkQuality.POOR def test_network_quality_bad(self): """测试很差网络质量""" stats = NetworkStats(avg_latency=400) assert stats.get_quality() == NetworkQuality.BAD class TestJitterBuffer: """抖动缓冲区测试""" def test_buffer_creation(self): """测试缓冲区创建""" buffer = JitterBuffer() assert buffer.size == 0 assert buffer.delay == 0.0 def test_push_and_pop(self): """测试数据包入队和出队""" buffer = JitterBuffer(target_delay=0.0) # 禁用延迟等待 # 添加数据包 buffer.push(1, b"audio_data_1", time.time() - 0.1) buffer.push(2, b"audio_data_2", time.time() - 0.05) buffer.push(3, b"audio_data_3", time.time()) assert buffer.size == 3 # 取出数据包(按序列号顺序) data = buffer.pop() assert data == b"audio_data_1" assert buffer.size == 2 def test_out_of_order_packets(self): """测试乱序数据包处理""" buffer = JitterBuffer(target_delay=0.0) # 乱序添加 buffer.push(3, b"data_3", time.time()) buffer.push(1, b"data_1", time.time()) buffer.push(2, b"data_2", time.time()) # 应该按序列号顺序取出 assert buffer.pop() == b"data_1" assert buffer.pop() == b"data_2" assert buffer.pop() == b"data_3" def test_duplicate_packet_ignored(self): """测试重复数据包被忽略""" buffer = JitterBuffer(target_delay=0.0) buffer.push(1, b"data_1", time.time()) buffer.push(1, b"data_1_dup", time.time()) # 重复 assert buffer.size == 1 def test_old_packet_ignored(self): """测试过期数据包被忽略""" buffer = JitterBuffer(target_delay=0.0) buffer.push(5, b"data_5", time.time()) buffer.pop() # 取出序列号5 buffer.push(3, b"data_3", time.time()) # 旧包,应被忽略 assert buffer.size == 0 def test_clear_buffer(self): """测试清空缓冲区""" buffer = JitterBuffer() buffer.push(1, b"data", time.time()) buffer.push(2, b"data", time.time()) buffer.clear() assert buffer.size == 0 class TestVoiceChatModule: """语音聊天模块测试""" @pytest.fixture def voice_chat(self): """创建语音聊天模块实例""" module = VoiceChatModule() module.set_user_info("test_user", "Test User") return module def test_initial_state(self, voice_chat): """测试初始状态""" assert voice_chat.state == CallState.IDLE assert voice_chat.is_in_call is False assert voice_chat.is_muted is False assert voice_chat.call_info is None def test_mute_toggle(self, voice_chat): """测试静音切换""" assert voice_chat.is_muted is False voice_chat.mute(True) assert voice_chat.is_muted is True voice_chat.mute(False) assert voice_chat.is_muted is False def test_toggle_mute(self, voice_chat): """测试静音切换方法""" assert voice_chat.toggle_mute() is True assert voice_chat.toggle_mute() is False def test_get_call_duration_no_call(self, voice_chat): """测试无通话时的时长""" assert voice_chat.get_call_duration() == 0.0 def test_get_network_quality_default(self, voice_chat): """测试默认网络质量""" # 默认延迟为0,应该是EXCELLENT quality = voice_chat.get_network_quality() assert quality == NetworkQuality.EXCELLENT def test_state_callback(self, voice_chat): """测试状态回调""" callback_called = [] def state_callback(state, reason): callback_called.append((state, reason)) voice_chat.add_state_callback(state_callback) voice_chat._set_state(CallState.CALLING, "test") assert len(callback_called) == 1 assert callback_called[0][0] == CallState.CALLING assert callback_called[0][1] == "test" def test_remove_state_callback(self, voice_chat): """测试移除状态回调""" callback_called = [] def state_callback(state, reason): callback_called.append((state, reason)) voice_chat.add_state_callback(state_callback) voice_chat.remove_state_callback(state_callback) voice_chat._set_state(CallState.CALLING, "test") assert len(callback_called) == 0 @pytest.mark.asyncio async def test_start_call_no_callback(self, voice_chat): """测试无消息回调时发起通话""" result = await voice_chat.start_call("peer123", "Peer User") assert result is False assert voice_chat.state == CallState.IDLE @pytest.mark.asyncio async def test_start_call_not_idle(self, voice_chat): """测试非空闲状态发起通话""" voice_chat._state = CallState.CONNECTED async def mock_send(peer_id, msg): return True voice_chat.set_send_message_callback(mock_send) result = await voice_chat.start_call("peer123") assert result is False def test_reject_call_not_ringing(self, voice_chat): """测试非响铃状态拒绝通话""" voice_chat.reject_call("peer123") # 应该不会改变状态 assert voice_chat.state == CallState.IDLE def test_end_call_idle(self, voice_chat): """测试空闲状态结束通话""" voice_chat.end_call() # 应该保持空闲状态 assert voice_chat.state == CallState.IDLE @pytest.mark.asyncio async def test_accept_call_not_ringing(self, voice_chat): """测试非响铃状态接听通话""" result = await voice_chat.accept_call("peer123") assert result is False class TestAudioEncoderDecoder: """音频编解码器测试""" def test_encoder_without_opus(self): """测试无Opus时的编码器""" config = AudioConfig() with patch.dict('sys.modules', {'opuslib': None}): encoder = AudioEncoder(config) # 应该回退到原始音频 assert encoder.is_opus_enabled is False or encoder._encoder is None def test_decoder_without_opus(self): """测试无Opus时的解码器""" config = AudioConfig() with patch.dict('sys.modules', {'opuslib': None}): decoder = AudioDecoder(config) # 应该回退到原始音频 assert decoder.is_opus_enabled is False or decoder._decoder is None def test_encoder_raw_passthrough(self): """测试编码器原始数据透传""" config = AudioConfig() encoder = AudioEncoder(config) encoder._use_opus = False test_data = b"test_audio_data" result = encoder.encode(test_data) assert result == test_data def test_decoder_raw_passthrough(self): """测试解码器原始数据透传""" config = AudioConfig() decoder = AudioDecoder(config) decoder._use_opus = False test_data = b"test_audio_data" result = decoder.decode(test_data) assert result == test_data def test_encoder_bitrate_property(self): """测试编码器比特率属性""" config = AudioConfig() encoder = AudioEncoder(config, bitrate=32000) assert encoder.bitrate == 32000 class TestAudioPacketFormat: """音频数据包格式测试""" def test_header_format(self): """测试数据包头格式""" # 格式: 序列号(4字节) + 时间戳(8字节) header_size = struct.calcsize("!Id") assert header_size == VoiceChatModule.AUDIO_HEADER_SIZE def test_pack_unpack_header(self): """测试数据包头打包和解包""" sequence = 12345 timestamp = time.time() # 打包 header = struct.pack("!Id", sequence, timestamp) # 解包 unpacked_seq, unpacked_ts = struct.unpack("!Id", header) assert unpacked_seq == sequence assert abs(unpacked_ts - timestamp) < 0.001 if __name__ == "__main__": pytest.main([__file__, "-v"])