#include #include #include #include #include #include #include "OwnedPduPacket.h" #include "cfdp/PduSenderMock.h" #include "fsfw/cfdp.h" #include "fsfw/cfdp/pdu/AckPduCreator.h" #include "fsfw/cfdp/pdu/AckPduReader.h" #include "fsfw/cfdp/pdu/EofPduCreator.h" #include "fsfw/cfdp/pdu/FileDataCreator.h" #include "fsfw/cfdp/pdu/MetadataPduCreator.h" #include "fsfw/cfdp/pdu/NakPduReader.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 Dest Handler", "[cfdp]") { using namespace cfdp; using namespace returnvalue; MessageQueueId_t destQueueId = 2; AcceptsTmMock tmReceiver(destQueueId); MessageQueueMock mqMock(destQueueId, MessageQueueIF::NO_QUEUE); auto localId = EntityId(UnsignedByteField(2)); auto remoteId = EntityId(UnsignedByteField(3)); FaultHandlerMock fhMock; LocalEntityCfg localEntityCfg(localId, IndicationCfg(), fhMock); FilesystemMock fsMock; UserMock userMock(fsMock); RemoteConfigTableMock remoteCfgTableMock; LostSegmentsList<128> lostSegmentsList; DestHandlerParams dp(localEntityCfg, userMock, remoteCfgTableMock, lostSegmentsList); EventReportingProxyMock eventReporterMock; LocalPool::LocalPoolConfig storeCfg = {{10, 32}, {10, 64}, {10, 128}, {10, 1024}}; PduSenderMock senderMock; FsfwParams fp(&eventReporterMock); RemoteEntityCfg cfg(remoteId); remoteCfgTableMock.addRemoteConfig(cfg); std::array pduBuf{}; size_t serLen = 0; store_address_t storeId; PduConfig conf; auto destHandler = DestHandler(senderMock, 4096, dp, fp); auto metadataPreparation = [&](Fss cfdpFileSize, ChecksumType checksumType, bool closureRequested = false) { const std::string srcNameString = "hello.txt"; const std::string destNameString = "hello-cpy.txt"; StringLv srcName(srcNameString); StringLv destName(destNameString); MetadataGenericInfo info(closureRequested, checksumType, std::move(cfdpFileSize)); const TransactionSeqNum seqNum(UnsignedByteField(1)); conf.sourceId = remoteId; conf.destId = localId; conf.mode = TransmissionMode::UNACKNOWLEDGED; conf.seqNum = seqNum; const MetadataPduCreator metadataCreator(conf, info, srcName, destName, nullptr, 0); REQUIRE(metadataCreator.serialize(pduBuf.data(), serLen, metadataCreator.getSerializedSize()) == OK); return OwnedPduPacket(metadataCreator.getPduType(), metadataCreator.getDirectiveCode(), pduBuf.data(), serLen); }; auto metadataCheck = [&](const cfdp::DestHandler::FsmResult& res, const char* sourceName, const char* destName, size_t fileLen) { REQUIRE(res.result == OK); REQUIRE(res.errors == 0); REQUIRE(userMock.metadataRecvd.size() == 1); auto& idMetadataPair = userMock.metadataRecvd.back(); REQUIRE(idMetadataPair.first == destHandler.getTransactionId()); REQUIRE(idMetadataPair.second.sourceId.getValue() == 3); REQUIRE(idMetadataPair.second.fileSize.getSize(nullptr) == fileLen); REQUIRE(strcmp(idMetadataPair.second.destFileName, destName) == 0); REQUIRE(strcmp(idMetadataPair.second.sourceFileName, sourceName) == 0); userMock.metadataRecvd.pop(); REQUIRE(fsMock.fileMap.find(destName) != fsMock.fileMap.end()); REQUIRE(res.result == OK); REQUIRE(res.state == CfdpState::BUSY_CLASS_1_NACKED); REQUIRE(res.step == DestHandler::TransactionStep::RECEIVING_FILE_DATA_PDUS); }; auto eofPreparation = [&](Fss cfdpFileSize, uint32_t crc) { EofInfo eofInfo(cfdp::ConditionCode::NO_ERROR, crc, std::move(cfdpFileSize)); EofPduCreator eofCreator(conf, eofInfo); REQUIRE(eofCreator.serialize(pduBuf.data(), serLen, eofCreator.getSerializedSize()) == OK); OwnedPduPacket packet(eofCreator.getPduType(), eofCreator.getDirectiveCode(), pduBuf.data(), serLen); return packet; }; auto eofCheck = [&](const cfdp::DestHandler::FsmResult& res, const TransactionId& id) { REQUIRE(res.result == OK); REQUIRE(res.state == CfdpState::IDLE); REQUIRE(res.errors == 0); REQUIRE(res.step == DestHandler::TransactionStep::IDLE); REQUIRE(userMock.eofRecvdRecvd.size() == 1); auto& eofId = userMock.eofRecvdRecvd.back(); CHECK(eofId == id); REQUIRE(userMock.finishedRecvd.size() == 1); auto& idParamPair = userMock.finishedRecvd.back(); CHECK(idParamPair.first == id); CHECK(idParamPair.second.condCode == ConditionCode::NO_ERROR); }; auto fileDataPduCheck = [&](const cfdp::DestHandler::FsmResult& res, const std::vector& idsToCheck) { REQUIRE(res.result == OK); REQUIRE(res.state == CfdpState::BUSY_CLASS_1_NACKED); REQUIRE(res.step == DestHandler::TransactionStep::RECEIVING_FILE_DATA_PDUS); }; SECTION("State") { CHECK(destHandler.getCfdpState() == CfdpState::IDLE); CHECK(destHandler.getTransactionStep() == DestHandler::TransactionStep::IDLE); } SECTION("Idle State Machine Iteration") { auto [packetsSent, result, step, state, errors, errorCodes] = destHandler.stateMachineNoPacket(); CHECK(result == OK); CHECK(errors == 0); CHECK(destHandler.getCfdpState() == CfdpState::IDLE); CHECK(destHandler.getTransactionStep() == DestHandler::TransactionStep::IDLE); } SECTION("Empty File Transfer") { const DestHandler::FsmResult& res = destHandler.stateMachineNoPacket(); CHECK(res.result == OK); Fss cfdpFileSize(0); auto packet = metadataPreparation(cfdpFileSize, ChecksumType::NULL_CHECKSUM); destHandler.stateMachine(packet); metadataCheck(res, "hello.txt", "hello-cpy.txt", 0); destHandler.stateMachine(packet); // REQUIRE(res.callStatus == CallStatus::CALL_AFTER_DELAY); auto transactionId = destHandler.getTransactionId(); auto ownedEofPacket = eofPreparation(cfdpFileSize, 0); // After EOF, operation is done because no closure was requested destHandler.stateMachine(ownedEofPacket); eofCheck(res, transactionId); } SECTION("Small File Transfer") { const DestHandler::FsmResult& res = destHandler.stateMachineNoPacket(); CHECK(res.result == OK); std::string fileData = "hello test data"; etl::crc32 crcCalc; crcCalc.add(fileData.begin(), fileData.end()); uint32_t crc32 = crcCalc.value(); Fss cfdpFileSize(fileData.size()); auto metadataPacket = metadataPreparation(cfdpFileSize, ChecksumType::CRC_32); destHandler.stateMachine(metadataPacket); metadataCheck(res, "hello.txt", "hello-cpy.txt", fileData.size()); destHandler.stateMachineNoPacket(); auto transactionId = destHandler.getTransactionId(); Fss offset(0); FileDataInfo fdPduInfo(offset, reinterpret_cast(fileData.data()), fileData.size()); FileDataCreator fdPduCreator(conf, fdPduInfo); REQUIRE(fdPduCreator.serialize(pduBuf.data(), serLen, fdPduCreator.getSerializedSize()) == OK); OwnedPduPacket fdPdu(fdPduCreator.getPduType(), std::nullopt, pduBuf.data(), serLen); destHandler.stateMachine(fdPdu); fileDataPduCheck(res, {storeId}); auto eofPacket = eofPreparation(cfdpFileSize, crc32); // After EOF, operation is done because no closure was requested destHandler.stateMachine(eofPacket); eofCheck(res, transactionId); } SECTION("Segmented File Transfer") { const DestHandler::FsmResult& res = destHandler.stateMachineNoPacket(); CHECK(res.result == OK); 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); } etl::crc32 crcCalc; crcCalc.add(largerFileData.begin(), largerFileData.end()); uint32_t crc32 = crcCalc.value(); Fss cfdpFileSize(largerFileData.size()); auto metaPacket = metadataPreparation(cfdpFileSize, ChecksumType::CRC_32); destHandler.stateMachine(metaPacket); metadataCheck(res, "hello.txt", "hello-cpy.txt", largerFileData.size()); destHandler.stateMachineNoPacket(); auto transactionId = destHandler.getTransactionId(); std::vector idsToCheck; { Fss offset(0); FileDataInfo fdPduInfo(offset, (largerFileData.data()), largerFileData.size() / 2); FileDataCreator fdPduCreator(conf, fdPduInfo); REQUIRE(fdPduCreator.serialize(pduBuf.data(), serLen, fdPduCreator.getSerializedSize()) == OK); idsToCheck.push_back(storeId); OwnedPduPacket fdPdu(fdPduCreator.getPduType(), std::nullopt, pduBuf.data(), serLen); destHandler.stateMachine(fdPdu); } { Fss offset(512); FileDataInfo fdPduInfo(offset, largerFileData.data() + 512, largerFileData.size() / 2); FileDataCreator fdPduCreator(conf, fdPduInfo); REQUIRE(fdPduCreator.serialize(pduBuf.data(), serLen, fdPduCreator.getSerializedSize()) == OK); idsToCheck.push_back(storeId); OwnedPduPacket fdPdu(fdPduCreator.getPduType(), std::nullopt, pduBuf.data(), serLen); destHandler.stateMachine(fdPdu); } destHandler.stateMachineNoPacket(); fileDataPduCheck(res, idsToCheck); auto eofPacket = eofPreparation(cfdpFileSize, crc32); // After EOF, operation is done because no closure was requested destHandler.stateMachine(eofPacket); eofCheck(res, transactionId); } SECTION("A failed Finished PDU send is retried, then the transaction is released") { // Class 1 has no ACK for the Finished PDU, so a failed send used to be indistinguishable // from a successful one: the transaction was finished either way and the sender never heard // that a transfer which actually succeeded had completed. std::string fileData = "hello test data"; etl::crc32 crcCalc; crcCalc.add(fileData.begin(), fileData.end()); Fss cfdpFileSize(fileData.size()); auto metadataPacket = metadataPreparation(cfdpFileSize, ChecksumType::CRC_32, true); const DestHandler::FsmResult& res = destHandler.stateMachine(metadataPacket); Fss offset(0); FileDataInfo fdPduInfo(offset, reinterpret_cast(fileData.data()), fileData.size()); FileDataCreator fdPduCreator(conf, fdPduInfo); REQUIRE(fdPduCreator.serialize(pduBuf.data(), serLen, fdPduCreator.getSerializedSize()) == OK); OwnedPduPacket fdPdu(fdPduCreator.getPduType(), std::nullopt, pduBuf.data(), serLen); destHandler.stateMachine(fdPdu); // The Finished PDU send fails on the first attempt only. senderMock.failNextSend = true; auto eofPacket = eofPreparation(cfdpFileSize, crcCalc.value()); destHandler.stateMachine(eofPacket); CHECK(not senderMock.getNextSentPacket().has_value()); REQUIRE(res.step == DestHandler::TransactionStep::SENDING_FINISHED_PDU); // The retry succeeds and only then is the transaction released. destHandler.stateMachineNoPacket(); auto optPacket = senderMock.getNextSentPacket(); REQUIRE(optPacket.has_value()); REQUIRE(optPacket->fileDirective.has_value()); CHECK(*optPacket->fileDirective == FileDirective::FINISH); CHECK(destHandler.getCfdpState() == CfdpState::IDLE); } SECTION("A Finished PDU which can never be sent still releases the handler") { // The retry has to be bounded. While a transaction is held the handler refuses every new // metadata PDU, so retrying forever on a downstream which stays broken would mean no uplink // can ever start again. std::string fileData = "hello test data"; etl::crc32 crcCalc; crcCalc.add(fileData.begin(), fileData.end()); Fss cfdpFileSize(fileData.size()); auto metadataPacket = metadataPreparation(cfdpFileSize, ChecksumType::CRC_32, true); destHandler.stateMachine(metadataPacket); Fss offset(0); FileDataInfo fdPduInfo(offset, reinterpret_cast(fileData.data()), fileData.size()); FileDataCreator fdPduCreator(conf, fdPduInfo); REQUIRE(fdPduCreator.serialize(pduBuf.data(), serLen, fdPduCreator.getSerializedSize()) == OK); OwnedPduPacket fdPdu(fdPduCreator.getPduType(), std::nullopt, pduBuf.data(), serLen); destHandler.stateMachine(fdPdu); senderMock.failSendsFromIdx = 0; auto eofPacket = eofPreparation(cfdpFileSize, crcCalc.value()); destHandler.stateMachine(eofPacket); for (int idx = 0; idx < 100; idx++) { destHandler.stateMachineNoPacket(); } CHECK(destHandler.getCfdpState() == CfdpState::IDLE); CHECK(destHandler.getTransactionStep() == DestHandler::TransactionStep::IDLE); } } TEST_CASE("CFDP Dest Handler Acknowledged", "[cfdp]") { using namespace cfdp; using namespace returnvalue; auto localId = EntityId(UnsignedByteField(2)); auto remoteId = EntityId(UnsignedByteField(3)); FaultHandlerMock fhMock; LocalEntityCfg localEntityCfg(localId, IndicationCfg(), fhMock); FilesystemMock fsMock; UserMock userMock(fsMock); RemoteConfigTableMock remoteCfgTableMock; LostSegmentsList<128> lostSegmentsList; DestHandlerParams dp(localEntityCfg, userMock, remoteCfgTableMock, lostSegmentsList); EventReportingProxyMock eventReporterMock; PduSenderMock senderMock; FsfwParams fp(&eventReporterMock); RemoteEntityCfg cfg(remoteId); // Keep the timers short so the timeout paths are testable without stalling the suite. cfg.positiveAckTimerIntervalMs = 1; cfg.positiveAckTimerExpirationLimit = 2; cfg.nakTimerIntervalMs = 1; cfg.nakTimerExpirationLimit = 2; cfg.checkTimerIntervalMs = 100000; cfg.checkLimit = 2; remoteCfgTableMock.addRemoteConfig(cfg); std::array pduBuf{}; size_t serLen = 0; PduConfig conf; auto destHandler = DestHandler(senderMock, 4096, dp, fp); const std::string srcNameString = "hello.txt"; const std::string destNameString = "hello-cpy.txt"; const TransactionSeqNum seqNum(UnsignedByteField(1)); conf.sourceId = remoteId; conf.destId = localId; conf.mode = TransmissionMode::ACKNOWLEDGED; conf.seqNum = seqNum; std::array fileData{}; for (size_t idx = 0; idx < fileData.size(); idx++) { fileData[idx] = static_cast(idx); } etl::crc32 crcCalc; crcCalc.add(fileData.begin(), fileData.end()); const uint32_t crc32 = crcCalc.value(); auto makeMetadataPdu = [&]() { StringLv srcName(srcNameString); StringLv destName(destNameString); MetadataGenericInfo info(false, ChecksumType::CRC_32, Fss(fileData.size())); const MetadataPduCreator creator(conf, info, srcName, destName, nullptr, 0); REQUIRE(creator.serialize(pduBuf.data(), serLen, creator.getSerializedSize()) == OK); return OwnedPduPacket(creator.getPduType(), creator.getDirectiveCode(), pduBuf.data(), serLen); }; auto makeFileDataPdu = [&](uint64_t offset, size_t len) { Fss offsetFss(offset); FileDataInfo info(offsetFss, fileData.data() + offset, len); FileDataCreator creator(conf, info); REQUIRE(creator.serialize(pduBuf.data(), serLen, creator.getSerializedSize()) == OK); return OwnedPduPacket(creator.getPduType(), std::nullopt, pduBuf.data(), serLen); }; auto makeEofPdu = [&]() { EofInfo info(ConditionCode::NO_ERROR, crc32, Fss(fileData.size())); EofPduCreator creator(conf, info); REQUIRE(creator.serialize(pduBuf.data(), serLen, creator.getSerializedSize()) == OK); return OwnedPduPacket(creator.getPduType(), creator.getDirectiveCode(), pduBuf.data(), serLen); }; auto makeFinishedAckPdu = [&]() { AckInfo info(FileDirective::FINISH, ConditionCode::NO_ERROR, AckTransactionStatus::ACTIVE, 1); PduConfig ackConf = conf; AckPduCreator creator(info, ackConf); REQUIRE(creator.serialize(pduBuf.data(), serLen, creator.getSerializedSize()) == OK); return OwnedPduPacket(creator.getPduType(), creator.getDirectiveCode(), pduBuf.data(), serLen); }; // Pops the next sent PDU and checks it is the expected directive. auto expectDirective = [&](FileDirective expected) { auto optPacket = senderMock.getNextSentPacket(); REQUIRE(optPacket.has_value()); REQUIRE(optPacket->pduType == PduType::FILE_DIRECTIVE); REQUIRE(optPacket->fileDirective.has_value()); REQUIRE(*optPacket->fileDirective == expected); return *optPacket; }; auto parseNak = [&](const SentPdu& pdu, std::vector>& segments) { NakInfo info(Fss(0), Fss(0)); std::array segBuf{}; size_t segLen = 0; size_t maxSegLen = segBuf.size(); info.setSegmentRequests(segBuf.data(), &segLen, &maxSegLen); NakPduReader reader(pdu.rawPdu.data(), pdu.rawPdu.size(), info); REQUIRE(reader.parseData() == OK); segments.clear(); for (size_t idx = 0; idx < info.getSegmentRequestsLen(); idx++) { segments.emplace_back(segBuf[idx].first.value(), segBuf[idx].second.value()); } }; SECTION("Nominal acknowledged transfer") { auto metadataPdu = makeMetadataPdu(); const DestHandler::FsmResult& res = destHandler.stateMachine(metadataPdu); REQUIRE(res.state == CfdpState::BUSY_CLASS_2_ACKED); REQUIRE(res.step == DestHandler::TransactionStep::RECEIVING_FILE_DATA_PDUS); auto fdPdu = makeFileDataPdu(0, fileData.size()); destHandler.stateMachine(fdPdu); REQUIRE(destHandler.getNumLostSegments() == 0); auto eofPdu = makeEofPdu(); destHandler.stateMachine(eofPdu); // D2: the ACK for the EOF PDU is emitted before the Finished PDU, and before the checksum // verification which produced it. expectDirective(FileDirective::ACK); expectDirective(FileDirective::FINISH); // The transaction is retained until the Finished PDU is acknowledged. REQUIRE(res.state == CfdpState::BUSY_CLASS_2_ACKED); REQUIRE(res.step == DestHandler::TransactionStep::WAITING_FOR_FINISHED_ACK); REQUIRE(userMock.finishedRecvd.size() == 1); CHECK(userMock.finishedRecvd.back().second.condCode == ConditionCode::NO_ERROR); auto ackPdu = makeFinishedAckPdu(); destHandler.stateMachine(ackPdu); CHECK(destHandler.getCfdpState() == CfdpState::IDLE); CHECK(destHandler.getTransactionStep() == DestHandler::TransactionStep::IDLE); } SECTION("Cancel EOF is parsed, acknowledged and ends the transaction") { // A Cancel EOF carries a condition code other than NO_ERROR and a fault location TLV. The // reader refuses to parse one unless it is given somewhere to put that TLV, so this used to // fail with "Ca not deserialize fault location" and the whole PDU was dropped: the sender // never got its ACK and retransmitted to its positive ACK limit, while this handler held the // transaction open until its own check limit expired. auto metadataPdu = makeMetadataPdu(); const DestHandler::FsmResult& res = destHandler.stateMachine(metadataPdu); REQUIRE(res.state == CfdpState::BUSY_CLASS_2_ACKED); // Only part of the file arrives before the sender gives up on it. auto fdPdu = makeFileDataPdu(0, 10); destHandler.stateMachine(fdPdu); EntityId faultLocId(UnsignedByteField(2)); EntityIdTlv faultLoc(faultLocId); EofInfo cancelInfo(ConditionCode::CANCEL_REQUEST_RECEIVED, 0, Fss(fileData.size()), &faultLoc); EofPduCreator cancelCreator(conf, cancelInfo); REQUIRE(cancelCreator.serialize(pduBuf.data(), serLen, cancelCreator.getSerializedSize()) == OK); auto cancelEof = OwnedPduPacket(cancelCreator.getPduType(), cancelCreator.getDirectiveCode(), pduBuf.data(), serLen); destHandler.stateMachine(cancelEof); // The cancellation is acknowledged rather than ignored, and it is reported with the condition // code the sender gave instead of a checksum failure over the partial file. auto ackPdu = expectDirective(FileDirective::ACK); AckInfo ackInfo; AckPduReader ackReader(ackPdu.rawPdu.data(), ackPdu.rawPdu.size(), ackInfo); REQUIRE(ackReader.parseData() == OK); CHECK(ackInfo.getAckedDirective() == FileDirective::EOF_DIRECTIVE); CHECK(ackInfo.getAckedConditionCode() == ConditionCode::CANCEL_REQUEST_RECEIVED); expectDirective(FileDirective::FINISH); REQUIRE(userMock.finishedRecvd.size() == 1); CHECK(userMock.finishedRecvd.back().second.condCode == ConditionCode::CANCEL_REQUEST_RECEIVED); CHECK(userMock.finishedRecvd.back().second.deliveryCode == FileDeliveryCode::DATA_INCOMPLETE); } SECTION("Dropped file data PDU is recovered via NAK") { auto metadataPdu = makeMetadataPdu(); const DestHandler::FsmResult& res = destHandler.stateMachine(metadataPdu); // The segment from 256 to 512 is dropped on the way. auto firstPdu = makeFileDataPdu(0, 256); destHandler.stateMachine(firstPdu); auto thirdPdu = makeFileDataPdu(512, 512); destHandler.stateMachine(thirdPdu); REQUIRE(destHandler.getNumLostSegments() == 1); auto eofPdu = makeEofPdu(); destHandler.stateMachine(eofPdu); expectDirective(FileDirective::ACK); auto nakPdu = expectDirective(FileDirective::NAK); std::vector> segments; parseNak(nakPdu, segments); REQUIRE(segments.size() == 1); CHECK(segments[0].first == 256); CHECK(segments[0].second == 512); REQUIRE(res.step == DestHandler::TransactionStep::WAITING_FOR_MISSING_DATA); // The retransmission closes the gap and the transfer completes. auto retransmit = makeFileDataPdu(256, 256); destHandler.stateMachine(retransmit); CHECK(destHandler.getNumLostSegments() == 0); expectDirective(FileDirective::FINISH); REQUIRE(res.step == DestHandler::TransactionStep::WAITING_FOR_FINISHED_ACK); REQUIRE(userMock.finishedRecvd.size() == 1); CHECK(userMock.finishedRecvd.back().second.condCode == ConditionCode::NO_ERROR); CHECK(userMock.finishedRecvd.back().second.deliveryCode == FileDeliveryCode::DATA_COMPLETE); } SECTION("Dropped metadata PDU is requested with a scope 0 to 0 NAK") { // The first PDU the destination sees is a file data PDU, which in class 2 starts the // transaction instead of being rejected. auto fdPdu = makeFileDataPdu(0, 256); const DestHandler::FsmResult& res = destHandler.stateMachine(fdPdu); REQUIRE(res.result == OK); REQUIRE(res.state == CfdpState::BUSY_CLASS_2_ACKED); auto nakPdu = expectDirective(FileDirective::NAK); std::vector> segments; parseNak(nakPdu, segments); REQUIRE(segments.size() == 1); CHECK(segments[0].first == 0); CHECK(segments[0].second == 0); // Nothing was written, there is no destination file name yet. CHECK(fsMock.fileMap.find(destNameString) == fsMock.fileMap.end()); // The metadata retransmission completes the transaction setup. auto metadataPdu = makeMetadataPdu(); destHandler.stateMachine(metadataPdu); CHECK(fsMock.fileMap.find(destNameString) != fsMock.fileMap.end()); auto allData = makeFileDataPdu(0, fileData.size()); destHandler.stateMachine(allData); auto eofPdu = makeEofPdu(); destHandler.stateMachine(eofPdu); expectDirective(FileDirective::ACK); expectDirective(FileDirective::FINISH); CHECK(userMock.finishedRecvd.back().second.condCode == ConditionCode::NO_ERROR); } SECTION("Finished PDU is retransmitted on positive ACK timeout") { auto metadataPdu = makeMetadataPdu(); destHandler.stateMachine(metadataPdu); auto fdPdu = makeFileDataPdu(0, fileData.size()); destHandler.stateMachine(fdPdu); auto eofPdu = makeEofPdu(); const DestHandler::FsmResult& res = destHandler.stateMachine(eofPdu); expectDirective(FileDirective::ACK); expectDirective(FileDirective::FINISH); REQUIRE(res.step == DestHandler::TransactionStep::WAITING_FOR_FINISHED_ACK); // Two expirations are tolerated, each retransmits the Finished PDU. for (uint32_t idx = 0; idx < cfg.positiveAckTimerExpirationLimit; idx++) { std::this_thread::sleep_for(std::chrono::milliseconds(5)); destHandler.stateMachineNoPacket(); expectDirective(FileDirective::FINISH); CHECK(destHandler.getCfdpState() == CfdpState::BUSY_CLASS_2_ACKED); } // The next one reaches the limit and releases the handler. std::this_thread::sleep_for(std::chrono::milliseconds(5)); destHandler.stateMachineNoPacket(); CHECK(destHandler.getCfdpState() == CfdpState::IDLE); auto& fhInfo = fhMock.getFhInfo(FaultHandlerCode::IGNORE_ERROR); REQUIRE(fhInfo.callCount == 1); CHECK(fhInfo.condCodes.front() == ConditionCode::POSITIVE_ACK_LIMIT_REACHED); } SECTION("NAK limit reached cancels the transaction") { auto metadataPdu = makeMetadataPdu(); destHandler.stateMachine(metadataPdu); auto firstPdu = makeFileDataPdu(0, 256); destHandler.stateMachine(firstPdu); auto eofPdu = makeEofPdu(); const DestHandler::FsmResult& res = destHandler.stateMachine(eofPdu); expectDirective(FileDirective::ACK); expectDirective(FileDirective::NAK); REQUIRE(res.step == DestHandler::TransactionStep::WAITING_FOR_MISSING_DATA); for (uint32_t idx = 0; idx < cfg.nakTimerExpirationLimit; idx++) { std::this_thread::sleep_for(std::chrono::milliseconds(5)); destHandler.stateMachineNoPacket(); expectDirective(FileDirective::NAK); } std::this_thread::sleep_for(std::chrono::milliseconds(5)); destHandler.stateMachineNoPacket(); // The transaction is cancelled and the failure is reported in the Finished PDU rather than // pinning the handler. expectDirective(FileDirective::FINISH); REQUIRE(userMock.finishedRecvd.size() == 1); CHECK(userMock.finishedRecvd.back().second.condCode == ConditionCode::NAK_LIMIT_REACHED); CHECK(userMock.finishedRecvd.back().second.deliveryCode == FileDeliveryCode::DATA_INCOMPLETE); auto& fhInfo = fhMock.getFhInfo(FaultHandlerCode::IGNORE_ERROR); REQUIRE(fhInfo.callCount == 1); CHECK(fhInfo.condCodes.front() == ConditionCode::NAK_LIMIT_REACHED); } SECTION("EOF PDU for an inactive transaction is acknowledged") { // D7: the sender retransmitted an EOF after we already finished. Without an ACK it would // retransmit to its own limit and declare a fault at the end of a successful transfer. auto eofPdu = makeEofPdu(); const DestHandler::FsmResult& res = destHandler.stateMachine(eofPdu); CHECK(res.result == OK); auto ackPdu = expectDirective(FileDirective::ACK); AckInfo ackInfo; AckPduReader reader(ackPdu.rawPdu.data(), ackPdu.rawPdu.size(), ackInfo); REQUIRE(reader.parseData() == OK); CHECK(ackInfo.getAckedDirective() == FileDirective::EOF_DIRECTIVE); CHECK(ackInfo.getTransactionStatus() == AckTransactionStatus::UNRECOGNIZED); CHECK(destHandler.getCfdpState() == CfdpState::IDLE); } SECTION("A Finished PDU which failed to send is not treated as awaiting its ACK") { // The EOF PDU produces two sends in one state machine call: the ACK first, then the Finished // PDU. Fail only the second one, so the peer never sees a Finished PDU at all. auto metadataPdu = makeMetadataPdu(); destHandler.stateMachine(metadataPdu); auto fdPdu = makeFileDataPdu(0, fileData.size()); destHandler.stateMachine(fdPdu); senderMock.failSendAtIdx = 1; auto eofPdu = makeEofPdu(); destHandler.stateMachine(eofPdu); expectDirective(FileDirective::ACK); CHECK(not senderMock.getNextSentPacket().has_value()); // Waiting for the ACK of a PDU that was never sent costs a full positive ACK interval before // the first retransmission, so the step has to stay on the send instead. CHECK(destHandler.getTransactionStep() == DestHandler::TransactionStep::SENDING_FINISHED_PDU); } SECTION("An unsendable Finished PDU does not pin the handler in acknowledged mode") { // Keeping the step on SENDING_FINISHED_PDU retries the send, but nothing arms a timer or // counts the attempts there, so a downstream which stays broken has to be bounded the same // way class 1 bounds it. Otherwise the transaction is never released and, because a busy // handler discards incoming metadata PDUs, no later uplink can start. auto metadataPdu = makeMetadataPdu(); destHandler.stateMachine(metadataPdu); auto fdPdu = makeFileDataPdu(0, fileData.size()); destHandler.stateMachine(fdPdu); // The EOF ACK goes out, every Finished PDU send after it fails. senderMock.failSendsFromIdx = 1; auto eofPdu = makeEofPdu(); destHandler.stateMachine(eofPdu); expectDirective(FileDirective::ACK); for (int idx = 0; idx < 100; idx++) { destHandler.stateMachineNoPacket(); } CHECK(destHandler.getCfdpState() == CfdpState::IDLE); CHECK(destHandler.getTransactionStep() == DestHandler::TransactionStep::IDLE); } SECTION("A Finished ACK for a different transaction is ignored") { // CfdpHandler routes ACK PDUs on the acked directive alone, so a late ACK from an earlier // transaction reaches whichever transaction is running now. auto metadataPdu = makeMetadataPdu(); destHandler.stateMachine(metadataPdu); auto fdPdu = makeFileDataPdu(0, fileData.size()); destHandler.stateMachine(fdPdu); auto eofPdu = makeEofPdu(); destHandler.stateMachine(eofPdu); expectDirective(FileDirective::ACK); expectDirective(FileDirective::FINISH); REQUIRE(destHandler.getTransactionStep() == DestHandler::TransactionStep::WAITING_FOR_FINISHED_ACK); conf.seqNum = TransactionSeqNum(UnsignedByteField(42)); auto staleAck = makeFinishedAckPdu(); destHandler.stateMachine(staleAck); CHECK(destHandler.getCfdpState() == CfdpState::BUSY_CLASS_2_ACKED); CHECK(destHandler.getTransactionStep() == DestHandler::TransactionStep::WAITING_FOR_FINISHED_ACK); } }