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
637 lines
30 KiB
C++
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);
|
|
}
|
|
}
|