Files
fsfw/unittests/cfdp/handler/testDestHandler.cpp
T
tbaumgartlandTobias Baumgartl 8f938a0e13 cfdp: implement acknowledged mode (class 2) (#73)
# CFDP acknowledged mode (class 2)

## Summary

Both CFDP handlers implement class 2. Before this, `BUSY_CLASS_2_ACKED` was a stub on the
destination side and the source handler ignored every incoming PDU.

Destination handler:

```diff
 RECEIVING_FILE_DATA_PDUS        track gaps per received segment
   EOF
+  SENDING_ACK_PDU               ACK(EOF) before the checksum pass
+  WAITING_FOR_MISSING_DATA      deferred NAK sequence, NAK timer, check timer
   TRANSFER_COMPLETION
   SENDING_FINISHED_PDU
+  WAITING_FOR_FINISHED_ACK      positive ACK timer, retransmit Finished
```

Source handler:

```diff
 SENDING_FILE_DATA
 SENDING_EOF
+WAIT_FOR_ACK                    positive ACK timer, answer NAKs with retransmissions
 WAIT_FOR_FINISH                 now actually parses the Finished PDU
+  send ACK(Finished)
 NOTICE_OF_COMPLETION            reports the received condition and delivery codes
```

Behaviour worth knowing:

- Lost metadata is recovered: the transaction starts from the first file data or EOF PDU and
  requests the metadata with a NAK of scope 0..0.
- An EOF for a transaction that is already finished is still acknowledged.
- The NAK and check limits count expirations without progress. Missing data arriving resets them
  (CFDP 4.6.4.7).
- A class 2 metadata only transaction (a proxy put request) waits for its EOF, so the Finished PDU
  never overtakes the EOF ACK.
- NAK segment requests that do not fit one PDU are carried into the next PDU of the sequence.

Fixes found along the way, all of them also affect class 1:

- Metadata, NAK and Finished readers take the PDU CRC out of the parsed range. Before, any PDU
  with a CRC and options, segment requests or filestore responses failed with
  `INVALID_TLV_TYPE`.
- A Cancel EOF is parsed and ends the transaction. It was dropped as unparseable.
- `PduSenderIF::sendPdu()` failures are no longer treated as sent. Both handlers retry, the
  Finished PDU with a bounded number of attempts.
- PDUs are filtered by transaction ID, so a late PDU from an earlier transaction cannot drive the
  current one.
- A metadata only transaction reports `DATA_COMPLETE` instead of the "Data Incomplete" defaults.
- `arrayprinter` prints in fixed chunks instead of a stack buffer sized from the input. Dumping a
  12 KB buffer overflowed an 8 KB task.
- Metadata options beyond the option array are rejected instead of written past it.

New configuration, all with defaults:

```text
RemoteEntityCfg      positiveAckTimerIntervalMs, positiveAckTimerExpirationLimit,
                     nakTimerIntervalMs, nakTimerExpirationLimit, immediateNakMode,
                     checkLimit, checkTimerIntervalMs
DestHandlerParams    maxSegmentRequestsPerNakPdu, maxFinishedPduSendAttempts
returnvalue          cfdp::TRANSMISSION_MODE_NOT_SUPPORTED
```

## Evidence

- **Before:** an acknowledged uplink failed after about 40 s. The sender retransmitted EOF to
  `POSITIVE_ACK_LIMIT_REACHED` because the destination never sent an ACK. An acknowledged put
  request left the source handler busy for good.
  **After:** `fsfw-tests "[cfdp]"` passes, 26 test cases. New sections include:

  ```text
  CFDP Dest Handler Acknowledged
    Nominal acknowledged transfer
    Dropped file data PDU is recovered via NAK
    Dropped metadata PDU is requested with a scope 0 to 0 NAK
    Finished PDU is retransmitted on positive ACK timeout
    NAK limit reached cancels the transaction
    Retransmitted data resets the NAK and check limits
    Metadata only transaction sends Finished after the EOF ACK
    EOF PDU for an inactive transaction is acknowledged
    Cancel EOF is parsed, acknowledged and ends the transaction
    A Finished ACK for a different transaction is ignored
  CFDP Source Handler Acknowledged
    Nominal acknowledged transfer
    EOF PDU is retransmitted on positive ACK timeout
    NAK PDU drives segment retransmission
    NAK PDU of scope 0 to 0 retransmits the metadata PDU
    A NAK keeps the inactivity guard from firing during recovery
    Finished PDU condition code is reported instead of a hardcoded success
  Finished / Metadata / NAK PDU with CRC
  ```

  Plus class 1 sections for send failures, Cancel EOF and metadata only transfers. The last two
  destination fixes were checked with their tests failing before the fix.

- **Hardware:** SOURCE flatsat, iOBC, over the maintenance port with a proxy dropping 15 % of the
  packets in each direction. A 200 kB uplink and the downlink of the same file both completed
  with `NO_ERROR`, and the downlinked file was byte identical to the original.

## Merge Danger

**Door:** two-way

Plain code changes, no stored data or formats change. A revert restores the old behaviour.

**Blast Radius:** CFDP users

Every mission using the CFDP handlers gets the class 1 fixes too: send failures are retried
instead of dropped, a Cancel EOF ends the transaction, and CRC-bearing PDUs with variable length
tails now parse. Existing `RemoteEntityCfg` setups compile unchanged, the new fields default to
values that leave class 1 alone. Not covered: the PDU creators still cannot size a CRC-bearing PDU,
so `crcOnTransmission` has to stay false as before.

---------

Co-authored-by: Tobias Baumgartl <tobias.baumgartl@ksat-stuttgart.de>
Reviewed-on: #73
2026-10-27 09:03:13 +01:00

722 lines
34 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("Metadata only transfer reports a complete delivery") {
// A proxy put request is metadata only: empty file names, the request itself in a message to
// user, and no file data to wait for - so the transaction completes the moment the metadata
// lands. It must report a complete delivery, not the "Data Incomplete" reset defaults.
StringLv emptySrcName;
StringLv emptyDestName;
MetadataGenericInfo info(false, ChecksumType::NULL_CHECKSUM, Fss(0));
const TransactionSeqNum seqNum(UnsignedByteField<uint16_t>(1));
conf.sourceId = remoteId;
conf.destId = localId;
conf.mode = TransmissionMode::UNACKNOWLEDGED;
conf.seqNum = seqNum;
const MetadataPduCreator creator(conf, info, emptySrcName, emptyDestName, nullptr, 0);
REQUIRE(creator.serialize(pduBuf.data(), serLen, creator.getSerializedSize()) == OK);
auto packet =
OwnedPduPacket(creator.getPduType(), creator.getDirectiveCode(), pduBuf.data(), serLen);
destHandler.stateMachine(packet);
destHandler.stateMachineNoPacket();
REQUIRE(userMock.finishedRecvd.size() == 1);
const auto& finished = userMock.finishedRecvd.back().second;
CHECK(finished.condCode == ConditionCode::NO_ERROR);
CHECK(finished.deliveryCode == FileDeliveryCode::DATA_COMPLETE);
CHECK(finished.status == FileDeliveryStatus::FILE_STATUS_UNREPORTED);
CHECK(destHandler.getCfdpState() == CfdpState::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. Finishing on a failed send means the sender never
// hears that the transfer 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);
// 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("Metadata only transaction sends Finished after the EOF ACK") {
// The shape of a proxy put request in class 2. The sender ignores a Finished PDU while it is
// still waiting for the EOF ACK, so the order matters.
StringLv emptySrcName;
StringLv emptyDestName;
MetadataGenericInfo info(false, ChecksumType::NULL_CHECKSUM, Fss(0));
const MetadataPduCreator mdCreator(conf, info, emptySrcName, emptyDestName, nullptr, 0);
REQUIRE(mdCreator.serialize(pduBuf.data(), serLen, mdCreator.getSerializedSize()) == OK);
auto metadataPdu =
OwnedPduPacket(mdCreator.getPduType(), mdCreator.getDirectiveCode(), pduBuf.data(), serLen);
const DestHandler::FsmResult& res = destHandler.stateMachine(metadataPdu);
CHECK(res.state == CfdpState::BUSY_CLASS_2_ACKED);
// The CFDP task keeps calling the state machine while the EOF PDU is still on its way.
destHandler.stateMachineNoPacket();
CHECK(not senderMock.getNextSentPacket().has_value());
EofInfo eofInfo(ConditionCode::NO_ERROR, 0, Fss(0));
EofPduCreator eofCreator(conf, eofInfo);
REQUIRE(eofCreator.serialize(pduBuf.data(), serLen, eofCreator.getSerializedSize()) == OK);
auto eofPdu = OwnedPduPacket(eofCreator.getPduType(), eofCreator.getDirectiveCode(),
pduBuf.data(), serLen);
destHandler.stateMachine(eofPdu);
expectDirective(FileDirective::ACK);
expectDirective(FileDirective::FINISH);
REQUIRE(userMock.finishedRecvd.size() == 1);
CHECK(userMock.finishedRecvd.back().second.deliveryCode == FileDeliveryCode::DATA_COMPLETE);
auto ackPdu = makeFinishedAckPdu();
destHandler.stateMachine(ackPdu);
CHECK(destHandler.getCfdpState() == CfdpState::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.
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("Retransmitted data resets the NAK and check limits") {
// Expires on every state machine call, so without the reset the check limit is reached in
// the second round.
remoteCfgTableMock.remoteCfgTable.at(remoteId).checkTimerIntervalMs = 1;
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);
// More NAK rounds than the limit allows, but each one recovers part of the gap.
for (uint64_t offset = 256; offset < 768; offset += 128) {
for (uint32_t idx = 0; idx < cfg.nakTimerExpirationLimit; idx++) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
destHandler.stateMachineNoPacket();
expectDirective(FileDirective::NAK);
}
auto retransmission = makeFileDataPdu(offset, 128);
destHandler.stateMachine(retransmission);
REQUIRE(res.step == DestHandler::TransactionStep::WAITING_FOR_MISSING_DATA);
CHECK(destHandler.getNakCounter() == 0);
}
auto rest = makeFileDataPdu(768, fileData.size() - 768);
destHandler.stateMachine(rest);
expectDirective(FileDirective::FINISH);
CHECK(userMock.finishedRecvd.back().second.condCode == ConditionCode::NO_ERROR);
}
SECTION("EOF PDU for an inactive transaction is acknowledged") {
// 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);
}
}