/** * @file ResumeStateTest.cpp * @brief 断点续传状态管理测试 * * 测试覆盖: * - 续传状态序列化/反序列化 * - 续传请求/响应包构建 * - 状态文件格式 * - 多文件续传管理 */ #include #include #include #include #include #include // ============================================ // 协议结构(测试专用) // ============================================ #pragma pack(push, 1) struct FileRangeV2 { uint64_t offset; uint64_t length; }; struct FileResumePacketV2 { uint8_t cmd; uint64_t transferID; uint64_t srcClientID; uint64_t dstClientID; uint32_t fileIndex; uint64_t fileSize; uint64_t receivedBytes; uint16_t flags; uint16_t rangeCount; }; enum FileFlagsV2 : uint16_t { FFV2_NONE = 0x0000, FFV2_RESUME_REQ = 0x0002, FFV2_RESUME_RESP = 0x0004, }; struct FileResumeResponseV2 { uint8_t cmd; uint64_t srcClientID; uint64_t dstClientID; uint16_t flags; uint32_t fileCount; }; struct FileResumeResponseEntryV2 { uint32_t fileIndex; uint64_t receivedBytes; }; #pragma pack(pop) // ============================================ // 续传状态管理类(测试专用实现) // ============================================ struct FileResumeEntry { uint32_t fileIndex; uint64_t fileSize; uint64_t receivedBytes; std::string relativePath; std::vector> receivedRanges; }; class ResumeStateManager { public: ResumeStateManager() : m_transferID(0), m_srcClientID(0), m_dstClientID(0) {} void Initialize(uint64_t transferID, uint64_t srcClientID, uint64_t dstClientID, const std::string& targetDir) { m_transferID = transferID; m_srcClientID = srcClientID; m_dstClientID = dstClientID; m_targetDir = targetDir; m_entries.clear(); } void AddFile(uint32_t fileIndex, uint64_t fileSize, const std::string& path) { FileResumeEntry entry; entry.fileIndex = fileIndex; entry.fileSize = fileSize; entry.receivedBytes = 0; entry.relativePath = path; m_entries.push_back(entry); } void UpdateProgress(uint32_t fileIndex, uint64_t offset, uint64_t length) { for (auto& entry : m_entries) { if (entry.fileIndex == fileIndex) { entry.receivedRanges.emplace_back(offset, length); entry.receivedBytes += length; break; } } } bool GetFileState(uint32_t fileIndex, FileResumeEntry& outEntry) const { for (const auto& entry : m_entries) { if (entry.fileIndex == fileIndex) { outEntry = entry; return true; } } return false; } // 序列化为字节流 std::vector Serialize() const { std::vector buffer; // Header auto appendU64 = [&buffer](uint64_t val) { for (int i = 0; i < 8; ++i) { buffer.push_back(static_cast(val >> (i * 8))); } }; auto appendU32 = [&buffer](uint32_t val) { for (int i = 0; i < 4; ++i) { buffer.push_back(static_cast(val >> (i * 8))); } }; auto appendU16 = [&buffer](uint16_t val) { buffer.push_back(static_cast(val & 0xFF)); buffer.push_back(static_cast(val >> 8)); }; auto appendString = [&buffer, &appendU16](const std::string& str) { appendU16(static_cast(str.size())); buffer.insert(buffer.end(), str.begin(), str.end()); }; // Magic buffer.push_back('R'); buffer.push_back('S'); buffer.push_back('T'); buffer.push_back('V'); // Resume State V2 appendU64(m_transferID); appendU64(m_srcClientID); appendU64(m_dstClientID); appendString(m_targetDir); appendU32(static_cast(m_entries.size())); for (const auto& entry : m_entries) { appendU32(entry.fileIndex); appendU64(entry.fileSize); appendU64(entry.receivedBytes); appendString(entry.relativePath); appendU16(static_cast(entry.receivedRanges.size())); for (const auto& range : entry.receivedRanges) { appendU64(range.first); appendU64(range.second); } } return buffer; } // 从字节流反序列化 bool Deserialize(const std::vector& buffer) { if (buffer.size() < 8) return false; size_t pos = 0; auto readU64 = [&buffer, &pos]() -> uint64_t { if (pos + 8 > buffer.size()) return 0; uint64_t val = 0; for (int i = 0; i < 8; ++i) { val |= static_cast(buffer[pos++]) << (i * 8); } return val; }; auto readU32 = [&buffer, &pos]() -> uint32_t { if (pos + 4 > buffer.size()) return 0; uint32_t val = 0; for (int i = 0; i < 4; ++i) { val |= static_cast(buffer[pos++]) << (i * 8); } return val; }; auto readU16 = [&buffer, &pos]() -> uint16_t { if (pos + 2 > buffer.size()) return 0; uint16_t val = buffer[pos] | (buffer[pos + 1] << 8); pos += 2; return val; }; auto readString = [&buffer, &pos, &readU16]() -> std::string { uint16_t len = readU16(); if (pos + len > buffer.size()) return ""; std::string str(buffer.begin() + pos, buffer.begin() + pos + len); pos += len; return str; }; // Check magic if (buffer[0] != 'R' || buffer[1] != 'S' || buffer[2] != 'T' || buffer[3] != 'V') { return false; } pos = 4; m_transferID = readU64(); m_srcClientID = readU64(); m_dstClientID = readU64(); m_targetDir = readString(); uint32_t entryCount = readU32(); m_entries.clear(); for (uint32_t i = 0; i < entryCount; ++i) { FileResumeEntry entry; entry.fileIndex = readU32(); entry.fileSize = readU64(); entry.receivedBytes = readU64(); entry.relativePath = readString(); uint16_t rangeCount = readU16(); for (uint16_t j = 0; j < rangeCount; ++j) { uint64_t offset = readU64(); uint64_t length = readU64(); entry.receivedRanges.emplace_back(offset, length); } m_entries.push_back(entry); } return true; } uint64_t GetTransferID() const { return m_transferID; } uint64_t GetSrcClientID() const { return m_srcClientID; } uint64_t GetDstClientID() const { return m_dstClientID; } const std::string& GetTargetDir() const { return m_targetDir; } size_t GetFileCount() const { return m_entries.size(); } // 获取所有文件的接收偏移映射 std::map GetReceivedOffsets() const { std::map offsets; for (const auto& entry : m_entries) { offsets[entry.fileIndex] = entry.receivedBytes; } return offsets; } private: uint64_t m_transferID; uint64_t m_srcClientID; uint64_t m_dstClientID; std::string m_targetDir; std::vector m_entries; }; // ============================================ // 续传包构建/解析辅助函数 // ============================================ std::vector BuildResumeRequest( uint64_t transferID, uint64_t srcClientID, uint64_t dstClientID, uint32_t fileIndex, uint64_t fileSize, uint64_t receivedBytes, const std::vector>& ranges) { size_t size = sizeof(FileResumePacketV2) + ranges.size() * sizeof(FileRangeV2); std::vector buffer(size); FileResumePacketV2* pkt = reinterpret_cast(buffer.data()); pkt->cmd = 86; // COMMAND_FILE_RESUME pkt->transferID = transferID; pkt->srcClientID = srcClientID; pkt->dstClientID = dstClientID; pkt->fileIndex = fileIndex; pkt->fileSize = fileSize; pkt->receivedBytes = receivedBytes; pkt->flags = FFV2_RESUME_REQ; pkt->rangeCount = static_cast(ranges.size()); FileRangeV2* rangePtr = reinterpret_cast(buffer.data() + sizeof(FileResumePacketV2)); for (size_t i = 0; i < ranges.size(); ++i) { rangePtr[i].offset = ranges[i].first; rangePtr[i].length = ranges[i].second; } return buffer; } bool ParseResumeRequest( const uint8_t* buffer, size_t len, FileResumePacketV2& header, std::vector>& ranges) { if (len < sizeof(FileResumePacketV2)) { return false; } memcpy(&header, buffer, sizeof(FileResumePacketV2)); size_t expectedLen = sizeof(FileResumePacketV2) + header.rangeCount * sizeof(FileRangeV2); if (len < expectedLen) { return false; } ranges.clear(); const FileRangeV2* rangePtr = reinterpret_cast(buffer + sizeof(FileResumePacketV2)); for (uint16_t i = 0; i < header.rangeCount; ++i) { ranges.emplace_back(rangePtr[i].offset, rangePtr[i].length); } return true; } std::vector BuildResumeResponse( uint64_t srcClientID, uint64_t dstClientID, const std::map& offsets) { size_t size = sizeof(FileResumeResponseV2) + offsets.size() * sizeof(FileResumeResponseEntryV2); std::vector buffer(size); FileResumeResponseV2* pkt = reinterpret_cast(buffer.data()); pkt->cmd = 86; // COMMAND_FILE_RESUME pkt->srcClientID = srcClientID; pkt->dstClientID = dstClientID; pkt->flags = FFV2_RESUME_RESP; pkt->fileCount = static_cast(offsets.size()); FileResumeResponseEntryV2* entryPtr = reinterpret_cast( buffer.data() + sizeof(FileResumeResponseV2)); size_t i = 0; for (const auto& kv : offsets) { entryPtr[i].fileIndex = kv.first; entryPtr[i].receivedBytes = kv.second; ++i; } return buffer; } bool ParseResumeResponse( const uint8_t* buffer, size_t len, std::map& offsets) { if (len < sizeof(FileResumeResponseV2)) { return false; } const FileResumeResponseV2* pkt = reinterpret_cast(buffer); if ((pkt->flags & FFV2_RESUME_RESP) == 0) { return false; } size_t expectedLen = sizeof(FileResumeResponseV2) + pkt->fileCount * sizeof(FileResumeResponseEntryV2); if (len < expectedLen) { return false; } offsets.clear(); const FileResumeResponseEntryV2* entryPtr = reinterpret_cast( buffer + sizeof(FileResumeResponseV2)); for (uint32_t i = 0; i < pkt->fileCount; ++i) { offsets[entryPtr[i].fileIndex] = entryPtr[i].receivedBytes; } return true; } // ============================================ // ResumeStateManager 测试 // ============================================ class ResumeStateManagerTest : public ::testing::Test {}; TEST_F(ResumeStateManagerTest, Initialize) { ResumeStateManager mgr; mgr.Initialize(12345, 100, 200, "C:\\Downloads\\"); EXPECT_EQ(mgr.GetTransferID(), 12345u); EXPECT_EQ(mgr.GetSrcClientID(), 100u); EXPECT_EQ(mgr.GetDstClientID(), 200u); EXPECT_EQ(mgr.GetTargetDir(), "C:\\Downloads\\"); EXPECT_EQ(mgr.GetFileCount(), 0u); } TEST_F(ResumeStateManagerTest, AddFiles) { ResumeStateManager mgr; mgr.Initialize(1, 0, 0, ""); mgr.AddFile(0, 1000, "file1.txt"); mgr.AddFile(1, 2000, "subdir/file2.bin"); mgr.AddFile(2, 3000, "another/path/file3.dat"); EXPECT_EQ(mgr.GetFileCount(), 3u); FileResumeEntry entry; ASSERT_TRUE(mgr.GetFileState(1, entry)); EXPECT_EQ(entry.fileSize, 2000u); EXPECT_EQ(entry.relativePath, "subdir/file2.bin"); } TEST_F(ResumeStateManagerTest, UpdateProgress) { ResumeStateManager mgr; mgr.Initialize(1, 0, 0, ""); mgr.AddFile(0, 10000, "file.bin"); mgr.UpdateProgress(0, 0, 2000); mgr.UpdateProgress(0, 2000, 3000); FileResumeEntry entry; ASSERT_TRUE(mgr.GetFileState(0, entry)); EXPECT_EQ(entry.receivedBytes, 5000u); EXPECT_EQ(entry.receivedRanges.size(), 2u); } TEST_F(ResumeStateManagerTest, GetReceivedOffsets) { ResumeStateManager mgr; mgr.Initialize(1, 0, 0, ""); mgr.AddFile(0, 1000, "a.txt"); mgr.AddFile(1, 2000, "b.txt"); mgr.AddFile(2, 3000, "c.txt"); mgr.UpdateProgress(0, 0, 500); mgr.UpdateProgress(1, 0, 1500); mgr.UpdateProgress(2, 0, 2500); auto offsets = mgr.GetReceivedOffsets(); EXPECT_EQ(offsets.size(), 3u); EXPECT_EQ(offsets[0], 500u); EXPECT_EQ(offsets[1], 1500u); EXPECT_EQ(offsets[2], 2500u); } // ============================================ // 序列化/反序列化测试 // ============================================ class ResumeSerializationTest : public ::testing::Test {}; TEST_F(ResumeSerializationTest, EmptyState) { ResumeStateManager mgr1, mgr2; mgr1.Initialize(12345, 100, 200, "C:\\Target\\"); auto buffer = mgr1.Serialize(); ASSERT_TRUE(mgr2.Deserialize(buffer)); EXPECT_EQ(mgr2.GetTransferID(), 12345u); EXPECT_EQ(mgr2.GetSrcClientID(), 100u); EXPECT_EQ(mgr2.GetDstClientID(), 200u); EXPECT_EQ(mgr2.GetTargetDir(), "C:\\Target\\"); EXPECT_EQ(mgr2.GetFileCount(), 0u); } TEST_F(ResumeSerializationTest, SingleFile) { ResumeStateManager mgr1, mgr2; mgr1.Initialize(1, 0, 0, "/tmp/download/"); mgr1.AddFile(0, 10000, "test.bin"); mgr1.UpdateProgress(0, 0, 5000); auto buffer = mgr1.Serialize(); ASSERT_TRUE(mgr2.Deserialize(buffer)); EXPECT_EQ(mgr2.GetFileCount(), 1u); FileResumeEntry entry; ASSERT_TRUE(mgr2.GetFileState(0, entry)); EXPECT_EQ(entry.fileSize, 10000u); EXPECT_EQ(entry.receivedBytes, 5000u); EXPECT_EQ(entry.relativePath, "test.bin"); } TEST_F(ResumeSerializationTest, MultipleFiles) { ResumeStateManager mgr1, mgr2; mgr1.Initialize(99999, 111, 222, "D:\\Backup\\"); mgr1.AddFile(0, 1000, "file1.txt"); mgr1.AddFile(1, 2000, "dir/file2.bin"); mgr1.AddFile(2, 3000, "path/to/file3.dat"); mgr1.UpdateProgress(0, 0, 1000); // 完成 mgr1.UpdateProgress(1, 0, 500); mgr1.UpdateProgress(1, 500, 500); mgr1.UpdateProgress(2, 0, 1000); mgr1.UpdateProgress(2, 2000, 500); auto buffer = mgr1.Serialize(); ASSERT_TRUE(mgr2.Deserialize(buffer)); EXPECT_EQ(mgr2.GetFileCount(), 3u); FileResumeEntry entry; ASSERT_TRUE(mgr2.GetFileState(0, entry)); EXPECT_EQ(entry.receivedBytes, 1000u); ASSERT_TRUE(mgr2.GetFileState(1, entry)); EXPECT_EQ(entry.receivedBytes, 1000u); EXPECT_EQ(entry.receivedRanges.size(), 2u); ASSERT_TRUE(mgr2.GetFileState(2, entry)); EXPECT_EQ(entry.receivedBytes, 1500u); EXPECT_EQ(entry.receivedRanges.size(), 2u); } TEST_F(ResumeSerializationTest, InvalidMagic) { std::vector invalidBuffer = {'X', 'X', 'X', 'X', 0, 0, 0, 0}; ResumeStateManager mgr; EXPECT_FALSE(mgr.Deserialize(invalidBuffer)); } TEST_F(ResumeSerializationTest, TruncatedBuffer) { ResumeStateManager mgr1; mgr1.Initialize(1, 0, 0, "test"); mgr1.AddFile(0, 1000, "file.txt"); auto buffer = mgr1.Serialize(); // 截断 std::vector truncated(buffer.begin(), buffer.begin() + 10); ResumeStateManager mgr2; // 可能成功也可能失败,取决于截断位置 // 主要验证不会崩溃 mgr2.Deserialize(truncated); SUCCEED(); } // ============================================ // 续传请求/响应包测试 // ============================================ class ResumePacketTest : public ::testing::Test {}; TEST_F(ResumePacketTest, BuildResumeRequest_NoRanges) { std::vector> ranges; auto buffer = BuildResumeRequest(12345, 100, 200, 5, 10000, 0, ranges); EXPECT_EQ(buffer.size(), sizeof(FileResumePacketV2)); const FileResumePacketV2* pkt = reinterpret_cast(buffer.data()); EXPECT_EQ(pkt->cmd, 86); EXPECT_EQ(pkt->flags, FFV2_RESUME_REQ); EXPECT_EQ(pkt->rangeCount, 0); } TEST_F(ResumePacketTest, BuildResumeRequest_WithRanges) { std::vector> ranges = { {0, 1000}, {2000, 500}, {5000, 2000} }; auto buffer = BuildResumeRequest(1, 0, 0, 0, 10000, 3500, ranges); FileResumePacketV2 header; std::vector> parsedRanges; ASSERT_TRUE(ParseResumeRequest(buffer.data(), buffer.size(), header, parsedRanges)); EXPECT_EQ(header.fileSize, 10000u); EXPECT_EQ(header.receivedBytes, 3500u); ASSERT_EQ(parsedRanges.size(), 3u); EXPECT_EQ(parsedRanges[0].first, 0u); EXPECT_EQ(parsedRanges[0].second, 1000u); EXPECT_EQ(parsedRanges[2].first, 5000u); } TEST_F(ResumePacketTest, BuildResumeResponse) { std::map offsets = { {0, 1000}, {1, 0}, {2, 5000} }; auto buffer = BuildResumeResponse(100, 200, offsets); std::map parsedOffsets; ASSERT_TRUE(ParseResumeResponse(buffer.data(), buffer.size(), parsedOffsets)); EXPECT_EQ(parsedOffsets.size(), 3u); EXPECT_EQ(parsedOffsets[0], 1000u); EXPECT_EQ(parsedOffsets[1], 0u); EXPECT_EQ(parsedOffsets[2], 5000u); } TEST_F(ResumePacketTest, ParseTruncatedRequest) { auto buffer = BuildResumeRequest(1, 0, 0, 0, 1000, 0, {{0, 500}}); // 截断 FileResumePacketV2 header; std::vector> ranges; EXPECT_FALSE(ParseResumeRequest(buffer.data(), sizeof(FileResumePacketV2) - 5, header, ranges)); } TEST_F(ResumePacketTest, ParseTruncatedResponse) { std::map offsets = {{0, 1000}}; auto buffer = BuildResumeResponse(0, 0, offsets); // 截断 std::map parsedOffsets; EXPECT_FALSE(ParseResumeResponse(buffer.data(), sizeof(FileResumeResponseV2) - 5, parsedOffsets)); } // ============================================ // 续传场景测试 // ============================================ class ResumeScenarioTest : public ::testing::Test {}; TEST_F(ResumeScenarioTest, SimulateInterruptAndResume) { // 第一次传输,接收了部分数据 ResumeStateManager session1; session1.Initialize(12345, 100, 0, "C:\\Downloads\\"); session1.AddFile(0, 10000, "large_file.bin"); session1.AddFile(1, 5000, "small_file.txt"); session1.UpdateProgress(0, 0, 3000); // 30% session1.UpdateProgress(1, 0, 5000); // 100% // 保存状态 auto savedState = session1.Serialize(); // 模拟程序重启,恢复状态 ResumeStateManager session2; ASSERT_TRUE(session2.Deserialize(savedState)); // 获取续传偏移 auto offsets = session2.GetReceivedOffsets(); EXPECT_EQ(offsets[0], 3000u); // 从 3000 继续 EXPECT_EQ(offsets[1], 5000u); // 已完成 // 继续传输 session2.UpdateProgress(0, 3000, 7000); FileResumeEntry entry; ASSERT_TRUE(session2.GetFileState(0, entry)); EXPECT_EQ(entry.receivedBytes, 10000u); } TEST_F(ResumeScenarioTest, C2CResumeNegotiation) { // 源客户端查询续传状态 std::vector> files = { {"file1.txt", 1000}, {"file2.txt", 2000}, {"file3.txt", 3000} }; // 目标客户端已有部分数据 std::map receivedOffsets = { {0, 500}, // file1: 50% {1, 2000}, // file2: 100% {2, 0} // file3: 0% }; // 构建响应 auto response = BuildResumeResponse(100, 200, receivedOffsets); // 源客户端解析响应 std::map parsedOffsets; ASSERT_TRUE(ParseResumeResponse(response.data(), response.size(), parsedOffsets)); // 根据偏移决定发送策略 for (size_t i = 0; i < files.size(); ++i) { uint32_t fileIndex = static_cast(i); uint64_t startOffset = parsedOffsets[fileIndex]; if (startOffset >= files[i].second) { // 已完成,跳过 EXPECT_EQ(i, 1u) << "Only file2 should be complete"; } else if (startOffset > 0) { // 部分完成,从偏移继续 EXPECT_EQ(i, 0u) << "Only file1 should be partial"; EXPECT_EQ(startOffset, 500u); } else { // 未开始,从头发送 EXPECT_EQ(i, 2u) << "Only file3 should start from beginning"; } } } TEST_F(ResumeScenarioTest, LargeFileResume) { ResumeStateManager mgr; uint64_t fileSize = 10ULL * 1024 * 1024 * 1024; // 10 GB mgr.Initialize(1, 0, 0, "/data/"); mgr.AddFile(0, fileSize, "huge.bin"); // 模拟接收了 5 GB mgr.UpdateProgress(0, 0, 5ULL * 1024 * 1024 * 1024); auto offsets = mgr.GetReceivedOffsets(); EXPECT_EQ(offsets[0], 5ULL * 1024 * 1024 * 1024); // 序列化/反序列化 auto buffer = mgr.Serialize(); ResumeStateManager mgr2; ASSERT_TRUE(mgr2.Deserialize(buffer)); FileResumeEntry entry; ASSERT_TRUE(mgr2.GetFileState(0, entry)); EXPECT_EQ(entry.fileSize, 10ULL * 1024 * 1024 * 1024); EXPECT_EQ(entry.receivedBytes, 5ULL * 1024 * 1024 * 1024); } // ============================================ // 边界条件测试 // ============================================ class ResumeBoundaryTest : public ::testing::Test {}; TEST_F(ResumeBoundaryTest, EmptyFileName) { ResumeStateManager mgr; mgr.Initialize(1, 0, 0, ""); mgr.AddFile(0, 100, ""); auto buffer = mgr.Serialize(); ResumeStateManager mgr2; ASSERT_TRUE(mgr2.Deserialize(buffer)); FileResumeEntry entry; ASSERT_TRUE(mgr2.GetFileState(0, entry)); EXPECT_EQ(entry.relativePath, ""); } TEST_F(ResumeBoundaryTest, SpecialCharactersInPath) { ResumeStateManager mgr; mgr.Initialize(1, 0, 0, "C:\\My Files\\测试目录\\"); mgr.AddFile(0, 100, "文件 (1).txt"); auto buffer = mgr.Serialize(); ResumeStateManager mgr2; ASSERT_TRUE(mgr2.Deserialize(buffer)); EXPECT_EQ(mgr2.GetTargetDir(), "C:\\My Files\\测试目录\\"); FileResumeEntry entry; ASSERT_TRUE(mgr2.GetFileState(0, entry)); EXPECT_EQ(entry.relativePath, "文件 (1).txt"); } TEST_F(ResumeBoundaryTest, ManyRanges) { ResumeStateManager mgr; mgr.Initialize(1, 0, 0, ""); mgr.AddFile(0, 100000, "fragmented.bin"); // 添加很多小区间 for (uint64_t i = 0; i < 100000; i += 20) { mgr.UpdateProgress(0, i, 10); } auto buffer = mgr.Serialize(); ResumeStateManager mgr2; ASSERT_TRUE(mgr2.Deserialize(buffer)); FileResumeEntry entry; ASSERT_TRUE(mgr2.GetFileState(0, entry)); EXPECT_EQ(entry.receivedRanges.size(), 5000u); } TEST_F(ResumeBoundaryTest, MaxFileCount) { ResumeStateManager mgr; mgr.Initialize(1, 0, 0, ""); // 添加大量文件 for (uint32_t i = 0; i < 1000; ++i) { mgr.AddFile(i, 100 * (i + 1), "file" + std::to_string(i) + ".txt"); } auto buffer = mgr.Serialize(); ResumeStateManager mgr2; ASSERT_TRUE(mgr2.Deserialize(buffer)); EXPECT_EQ(mgr2.GetFileCount(), 1000u); }