Files
fsfw/unittests/cfdp/handler/testSourceHandler.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

639 lines
29 KiB
C++

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