#include #include #include #include #include #include #include "OwnedPduPacket.h" #include "cfdp/PduSenderMock.h" #include "fsfw/cfdp.h" #include "fsfw/cfdp/handler/PutRequest.h" #include "fsfw/cfdp/handler/SourceHandler.h" #include "fsfw/cfdp/pdu/AckPduCreator.h" #include "fsfw/cfdp/pdu/AckPduReader.h" #include "fsfw/cfdp/pdu/EofPduCreator.h" #include "fsfw/cfdp/pdu/EofPduReader.h" #include "fsfw/cfdp/pdu/FileDataReader.h" #include "fsfw/cfdp/pdu/FinishedPduCreator.h" #include "fsfw/cfdp/pdu/MetadataPduCreator.h" #include "fsfw/cfdp/pdu/MetadataPduReader.h" #include "fsfw/cfdp/pdu/NakPduCreator.h" #include "fsfw/tmtcservices/TmTcMessage.h" #include "fsfw/util/SeqCountProvider.h" #include "mock/AcceptsTmMock.h" #include "mock/EventReportingProxyMock.h" #include "mock/FilesystemMock.h" #include "mock/MessageQueueMock.h" #include "mock/StorageManagerMock.h" #include "mock/cfdp/FaultHandlerMock.h" #include "mock/cfdp/RemoteConfigTableMock.h" #include "mock/cfdp/UserMock.h" TEST_CASE("CFDP Source Handler", "[cfdp]") { using namespace cfdp; using namespace returnvalue; using namespace std::filesystem; constexpr size_t MAX_FILE_SEGMENT_SIZE = 255; MessageQueueId_t destQueueId = 2; AcceptsTmMock tmReceiver(destQueueId); // MessageQueueMock mqMock(destQueueId); auto localId = EntityId(UnsignedByteField(2)); auto remoteId = EntityId(UnsignedByteField(5)); FaultHandlerMock fhMock; LocalEntityCfg localEntityCfg(localId, IndicationCfg(), fhMock); FilesystemMock fsMock; UserMock userMock(fsMock); SeqCountProviderU16 seqCountProvider; SourceHandlerParams dp(localEntityCfg, userMock, seqCountProvider); PduSenderMock pduSender; EventReportingProxyMock eventReporterMock; LocalPool::LocalPoolConfig storeCfg = {{10, 32}, {10, 64}, {10, 128}, {10, 1024}}; FsfwParams fp(&eventReporterMock); auto sourceHandler = SourceHandler(pduSender, 4096, dp, fp); RemoteEntityCfg cfg; cfg.maxFileSegmentLen = MAX_FILE_SEGMENT_SIZE; cfg.remoteId = remoteId; std::string srcFileName = "/tmp/cfdp-test.txt"; std::string destFileName = "/tmp/cfdp-test2.txt"; fsMock.createFile(srcFileName.c_str()); cfdp::StringLv srcNameLv(srcFileName.c_str(), srcFileName.length()); FilesystemParams destFileNameFs(destFileName.c_str()); cfdp::StringLv destNameLv(destFileNameFs.path, std::strlen(destFileNameFs.path)); PutRequest putRequest(remoteId, srcNameLv, destNameLv); CHECK(sourceHandler.initialize() == OK); auto onePduSentCheck = [&](const SourceHandler::FsmResult& fsmResult) { CHECK(fsmResult.errors == 0); CHECK(fsmResult.packetsSent == 1); }; auto genericMetadataCheck = [&](const SourceHandler::FsmResult& fsmResult, size_t expectedFileSize, uint16_t expectedSeqNum) { CHECK(fsmResult.errors == 0); auto optNextPacket = pduSender.getNextSentPacket(); CHECK(optNextPacket.has_value()); const auto& [pduType, fileDirective, rawPdu] = *optNextPacket; CHECK(rawPdu.size() == 55); MetadataGenericInfo metadataInfo; MetadataPduReader metadataReader(rawPdu.data(), rawPdu.size(), metadataInfo, nullptr, 0); REQUIRE(metadataReader.parseData() == OK); std::string srcNameRead = metadataReader.getSourceFileName().getString(); CHECK(srcNameRead == srcFileName); TransactionSeqNum seqNum; metadataReader.getTransactionSeqNum(seqNum); CHECK(seqNum.getValue() == expectedSeqNum); CHECK(userMock.transactionIndicRecvd.size() == 1); CHECK(userMock.transactionIndicRecvd.back() == TransactionId(localId, seqNum)); EntityId srcId; metadataReader.getSourceId(srcId); EntityId destId; metadataReader.getDestId(destId); CHECK(srcId.getValue() == localId.getValue()); CHECK(destId.getValue() == remoteId.getValue()); std::string destNameRead = metadataReader.getDestFileName().getString(); CHECK(destNameRead == destFileName); if (expectedFileSize == 0) { CHECK(metadataInfo.getChecksumType() == ChecksumType::NULL_CHECKSUM); } else { CHECK(metadataInfo.getChecksumType() == ChecksumType::CRC_32); } CHECK(metadataInfo.getFileSize().value() == expectedFileSize); CHECK(!metadataInfo.isClosureRequested()); }; auto genericEofCheck = [&](const SourceHandler::FsmResult& fsmResult, size_t expectedFileSize, uint32_t expectedChecksum, uint16_t expectedSeqNum) { onePduSentCheck(fsmResult); TmTcMessage tmtcMessage; auto optNextPacket = pduSender.getNextSentPacket(); CHECK(optNextPacket.has_value()); const auto& [pduType, fileDirective, rawPdu] = *optNextPacket; // 10 byte PDU header, 1 byte directive field, 1 byte condition code, 4 byte checksum, // 4 byte FSS CHECK(rawPdu.size() == 20); EofInfo eofInfo; EofPduReader eofReader(rawPdu.data(), rawPdu.size(), eofInfo); REQUIRE(eofReader.parseData() == OK); TransactionSeqNum seqNum; eofReader.getTransactionSeqNum(seqNum); CHECK(seqNum.getValue() == expectedSeqNum); auto transactionId = TransactionId(localId, seqNum); CHECK(userMock.eofSentRecvd.size() == 1); CHECK(userMock.eofSentRecvd.back() == transactionId); CHECK(eofInfo.getChecksum() == expectedChecksum); CHECK(eofInfo.getConditionCode() == ConditionCode::NO_ERROR); CHECK(eofInfo.getFileSize().value() == expectedFileSize); }; auto genericNoticeOfCompletionCheck = [&](const SourceHandler::FsmResult& fsmResult, const uint16_t expectedSeqNum) { CHECK(userMock.finishedRecvd.size() == 1); CHECK(userMock.finishedRecvd.back().first == TransactionId(localId, TransactionSeqNum(cfdp::WidthInBytes::TWO_BYTES, expectedSeqNum))); CHECK(sourceHandler.getStep() == SourceHandler::TransactionStep::IDLE); CHECK(sourceHandler.getState() == CfdpState::IDLE); }; SECTION("Test Basic") { CHECK(sourceHandler.getState() == CfdpState::IDLE); CHECK(sourceHandler.getStep() == SourceHandler::TransactionStep::IDLE); } SECTION("Transfer empty file") { CHECK(sourceHandler.transactionStart(putRequest, cfg) == OK); size_t expectedFileSize = 0; const SourceHandler::FsmResult& fsmResult = sourceHandler.stateMachineNoPacket(); // Verify metadata PDU was sent. genericMetadataCheck(fsmResult, expectedFileSize, 0); sourceHandler.stateMachineNoPacket(); // Verify EOF PDU was sent. No file data PDU is sent for an empty file and the checksum is 0. genericEofCheck(fsmResult, expectedFileSize, 0, 0); // Verify notice of completion. sourceHandler.stateMachineNoPacket(); genericNoticeOfCompletionCheck(fsmResult, 0); } SECTION("Transfer small file") { uint16_t expectedSeqNum = 0; fsMock.createFile(srcFileName.c_str()); std::string fileContent = "hello world\n"; size_t expectedFileSize = fileContent.size(); fsMock.writeToFile(srcFileName.c_str(), 0, reinterpret_cast(fileContent.data()), expectedFileSize); CHECK(sourceHandler.transactionStart(putRequest, cfg) == OK); const SourceHandler::FsmResult& fsmResult = sourceHandler.stateMachineNoPacket(); // Verify metadata PDU was sent. genericMetadataCheck(fsmResult, expectedFileSize, expectedSeqNum); // Verify that a small file data PDU was sent. sourceHandler.stateMachineNoPacket(); onePduSentCheck(fsmResult); auto optNextPacket = pduSender.getNextSentPacket(); CHECK(optNextPacket.has_value()); const auto& [pduType, fileDirective, rawPdu] = *optNextPacket; FileDataInfo fdInfo; FileDataReader fdReader(rawPdu.data(), rawPdu.size(), fdInfo); // 10 byte PDU header, 4 byte offset, 12 bytes file data. CHECK(rawPdu.size() == 26); CHECK(fdReader.parseData() == OK); CHECK(fdInfo.getOffset().value() == 0); size_t fileSize = 0; const uint8_t* fileData = fdInfo.getFileData(&fileSize); REQUIRE(fileSize == fileContent.size()); CHECK(fileData != nullptr); std::string dataReadBack(reinterpret_cast(fileData), fileSize); CHECK(dataReadBack == fileContent); sourceHandler.stateMachineNoPacket(); etl::crc32 crcCalc; crcCalc.add(fileContent.data(), fileContent.data() + fileContent.size()); // Verify EOF PDU was sent. genericEofCheck(fsmResult, expectedFileSize, crcCalc.value(), expectedSeqNum); // Verify notice of completion. sourceHandler.stateMachineNoPacket(); genericNoticeOfCompletionCheck(fsmResult, expectedSeqNum); } SECTION("File data PDU send failure is retried, not dropped") { // A downstream send failure (e.g. a full TM store) must not be treated as if the PDU went // out: the FSM has to retry the same segment, not silently advance past lost data. uint16_t expectedSeqNum = 0; fsMock.createFile(srcFileName.c_str()); std::string fileContent = "hello world\n"; size_t expectedFileSize = fileContent.size(); fsMock.writeToFile(srcFileName.c_str(), 0, reinterpret_cast(fileContent.data()), expectedFileSize); CHECK(sourceHandler.transactionStart(putRequest, cfg) == OK); const SourceHandler::FsmResult& fsmResult = sourceHandler.stateMachineNoPacket(); genericMetadataCheck(fsmResult, expectedFileSize, expectedSeqNum); // The file data PDU send fails. No packet must be recorded and no progress made. pduSender.failNextSend = true; sourceHandler.stateMachineNoPacket(); CHECK(fsmResult.packetsSent == 0); CHECK(fsmResult.errors == 1); CHECK(not pduSender.getNextSentPacket().has_value()); CHECK(sourceHandler.getStep() == SourceHandler::TransactionStep::SENDING_FILE_DATA); // Retrying must send the same segment from offset 0, not skip ahead. sourceHandler.stateMachineNoPacket(); onePduSentCheck(fsmResult); auto optNextPacket = pduSender.getNextSentPacket(); CHECK(optNextPacket.has_value()); const auto& [pduType, fileDirective, rawPdu] = *optNextPacket; FileDataInfo fdInfo; FileDataReader fdReader(rawPdu.data(), rawPdu.size(), fdInfo); CHECK(fdReader.parseData() == OK); CHECK(fdInfo.getOffset().value() == 0); } SECTION("Transfer two segment file") { uint16_t expectedSeqNum = 0; // Create 400 bytes of random data. This should result in two file segments, with one // having the maximum size. std::random_device dev; std::mt19937 rng(dev()); std::uniform_int_distribution distU8(0, 255); std::array largerFileData{}; for (auto& val : largerFileData) { val = distU8(rng); } size_t expectedFileSize = largerFileData.size(); fsMock.createFile(srcFileName.c_str()); fsMock.writeToFile(srcFileName.c_str(), 0, largerFileData.data(), largerFileData.size()); CHECK(sourceHandler.transactionStart(putRequest, cfg) == OK); const SourceHandler::FsmResult& fsmResult = sourceHandler.stateMachineNoPacket(); // Verify metadata PDU was sent. genericMetadataCheck(fsmResult, expectedFileSize, expectedSeqNum); // Check first file data PDU. It should have the maximum file segment size. sourceHandler.stateMachineNoPacket(); onePduSentCheck(fsmResult); auto optNextPacket = pduSender.getNextSentPacket(); CHECK(optNextPacket.has_value()); const auto& [pduType, fileDirective, rawPdu] = *optNextPacket; FileDataInfo fdInfo; { // auto accessor = onePduSentCheck(fsmResult, tmtcMessage, &pduPtr); FileDataReader fdReader(rawPdu.data(), rawPdu.size(), fdInfo); // 10 byte PDU header, 4 byte offset, 255 byte file data CHECK(rawPdu.size() == 269); CHECK(fdReader.parseData() == OK); CHECK(fdInfo.getOffset().value() == 0); size_t fileSize = 0; const uint8_t* fileData = fdInfo.getFileData(&fileSize); // Maximum file segment size. REQUIRE(fileSize == MAX_FILE_SEGMENT_SIZE); for (unsigned i = 0; i < fileSize; i++) { CHECK(fileData[i] == largerFileData[i]); } } // Check second file data PDU. sourceHandler.stateMachineNoPacket(); { optNextPacket = pduSender.getNextSentPacket(); CHECK(optNextPacket.has_value()); const auto& [pduType, fileDirective, rawPdu] = *optNextPacket; FileDataReader fdReader(rawPdu.data(), rawPdu.size(), fdInfo); // 10 byte PDU header, 4 byte offset, remaining file data (400 - 255 == 145). CHECK(rawPdu.size() == 10 + 4 + largerFileData.size() - MAX_FILE_SEGMENT_SIZE); CHECK(fdReader.parseData() == OK); CHECK(fdInfo.getOffset().value() == MAX_FILE_SEGMENT_SIZE); size_t fileDataSize = 0; const uint8_t* fileData = fdInfo.getFileData(&fileDataSize); // Maximum file segment size. REQUIRE(fileDataSize == largerFileData.size() - MAX_FILE_SEGMENT_SIZE); for (unsigned i = 0; i < fileDataSize; i++) { CHECK(fileData[i] == largerFileData[MAX_FILE_SEGMENT_SIZE + i]); } } // Check EOF and verify checksum. sourceHandler.stateMachineNoPacket(); etl::crc32 crcCalc; crcCalc.add(largerFileData.data(), largerFileData.data() + largerFileData.size()); // Verify EOF PDU was sent. genericEofCheck(fsmResult, expectedFileSize, crcCalc.value(), expectedSeqNum); // Verify notice of completion. sourceHandler.stateMachineNoPacket(); genericNoticeOfCompletionCheck(fsmResult, expectedSeqNum); } } TEST_CASE("CFDP Source Handler Acknowledged", "[cfdp]") { using namespace cfdp; using namespace returnvalue; constexpr size_t MAX_FILE_SEGMENT_SIZE = 256; auto localId = EntityId(UnsignedByteField(2)); auto remoteId = EntityId(UnsignedByteField(5)); FaultHandlerMock fhMock; LocalEntityCfg localEntityCfg(localId, IndicationCfg(), fhMock); FilesystemMock fsMock; UserMock userMock(fsMock); SeqCountProviderU16 seqCountProvider; SourceHandlerParams dp(localEntityCfg, userMock, seqCountProvider); PduSenderMock pduSender; EventReportingProxyMock eventReporterMock; FsfwParams fp(&eventReporterMock); auto sourceHandler = SourceHandler(pduSender, 4096, dp, fp); RemoteEntityCfg cfg; cfg.maxFileSegmentLen = MAX_FILE_SEGMENT_SIZE; cfg.remoteId = remoteId; cfg.defaultTransmissionMode = TransmissionMode::ACKNOWLEDGED; // Keep the timers short so the timeout paths are testable without stalling the suite. cfg.positiveAckTimerIntervalMs = 1; cfg.positiveAckTimerExpirationLimit = 2; std::string srcFileName = "/tmp/cfdp-acked-test.txt"; std::string destFileName = "/tmp/cfdp-acked-test2.txt"; std::array fileData{}; for (size_t idx = 0; idx < fileData.size(); idx++) { fileData[idx] = static_cast(idx); } fsMock.createFile(srcFileName.c_str()); fsMock.writeToFile(srcFileName.c_str(), 0, fileData.data(), fileData.size()); cfdp::StringLv srcNameLv(srcFileName.c_str(), srcFileName.length()); cfdp::StringLv destNameLv(destFileName.c_str(), destFileName.length()); PutRequest putRequest(remoteId, srcNameLv, destNameLv); CHECK(sourceHandler.initialize() == OK); // The PDU configuration the peer would use to address this handler. PduConfig peerConf; peerConf.sourceId = localId; peerConf.destId = remoteId; peerConf.mode = TransmissionMode::ACKNOWLEDGED; peerConf.seqNum = TransactionSeqNum(UnsignedByteField(0)); peerConf.direction = Direction::TOWARDS_SENDER; std::array pduBuf{}; size_t serLen = 0; auto makeEofAckPdu = [&]() { AckInfo info(FileDirective::EOF_DIRECTIVE, ConditionCode::NO_ERROR, AckTransactionStatus::ACTIVE, 0); AckPduCreator creator(info, peerConf); REQUIRE(creator.serialize(pduBuf.data(), serLen, creator.getSerializedSize()) == OK); return OwnedPduPacket(creator.getPduType(), creator.getDirectiveCode(), pduBuf.data(), serLen); }; auto makeFinishedPdu = [&](ConditionCode condCode, FileDeliveryCode deliveryCode, FileDeliveryStatus status) { FinishedInfo info(condCode, deliveryCode, status); FinishPduCreator creator(peerConf, info); REQUIRE(creator.serialize(pduBuf.data(), serLen, creator.getSerializedSize()) == OK); return OwnedPduPacket(creator.getPduType(), creator.getDirectiveCode(), pduBuf.data(), serLen); }; auto makeNakPdu = [&](const std::vector>& segments) { std::vector segBuf; segBuf.reserve(segments.size()); for (const auto& segment : segments) { segBuf.emplace_back(Fss(segment.first), Fss(segment.second)); } NakInfo info(Fss(0), Fss(fileData.size())); size_t segLen = segBuf.size(); size_t maxSegLen = segBuf.size(); info.setSegmentRequests(segBuf.data(), &segLen, &maxSegLen); NakPduCreator creator(peerConf, info); REQUIRE(creator.serialize(pduBuf.data(), serLen, creator.getSerializedSize()) == OK); return OwnedPduPacket(creator.getPduType(), creator.getDirectiveCode(), pduBuf.data(), serLen); }; auto expectDirective = [&](FileDirective expected) { auto optPacket = pduSender.getNextSentPacket(); REQUIRE(optPacket.has_value()); REQUIRE(optPacket->pduType == PduType::FILE_DIRECTIVE); REQUIRE(optPacket->fileDirective.has_value()); CHECK(*optPacket->fileDirective == expected); return *optPacket; }; auto expectFileData = [&](uint64_t expectedOffset, size_t expectedLen) { auto optPacket = pduSender.getNextSentPacket(); REQUIRE(optPacket.has_value()); REQUIRE(optPacket->pduType == PduType::FILE_DATA); FileDataInfo fdInfo; FileDataReader reader(optPacket->rawPdu.data(), optPacket->rawPdu.size(), fdInfo); REQUIRE(reader.parseData() == OK); CHECK(fdInfo.getOffset().value() == expectedOffset); size_t len = 0; const uint8_t* data = fdInfo.getFileData(&len); CHECK(len == expectedLen); for (size_t idx = 0; idx < len; idx++) { CHECK(data[idx] == fileData[expectedOffset + idx]); } }; // Drives the handler until the EOF PDU has been sent, draining every PDU on the way. auto runUntilEofSent = [&]() { REQUIRE(sourceHandler.transactionStart(putRequest, cfg) == OK); REQUIRE(sourceHandler.getState() == CfdpState::BUSY_CLASS_2_ACKED); sourceHandler.stateMachineNoPacket(); expectDirective(FileDirective::METADATA); for (size_t offset = 0; offset < fileData.size(); offset += MAX_FILE_SEGMENT_SIZE) { sourceHandler.stateMachineNoPacket(); expectFileData(offset, MAX_FILE_SEGMENT_SIZE); } sourceHandler.stateMachineNoPacket(); expectDirective(FileDirective::EOF_DIRECTIVE); CHECK(sourceHandler.getStep() == SourceHandler::TransactionStep::WAIT_FOR_ACK); }; SECTION("Nominal acknowledged transfer") { runUntilEofSent(); auto ackPdu = makeEofAckPdu(); sourceHandler.stateMachine(ackPdu); CHECK(sourceHandler.getStep() == SourceHandler::TransactionStep::WAIT_FOR_FINISH); auto finishedPdu = makeFinishedPdu(ConditionCode::NO_ERROR, FileDeliveryCode::DATA_COMPLETE, FileDeliveryStatus::RETAINED_IN_FILESTORE); sourceHandler.stateMachine(finishedPdu); // The Finished PDU is acknowledged and the transaction completes. expectDirective(FileDirective::ACK); CHECK(sourceHandler.getState() == CfdpState::IDLE); REQUIRE(userMock.finishedRecvd.size() == 1); const auto& params = userMock.finishedRecvd.back().second; CHECK(params.condCode == ConditionCode::NO_ERROR); CHECK(params.deliveryCode == FileDeliveryCode::DATA_COMPLETE); CHECK(params.status == FileDeliveryStatus::RETAINED_IN_FILESTORE); } SECTION("Finished PDU condition code is reported instead of a hardcoded success") { runUntilEofSent(); auto ackPdu = makeEofAckPdu(); sourceHandler.stateMachine(ackPdu); auto finishedPdu = makeFinishedPdu(ConditionCode::FILE_CHECKSUM_FAILURE, FileDeliveryCode::DATA_INCOMPLETE, FileDeliveryStatus::DISCARDED_DELIBERATELY); sourceHandler.stateMachine(finishedPdu); expectDirective(FileDirective::ACK); REQUIRE(userMock.finishedRecvd.size() == 1); const auto& params = userMock.finishedRecvd.back().second; CHECK(params.condCode == ConditionCode::FILE_CHECKSUM_FAILURE); CHECK(params.deliveryCode == FileDeliveryCode::DATA_INCOMPLETE); } SECTION("EOF PDU is retransmitted on positive ACK timeout") { runUntilEofSent(); for (uint32_t idx = 0; idx < cfg.positiveAckTimerExpirationLimit; idx++) { std::this_thread::sleep_for(std::chrono::milliseconds(5)); sourceHandler.stateMachineNoPacket(); expectDirective(FileDirective::EOF_DIRECTIVE); CHECK(sourceHandler.getState() == CfdpState::BUSY_CLASS_2_ACKED); } std::this_thread::sleep_for(std::chrono::milliseconds(5)); sourceHandler.stateMachineNoPacket(); CHECK(sourceHandler.getState() == CfdpState::IDLE); auto& fhInfo = fhMock.getFhInfo(FaultHandlerCode::IGNORE_ERROR); REQUIRE(fhInfo.callCount == 1); CHECK(fhInfo.condCodes.front() == ConditionCode::POSITIVE_ACK_LIMIT_REACHED); REQUIRE(userMock.finishedRecvd.size() == 1); CHECK(userMock.finishedRecvd.back().second.condCode == ConditionCode::POSITIVE_ACK_LIMIT_REACHED); } SECTION("NAK PDU drives segment retransmission") { runUntilEofSent(); // Two gaps, the first of which spans more than one file segment. auto nakPdu = makeNakPdu({{256, 768}, {896, 1024}}); sourceHandler.stateMachine(nakPdu); // One PDU per state machine call, exactly like the regular file data phase. expectFileData(256, MAX_FILE_SEGMENT_SIZE); sourceHandler.stateMachineNoPacket(); expectFileData(512, MAX_FILE_SEGMENT_SIZE); sourceHandler.stateMachineNoPacket(); expectFileData(896, 128); // Nothing is outstanding any more, so the handler is back to guarding the EOF PDU. CHECK(sourceHandler.getStep() == SourceHandler::TransactionStep::WAIT_FOR_ACK); auto ackPdu = makeEofAckPdu(); sourceHandler.stateMachine(ackPdu); auto finishedPdu = makeFinishedPdu(ConditionCode::NO_ERROR, FileDeliveryCode::DATA_COMPLETE, FileDeliveryStatus::RETAINED_IN_FILESTORE); sourceHandler.stateMachine(finishedPdu); expectDirective(FileDirective::ACK); CHECK(sourceHandler.getState() == CfdpState::IDLE); } SECTION("A NAK keeps the inactivity guard from firing during recovery") { // After the EOF is acknowledged the positive ACK timer is re-armed as an inactivity guard for // the Finished PDU. In deferred NAK mode the receiver cannot send one until it has every // segment, so between NAK rounds it is deliberately silent. Every serviced NAK has to reset // the guard, otherwise it fires while recovery is still working. runUntilEofSent(); auto ackPdu = makeEofAckPdu(); sourceHandler.stateMachine(ackPdu); REQUIRE(sourceHandler.getStep() == SourceHandler::TransactionStep::WAIT_FOR_FINISH); // Several rounds of: the guard expires while the receiver is quiet, then a NAK arrives and is // answered. Enough rounds that the expiries alone would exceed the limit. for (uint32_t round = 0; round < cfg.positiveAckTimerExpirationLimit + 2; round++) { std::this_thread::sleep_for(std::chrono::milliseconds(5)); // No retransmission outstanding, so this call reaches the guard rather than returning early. sourceHandler.stateMachineNoPacket(); REQUIRE(sourceHandler.getState() == CfdpState::BUSY_CLASS_2_ACKED); auto nakPdu = makeNakPdu({{256, 512}}); sourceHandler.stateMachine(nakPdu); expectFileData(256, MAX_FILE_SEGMENT_SIZE); // Answering a NAK puts the handler back to guarding the EOF, so acknowledge it again to // return to the state the guard belongs to. auto reAck = makeEofAckPdu(); sourceHandler.stateMachine(reAck); } CHECK(fhMock.getFhInfo(FaultHandlerCode::IGNORE_ERROR).callCount == 0); CHECK(sourceHandler.getState() == CfdpState::BUSY_CLASS_2_ACKED); // The guard still fires when the receiver really has gone silent. for (uint32_t idx = 0; idx < cfg.positiveAckTimerExpirationLimit + 2; idx++) { std::this_thread::sleep_for(std::chrono::milliseconds(5)); sourceHandler.stateMachineNoPacket(); } CHECK(sourceHandler.getState() == CfdpState::IDLE); CHECK(fhMock.getFhInfo(FaultHandlerCode::IGNORE_ERROR).condCodes.front() == ConditionCode::INACTIVITY_DETECTED); } SECTION("NAK PDU of scope 0 to 0 retransmits the metadata PDU") { runUntilEofSent(); auto nakPdu = makeNakPdu({{0, 0}}); sourceHandler.stateMachine(nakPdu); expectDirective(FileDirective::METADATA); CHECK(sourceHandler.getStep() == SourceHandler::TransactionStep::WAIT_FOR_ACK); } SECTION("A failed retransmission send is retried, not skipped") { // Same contract as "File data PDU send failure is retried, not dropped" for the forward // path: a downstream send failure must leave the requested segment outstanding. The peer // only re-requests it on its NAK timer, so dropping it here burns a NAK limit credit and, // once the limit is reached, loses the transfer. runUntilEofSent(); auto nakPdu = makeNakPdu({{256, 512}}); pduSender.failNextSend = true; sourceHandler.stateMachine(nakPdu); CHECK(not pduSender.getNextSentPacket().has_value()); // The retransmission is still outstanding, so the next call has to send that same segment. sourceHandler.stateMachineNoPacket(); expectFileData(256, MAX_FILE_SEGMENT_SIZE); } SECTION("A failed metadata retransmission is retried, not dropped") { // The scope 0 to 0 request is the receiver saying it never got the metadata PDU and cannot // write the file at all. Losing the answer to a full TM store strands the whole transfer. runUntilEofSent(); auto nakPdu = makeNakPdu({{0, 0}}); pduSender.failNextSend = true; sourceHandler.stateMachine(nakPdu); CHECK(not pduSender.getNextSentPacket().has_value()); sourceHandler.stateMachineNoPacket(); expectDirective(FileDirective::METADATA); } SECTION("A NAK with more segment requests than fit still answers the ones which do") { // NakPduReader fills the caller's array incrementally but reports a segment request length // of 0 when it runs out of room, so the handler must not rely on that length alone. A peer // which packs more requests into one NAK than RetransmitState can hold otherwise gets no // retransmission at all. runUntilEofSent(); std::vector> segments; for (uint64_t idx = 0; idx < 33; idx++) { segments.emplace_back(idx * 2, idx * 2 + 1); } auto nakPdu = makeNakPdu(segments); sourceHandler.stateMachine(nakPdu); expectFileData(0, 1); } SECTION("An unparseable Finished PDU does not complete the transaction") { // A Finished PDU truncated before its condition code byte cannot say anything about how the // transfer went. Completing on it reports the default-constructed DATA_COMPLETE, i.e. a // fabricated success, and discards the state the peer's retransmission would have landed in. runUntilEofSent(); auto eofAck = makeEofAckPdu(); sourceHandler.stateMachine(eofAck); REQUIRE(sourceHandler.getStep() == SourceHandler::TransactionStep::WAIT_FOR_FINISH); auto finishedPdu = makeFinishedPdu(ConditionCode::NO_ERROR, FileDeliveryCode::DATA_COMPLETE, FileDeliveryStatus::RETAINED_IN_FILESTORE); // Cut the PDU short so the header declares more bytes than are actually present, which is // what a truncated uplink looks like. finishedPdu.rawPdu.resize(finishedPdu.rawPdu.size() - 1); sourceHandler.stateMachine(finishedPdu); CHECK(sourceHandler.getState() == CfdpState::BUSY_CLASS_2_ACKED); CHECK(userMock.finishedRecvd.empty()); } SECTION("An ACK PDU for a different transaction is ignored") { // Nothing upstream filters by transaction: CfdpHandler routes on direction alone. A late ACK // from a previous transaction therefore lands in whichever one is running now. runUntilEofSent(); peerConf.seqNum = TransactionSeqNum(UnsignedByteField(42)); auto staleAck = makeEofAckPdu(); sourceHandler.stateMachine(staleAck); CHECK(sourceHandler.getStep() == SourceHandler::TransactionStep::WAIT_FOR_ACK); } SECTION("A Finished PDU for a different transaction is ignored") { runUntilEofSent(); auto eofAck = makeEofAckPdu(); sourceHandler.stateMachine(eofAck); REQUIRE(sourceHandler.getStep() == SourceHandler::TransactionStep::WAIT_FOR_FINISH); peerConf.seqNum = TransactionSeqNum(UnsignedByteField(42)); auto staleFinished = makeFinishedPdu(ConditionCode::NO_ERROR, FileDeliveryCode::DATA_COMPLETE, FileDeliveryStatus::RETAINED_IN_FILESTORE); sourceHandler.stateMachine(staleFinished); CHECK(sourceHandler.getState() == CfdpState::BUSY_CLASS_2_ACKED); CHECK(userMock.finishedRecvd.empty()); } }