Files
fsfw/unittests/cfdp/handler/testDestHandler.cpp
T
Tobias BaumgartlandClaude Opus 5 e256fa4b92 cfdp: parse and act on a Cancel EOF at the destination
handleEofPdu built its EofInfo with a null fault location TLV pointer.
EofPduReader refuses to parse any EOF whose condition code is not NO_ERROR
unless it has somewhere to put that TLV, so every Cancel EOF a sender emits
was rejected with "Ca not deserialize fault location" and dropped before the
handler saw it.

The consequences were invisible from the ground until now: the sender's
cancellation was never acknowledged, so it retransmitted the EOF to its
positive ACK limit and declared a fault, while this handler kept the
transaction open until its own check limit expired. A flatsat uplink hit
exactly that, twice ten seconds apart, which is the sender's retransmission
interval.

Give both EOF parse sites a real EntityIdTlv, held by the handler so no
allocation happens per PDU, and adopt a non-NO_ERROR condition code into the
transaction. Without the second part the cancellation would parse but then
fall through to transfer completion, which would run a checksum pass over a
file the sender has already abandoned and report a checksum failure instead of
the cancellation.

The new test fails without the fix in the same way the flatsat did: no ACK is
emitted at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4eBFnanCACYWdKCzhHMcC
2026-09-16 11:54:34 +02:00

637 lines
30 KiB
C++

#include <etl/crc32.h>
#include <catch2/catch_test_macros.hpp>
#include <chrono>
#include <random>
#include <thread>
#include <utility>
#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<uint16_t>(2));
auto remoteId = EntityId(UnsignedByteField<uint16_t>(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<uint8_t, 4096> 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<uint16_t>(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<store_address_t>& 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<const uint8_t*>(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<std::mt19937::result_type> distU8(0, 255);
std::array<uint8_t, 1024> 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<store_address_t> 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<const uint8_t*>(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<const uint8_t*>(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<uint16_t>(2));
auto remoteId = EntityId(UnsignedByteField<uint16_t>(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<uint8_t, 4096> 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<uint16_t>(1));
conf.sourceId = remoteId;
conf.destId = localId;
conf.mode = TransmissionMode::ACKNOWLEDGED;
conf.seqNum = seqNum;
std::array<uint8_t, 1024> fileData{};
for (size_t idx = 0; idx < fileData.size(); idx++) {
fileData[idx] = static_cast<uint8_t>(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<std::pair<uint64_t, uint64_t>>& segments) {
NakInfo info(Fss(0), Fss(0));
std::array<NakInfo::SegmentRequest, 16> 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<uint16_t>(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<std::pair<uint64_t, uint64_t>> 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<std::pair<uint64_t, uint64_t>> 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<uint16_t>(42));
auto staleAck = makeFinishedAckPdu();
destHandler.stateMachine(staleAck);
CHECK(destHandler.getCfdpState() == CfdpState::BUSY_CLASS_2_ACKED);
CHECK(destHandler.getTransactionStep() ==
DestHandler::TransactionStep::WAITING_FOR_FINISHED_ACK);
}
}