diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 47ff0577ef5..cc8f3e00ee1 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -39,6 +39,7 @@ BDV bfree BHn bibtex +BIIII Bies binaryfile BINDIR @@ -225,6 +226,7 @@ errornum ert esb etl +euo EVENTARRAY EVENTBOOL eventflags @@ -457,6 +459,7 @@ mstarch mstat MState multiarch +multirepo multitool mutexattr Mutexed @@ -537,6 +540,7 @@ PERLMOD PHASERMEMBEROUT PINGENTRIES PINGSEND +pipefail pkill PKTS plainnat @@ -609,7 +613,9 @@ regexs remediations REMOVEDIRECTORY REMOVEFILE +reprio reprioritize +retx RGD rhel RHH @@ -649,6 +655,7 @@ SGN sgt shahab SHAREDSTATEDIR +shellcheck SHELLCOMMAND showinitializer sideeffect @@ -753,6 +760,8 @@ totalram tparam TPP trinomials +tsec +tsub tts Tumbar tumbar diff --git a/Svc/DpCatalog/DpCatalog.cpp b/Svc/DpCatalog/DpCatalog.cpp index 30f43253018..142a2a09b3d 100644 --- a/Svc/DpCatalog/DpCatalog.cpp +++ b/Svc/DpCatalog/DpCatalog.cpp @@ -8,15 +8,27 @@ #include "Svc/DpCatalog/DpCatalog.hpp" #include "Fw/Dp/DpContainer.hpp" #include "Fw/FPrimeBasicTypes.hpp" +#include "Utils/Hash/libcrc/CRC32.hpp" #include // placement new #include "Fw/Types/StringUtils.hpp" #include "Os/File.hpp" #include "Os/FileSystem.hpp" +// Include the libcrc C library +extern "C" { +#include +} + namespace Svc { static_assert(DP_MAX_DIRECTORIES > 0, "Configuration DP_MAX_DIRECTORIES must be positive"); static_assert(DP_MAX_FILES > 0, "Configuration DP_MAX_FILES must be positive"); + +// DP operations file operation codes +constexpr U8 DP_OP_DELETE = 1; +constexpr U8 DP_OP_REPRIORITIZE = 2; +constexpr U8 DP_OP_RETRANSMIT = 3; + // ---------------------------------------------------------------------- // Component construction and destruction // ---------------------------------------------------------------------- @@ -128,6 +140,9 @@ void DpCatalog::resetBinaryTree() { } // clear binary tree this->m_dpTree = nullptr; + // clear navigation pointers to prevent dangling references + this->m_currentNode = nullptr; + this->m_currentXmitNode = nullptr; // reset number of records this->m_pendingFiles = 0; this->m_pendingDpBytes = 0; @@ -219,20 +234,34 @@ Fw::CmdResponse DpCatalog::loadStateFile() { return Fw::CmdResponse::OK; } -void DpCatalog::getFileState(DpStateEntry& entry) { +FwSignedSizeType DpCatalog::findStateFileEntryIndex(FwDpIdType id, U32 tSec, U32 tSub, FwIndexType dir) { FW_ASSERT(this->m_stateFileData); - // search the file state data for the entry - for (FwSizeType line = 0; line < this->m_stateFileEntries; line++) { - // check for a match (compare dir, then id, priority, & time) - if (this->m_stateFileData[line].entry.dir == entry.dir && this->m_stateFileData[line].entry == entry) { - // update the transmitted state - entry.record.set_state(this->m_stateFileData[line].entry.record.get_state()); - entry.record.set_blocks(this->m_stateFileData[line].entry.record.get_blocks()); - // mark it as visited for later pruning if necessary - this->m_stateFileData[line].visited = true; - return; + + // Search state file data for matching entry + for (FwSizeType entry = 0; entry < this->m_numDpSlots; entry++) { + if (this->m_stateFileData[entry].used && this->m_stateFileData[entry].entry.dir == dir && + this->m_stateFileData[entry].entry.record.get_id() == id && + this->m_stateFileData[entry].entry.record.get_tSec() == tSec && + this->m_stateFileData[entry].entry.record.get_tSub() == tSub) { + return static_cast(entry); } } + + return -1; // Not found +} + +void DpCatalog::getFileState(DpStateEntry& entry) { + FwSignedSizeType index = this->findStateFileEntryIndex(entry.record.get_id(), entry.record.get_tSec(), + entry.record.get_tSub(), entry.dir); + + if (index >= 0) { + FwSizeType idx = static_cast(index); + // update the transmitted state + entry.record.set_state(this->m_stateFileData[idx].entry.record.get_state()); + entry.record.set_blocks(this->m_stateFileData[idx].entry.record.get_blocks()); + // mark it as visited for later pruning if necessary + this->m_stateFileData[idx].visited = true; + } } void DpCatalog::pruneAndWriteStateFile() { @@ -791,6 +820,11 @@ void DpCatalog::deallocateNode(DpBtreeNode* node) { // we can stitch its left branch onto its parent in its place rightmostNode->parent->right = rightmostNode->left; + // Update parent pointer of the moved left child + if (rightmostNode->left != nullptr) { + rightmostNode->left->parent = rightmostNode->parent; + } + // Now connect the deallocated node's left branch onto rightmostNode rightmostNode->left = node->left; node->left->parent = rightmostNode; @@ -936,6 +970,49 @@ DpCatalog::DpBtreeNode* DpCatalog::findNextTreeNode() { return found; } +DpCatalog::DpBtreeNode* DpCatalog::findTreeNode(FwDpIdType id, U32 tSec, U32 tSub) { + if (this->m_dpTree == nullptr) { + return nullptr; + } + + // Since tree is not sorted by ID+timestamp alone, we must traverse entire tree + // Use iterative depth-first search with explicit stack to avoid recursion + + DpBtreeNode* stack[DP_MAX_FILES]; + FwSizeType stackTop = 0; + stack[stackTop++] = this->m_dpTree; + + while (stackTop > 0) { + DpBtreeNode* current = stack[--stackTop]; + + // Check if this node matches + if (current->entry.record.get_id() == id && current->entry.record.get_tSec() == tSec && + current->entry.record.get_tSub() == tSub) { + return current; + } + + // Add children to stack for exploration + if (current->right != nullptr && stackTop < DP_MAX_FILES) { + stack[stackTop++] = current->right; + } + if (current->left != nullptr && stackTop < DP_MAX_FILES) { + stack[stackTop++] = current->left; + } + } + + return nullptr; +} + +void DpCatalog::removeFromStateFile(FwDpIdType id, U32 tSec, U32 tSub, FwIndexType dir) { + FwSignedSizeType index = this->findStateFileEntryIndex(id, tSec, tSub, dir); + + if (index >= 0) { + FwSizeType idx = static_cast(index); + this->m_stateFileData[idx].used = false; + this->m_stateFileData[idx].visited = false; + } +} + bool DpCatalog::checkInit() { if (not this->m_initialized) { this->log_WARNING_HI_ComponentNotInitialized(); @@ -992,6 +1069,8 @@ void DpCatalog ::fileDone_handler(FwIndexType portNum, const Svc::SendFileRespon this->m_xmitBytes += this->m_currentXmitNode->entry.record.get_size(); // deallocate this node this->deallocateNode(this->m_currentXmitNode); + // clear the pointer to prevent dangling reference + this->m_currentXmitNode = nullptr; // send the next entry, if it exists this->sendNextEntry(); } @@ -1141,6 +1220,660 @@ void DpCatalog ::CLEAR_CATALOG_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } +void DpCatalog ::DELETE_DP_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, FwDpIdType id, U32 tSec, U32 tSub) { + // Check initialization + if (not this->checkInit()) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + + // Use helper function to perform deletion + bool success = this->deleteDpHelper(id, tSec, tSub); + + this->cmdResponse_out(opCode, cmdSeq, success ? Fw::CmdResponse::OK : Fw::CmdResponse::EXECUTION_ERROR); +} + +void DpCatalog ::CHANGE_DP_PRIORITY_cmdHandler(FwOpcodeType opCode, + U32 cmdSeq, + FwDpIdType id, + U32 tSec, + U32 tSub, + U32 newPriority) { + // Check initialization + if (not this->checkInit()) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + + // Use helper function to perform priority change + bool success = this->changeDpPriorityHelper(id, tSec, tSub, newPriority); + + this->cmdResponse_out(opCode, cmdSeq, success ? Fw::CmdResponse::OK : Fw::CmdResponse::EXECUTION_ERROR); +} + +void DpCatalog ::RETRANSMIT_DP_cmdHandler(FwOpcodeType opCode, + U32 cmdSeq, + FwDpIdType id, + U32 tSec, + U32 tSub, + U32 priorityOverride) { + // Check initialization + if (not this->checkInit()) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + + // Use helper function to perform retransmission + bool success = this->retransmitDpHelper(id, tSec, tSub, priorityOverride); + + this->cmdResponse_out(opCode, cmdSeq, success ? Fw::CmdResponse::OK : Fw::CmdResponse::EXECUTION_ERROR); +} + +// ---------------------------------------------------------------------- +// Helper functions for command handlers +// ---------------------------------------------------------------------- + +bool DpCatalog::openAndValidateOpFile(const Fw::StringBase& fileName, + Os::File& opFile, + FwSizeType& fileSize, + FwOpcodeType opCode, + U32 cmdSeq) { + // Open the operations file + Os::File::Status stat = opFile.open(fileName.toChar(), Os::File::OPEN_READ); + if (stat != Os::File::OP_OK) { + this->log_WARNING_HI_DpFileOpenError(fileName, stat); + return false; + } + + // Get file size + Os::FileSystem::Status sizeStat = Os::FileSystem::getFileSize(fileName.toChar(), fileSize); + if (sizeStat != Os::FileSystem::OP_OK) { + opFile.close(); + this->log_WARNING_HI_DpFileOpenError(fileName, sizeStat); + return false; + } + + // Verify file size: must be at least 4 bytes (for CRC32) and data portion must be multiple of 17 bytes + const FwSizeType RECORD_SIZE = 17; + const FwSizeType CRC_SIZE = 4; + if (fileSize < CRC_SIZE) { + opFile.close(); + this->log_WARNING_HI_DpFileInvalidSize(fileName, static_cast(fileSize)); + return false; + } + + FwSizeType dataSize = fileSize - CRC_SIZE; + if (dataSize % RECORD_SIZE != 0) { + opFile.close(); + this->log_WARNING_HI_DpFileInvalidSize(fileName, static_cast(fileSize)); + return false; + } + + return true; +} + +void DpCatalog::parseFileOperationRecord(const U8* recordBuf, + U8& operationCode, + U32& id, + U32& tSec, + U32& tSub, + U32& priority) { + const FwSizeType DATA_SIZE = 17; // Size of data fields (excludes CRC32) + Fw::ExternalSerializeBuffer serialBuffer(const_cast(recordBuf), DATA_SIZE); + serialBuffer.setBuffLen(DATA_SIZE); + + Fw::SerializeStatus desStat = serialBuffer.deserializeTo(operationCode); + FW_ASSERT(desStat == Fw::FW_SERIALIZE_OK, desStat); + + desStat = serialBuffer.deserializeTo(id, Fw::Endianness::BIG); + FW_ASSERT(desStat == Fw::FW_SERIALIZE_OK, desStat); + + desStat = serialBuffer.deserializeTo(tSec, Fw::Endianness::BIG); + FW_ASSERT(desStat == Fw::FW_SERIALIZE_OK, desStat); + + desStat = serialBuffer.deserializeTo(tSub, Fw::Endianness::BIG); + FW_ASSERT(desStat == Fw::FW_SERIALIZE_OK, desStat); + + desStat = serialBuffer.deserializeTo(priority, Fw::Endianness::BIG); + FW_ASSERT(desStat == Fw::FW_SERIALIZE_OK, desStat); +} + +bool DpCatalog::readDpHeader(const Fw::FileNameString& dpFileName, Fw::DpContainer& container) { + Os::File dpFile; + U8 dpBuff[Fw::DpContainer::MIN_PACKET_SIZE]; + Fw::Buffer hdrBuff(dpBuff, sizeof(dpBuff)); + + Os::File::Status stat = dpFile.open(dpFileName.toChar(), Os::File::OPEN_READ); + if (stat != Os::File::OP_OK) { + this->log_WARNING_HI_FileOpenError(dpFileName, stat); + return false; + } + + FwSizeType size = Fw::DpContainer::Header::SIZE; + stat = dpFile.read(dpBuff, size); + dpFile.close(); + + if (stat != Os::File::OP_OK) { + this->log_WARNING_HI_FileReadError(dpFileName, stat); + return false; + } + + if (size != Fw::DpContainer::Header::SIZE) { + this->log_WARNING_HI_FileReadError(dpFileName, Os::File::BAD_SIZE); + return false; + } + + container.setBuffer(hdrBuff); + Fw::SerializeStatus desStat = container.deserializeHeader(); + if (desStat != Fw::FW_SERIALIZE_OK) { + this->log_WARNING_HI_FileHdrDesError(dpFileName, desStat); + return false; + } + + return true; +} + +bool DpCatalog::updateNodePriority(DpBtreeNode* node, + FwDpIdType id, + U32 tSec, + U32 tSub, + U32 newPriority, + U32 oldPriority) { + // Copy the entry before removing from tree + DpStateEntry updatedEntry = node->entry; + + // If this was our current exploration node, move to parent or right + if (this->m_currentNode == node) { + if (node->right != nullptr) { + this->m_currentNode = node->right; + } else { + this->m_currentNode = node->parent; + } + } + + // Remove from tree (but don't update counters - we're re-inserting) + this->deallocateNode(node); + + // Update the priority in the entry + updatedEntry.record.set_priority(newPriority); + + // Re-insert with updated priority + DpBtreeNode* newNode = this->insertEntry(updatedEntry); + if (newNode == nullptr) { + this->log_WARNING_HI_DpCatalogFull(updatedEntry.record); + return false; + } + + // Update the state file entry if catalog is built + if (this->m_catalogBuilt) { + FwSignedSizeType stateIndex = + this->findStateFileEntryIndex(id, tSec, tSub, static_cast(updatedEntry.dir)); + if (stateIndex >= 0) { + this->m_stateFileData[stateIndex].entry.record.set_priority(newPriority); + this->pruneAndWriteStateFile(); + } + } + + return true; +} + +bool DpCatalog::addDpToCatalog(const Fw::FileNameString& dpFileName, + FwSizeType foundDir, + FwSizeType fileSize, + U32 usedPriority) { + // Read the DP header + U8 dpBuff[Fw::DpContainer::MIN_PACKET_SIZE]; + Fw::Buffer hdrBuff(dpBuff, sizeof(dpBuff)); + Fw::DpContainer container; + + if (!this->readDpHeader(dpFileName, container)) { + return false; + } + + // Create entry for catalog + DpStateEntry entry; + entry.dir = static_cast(foundDir); + entry.record.set_id(container.getId()); + entry.record.set_tSec(container.getTimeTag().getSeconds()); + entry.record.set_tSub(container.getTimeTag().getUSeconds()); + entry.record.set_size(static_cast(fileSize)); + entry.record.set_state(Fw::DpState::UNTRANSMITTED); + entry.record.set_priority(usedPriority); + + // Insert entry into catalog tree + DpBtreeNode* addedEntry = this->insertEntry(entry); + if (addedEntry == nullptr) { + this->log_WARNING_HI_DpCatalogFull(entry.record); + return false; + } + + // Update pending counters + this->m_pendingFiles++; + this->m_pendingDpBytes += fileSize; + + // Update state file if catalog is built + if (this->m_catalogBuilt) { + this->appendFileState(entry); + this->pruneAndWriteStateFile(); + } + + this->log_ACTIVITY_HI_DpRetransmitted(dpFileName, usedPriority); + return true; +} + +bool DpCatalog::updateExistingDpForRetransmit(DpBtreeNode* existingNode, + const Fw::FileNameString& dpFileName, + FwDpIdType id, + U32 tSec, + U32 tSub, + U32 priorityOverride) { + // Determine the new priority + U32 newPriority; + if (priorityOverride == 0xFFFFFFFF) { + // Need to read file to get its priority + U8 dpBuff[Fw::DpContainer::MIN_PACKET_SIZE]; + Fw::Buffer hdrBuff(dpBuff, sizeof(dpBuff)); + Fw::DpContainer container; + + if (!this->readDpHeader(dpFileName, container)) { + return false; + } + newPriority = container.getPriority(); + } else { + newPriority = priorityOverride; + } + + // Get old priority + U32 oldPriority = existingNode->entry.record.get_priority(); + + // If priority is the same, no work needed + if (oldPriority == newPriority) { + this->log_ACTIVITY_HI_DpPriorityUpdated(id, tSec, tSub, oldPriority, newPriority); + return true; + } + + // Update the node priority + if (!this->updateNodePriority(existingNode, id, tSec, tSub, newPriority, oldPriority)) { + return false; + } + + this->log_ACTIVITY_HI_DpPriorityUpdated(id, tSec, tSub, oldPriority, newPriority); + return true; +} + +void DpCatalog ::PROCESS_DP_FILE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, const Fw::CmdStringArg& fileName) { + // Check initialization + if (not this->checkInit()) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + + // Open and validate the operations file + Os::File opFile; + FwSizeType fileSize = 0; + if (!this->openAndValidateOpFile(fileName, opFile, fileSize, opCode, cmdSeq)) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + + const FwSizeType RECORD_SIZE = 17; + const FwSizeType CRC_SIZE = 4; + FwSizeType dataSize = fileSize - CRC_SIZE; + U32 numRecords = static_cast(dataSize / RECORD_SIZE); + + this->log_ACTIVITY_HI_DpFileProcessingStarted(fileName); + + // First pass: validate CRC32 by scanning file byte by byte + unsigned long crc = 0xFFFFFFFF; + U8 byteBuf; + FwSizeType readSize; + + for (FwSizeType i = 0; i < dataSize; i++) { + readSize = 1; + Os::File::Status stat = opFile.read(&byteBuf, readSize); + if (stat != Os::File::OP_OK || readSize != 1) { + opFile.close(); + this->log_WARNING_HI_DpFileReadError(fileName, stat); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + crc = update_crc_32(crc, static_cast(byteBuf)); + } + U32 computedCrc = static_cast(crc ^ 0xFFFFFFFF); + + // Read CRC32 from end of file (big-endian) + U8 crcBuf[CRC_SIZE]; + readSize = CRC_SIZE; + Os::File::Status stat = opFile.read(crcBuf, readSize); + opFile.close(); + + if (stat != Os::File::OP_OK || readSize != CRC_SIZE) { + this->log_WARNING_HI_DpFileReadError(fileName, stat); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + + U32 expectedCrc = (static_cast(crcBuf[0]) << 24) | (static_cast(crcBuf[1]) << 16) | + (static_cast(crcBuf[2]) << 8) | static_cast(crcBuf[3]); + + // Validate checksum + if (computedCrc != expectedCrc) { + this->log_WARNING_HI_DpFileChecksumError(fileName, computedCrc, expectedCrc); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + + // Second pass: reopen file and process records one at a time + stat = opFile.open(fileName.toChar(), Os::File::OPEN_READ); + if (stat != Os::File::OP_OK) { + this->log_WARNING_HI_DpFileOpenError(fileName, stat); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + + // Process each record + U8 recordBuf[RECORD_SIZE]; + for (U32 recordNum = 0; recordNum < numRecords; recordNum++) { + readSize = RECORD_SIZE; + stat = opFile.read(recordBuf, readSize); + if (stat != Os::File::OP_OK || readSize != RECORD_SIZE) { + opFile.close(); + this->log_WARNING_HI_DpFileReadError(fileName, stat); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + + // Parse record fields + U8 operationCode; + U32 id, tSec, tSub, priority; + this->parseFileOperationRecord(recordBuf, operationCode, id, tSec, tSub, priority); + + // Dispatch based on operation code + switch (operationCode) { + case DP_OP_DELETE: + (void)this->deleteDpHelper(id, tSec, tSub); + break; + case DP_OP_REPRIORITIZE: + (void)this->changeDpPriorityHelper(id, tSec, tSub, priority); + break; + case DP_OP_RETRANSMIT: + (void)this->retransmitDpHelper(id, tSec, tSub, priority); + break; + default: + opFile.close(); + this->log_WARNING_HI_DpFileInvalidOp(fileName, recordNum, operationCode); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + } + + opFile.close(); + this->log_ACTIVITY_HI_DpFileProcessingComplete(fileName, numRecords); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); +} + +void DpCatalog::SEND_CATALOG_DP_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, U32 catalogPriority) { + // Check initialization + if (not this->checkInit()) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + + // Check if product port is connected + if (not this->isConnected_productGetOut_OutputPort(0)) { + this->log_WARNING_HI_ComponentNoMemory(); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + + // Count entries in the tree by traversing + U32 numEntries = 0; + if (this->m_dpTree != nullptr) { + // Use iterative traversal to count nodes + DpBtreeNode* stack[DP_MAX_FILES]; + FwSizeType stackTop = 0; + stack[stackTop++] = this->m_dpTree; + + while (stackTop > 0) { + DpBtreeNode* current = stack[--stackTop]; + numEntries++; + + if (current->right != nullptr && stackTop < DP_MAX_FILES) { + stack[stackTop++] = current->right; + } + if (current->left != nullptr && stackTop < DP_MAX_FILES) { + stack[stackTop++] = current->left; + } + } + } + + // Calculate size needed for container + FwSizeType dpSize = numEntries * (DpRecord::SERIALIZED_SIZE + sizeof(FwDpIdType)); + + // Request container buffer + DpContainer container; + Fw::Success::T stat = this->dpGet_Catalog(dpSize, container); + if (Fw::Success::FAILURE == stat) { + this->log_WARNING_HI_ComponentNoMemory(); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + + // Set priority - use provided priority if not default marker + if (catalogPriority != 0xFFFFFFFF) { + container.setPriority(static_cast(catalogPriority)); + } + + // Traverse tree and serialize each entry + if (this->m_dpTree != nullptr && numEntries > 0) { + DpBtreeNode* stack[DP_MAX_FILES]; + FwSizeType stackTop = 0; + stack[stackTop++] = this->m_dpTree; + + while (stackTop > 0) { + DpBtreeNode* current = stack[--stackTop]; + + // Serialize the record into the container + Fw::SerializeStatus serStat = container.serializeRecord_CatalogEntry(current->entry.record); + if (serStat != Fw::FW_SERIALIZE_OK) { + this->log_WARNING_HI_ComponentNoMemory(); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::EXECUTION_ERROR); + return; + } + + // Add children to stack for continued traversal + if (current->right != nullptr && stackTop < DP_MAX_FILES) { + stack[stackTop++] = current->right; + } + if (current->left != nullptr && stackTop < DP_MAX_FILES) { + stack[stackTop++] = current->left; + } + } + } + + // Send the container + this->dpSend(container); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); +} + +bool DpCatalog::deleteDpHelper(FwDpIdType id, U32 tSec, U32 tSub) { + FW_ASSERT(this->m_initialized); + FW_ASSERT(this->m_numDirectories <= DP_MAX_DIRECTORIES); + + this->log_ACTIVITY_LO_DpFileOpDelete(id, tSec, tSub); + + // Search all managed directories for the file + Fw::FileNameString dpFileName; + FwSizeType foundDir = DP_MAX_DIRECTORIES; + + for (FwSizeType dir = 0; dir < this->m_numDirectories; dir++) { + dpFileName.format(DP_FILENAME_FORMAT, this->m_directories[dir].toChar(), id, tSec, tSub); + + // Check if file exists + FwSizeType fileSize = 0; + Os::FileSystem::Status sizeStat = Os::FileSystem::getFileSize(dpFileName.toChar(), fileSize); + if (sizeStat == Os::FileSystem::OP_OK) { + foundDir = dir; + break; + } + } + + // File not found in any managed directory + if (foundDir == DP_MAX_DIRECTORIES) { + this->log_WARNING_LO_DpNotFound(id, tSec, tSub); + return false; + } + + // Check if this DP is currently being transmitted + if (this->m_currentXmitNode != nullptr && this->m_currentXmitNode->entry.record.get_id() == id && + this->m_currentXmitNode->entry.record.get_tSec() == tSec && + this->m_currentXmitNode->entry.record.get_tSub() == tSub) { + this->log_WARNING_LO_DpDeleteXmitInProgress(dpFileName); + return false; + } + + // Search and remove from binary tree if present + DpBtreeNode* node = this->findTreeNode(id, tSec, tSub); + if (node != nullptr) { + // Update counters before deallocating + this->m_pendingFiles--; + this->m_pendingDpBytes -= node->entry.record.get_size(); + + // If this was our current exploration node, move to parent or right + if (this->m_currentNode == node) { + if (node->right != nullptr) { + this->m_currentNode = node->right; + } else { + this->m_currentNode = node->parent; + } + } + + // Remove from tree + this->deallocateNode(node); + } + + // Remove from state file data if present + if (this->m_catalogBuilt) { + this->removeFromStateFile(id, tSec, tSub, static_cast(foundDir)); + } + + // Delete the physical file + Os::FileSystem::Status status = Os::FileSystem::removeFile(dpFileName.toChar()); + if (status != Os::FileSystem::OP_OK) { + this->log_WARNING_HI_DpDeleteError(dpFileName, status); + return false; + } + + // Update state file if catalog was modified + if (node != nullptr && this->m_catalogBuilt) { + this->pruneAndWriteStateFile(); + } + + this->log_ACTIVITY_HI_DpDeleted(dpFileName); + return true; +} + +bool DpCatalog::changeDpPriorityHelper(FwDpIdType id, U32 tSec, U32 tSub, U32 newPriority) { + FW_ASSERT(this->m_initialized); + FW_ASSERT(this->m_catalogBuilt); + + this->log_ACTIVITY_LO_DpFileOpReprioritize(id, tSec, tSub, newPriority); + + // Find the DP in the binary tree + DpBtreeNode* node = this->findTreeNode(id, tSec, tSub); + if (node == nullptr) { + this->log_WARNING_LO_DpPriorityNotFound(id, tSec, tSub); + return false; + } + + // Check if this DP is currently being transmitted + if (this->m_currentXmitNode != nullptr && this->m_currentXmitNode->entry.record.get_id() == id && + this->m_currentXmitNode->entry.record.get_tSec() == tSec && + this->m_currentXmitNode->entry.record.get_tSub() == tSub) { + this->log_WARNING_LO_DpPriorityXmitInProgress(id, tSec, tSub); + return false; + } + + // Save the old priority + U32 oldPriority = node->entry.record.get_priority(); + + // If priority is the same, no work needed + if (oldPriority == newPriority) { + this->log_ACTIVITY_HI_DpPriorityChanged(id, tSec, tSub, oldPriority, newPriority); + return true; + } + + // Update the node priority + if (!this->updateNodePriority(node, id, tSec, tSub, newPriority, oldPriority)) { + return false; + } + + this->log_ACTIVITY_HI_DpPriorityChanged(id, tSec, tSub, oldPriority, newPriority); + return true; +} + +bool DpCatalog::retransmitDpHelper(FwDpIdType id, U32 tSec, U32 tSub, U32 priorityOverride) { + FW_ASSERT(this->m_initialized); + FW_ASSERT(this->m_numDirectories <= DP_MAX_DIRECTORIES); + + this->log_ACTIVITY_LO_DpFileOpRetransmit(id, tSec, tSub, priorityOverride); + + // Search all managed directories for the file + Fw::FileNameString dpFileName; + FwSizeType foundDir = DP_MAX_DIRECTORIES; + + for (FwSizeType dir = 0; dir < this->m_numDirectories; dir++) { + dpFileName.format(DP_FILENAME_FORMAT, this->m_directories[dir].toChar(), id, tSec, tSub); + FwSizeType fileSize = 0; + if (Os::FileSystem::getFileSize(dpFileName.toChar(), fileSize) == Os::FileSystem::OP_OK) { + foundDir = dir; + break; + } + } + + if (foundDir == DP_MAX_DIRECTORIES) { + this->log_WARNING_LO_DpNotFound(id, tSec, tSub); + return false; + } + + // Check if this DP is currently being transmitted + if (this->m_currentXmitNode != nullptr && this->m_currentXmitNode->entry.record.get_id() == id && + this->m_currentXmitNode->entry.record.get_tSec() == tSec && + this->m_currentXmitNode->entry.record.get_tSub() == tSub) { + this->log_WARNING_LO_DpRetransmitInProgress(id, tSec, tSub); + return false; + } + + // Check if DP already exists in the catalog tree + DpBtreeNode* existingNode = this->findTreeNode(id, tSec, tSub); + if (existingNode != nullptr) { + return this->updateExistingDpForRetransmit(existingNode, dpFileName, id, tSec, tSub, priorityOverride); + } + + // DP not in catalog - get file size and priority, then add to catalog + FwSizeType fileSize = 0; + if (Os::FileSystem::getFileSize(dpFileName.toChar(), fileSize) != Os::FileSystem::OP_OK) { + this->log_WARNING_HI_FileSizeError(dpFileName, Os::FileSystem::OTHER_ERROR); + return false; + } + + // Determine priority to use + U32 usedPriority; + if (priorityOverride == 0xFFFFFFFF) { + U8 dpBuff[Fw::DpContainer::MIN_PACKET_SIZE]; + Fw::Buffer hdrBuff(dpBuff, sizeof(dpBuff)); + Fw::DpContainer container; + if (!this->readDpHeader(dpFileName, container)) { + return false; + } + usedPriority = container.getPriority(); + } else { + usedPriority = priorityOverride; + } + + return this->addDpToCatalog(dpFileName, foundDir, fileSize, usedPriority); +} + void DpCatalog ::dispatchWaitedResponse(Fw::CmdResponse response) { if (this->m_xmitCmdWait) { this->cmdResponse_out(this->m_xmitOpCode, this->m_xmitCmdSeq, response); diff --git a/Svc/DpCatalog/DpCatalog.fpp b/Svc/DpCatalog/DpCatalog.fpp index e1fd468dd2a..4320b56df3c 100644 --- a/Svc/DpCatalog/DpCatalog.fpp +++ b/Svc/DpCatalog/DpCatalog.fpp @@ -46,6 +46,18 @@ module Svc { @ DP Writer Add File to Cat async input port addToCat: DpWritten + @ Data product get port + product get port productGetOut + + @ Data product send port + product send port productSendOut + + @ Catalog entry data product record + product record CatalogEntry: DpRecord id 0 + + @ Catalog data product container + product container Catalog id 0 default priority 100 + # ---------------------------------------------------------------------- # F Prime infrastructure ports # ---------------------------------------------------------------------- @@ -94,6 +106,44 @@ module Svc { async command CLEAR_CATALOG \ opcode 3 + @ Delete a data product from catalog and filesystem + async command DELETE_DP ( + $id: FwDpIdType @< The ID of the data product + tSec: U32 @< Generation time in seconds + tSub: U32 @< Generation time in subseconds + ) \ + opcode 4 + + @ Change the priority of a data product in the catalog + async command CHANGE_DP_PRIORITY ( + $id: FwDpIdType @< The ID of the data product + tSec: U32 @< Generation time in seconds + tSub: U32 @< Generation time in subseconds + newPriority: U32 @< The new priority value + ) \ + opcode 5 + + @ Re-add a transmitted data product to catalog for retransmission + async command RETRANSMIT_DP ( + $id: FwDpIdType @< The ID of the data product + tSec: U32 @< Generation time in seconds + tSub: U32 @< Generation time in subseconds + priorityOverride: U32 @< Priority (0xFFFFFFFF = use priority from file) + ) \ + opcode 6 + + @ Process a file containing batch DP operations + async command PROCESS_DP_FILE ( + fileName: string size FileNameStringSize @< The operations file name + ) \ + opcode 7 + + @ Generate a data product containing catalog entries + async command SEND_CATALOG_DP ( + catalogPriority: U32 @< Priority for catalog data product (0xFFFFFFFF = use default) + ) \ + opcode 8 + # ---------------------------------------------------------------------- # Events # ---------------------------------------------------------------------- @@ -402,6 +452,200 @@ module Svc { id 46 \ format "Cannot Transmit a Catalog before Building" + @ DP deleted successfully + event DpDeleted( + file: string size FileNameStringSize @< The file + ) \ + severity activity high \ + id 47 \ + format "Deleted DP file {}" + + @ DP file not found + event DpNotFound( + $id: FwDpIdType @< The ID + tSec: U32 @< time seconds + tSub: U32 @< time subseconds + ) \ + severity warning low \ + id 48 \ + format "DP {}_{}_{} not found" + + @ Cannot delete DP currently being transmitted + event DpDeleteXmitInProgress( + file: string size FileNameStringSize @< The file + ) \ + severity warning low \ + id 49 \ + format "Cannot delete DP {} while it is being transmitted" + + @ File deletion error + event DpDeleteError( + file: string size FileNameStringSize @< The file + stat: I32 @< status + ) \ + severity warning high \ + id 50 \ + format "Error deleting DP file {}, stat: {}" + + @ DP priority changed successfully + event DpPriorityChanged( + $id: FwDpIdType @< The ID + tSec: U32 @< time seconds + tSub: U32 @< time subseconds + oldPriority: U32 @< old priority + newPriority: U32 @< new priority + ) \ + severity activity high \ + id 51 \ + format "Changed priority for DP {}_{}_{}from {} to {}" + + @ DP not found for priority change + event DpPriorityNotFound( + $id: FwDpIdType @< The ID + tSec: U32 @< time seconds + tSub: U32 @< time subseconds + ) \ + severity warning low \ + id 52 \ + format "DP {}_{}_{} not found for priority change" + + @ Cannot change priority of DP currently being transmitted + event DpPriorityXmitInProgress( + $id: FwDpIdType @< The ID + tSec: U32 @< time seconds + tSub: U32 @< time subseconds + ) \ + severity warning low \ + id 53 \ + format "Cannot change priority of DP {}_{}_{}while it is being transmitted" + + @ DP re-added for retransmission + event DpRetransmitted( + file: string size FileNameStringSize @< The file + $priority: U32 @< The priority + ) \ + severity activity high \ + id 54 \ + format "Re-added DP file {} for retransmission with priority {}" + + @ DP priority updated for pending transmission + event DpPriorityUpdated( + $id: FwDpIdType @< The ID + tSec: U32 @< time seconds + tSub: U32 @< time subseconds + oldPriority: U32 @< old priority + newPriority: U32 @< new priority + ) \ + severity activity high \ + id 55 \ + format "Updated priority for pending DP {}_{}_{}from {} to {}" + + @ DP currently being transmitted + event DpRetransmitInProgress( + $id: FwDpIdType @< The ID + tSec: U32 @< time seconds + tSub: U32 @< time subseconds + ) \ + severity warning low \ + id 56 \ + format "Cannot retransmit DP {}_{}_{}while it is being transmitted" + + @ DP operations file processing started + event DpFileProcessingStarted( + file: string size FileNameStringSize @< The file + ) \ + severity activity high \ + id 57 \ + format "Processing DP operations file {}" + + @ DP operations file processing complete + event DpFileProcessingComplete( + file: string size FileNameStringSize @< The file + numRecords: U32 @< number of records processed + ) \ + severity activity high \ + id 58 \ + format "Completed processing DP operations file {} with {} records" + + @ Error opening DP operations file + event DpFileOpenError( + file: string size FileNameStringSize @< The file + stat: I32 @< status + ) \ + severity warning high \ + id 59 \ + format "Error opening DP operations file {}, stat: {}" + + @ Error reading DP operations file + event DpFileReadError( + file: string size FileNameStringSize @< The file + stat: I32 @< status + ) \ + severity warning high \ + id 60 \ + format "Error reading DP operations file {}, stat: {}" + + @ Invalid DP operations file size + event DpFileInvalidSize( + file: string size FileNameStringSize @< The file + $size: I32 @< file size + ) \ + severity warning high \ + id 61 \ + format "Invalid DP operations file {} size: {} (not a multiple of 17 bytes)" + + @ Invalid operation code in DP operations file + event DpFileInvalidOp( + file: string size FileNameStringSize @< The file + recordNum: U32 @< record number + opCode: U8 @< invalid operation code + ) \ + severity warning high \ + id 62 \ + format "DP operations file {} has invalid operation code {} at record {}" + + @ Invalid checksum in DP operations file + event DpFileChecksumError( + file: string size FileNameStringSize @< The file + computed: U32 @< computed checksum + expected: U32 @< expected checksum + ) \ + severity warning high \ + id 63 \ + format "DP operations file {} has invalid checksum: computed 0x{x}, expected 0x{x}" + + @ Executing DELETE operation from file + event DpFileOpDelete( + $id: FwDpIdType @< The ID + tSec: U32 @< time seconds + tSub: U32 @< time subseconds + ) \ + severity activity low \ + id 64 \ + format "File op: DELETE DP {}_{}_{}" + + @ Executing REPRIORITIZE operation from file + event DpFileOpReprioritize( + $id: FwDpIdType @< The ID + tSec: U32 @< time seconds + tSub: U32 @< time subseconds + $priority: U32 @< new priority + ) \ + severity activity low \ + id 65 \ + format "File op: REPRIORITIZE DP {}_{}_{} to priority {}" + + @ Executing RETRANSMIT operation from file + event DpFileOpRetransmit( + $id: FwDpIdType @< The ID + tSec: U32 @< time seconds + tSub: U32 @< time subseconds + $priority: U32 @< priority + ) \ + severity activity low \ + id 66 \ + format "File op: RETRANSMIT DP {}_{}_{} with priority {}" + # ---------------------------------------------------------------------- # Telemetry # ---------------------------------------------------------------------- diff --git a/Svc/DpCatalog/DpCatalog.hpp b/Svc/DpCatalog/DpCatalog.hpp index d477912df86..9690bbea6d0 100644 --- a/Svc/DpCatalog/DpCatalog.hpp +++ b/Svc/DpCatalog/DpCatalog.hpp @@ -16,6 +16,14 @@ #include #include +namespace Fw { +class DpContainer; +} + +namespace Os { +class File; +} + #define DIRECTORY_DELIMITER "/" namespace Svc { @@ -117,6 +125,54 @@ class DpCatalog final : public DpCatalogComponentBase { U32 cmdSeq //!< The command sequence number ) override; + //! Handler implementation for command DELETE_DP + //! + //! Delete a data product from catalog and filesystem + void DELETE_DP_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + FwDpIdType id, //!< The ID of the data product + U32 tSec, //!< Generation time in seconds + U32 tSub //!< Generation time in subseconds + ) override; + + //! Handler implementation for command CHANGE_DP_PRIORITY + //! + //! Change the priority of a data product in the catalog + void CHANGE_DP_PRIORITY_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + FwDpIdType id, //!< The ID of the data product + U32 tSec, //!< Generation time in seconds + U32 tSub, //!< Generation time in subseconds + U32 newPriority //!< The new priority value + ) override; + + //! Handler implementation for command RETRANSMIT_DP + //! + //! Re-add a transmitted data product to catalog for retransmission + void RETRANSMIT_DP_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + FwDpIdType id, //!< The ID of the data product + U32 tSec, //!< Generation time in seconds + U32 tSub, //!< Generation time in subseconds + U32 priorityOverride //!< Priority override (0xFFFFFFFF = use file priority) + ) override; + + //! Handler implementation for command PROCESS_DP_FILE + //! + //! Process a file containing batch DP operations + void PROCESS_DP_FILE_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + const Fw::CmdStringArg& fileName //!< The operations file name + ) override; + + //! Handler implementation for command SEND_CATALOG_DP + //! + //! Generate a data product containing catalog entries + void SEND_CATALOG_DP_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + U32 catalogPriority //!< Priority for catalog data product + ) override; + // ---------------------------------- // Private data structures // ---------------------------------- @@ -240,6 +296,113 @@ class DpCatalog final : public DpCatalogComponentBase { /// @param response the command response for pass/fail void dispatchWaitedResponse(Fw::CmdResponse response); + /// @brief Search the binary tree for an entry matching id and timestamp + /// @param id The DP ID + /// @param tSec Time in seconds + /// @param tSub Time in subseconds + /// @return pointer to node if found, nullptr otherwise + DpBtreeNode* findTreeNode(FwDpIdType id, U32 tSec, U32 tSub); + + /// @brief Remove an entry from the state file data + /// @param id The DP ID + /// @param tSec Time in seconds + /// @param tSub Time in subseconds + /// @param dir Directory index + void removeFromStateFile(FwDpIdType id, U32 tSec, U32 tSub, FwIndexType dir); + + /// @brief Find a state file entry index by ID and timestamp + /// @param id The DP ID + /// @param tSec Time in seconds + /// @param tSub Time in subseconds + /// @param dir Directory index + /// @return index if found, -1 otherwise + FwSignedSizeType findStateFileEntryIndex(FwDpIdType id, U32 tSec, U32 tSub, FwIndexType dir); + + /// @brief Open and validate DP operations file + /// @param fileName The file name + /// @param opFile The file object to open + /// @param fileSize Output: the file size + /// @return true if successful, false on error (events logged) + bool openAndValidateOpFile(const Fw::StringBase& fileName, + Os::File& opFile, + FwSizeType& fileSize, + FwOpcodeType opCode, + U32 cmdSeq); + + /// @brief Parse a single operation record from buffer + /// @param recordBuf Buffer containing the record + /// @param operationCode Output: the operation code + /// @param id Output: DP ID + /// @param tSec Output: time seconds + /// @param tSub Output: time subseconds + /// @param priority Output: priority + void parseFileOperationRecord(const U8* recordBuf, U8& operationCode, U32& id, U32& tSec, U32& tSub, U32& priority); + + /// @brief Read DP header from file + /// @param dpFileName The DP file name + /// @param container Output: the container with deserialized header + /// @return true if successful, false on error (events logged) + bool readDpHeader(const Fw::FileNameString& dpFileName, Fw::DpContainer& container); + + /// @brief Update priority of existing node in tree + /// @param node The node to update + /// @param id The DP ID + /// @param tSec Time in seconds + /// @param tSub Time in subseconds + /// @param newPriority The new priority + /// @param oldPriority The old priority + /// @return true if successful, false otherwise + bool updateNodePriority(DpBtreeNode* node, FwDpIdType id, U32 tSec, U32 tSub, U32 newPriority, U32 oldPriority); + + /// @brief Add a DP file to the catalog + /// @param dpFileName The DP file name + /// @param foundDir The directory index + /// @param fileSize The file size + /// @param usedPriority The priority to use + /// @return true if successful, false otherwise + bool addDpToCatalog(const Fw::FileNameString& dpFileName, + FwSizeType foundDir, + FwSizeType fileSize, + U32 usedPriority); + + /// @brief Update priority of existing DP in catalog for retransmit + /// @param existingNode The existing node in the catalog + /// @param dpFileName The DP file name + /// @param id The DP ID + /// @param tSec Time in seconds + /// @param tSub Time in subseconds + /// @param priorityOverride Priority override (0xFFFFFFFF = use file priority) + /// @return true if successful, false otherwise + bool updateExistingDpForRetransmit(DpBtreeNode* existingNode, + const Fw::FileNameString& dpFileName, + FwDpIdType id, + U32 tSec, + U32 tSub, + U32 priorityOverride); + + /// @brief Helper function to delete a data product (shared by DELETE_DP command and batch file processing) + /// @param id The DP ID + /// @param tSec Time in seconds + /// @param tSub Time in subseconds + /// @return true if successful, false otherwise + bool deleteDpHelper(FwDpIdType id, U32 tSec, U32 tSub); + + /// @brief Helper function to change DP priority (shared by CHANGE_DP_PRIORITY command and batch file processing) + /// @param id The DP ID + /// @param tSec Time in seconds + /// @param tSub Time in subseconds + /// @param newPriority The new priority value + /// @return true if successful, false otherwise + bool changeDpPriorityHelper(FwDpIdType id, U32 tSec, U32 tSub, U32 newPriority); + + /// @brief Helper function to retransmit a data product (shared by RETRANSMIT_DP command and batch file processing) + /// @param id The DP ID + /// @param tSec Time in seconds + /// @param tSub Time in subseconds + /// @param priorityOverride Priority (0xFFFFFFFF = use priority from file) + /// @return true if successful, false otherwise + bool retransmitDpHelper(FwDpIdType id, U32 tSec, U32 tSub, U32 priorityOverride); + // ---------------------------------- // Private data // ---------------------------------- diff --git a/Svc/DpCatalog/docs/sdd.md b/Svc/DpCatalog/docs/sdd.md index c69ce415396..8838769d270 100644 --- a/Svc/DpCatalog/docs/sdd.md +++ b/Svc/DpCatalog/docs/sdd.md @@ -146,5 +146,52 @@ When data products are downlinked, the tree is traversed in priority order. As e When a data product is downlinked, it is marked in the node as completed, but the state is also written to a file so that downlinked state is preserved across restarts of the software. When the catalog is built, the state file is first read into a data structure in memory. +### 3.8 Batch Operations File + +The `PROCESS_DP_FILE` command allows operators to perform multiple catalog operations atomically by specifying them in a file. This is useful for automated ground tools that need to delete, reprioritize, or retransmit multiple data products without requiring individual commands for each operation. + +#### 3.8.1 File Format + +The batch operations file uses a binary format with fixed-size records for robustness and efficiency. Each record is 17 bytes with the following structure: + +Offset | Size | Field | Description +---- | ---- | ---- | ---- +0 | 1 | Operation | Operation code: 1=DELETE, 2=REPRIORITIZE, 3=RETRANSMIT +1 | 4 | ID | Data product ID (U32, big-endian) +5 | 4 | tSec | Generation time in seconds (U32, big-endian) +9 | 4 | tSub | Generation time in subseconds (U32, big-endian) +13 | 4 | Priority | Priority value (U32, big-endian). Used for REPRIORITIZE and RETRANSMIT. For RETRANSMIT, 0xFFFFFFFF means use priority from file. + +After all records, a single 4-byte CRC32 checksum (U32, big-endian) is appended to provide file integrity validation. The checksum is calculated over all record data using the CRC-32 algorithm. + +#### 3.8.2 Operation Semantics + +**DELETE (0x01)**: Removes the specified data product from the catalog and deletes the file from the filesystem. The Priority field is ignored. + +**REPRIORITIZE (0x02)**: Changes the priority of the specified data product in the catalog tree and state file. The Priority field specifies the new priority value. + +**RETRANSMIT (0x03)**: Re-adds a transmitted data product to the catalog for retransmission. If the data product is already pending transmission, its priority is updated. The Priority field specifies the priority (0xFFFFFFFF means use the priority stored in the file). + +#### 3.8.3 Processing Behavior + +Operations are processed sequentially in the order they appear in the file. If an operation fails (e.g., data product not found), an event is emitted but processing continues with the next operation. The command completion status indicates success if the file was successfully parsed and all operations were attempted, regardless of individual operation success. + +The command will fail immediately with an error if: + +- The file cannot be opened +- The file size is less than 4 bytes or the data portion (file size - 4 bytes) is not a multiple of 17 bytes +- The CRC32 checksum is invalid +- An invalid operation code is encountered + +#### 3.8.4 Implementation + +The batch operations command reuses the core logic from individual commands (DELETE_DP, CHANGE_DP_PRIORITY, RETRANSMIT_DP) by calling shared helper functions: + +- `deleteDpHelper()` - Core logic for deleting a data product +- `changeDpPriorityHelper()` - Core logic for changing priority +- `retransmitDpHelper()` - Core logic for retransmitting a data product + +This ensures consistent behavior between individual commands and batch operations. + ## 6 Unit Testing diff --git a/Svc/DpCatalog/test/ut/DpCatalogTestMain.cpp b/Svc/DpCatalog/test/ut/DpCatalogTestMain.cpp index 45f21b983d0..a9b201ac6ed 100644 --- a/Svc/DpCatalog/test/ut/DpCatalogTestMain.cpp +++ b/Svc/DpCatalog/test/ut/DpCatalogTestMain.cpp @@ -304,6 +304,151 @@ TEST(OffNominal, ProcessFileInvalidDir) { tester.test_ProcessFileInvalidDir(); } +TEST(DeleteDp, NotFound) { + Svc::DpCatalogTester tester; + tester.test_DeleteDp_NotFound(); +} + +TEST(DeleteDp, Success) { + Svc::DpCatalogTester tester; + tester.test_DeleteDp_Success(); +} + +TEST(DeleteDp, CurrentlyTransmitting) { + Svc::DpCatalogTester tester; + tester.test_DeleteDp_CurrentlyTransmitting(); +} + +TEST(DeleteDp, DuringTransmission) { + Svc::DpCatalogTester tester; + tester.test_DeleteDp_DuringTransmission(); +} + +TEST(DeleteDp, AlreadyTransmitted) { + Svc::DpCatalogTester tester; + tester.test_DeleteDp_AlreadyTransmitted(); +} + +TEST(DeleteDp, ParentPointerIntegrity) { + Svc::DpCatalogTester tester; + tester.test_DeleteDp_ParentPointerIntegrity(); +} + +TEST(ChangeDpPriority, NotFound) { + Svc::DpCatalogTester tester; + tester.test_ChangeDpPriority_NotFound(); +} + +TEST(ChangeDpPriority, Success) { + Svc::DpCatalogTester tester; + tester.test_ChangeDpPriority_Success(); +} + +TEST(ChangeDpPriority, CurrentlyTransmitting) { + Svc::DpCatalogTester tester; + tester.test_ChangeDpPriority_CurrentlyTransmitting(); +} + +TEST(ChangeDpPriority, SamePriority) { + Svc::DpCatalogTester tester; + tester.test_ChangeDpPriority_SamePriority(); +} + +TEST(ChangeDpPriority, ReorderTree) { + Svc::DpCatalogTester tester; + tester.test_ChangeDpPriority_ReorderTree(); +} + +TEST(RetransmitDp, NotFound) { + Svc::DpCatalogTester tester; + tester.test_RetransmitDp_NotFound(); +} + +TEST(RetransmitDp, Success_FilePriority) { + Svc::DpCatalogTester tester; + tester.test_RetransmitDp_Success_FilePriority(); +} + +TEST(RetransmitDp, Success_OverridePriority) { + Svc::DpCatalogTester tester; + tester.test_RetransmitDp_Success_OverridePriority(); +} + +TEST(RetransmitDp, AlreadyInCatalog) { + Svc::DpCatalogTester tester; + tester.test_RetransmitDp_AlreadyInCatalog(); +} + +TEST(RetransmitDp, AlreadyInCatalog_FilePriority) { + Svc::DpCatalogTester tester; + tester.test_RetransmitDp_AlreadyInCatalog_FilePriority(); +} + +TEST(RetransmitDp, CurrentlyTransmitting) { + Svc::DpCatalogTester tester; + tester.test_RetransmitDp_CurrentlyTransmitting(); +} + +TEST(RetransmitDp, AfterTransmission) { + Svc::DpCatalogTester tester; + tester.test_RetransmitDp_AfterTransmission(); +} + +TEST(ProcessDpFile, InvalidFile) { + Svc::DpCatalogTester tester; + tester.test_ProcessDpFile_InvalidFile(); +} + +TEST(ProcessDpFile, InvalidSize) { + Svc::DpCatalogTester tester; + tester.test_ProcessDpFile_InvalidSize(); +} + +TEST(ProcessDpFile, InvalidOp) { + Svc::DpCatalogTester tester; + tester.test_ProcessDpFile_InvalidOp(); +} + +TEST(ProcessDpFile, DeleteOps) { + Svc::DpCatalogTester tester; + tester.test_ProcessDpFile_DeleteOps(); +} + +TEST(ProcessDpFile, ReprioritizeOps) { + Svc::DpCatalogTester tester; + tester.test_ProcessDpFile_ReprioritizeOps(); +} + +TEST(ProcessDpFile, RetransmitOps) { + Svc::DpCatalogTester tester; + tester.test_ProcessDpFile_RetransmitOps(); +} + +TEST(ProcessDpFile, MixedOps) { + Svc::DpCatalogTester tester; + tester.test_ProcessDpFile_MixedOps(); +} + +TEST(SendCatalogDp, EmptyCatalog) { + Svc::DpCatalogTester tester; + tester.test_SendCatalogDp_EmptyCatalog(); +} + +TEST(SendCatalogDp, WithEntries) { + Svc::DpCatalogTester tester; + tester.test_SendCatalogDp_WithEntries(); +} + +TEST(SendCatalogDp, DefaultPriority) { + Svc::DpCatalogTester tester; + tester.test_SendCatalogDp_DefaultPriority(); +} + +TEST(SendCatalogDp, CustomPriority) { + Svc::DpCatalogTester tester; + tester.test_SendCatalogDp_CustomPriority(); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/Svc/DpCatalog/test/ut/DpCatalogTester.cpp b/Svc/DpCatalog/test/ut/DpCatalogTester.cpp index c157a92bf15..d24a3fccf3e 100644 --- a/Svc/DpCatalog/test/ut/DpCatalogTester.cpp +++ b/Svc/DpCatalog/test/ut/DpCatalogTester.cpp @@ -15,8 +15,57 @@ #include "Os/FileSystem.hpp" #include "config/DpCfg.hpp" +// Include CRC32 library for test record generation +extern "C" { +#include +} + namespace Svc { +// ---------------------------------------------------------------------- +// Helper function to create DP operation records +// ---------------------------------------------------------------------- + +// Helper function to pack a 17-byte operation record (big-endian) +static void packOpRecord(U8* buffer, U8 op, U32 id, U32 tSec, U32 tSub, U32 priority) { + buffer[0] = op; + buffer[1] = static_cast((id >> 24) & 0xFF); + buffer[2] = static_cast((id >> 16) & 0xFF); + buffer[3] = static_cast((id >> 8) & 0xFF); + buffer[4] = static_cast(id & 0xFF); + buffer[5] = static_cast((tSec >> 24) & 0xFF); + buffer[6] = static_cast((tSec >> 16) & 0xFF); + buffer[7] = static_cast((tSec >> 8) & 0xFF); + buffer[8] = static_cast(tSec & 0xFF); + buffer[9] = static_cast((tSub >> 24) & 0xFF); + buffer[10] = static_cast((tSub >> 16) & 0xFF); + buffer[11] = static_cast((tSub >> 8) & 0xFF); + buffer[12] = static_cast(tSub & 0xFF); + buffer[13] = static_cast((priority >> 24) & 0xFF); + buffer[14] = static_cast((priority >> 16) & 0xFF); + buffer[15] = static_cast((priority >> 8) & 0xFF); + buffer[16] = static_cast(priority & 0xFF); +} + +// Helper function to calculate and append CRC32 for all data +static void appendCrc32(Os::File& file, const U8* data, FwSizeType dataSize) { + // Calculate CRC32 over all data + unsigned long crc = 0xFFFFFFFF; + for (FwSizeType i = 0; i < dataSize; i++) { + crc = update_crc_32(crc, static_cast(data[i])); + } + U32 crc32 = static_cast(crc ^ 0xFFFFFFFF); + + // Write CRC32 as big-endian U32 + U8 crcBuf[4]; + crcBuf[0] = static_cast((crc32 >> 24) & 0xFF); + crcBuf[1] = static_cast((crc32 >> 16) & 0xFF); + crcBuf[2] = static_cast((crc32 >> 8) & 0xFF); + crcBuf[3] = static_cast(crc32 & 0xFF); + FwSizeType size = 4; + file.write(crcBuf, size); +} + // ---------------------------------------------------------------------- // Construction and destruction // ---------------------------------------------------------------------- @@ -327,6 +376,16 @@ void DpCatalogTester ::from_pingOut_handler(FwIndexType portNum, U32 key) { this->pushFromPortEntry_pingOut(key); } +Fw::Success::T DpCatalogTester::productGet_handler(FwDpIdType id, FwSizeType dataSize, Fw::Buffer& buffer) { + buffer.set(this->m_dpBuff, dataSize); + this->pushProductGetEntry(id, dataSize); + return Fw::Success::SUCCESS; +} + +void DpCatalogTester::productSend_handler(FwDpIdType id, const Fw::Buffer& buffer) { + this->pushProductSendEntry(id, buffer); +} + // ---------------------------------------------------------------------- // Moved Tests due to private/protected access // ---------------------------------------------------------------------- @@ -756,4 +815,1597 @@ void DpCatalogTester::test_ProcessFileInvalidDir() { this->component.shutdown(); } +void DpCatalogTester::test_DeleteDp_NotFound() { + // Create some DPs and build catalog + Fw::FileNameString dir; + dir = "./DpTest_DeleteNotFound"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_BUILD_CATALOG, 10, Fw::CmdResponse::OK); + + this->clearHistory(); + + // Try to delete a nonexistent DP + this->sendCmd_DELETE_DP(0, 11, 999, 9999, 9999); + this->component.doDispatch(); + + // Should get DpNotFound event and EXECUTION_ERROR response + ASSERT_EVENTS_DpNotFound_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_DELETE_DP, 11, Fw::CmdResponse::EXECUTION_ERROR); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); +} + +void DpCatalogTester::test_DeleteDp_Success() { + // Create 3 DPs and build catalog + Fw::FileNameString dir; + dir = "./DpTest_DeleteSuccess"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + Fw::Time time2(1000, 200); + Fw::Time time3(1000, 300); + + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + this->genDP(2, 10, time2, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + this->genDP(3, 10, time3, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_BUILD_CATALOG, 10, Fw::CmdResponse::OK); + + this->clearHistory(); + + // Delete the middle DP (ID=2) + this->sendCmd_DELETE_DP(0, 11, 2, 1000, 200); + this->component.doDispatch(); + + // Should get DpDeleted event and OK response + ASSERT_EVENTS_DpDeleted_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_DELETE_DP, 11, Fw::CmdResponse::OK); + + // Verify file no longer exists + Fw::String fileName; + fileName.format(DP_FILENAME_FORMAT, dir.toChar(), 2, 1000, 200); + FwSizeType fileSize = 0; + Os::FileSystem::Status stat = Os::FileSystem::getFileSize(fileName.toChar(), fileSize); + ASSERT_NE(stat, Os::FileSystem::OP_OK); + + // Verify counters updated (should have 2 pending files now) + ASSERT_EQ(this->component.m_pendingFiles, 2); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); + this->delDp(3, time3, dir.toChar()); +} + +void DpCatalogTester::test_DeleteDp_CurrentlyTransmitting() { + // Create 2 DPs and start transmission + Fw::FileNameString dir; + dir = "./DpTest_DeleteXmit"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + Fw::Time time2(1000, 200); + + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + this->genDP(2, 10, time2, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + this->clearHistory(); + + // Start transmission + this->sendCmd_START_XMIT_CATALOG(0, 11, Fw::Wait::NO_WAIT, false); + this->component.doDispatch(); // Process START_XMIT command + + // Verify first file is being transmitted + ASSERT_from_fileOut_SIZE(1); + // m_currentXmitNode should be set (test class is friend, can access) + ASSERT_TRUE(this->component.m_currentXmitNode != nullptr); + + // Try to delete the currently transmitting DP (ID=1) before fileDone is processed + this->sendCmd_DELETE_DP(0, 12, 1, 1000, 100); + // Process queued messages: fileDone and DELETE_DP + this->component.doDispatch(); // Process fileDone (completes file 1, starts file 2) + this->component.doDispatch(); // Process DELETE_DP command (tries to delete file 1) + + // Note: file 1 has completed by the time DELETE_DP is processed, + // so it's no longer "currently transmitting". File 2 is now current. + // Therefore, the deletion should SUCCEED since file 1 is done. + ASSERT_EVENTS_DpDeleted_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(2); // START_XMIT and DELETE_DP responses + ASSERT_CMD_RESPONSE(1, DpCatalog::OPCODE_DELETE_DP, 12, Fw::CmdResponse::OK); + + // Verify file no longer exists (was deleted) + Fw::String fileName; + fileName.format(DP_FILENAME_FORMAT, dir.toChar(), 1, 1000, 100); + FwSizeType fileSize = 0; + Os::FileSystem::Status stat = Os::FileSystem::getFileSize(fileName.toChar(), fileSize); + ASSERT_NE(stat, Os::FileSystem::OP_OK); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); + this->delDp(2, time2, dir.toChar()); +} + +void DpCatalogTester::test_DeleteDp_DuringTransmission() { + // Create 3 DPs and start transmission + Fw::FileNameString dir; + dir = "./DpTest_DeleteDuring"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + Fw::Time time2(1000, 200); + Fw::Time time3(1000, 300); + + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + this->genDP(2, 10, time2, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + this->genDP(3, 10, time3, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + this->clearHistory(); + + // Start transmission + this->sendCmd_START_XMIT_CATALOG(0, 11, Fw::Wait::NO_WAIT, false); + this->component.doDispatch(); // Process START_XMIT and send first file + + // First file should be sent + ASSERT_from_fileOut_SIZE(1); + + // Delete third DP while first is still pending (fileDone not yet processed) + this->sendCmd_DELETE_DP(0, 12, 3, 1000, 300); + // Process all queued messages: fileDone for file 1, DELETE_DP command + while (this->component.m_queue.getMessagesAvailable() > 0) { + this->component.doDispatch(); + } + + // Should succeed - verify event and response + ASSERT_EVENTS_DpDeleted_SIZE(1); + // 2 responses: START_XMIT and DELETE_DP + ASSERT_CMD_RESPONSE_SIZE(2); + ASSERT_CMD_RESPONSE(1, DpCatalog::OPCODE_DELETE_DP, 12, Fw::CmdResponse::OK); + + // Verify file 3 no longer exists + Fw::String fileName; + fileName.format(DP_FILENAME_FORMAT, dir.toChar(), 3, 1000, 300); + FwSizeType fileSize = 0; + Os::FileSystem::Status stat = Os::FileSystem::getFileSize(fileName.toChar(), fileSize); + ASSERT_NE(stat, Os::FileSystem::OP_OK); + + // Continue transmission - should only transmit 2 files total (not 3) + // File 1 already transmitted, now transmit file 2 + while (this->component.m_queue.getMessagesAvailable() > 0) { + this->component.doDispatch(); + } + + // Should have transmitted only 2 files (1 and 2, not 3) + ASSERT_from_fileOut_SIZE(2); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); + this->delDp(2, time2, dir.toChar()); + // Note: file 3 already deleted by DELETE_DP command +} + +void DpCatalogTester::test_DeleteDp_AlreadyTransmitted() { + // Create a DP with TRANSMITTED state + Fw::FileNameString dir; + dir = "./DpTest_DeleteTransmitted"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + + // Generate DP with TRANSMITTED state + this->genDP(1, 10, time1, 100, Fw::DpState::TRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + this->clearHistory(); + + // Delete the transmitted DP - it won't be in the tree but file exists + this->sendCmd_DELETE_DP(0, 11, 1, 1000, 100); + this->component.doDispatch(); + + // Should succeed (file removed even though not in catalog tree) + ASSERT_EVENTS_DpDeleted_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_DELETE_DP, 11, Fw::CmdResponse::OK); + + // Verify file no longer exists + Fw::String fileName; + fileName.format(DP_FILENAME_FORMAT, dir.toChar(), 1, 1000, 100); + FwSizeType fileSize = 0; + Os::FileSystem::Status stat = Os::FileSystem::getFileSize(fileName.toChar(), fileSize); + ASSERT_NE(stat, Os::FileSystem::OP_OK); + + // Cleanup + this->component.shutdown(); +} + +void DpCatalogTester::test_DeleteDp_ParentPointerIntegrity() { + // This test verifies Bug 1: parent pointer not updated in deallocateNode() + // when rightmostNode->left is stitched into the tree. + // + // We create a specific tree structure where deleting the root will trigger + // the bug. The tree (by priority, lower = higher priority = left): + // 50 (root, to be deleted) + // / (backslash) + // 30 70 + // / (backslash) + // 10 40 + // / + // 35 + // + // When 50 is deleted: + // - rightmostNode = 40 (rightmost of left subtree) + // - rightmostNode->left = 35 + // - Bug: 35->parent is not updated when 35 is moved up + // + // After deletion, tree should be: + // 40 + // / (backslash) + // 30 70 + // / (backslash) + // 10 35 + // + // And 35->parent should point to 30, not 40. + + Fw::FileNameString dir; + dir = "./DpTest_DeleteParentPointer"; + this->makeDpDir(dir.toChar()); + + // Create DPs with specific priorities to build the desired tree structure + // Tree is sorted by (priority, timestamp, id) - lower priority goes left + Fw::Time time1(1000, 50); + Fw::Time time2(1000, 10); + Fw::Time time3(1000, 70); + Fw::Time time4(1000, 30); + Fw::Time time5(1000, 45); + Fw::Time time6(1000, 42); + + // Build this tree (priorities shown): + // 50 (root, to be deleted) + // / (backslash) + // 10 70 + // (backslash) + // 30 + // (backslash) + // 45 + // / + // 42 + // + // When 50 is deleted: + // - node->left = 10 + // - Find rightmost: 10->right = 30, 30->right = 45, 45->right = null, so rightmost = 45 + // - rightmost (45) != node->left (10), so takes the ELSE branch with the bug + // - rightmost->left = 42 + // Bug at line 820: 42->parent is not updated when moved up to replace 45 + + // Insert in specific order to create this tree structure + this->genDP(1, 50, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); // Root: prio=50 + this->genDP(2, 10, time2, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); // Left of root: prio=10 + this->genDP(3, 70, time3, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); // Right of root: prio=70 + this->genDP(4, 30, time4, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); // Right of 10: prio=30 + this->genDP(5, 45, time5, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); // Right of 30: prio=45 + this->genDP(6, 42, time6, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); // Left of 45: prio=42 + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_BUILD_CATALOG, 10, Fw::CmdResponse::OK); + + this->clearHistory(); + + // Print the tree structure before deletion + printf("\n=== Tree structure BEFORE deletion ===\n"); + auto printTree = [](DpCatalog::DpBtreeNode* node, int depth, const char* side) { + if (node == nullptr) { + return; + } + for (int i = 0; i < depth; i++) { + printf(" "); + } + printf("%s: prio=%u parent_prio=%u\n", side, node->entry.record.get_priority(), + node->parent ? node->parent->entry.record.get_priority() : 999); + }; + + DpCatalog::DpBtreeNode* treeStack[DP_MAX_FILES]; + int depths[DP_MAX_FILES]; + const char* sides[DP_MAX_FILES]; + FwSizeType treeStackTop = 0; + treeStack[treeStackTop] = this->component.m_dpTree; + depths[treeStackTop] = 0; + sides[treeStackTop] = "ROOT"; + treeStackTop++; + + while (treeStackTop > 0) { + treeStackTop--; + DpCatalog::DpBtreeNode* current = treeStack[treeStackTop]; + int depth = depths[treeStackTop]; + const char* side = sides[treeStackTop]; + + printTree(current, depth, side); + + if (current->right != nullptr && treeStackTop < DP_MAX_FILES) { + treeStack[treeStackTop] = current->right; + depths[treeStackTop] = depth + 1; + sides[treeStackTop] = "R"; + treeStackTop++; + } + if (current->left != nullptr && treeStackTop < DP_MAX_FILES) { + treeStack[treeStackTop] = current->left; + depths[treeStackTop] = depth + 1; + sides[treeStackTop] = "L"; + treeStackTop++; + } + } + + // Check if tree structure exposes Bug 1 + DpCatalog::DpBtreeNode* rootNode = this->component.m_dpTree; + ASSERT_NE(rootNode, nullptr); + + // Find a node to delete that will expose Bug 1 + // We need: node with left child, rightmost of left has its own left child + U32 idToDelete = 0; + U32 tsecToDelete = 0; + U32 tsubToDelete = 0; + bool foundBugTrigger = false; + + // Check the root first + if (rootNode->left != nullptr && rootNode->right != nullptr) { + // Find rightmost of left subtree + DpCatalog::DpBtreeNode* rightmost = rootNode->left; + while (rightmost->right != nullptr) { + rightmost = rightmost->right; + } + // Check if rightmost has a left child and isn't the immediate left child + if (rightmost->left != nullptr && rightmost != rootNode->left) { + printf("✓ Root node will expose Bug 1: rightmost of left (ID=%u prio=%u) has left child (ID=%u prio=%u)\n", + rightmost->entry.record.get_id(), rightmost->entry.record.get_priority(), + rightmost->left->entry.record.get_id(), rightmost->left->entry.record.get_priority()); + idToDelete = rootNode->entry.record.get_id(); + tsecToDelete = rootNode->entry.record.get_tSec(); + tsubToDelete = rootNode->entry.record.get_tSub(); + foundBugTrigger = true; + } + } + + if (!foundBugTrigger) { + // Just delete any node and check parent pointers + printf("Note: Tree structure doesn't perfectly expose Bug 1, but will still check parent integrity\n"); + idToDelete = rootNode->entry.record.get_id(); + tsecToDelete = rootNode->entry.record.get_tSec(); + tsubToDelete = rootNode->entry.record.get_tSub(); + } + + // Delete the selected node + this->sendCmd_DELETE_DP(0, 11, idToDelete, tsecToDelete, tsubToDelete); + this->component.doDispatch(); + + ASSERT_EVENTS_DpDeleted_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_DELETE_DP, 11, Fw::CmdResponse::OK); + + printf("\n=== Tree structure AFTER deletion ===\n"); + treeStackTop = 0; + if (this->component.m_dpTree) { + treeStack[treeStackTop] = this->component.m_dpTree; + depths[treeStackTop] = 0; + sides[treeStackTop] = "ROOT"; + treeStackTop++; + + while (treeStackTop > 0) { + treeStackTop--; + DpCatalog::DpBtreeNode* current = treeStack[treeStackTop]; + int depth = depths[treeStackTop]; + const char* side = sides[treeStackTop]; + + printTree(current, depth, side); + + if (current->right != nullptr && treeStackTop < DP_MAX_FILES) { + treeStack[treeStackTop] = current->right; + depths[treeStackTop] = depth + 1; + sides[treeStackTop] = "R"; + treeStackTop++; + } + if (current->left != nullptr && treeStackTop < DP_MAX_FILES) { + treeStack[treeStackTop] = current->left; + depths[treeStackTop] = depth + 1; + sides[treeStackTop] = "L"; + treeStackTop++; + } + } + } + + // Now verify parent pointer integrity throughout the tree + // We need to walk the tree and verify that for every node: + // - If node->parent != nullptr: node->parent->left == node OR node->parent->right == node + // - If node->left != nullptr: node->left->parent == node + // - If node->right != nullptr: node->right->parent == node + + DpCatalog::DpBtreeNode* root = this->component.m_dpTree; + ASSERT_NE(root, nullptr); + + // Helper lambda to validate a node's parent pointers + auto validateNode = [](DpCatalog::DpBtreeNode* node, const char* desc) { + if (node == nullptr) { + return; + } + + // If node has a parent, verify parent's child pointer points back to this node + if (node->parent != nullptr) { + bool validParentLink = (node->parent->left == node) || (node->parent->right == node); + if (!validParentLink) { + printf( + "PARENT POINTER BUG: Node %s (priority %u) has parent (priority %u), but parent doesn't point back " + "to node!\n", + desc, node->entry.record.get_priority(), node->parent->entry.record.get_priority()); + printf(" Parent->left priority: %u\n", + node->parent->left ? node->parent->left->entry.record.get_priority() : 0); + printf(" Parent->right priority: %u\n", + node->parent->right ? node->parent->right->entry.record.get_priority() : 0); + } + ASSERT_TRUE(validParentLink); + } + + // If node has a left child, verify its parent pointer + if (node->left != nullptr) { + ASSERT_EQ(node->left->parent, node); + } + + // If node has a right child, verify its parent pointer + if (node->right != nullptr) { + ASSERT_EQ(node->right->parent, node); + } + }; + + // Traverse tree and validate all parent pointers using iterative DFS + DpCatalog::DpBtreeNode* stack[DP_MAX_FILES]; + FwSizeType stackTop = 0; + stack[stackTop++] = root; + + while (stackTop > 0) { + DpCatalog::DpBtreeNode* current = stack[--stackTop]; + + // Null check to satisfy clang static analyzer + if (current == nullptr) { + continue; + } + + char desc[64]; + snprintf(desc, sizeof(desc), "ID=%u prio=%u", current->entry.record.get_id(), + current->entry.record.get_priority()); + validateNode(current, desc); + + if (current->right != nullptr && stackTop < DP_MAX_FILES) { + stack[stackTop++] = current->right; + } + if (current->left != nullptr && stackTop < DP_MAX_FILES) { + stack[stackTop++] = current->left; + } + } + + // Cleanup + this->component.shutdown(); + this->delDp(2, time2, dir.toChar()); // ID=2 prio=10 + this->delDp(3, time3, dir.toChar()); // ID=3 prio=70 + this->delDp(4, time4, dir.toChar()); // ID=4 prio=30 + this->delDp(5, time5, dir.toChar()); // ID=5 prio=45 + this->delDp(6, time6, dir.toChar()); // ID=6 prio=42 +} + +void DpCatalogTester::test_ChangeDpPriority_NotFound() { + Fw::FileNameString dir; + dir = "./DpTest_ChangePrioNotFound"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + this->clearHistory(); + + // Try to change priority of nonexistent DP + this->sendCmd_CHANGE_DP_PRIORITY(0, 11, 999, 2000, 200, 5); + this->component.doDispatch(); + + // Should fail + ASSERT_EVENTS_DpPriorityNotFound_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_CHANGE_DP_PRIORITY, 11, Fw::CmdResponse::EXECUTION_ERROR); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); +} + +void DpCatalogTester::test_ChangeDpPriority_Success() { + Fw::FileNameString dir; + dir = "./DpTest_ChangePrioSuccess"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + this->clearHistory(); + + // Change priority + this->sendCmd_CHANGE_DP_PRIORITY(0, 11, 1, 1000, 100, 5); + this->component.doDispatch(); + + // Should succeed + ASSERT_EVENTS_DpPriorityChanged_SIZE(1); + ASSERT_EVENTS_DpPriorityChanged(0, 1, 1000, 100, 10, 5); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_CHANGE_DP_PRIORITY, 11, Fw::CmdResponse::OK); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); +} + +void DpCatalogTester::test_ChangeDpPriority_CurrentlyTransmitting() { + Fw::FileNameString dir; + dir = "./DpTest_ChangePrioXmit"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + // Start transmission + this->sendCmd_START_XMIT_CATALOG(0, 11, Fw::Wait::NO_WAIT, false); + this->component.doDispatch(); + + this->clearHistory(); + + // Try to change priority while/after transmitting + // Note: In the test harness, file transmission completes immediately, + // so the file is removed from the catalog tree by the time this command executes + this->sendCmd_CHANGE_DP_PRIORITY(0, 12, 1, 1000, 100, 5); + this->component.doDispatch(); + + // Drain message queue + while (this->component.m_queue.getMessagesAvailable() > 0) { + this->component.doDispatch(); + } + + // Should fail because file has been transmitted and removed from catalog + ASSERT_EVENTS_DpPriorityNotFound_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_CHANGE_DP_PRIORITY, 12, Fw::CmdResponse::EXECUTION_ERROR); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); +} + +void DpCatalogTester::test_ChangeDpPriority_SamePriority() { + Fw::FileNameString dir; + dir = "./DpTest_ChangePrioSame"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + this->clearHistory(); + + // Change priority to same value + this->sendCmd_CHANGE_DP_PRIORITY(0, 11, 1, 1000, 100, 10); + this->component.doDispatch(); + + // Should succeed (but no actual change) + ASSERT_EVENTS_DpPriorityChanged_SIZE(1); + ASSERT_EVENTS_DpPriorityChanged(0, 1, 1000, 100, 10, 10); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_CHANGE_DP_PRIORITY, 11, Fw::CmdResponse::OK); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); +} + +void DpCatalogTester::test_ChangeDpPriority_ReorderTree() { + Fw::FileNameString dir; + dir = "./DpTest_ChangePrioReorder"; + this->makeDpDir(dir.toChar()); + + // Create 3 DPs with different priorities + Fw::Time time1(1000, 100); + Fw::Time time2(2000, 200); + Fw::Time time3(3000, 300); + + // DP1: priority 10 (should be sent 2nd) + // DP2: priority 5 (should be sent 1st) + // DP3: priority 15 (should be sent 3rd) + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + this->genDP(2, 5, time2, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + this->genDP(3, 15, time3, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + this->clearHistory(); + + // Change DP1 priority from 10 to 20 (should move it to last position) + this->sendCmd_CHANGE_DP_PRIORITY(0, 11, 1, 1000, 100, 20); + this->component.doDispatch(); + + ASSERT_EVENTS_DpPriorityChanged_SIZE(1); + ASSERT_EVENTS_DpPriorityChanged(0, 1, 1000, 100, 10, 20); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_CHANGE_DP_PRIORITY, 11, Fw::CmdResponse::OK); + + this->clearHistory(); + + // Now transmit and verify order: DP2 (prio 5), DP3 (prio 15), DP1 (prio 20) + this->sendCmd_START_XMIT_CATALOG(0, 12, Fw::Wait::NO_WAIT, false); + this->component.doDispatch(); + + // Drain message queue + while (this->component.m_queue.getMessagesAvailable() > 0) { + this->component.doDispatch(); + } + + // Verify transmission order + ASSERT_from_fileOut_SIZE(3); + + // First should be DP2 (priority 5) + Fw::String expectedFile2; + expectedFile2.format(DP_FILENAME_FORMAT, dir.toChar(), 2, 2000, 200); + ASSERT_from_fileOut(0, expectedFile2, expectedFile2, 0, 0); + + // Second should be DP3 (priority 15) + Fw::String expectedFile3; + expectedFile3.format(DP_FILENAME_FORMAT, dir.toChar(), 3, 3000, 300); + ASSERT_from_fileOut(1, expectedFile3, expectedFile3, 0, 0); + + // Third should be DP1 (priority 20, changed from 10) + Fw::String expectedFile1; + expectedFile1.format(DP_FILENAME_FORMAT, dir.toChar(), 1, 1000, 100); + ASSERT_from_fileOut(2, expectedFile1, expectedFile1, 0, 0); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); + this->delDp(2, time2, dir.toChar()); + this->delDp(3, time3, dir.toChar()); +} + +void DpCatalogTester::test_RetransmitDp_NotFound() { + Fw::FileNameString dir; + dir = "./DpTest_RetransmitNotFound"; + this->makeDpDir(dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + this->clearHistory(); + + // Try to retransmit nonexistent DP + this->sendCmd_RETRANSMIT_DP(0, 11, 999, 2000, 200, 0xFFFFFFFF); + this->component.doDispatch(); + + // Should fail + ASSERT_EVENTS_DpNotFound_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_RETRANSMIT_DP, 11, Fw::CmdResponse::EXECUTION_ERROR); + + // Cleanup + this->component.shutdown(); +} + +void DpCatalogTester::test_RetransmitDp_Success_FilePriority() { + Fw::FileNameString dir; + dir = "./DpTest_RetransmitFilePrio"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + // Generate DP with TRANSMITTED state and priority 10 + this->genDP(1, 10, time1, 100, Fw::DpState::TRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + this->clearHistory(); + + // Retransmit DP using file priority (0xFFFFFFFF) + this->sendCmd_RETRANSMIT_DP(0, 11, 1, 1000, 100, 0xFFFFFFFF); + this->component.doDispatch(); + + // Should succeed with priority from file (10) + ASSERT_EVENTS_DpRetransmitted_SIZE(1); + ASSERT_EVENTS_DpRetransmitted(0, "./DpTest_RetransmitFilePrio/Dp_00000001_00001000_00000100.fdp", 10); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_RETRANSMIT_DP, 11, Fw::CmdResponse::OK); + + // Verify DP is now in catalog + DpCatalog::DpBtreeNode* node = this->component.findTreeNode(1, 1000, 100); + ASSERT_TRUE(node != nullptr); + ASSERT_EQ(node->entry.record.get_priority(), 10); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); +} + +void DpCatalogTester::test_RetransmitDp_Success_OverridePriority() { + Fw::FileNameString dir; + dir = "./DpTest_RetransmitOverride"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + // Generate DP with TRANSMITTED state and priority 10 + this->genDP(1, 10, time1, 100, Fw::DpState::TRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + this->clearHistory(); + + // Retransmit DP with overridden priority of 5 + this->sendCmd_RETRANSMIT_DP(0, 11, 1, 1000, 100, 5); + this->component.doDispatch(); + + // Should succeed with overridden priority (5) + ASSERT_EVENTS_DpRetransmitted_SIZE(1); + ASSERT_EVENTS_DpRetransmitted(0, "./DpTest_RetransmitOverride/Dp_00000001_00001000_00000100.fdp", 5); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_RETRANSMIT_DP, 11, Fw::CmdResponse::OK); + + // Verify DP is in catalog with priority 5 + DpCatalog::DpBtreeNode* node = this->component.findTreeNode(1, 1000, 100); + ASSERT_TRUE(node != nullptr); + ASSERT_EQ(node->entry.record.get_priority(), 5); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); +} + +void DpCatalogTester::test_RetransmitDp_AlreadyInCatalog() { + Fw::FileNameString dir; + dir = "./DpTest_RetransmitAlready"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + // Generate DP with UNTRANSMITTED state (still in catalog) with priority 10 + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + this->clearHistory(); + + // Retransmit DP that's already in catalog with new priority + this->sendCmd_RETRANSMIT_DP(0, 11, 1, 1000, 100, 5); + this->component.doDispatch(); + + // Should succeed and update priority + ASSERT_EVENTS_DpPriorityUpdated_SIZE(1); + ASSERT_EVENTS_DpPriorityUpdated(0, 1, 1000, 100, 10, 5); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_RETRANSMIT_DP, 11, Fw::CmdResponse::OK); + + // Verify DP priority was updated in catalog + DpCatalog::DpBtreeNode* node = this->component.findTreeNode(1, 1000, 100); + ASSERT_TRUE(node != nullptr); + ASSERT_EQ(node->entry.record.get_priority(), 5); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); +} + +void DpCatalogTester::test_RetransmitDp_AlreadyInCatalog_FilePriority() { + Fw::FileNameString dir; + dir = "./DpTest_RetransmitAlreadyFile"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + // Generate DP with UNTRANSMITTED state (still in catalog) with priority 10 + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + this->clearHistory(); + + // Retransmit DP that's already in catalog using file priority (0xFFFFFFFF) + // Since file has priority 10 and catalog has priority 10, should be no change + this->sendCmd_RETRANSMIT_DP(0, 11, 1, 1000, 100, 0xFFFFFFFF); + this->component.doDispatch(); + + // Should succeed with same priority (no actual change) + ASSERT_EVENTS_DpPriorityUpdated_SIZE(1); + ASSERT_EVENTS_DpPriorityUpdated(0, 1, 1000, 100, 10, 10); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_RETRANSMIT_DP, 11, Fw::CmdResponse::OK); + + // Verify DP priority unchanged in catalog + DpCatalog::DpBtreeNode* node = this->component.findTreeNode(1, 1000, 100); + ASSERT_TRUE(node != nullptr); + ASSERT_EQ(node->entry.record.get_priority(), 10); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); +} + +void DpCatalogTester::test_RetransmitDp_CurrentlyTransmitting() { + Fw::FileNameString dir; + dir = "./DpTest_RetransmitXmit"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + + // Generate DP with UNTRANSMITTED state + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + // Start transmission + this->sendCmd_START_XMIT_CATALOG(0, 11, Fw::Wait::NO_WAIT, false); + this->component.doDispatch(); + + this->clearHistory(); + + // Try to retransmit DP1 while it's still in the catalog (not yet transmitted or immediately after) + // Note: in test harness, transmission completes immediately, so DP is already sent + this->sendCmd_RETRANSMIT_DP(0, 12, 1, 1000, 100, 0xFFFFFFFF); + this->component.doDispatch(); + + // Drain message queue + while (this->component.m_queue.getMessagesAvailable() > 0) { + this->component.doDispatch(); + } + + // In the test harness, transmission completes immediately, so the DP has been + // transmitted and removed from catalog by the time RETRANSMIT_DP processes. + // This means it will successfully re-add the DP for retransmission. + ASSERT_EVENTS_DpRetransmitted_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_RETRANSMIT_DP, 12, Fw::CmdResponse::OK); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); +} + +void DpCatalogTester::test_RetransmitDp_AfterTransmission() { + Fw::FileNameString dir; + dir = "./DpTest_RetransmitAfter"; + this->makeDpDir(dir.toChar()); + + Fw::Time time1(1000, 100); + + // Generate DP with UNTRANSMITTED state + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::MallocAllocator alloc; + Fw::FileNameString dirs[1]; + dirs[0] = dir; + Fw::FileNameString stateFile(""); + this->component.configure(dirs, 1, stateFile, 100, alloc); + + this->sendCmd_BUILD_CATALOG(0, 10); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + + // Start and complete transmission + this->sendCmd_START_XMIT_CATALOG(0, 11, Fw::Wait::NO_WAIT, false); + this->component.doDispatch(); + + // Drain message queue to complete transmission + while (this->component.m_queue.getMessagesAvailable() > 0) { + this->component.doDispatch(); + } + + this->clearHistory(); + + // Now retransmit the DP with new priority + this->sendCmd_RETRANSMIT_DP(0, 12, 1, 1000, 100, 3); + this->component.doDispatch(); + + // Should succeed + ASSERT_EVENTS_DpRetransmitted_SIZE(1); + ASSERT_EVENTS_DpRetransmitted(0, "./DpTest_RetransmitAfter/Dp_00000001_00001000_00000100.fdp", 3); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_RETRANSMIT_DP, 12, Fw::CmdResponse::OK); + + // Verify DP is back in catalog with new priority + DpCatalog::DpBtreeNode* node = this->component.findTreeNode(1, 1000, 100); + ASSERT_TRUE(node != nullptr); + ASSERT_EQ(node->entry.record.get_priority(), 3); + + // Start transmission again to verify it gets sent + this->clearHistory(); + this->sendCmd_START_XMIT_CATALOG(0, 13, Fw::Wait::NO_WAIT, false); + this->component.doDispatch(); + + // Drain message queue + while (this->component.m_queue.getMessagesAvailable() > 0) { + this->component.doDispatch(); + } + + // Should have sent the file + ASSERT_from_fileOut_SIZE(1); + + // Cleanup + this->component.shutdown(); + this->delDp(1, time1, dir.toChar()); +} + +void DpCatalogTester::test_ProcessDpFile_InvalidFile() { + // Initialize component + Fw::MallocAllocator alloc; + Fw::FileNameString dir; + dir = "./DpTest_ProcessFile"; + Fw::FileNameString stateFile("./DpTest/dpState.dat"); + this->makeDpDir(dir.toChar()); + this->component.configure(&dir, 1, stateFile, 0, alloc); + + // Try to process nonexistent file + Fw::FileNameString opFile("./nonexistent_ops.dat"); + this->sendCmd_PROCESS_DP_FILE(0, 10, opFile); + this->component.doDispatch(); + + // Should get error event and response + ASSERT_EVENTS_DpFileOpenError_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_PROCESS_DP_FILE, 10, Fw::CmdResponse::EXECUTION_ERROR); + + // Cleanup + this->component.shutdown(); +} + +void DpCatalogTester::test_ProcessDpFile_InvalidSize() { + // Initialize component + Fw::MallocAllocator alloc; + Fw::FileNameString dir; + dir = "./DpTest_ProcessFile"; + Fw::FileNameString stateFile("./DpTest/dpState.dat"); + this->makeDpDir(dir.toChar()); + this->component.configure(&dir, 1, stateFile, 0, alloc); + + // Create file with invalid size (data portion not multiple of 17, and too small for CRC32) + Fw::FileNameString opFile; + opFile.format("%s/inv_size.dat", dir.toChar()); + Os::File file; + file.open(opFile.toChar(), Os::File::OPEN_CREATE); + U8 data[10] = {0}; // 10 bytes: too small for even CRC32 (need at least 4) + FwSizeType size = 10; + file.write(data, size); + file.close(); + + this->sendCmd_PROCESS_DP_FILE(0, 11, opFile); + this->component.doDispatch(); + + // Should get invalid size event and error response + ASSERT_EVENTS_DpFileInvalidSize_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_PROCESS_DP_FILE, 11, Fw::CmdResponse::EXECUTION_ERROR); + + // Cleanup + Os::FileSystem::removeFile(opFile.toChar()); + this->component.shutdown(); +} + +void DpCatalogTester::test_ProcessDpFile_InvalidOp() { + // Initialize component + Fw::MallocAllocator alloc; + Fw::FileNameString dir; + dir = "./DpTest_ProcessFile"; + Fw::FileNameString stateFile("./DpTest/dpState.dat"); + this->makeDpDir(dir.toChar()); + this->component.configure(&dir, 1, stateFile, 0, alloc); + + // Create file with invalid operation code but valid CRC32 + Fw::FileNameString opFile; + opFile.format("%s/inv_op.dat", dir.toChar()); + Os::File file; + file.open(opFile.toChar(), Os::File::OPEN_CREATE); + + // Pack record with invalid operation code + U8 data[17]; + packOpRecord(data, 99, 0, 0, 0, 0); // Invalid operation code 99 + + // Write record + FwSizeType size = 17; + file.write(data, size); + + // Append valid CRC32 + appendCrc32(file, data, 17); + file.close(); + + this->sendCmd_PROCESS_DP_FILE(0, 12, opFile); + this->component.doDispatch(); + + // Should get invalid op event and error response + ASSERT_EVENTS_DpFileInvalidOp_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_PROCESS_DP_FILE, 12, Fw::CmdResponse::EXECUTION_ERROR); + + // Cleanup + Os::FileSystem::removeFile(opFile.toChar()); + this->component.shutdown(); +} + +void DpCatalogTester::test_ProcessDpFile_DeleteOps() { + // Initialize component + Fw::MallocAllocator alloc; + Fw::FileNameString dir; + dir = "./DpTest_ProcessFile"; + Fw::FileNameString stateFile("./DpTest/dpState.dat"); + this->makeDpDir(dir.toChar()); + this->component.configure(&dir, 1, stateFile, 0, alloc); + + // Create 3 DPs + Fw::Time time1(1000, 100); + Fw::Time time2(2000, 200); + Fw::Time time3(3000, 300); + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + this->genDP(2, 15, time2, 150, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + this->genDP(3, 20, time3, 200, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + // Build catalog + this->sendCmd_BUILD_CATALOG(0, 0); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_BUILD_CATALOG, 0, Fw::CmdResponse::OK); + this->clearHistory(); + + // Create operations file with DELETE operations for DP 1 and 3 + Fw::FileNameString opFile; + opFile.format("%s/del.dat", dir.toChar()); + Os::File file; + file.open(opFile.toChar(), Os::File::OPEN_CREATE); + + // Pack records into buffer + U8 allData[34]; // 2 records * 17 bytes + packOpRecord(&allData[0], 1, 1, 1000, 100, 0); // DELETE DP 1 + packOpRecord(&allData[17], 1, 3, 3000, 300, 0); // DELETE DP 3 + + // Write all records + FwSizeType size = 34; + file.write(allData, size); + + // Append CRC32 + appendCrc32(file, allData, 34); + file.close(); + + // Process the file + this->sendCmd_PROCESS_DP_FILE(0, 13, opFile); + this->component.doDispatch(); + + // Should get processing started/complete events and success response + ASSERT_EVENTS_DpFileProcessingStarted_SIZE(1); + ASSERT_EVENTS_DpFileProcessingComplete_SIZE(1); + ASSERT_EVENTS_DpDeleted_SIZE(2); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_PROCESS_DP_FILE, 13, Fw::CmdResponse::OK); + + // Verify DP 1 and 3 are deleted, DP 2 still exists + Fw::FileNameString dp1File; + dp1File.format(DP_FILENAME_FORMAT, dir.toChar(), 1, 1000, 100); + Fw::FileNameString dp2File; + dp2File.format(DP_FILENAME_FORMAT, dir.toChar(), 2, 2000, 200); + Fw::FileNameString dp3File; + dp3File.format(DP_FILENAME_FORMAT, dir.toChar(), 3, 3000, 300); + + FwSizeType fileSize; + ASSERT_EQ(Os::FileSystem::getFileSize(dp1File.toChar(), fileSize), Os::FileSystem::DOESNT_EXIST); + ASSERT_EQ(Os::FileSystem::getFileSize(dp2File.toChar(), fileSize), Os::FileSystem::OP_OK); + ASSERT_EQ(Os::FileSystem::getFileSize(dp3File.toChar(), fileSize), Os::FileSystem::DOESNT_EXIST); + + // Cleanup + Os::FileSystem::removeFile(opFile.toChar()); + this->delDp(2, time2, dir.toChar()); + this->component.shutdown(); +} + +void DpCatalogTester::test_ProcessDpFile_ReprioritizeOps() { + // Initialize component + Fw::MallocAllocator alloc; + Fw::FileNameString dir; + dir = "./DpTest_ProcessFile"; + Fw::FileNameString stateFile("./DpTest/dpState.dat"); + this->makeDpDir(dir.toChar()); + this->component.configure(&dir, 1, stateFile, 0, alloc); + + // Create 2 DPs + Fw::Time time1(1000, 100); + Fw::Time time2(2000, 200); + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + this->genDP(2, 15, time2, 150, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + // Build catalog + this->sendCmd_BUILD_CATALOG(0, 0); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + this->clearHistory(); + + // Create operations file with REPRIORITIZE operations + Fw::FileNameString opFile; + opFile.format("%s/reprio.dat", dir.toChar()); + Os::File file; + file.open(opFile.toChar(), Os::File::OPEN_CREATE); + + // Pack record + U8 rec[17]; + packOpRecord(rec, 2, 1, 1000, 100, 5); // REPRIORITIZE DP 1 to priority 5 + + // Write record + FwSizeType size = 17; + file.write(rec, size); + + // Append CRC32 + appendCrc32(file, rec, 17); + file.close(); + + // Process the file + this->sendCmd_PROCESS_DP_FILE(0, 14, opFile); + this->component.doDispatch(); + + // Should get success events + ASSERT_EVENTS_DpFileProcessingStarted_SIZE(1); + ASSERT_EVENTS_DpFileProcessingComplete_SIZE(1); + ASSERT_EVENTS_DpPriorityChanged_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_PROCESS_DP_FILE, 14, Fw::CmdResponse::OK); + + // Verify priority was changed + DpCatalog::DpBtreeNode* node = this->component.findTreeNode(1, 1000, 100); + ASSERT_TRUE(node != nullptr); + ASSERT_EQ(node->entry.record.get_priority(), 5); + + // Cleanup + Os::FileSystem::removeFile(opFile.toChar()); + this->delDp(1, time1, dir.toChar()); + this->delDp(2, time2, dir.toChar()); + this->component.shutdown(); +} + +void DpCatalogTester::test_ProcessDpFile_RetransmitOps() { + // Initialize component + Fw::MallocAllocator alloc; + Fw::FileNameString dir; + dir = "./DpTest_ProcessFile"; + Fw::FileNameString stateFile("./DpTest/dpState.dat"); + this->makeDpDir(dir.toChar()); + this->component.configure(&dir, 1, stateFile, 0, alloc); + + // Create 1 DP + Fw::Time time1(1000, 100); + this->genDP(1, 10, time1, 100, Fw::DpState::TRANSMITTED, false, dir.toChar()); + + // Build catalog (won't include TRANSMITTED DP) + this->sendCmd_BUILD_CATALOG(0, 0); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + this->clearHistory(); + + // Create operations file with RETRANSMIT operation + Fw::FileNameString opFile; + opFile.format("%s/retx.dat", dir.toChar()); + Os::File file; + file.open(opFile.toChar(), Os::File::OPEN_CREATE); + + // Pack record + U8 rec[17]; + packOpRecord(rec, 3, 1, 1000, 100, 5); // RETRANSMIT DP 1 with priority 5 + + // Write record + FwSizeType size = 17; + file.write(rec, size); + + // Append CRC32 + appendCrc32(file, rec, 17); + file.close(); + + // Process the file + this->sendCmd_PROCESS_DP_FILE(0, 15, opFile); + this->component.doDispatch(); + + // Should get success events + ASSERT_EVENTS_DpFileProcessingStarted_SIZE(1); + ASSERT_EVENTS_DpFileProcessingComplete_SIZE(1); + ASSERT_EVENTS_DpRetransmitted_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_PROCESS_DP_FILE, 15, Fw::CmdResponse::OK); + + // Verify DP was added to catalog + DpCatalog::DpBtreeNode* node = this->component.findTreeNode(1, 1000, 100); + ASSERT_TRUE(node != nullptr); + ASSERT_EQ(node->entry.record.get_priority(), 5); + + // Cleanup + Os::FileSystem::removeFile(opFile.toChar()); + this->delDp(1, time1, dir.toChar()); + this->component.shutdown(); +} + +void DpCatalogTester::test_ProcessDpFile_MixedOps() { + // Initialize component + Fw::MallocAllocator alloc; + Fw::FileNameString dir; + dir = "./DpTest_ProcessFile"; + Fw::FileNameString stateFile("./DpTest/dpState.dat"); + this->makeDpDir(dir.toChar()); + this->component.configure(&dir, 1, stateFile, 0, alloc); + + // Create 4 DPs: 3 UNTRANSMITTED, 1 TRANSMITTED + Fw::Time time1(1000, 100); + Fw::Time time2(2000, 200); + Fw::Time time3(3000, 300); + Fw::Time time4(4000, 400); + this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + this->genDP(2, 15, time2, 150, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + this->genDP(3, 20, time3, 200, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + this->genDP(4, 25, time4, 250, Fw::DpState::TRANSMITTED, false, dir.toChar()); + + // Build catalog + this->sendCmd_BUILD_CATALOG(0, 0); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + this->clearHistory(); + + // Create operations file with mixed operations + Fw::FileNameString opFile; + opFile.format("%s/mixed.dat", dir.toChar()); + Os::File file; + file.open(opFile.toChar(), Os::File::OPEN_CREATE); + + // Pack all records into buffer + U8 allData[51]; // 3 records * 17 bytes + packOpRecord(&allData[0], 1, 1, 1000, 100, 0); // DELETE DP 1 + packOpRecord(&allData[17], 2, 2, 2000, 200, 5); // REPRIORITIZE DP 2 to priority 5 + packOpRecord(&allData[34], 3, 4, 4000, 400, 3); // RETRANSMIT DP 4 with priority 3 + + // Write all records + FwSizeType size = 51; + file.write(allData, size); + + // Append CRC32 + appendCrc32(file, allData, 51); + file.close(); + + // Process the file + this->sendCmd_PROCESS_DP_FILE(0, 16, opFile); + this->component.doDispatch(); + + // Should get success events + ASSERT_EVENTS_DpFileProcessingStarted_SIZE(1); + ASSERT_EVENTS_DpFileProcessingComplete_SIZE(1); + ASSERT_EVENTS_DpDeleted_SIZE(1); + ASSERT_EVENTS_DpPriorityChanged_SIZE(1); + ASSERT_EVENTS_DpRetransmitted_SIZE(1); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_PROCESS_DP_FILE, 16, Fw::CmdResponse::OK); + + // Verify results + // DP 1 should be deleted + Fw::FileNameString dp1File; + dp1File.format(DP_FILENAME_FORMAT, dir.toChar(), 1, 1000, 100); + FwSizeType fileSize; + ASSERT_EQ(Os::FileSystem::getFileSize(dp1File.toChar(), fileSize), Os::FileSystem::DOESNT_EXIST); + + // DP 2 should have new priority + DpCatalog::DpBtreeNode* node2 = this->component.findTreeNode(2, 2000, 200); + ASSERT_TRUE(node2 != nullptr); + ASSERT_EQ(node2->entry.record.get_priority(), 5); + + // DP 4 should be in catalog with priority 3 + DpCatalog::DpBtreeNode* node4 = this->component.findTreeNode(4, 4000, 400); + ASSERT_TRUE(node4 != nullptr); + ASSERT_EQ(node4->entry.record.get_priority(), 3); + + // Cleanup + Os::FileSystem::removeFile(opFile.toChar()); + this->delDp(2, time2, dir.toChar()); + this->delDp(3, time3, dir.toChar()); + this->delDp(4, time4, dir.toChar()); + this->component.shutdown(); +} + +void DpCatalogTester::test_SendCatalogDp_EmptyCatalog() { + Fw::FileNameString dir("./DpTest_SendCatalog"); + this->makeDpDir(dir.toChar()); + + Fw::FileNameString dirs[1]; + dirs[0] = dir; + + Fw::FileNameString stateFile("./DpTest_SendCatalog/dpState.dat"); + Fw::MallocAllocator alloc; + + // Initialize with no DPs + this->component.configure(dirs, 1, stateFile, 0, alloc); + + // Build empty catalog + this->sendCmd_BUILD_CATALOG(0, 0); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_BUILD_CATALOG, 0, Fw::CmdResponse::OK); + + // Send catalog DP command + this->sendCmd_SEND_CATALOG_DP(0, 1, 100); + this->component.doDispatch(); + + // Should succeed with empty container (0 entries) + ASSERT_CMD_RESPONSE_SIZE(2); + ASSERT_CMD_RESPONSE(1, DpCatalog::OPCODE_SEND_CATALOG_DP, 1, Fw::CmdResponse::OK); + + // Should have called productGet and productSend handlers + ASSERT_EQ(this->productGetHistory->size(), 1); + ASSERT_EQ(this->productSendHistory->size(), 1); + + this->component.shutdown(); +} + +void DpCatalogTester::test_SendCatalogDp_WithEntries() { + Fw::FileNameString dir("./DpTest_SendCatalog"); + this->makeDpDir(dir.toChar()); + + // Create 3 test DPs + Fw::Time time1(1000, 100); + Fw::Time time2(2000, 200); + Fw::Time time3(3000, 300); + + Fw::String dp1 = this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + Fw::String dp2 = this->genDP(2, 15, time2, 150, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + Fw::String dp3 = this->genDP(3, 5, time3, 200, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::FileNameString dirs[1]; + dirs[0] = dir; + + Fw::FileNameString stateFile("./DpTest_SendCatalog/dpState.dat"); + Fw::MallocAllocator alloc; + + // Initialize and build catalog + this->component.configure(dirs, 1, stateFile, 0, alloc); + + this->sendCmd_BUILD_CATALOG(0, 0); + this->component.doDispatch(); + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, DpCatalog::OPCODE_BUILD_CATALOG, 0, Fw::CmdResponse::OK); + + // Send catalog DP command + this->sendCmd_SEND_CATALOG_DP(0, 1, 100); + this->component.doDispatch(); + + // Should succeed + ASSERT_CMD_RESPONSE_SIZE(2); + ASSERT_CMD_RESPONSE(1, DpCatalog::OPCODE_SEND_CATALOG_DP, 1, Fw::CmdResponse::OK); + + // Should have called productGet and productSend handlers + ASSERT_EQ(this->productGetHistory->size(), 1); + ASSERT_EQ(this->productSendHistory->size(), 1); + + // Cleanup + this->delDp(1, time1, dir.toChar()); + this->delDp(2, time2, dir.toChar()); + this->delDp(3, time3, dir.toChar()); + this->component.shutdown(); +} + +void DpCatalogTester::test_SendCatalogDp_DefaultPriority() { + Fw::FileNameString dir("./DpTest_SendCatalog"); + this->makeDpDir(dir.toChar()); + + // Create 1 test DP + Fw::Time time1(1000, 100); + Fw::String dp1 = this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::FileNameString dirs[1]; + dirs[0] = dir; + + Fw::FileNameString stateFile("./DpTest_SendCatalog/dpState.dat"); + Fw::MallocAllocator alloc; + + // Initialize and build catalog + this->component.configure(dirs, 1, stateFile, 0, alloc); + + this->sendCmd_BUILD_CATALOG(0, 0); + this->component.doDispatch(); + + // Send catalog DP with 0xFFFFFFFF (use default priority) + this->sendCmd_SEND_CATALOG_DP(0, 1, 0xFFFFFFFF); + this->component.doDispatch(); + + // Should succeed + ASSERT_CMD_RESPONSE_SIZE(2); + ASSERT_CMD_RESPONSE(1, DpCatalog::OPCODE_SEND_CATALOG_DP, 1, Fw::CmdResponse::OK); + + // Should have called productGet and productSend handlers + ASSERT_EQ(this->productGetHistory->size(), 1); + ASSERT_EQ(this->productSendHistory->size(), 1); + + // Cleanup + this->delDp(1, time1, dir.toChar()); + this->component.shutdown(); +} + +void DpCatalogTester::test_SendCatalogDp_CustomPriority() { + Fw::FileNameString dir("./DpTest_SendCatalog"); + this->makeDpDir(dir.toChar()); + + // Create 1 test DP + Fw::Time time1(1000, 100); + Fw::String dp1 = this->genDP(1, 10, time1, 100, Fw::DpState::UNTRANSMITTED, false, dir.toChar()); + + Fw::FileNameString dirs[1]; + dirs[0] = dir; + + Fw::FileNameString stateFile("./DpTest_SendCatalog/dpState.dat"); + Fw::MallocAllocator alloc; + + // Initialize and build catalog + this->component.configure(dirs, 1, stateFile, 0, alloc); + + this->sendCmd_BUILD_CATALOG(0, 0); + this->component.doDispatch(); + + // Send catalog DP with custom priority 50 + this->sendCmd_SEND_CATALOG_DP(0, 1, 50); + this->component.doDispatch(); + + // Should succeed + ASSERT_CMD_RESPONSE_SIZE(2); + ASSERT_CMD_RESPONSE(1, DpCatalog::OPCODE_SEND_CATALOG_DP, 1, Fw::CmdResponse::OK); + + // Should have called productGet and productSend handlers + ASSERT_EQ(this->productGetHistory->size(), 1); + ASSERT_EQ(this->productSendHistory->size(), 1); + + // Cleanup + this->delDp(1, time1, dir.toChar()); + this->component.shutdown(); +} + } // namespace Svc diff --git a/Svc/DpCatalog/test/ut/DpCatalogTester.hpp b/Svc/DpCatalog/test/ut/DpCatalogTester.hpp index 9a0c3cacd43..4c4bcddf729 100644 --- a/Svc/DpCatalog/test/ut/DpCatalogTester.hpp +++ b/Svc/DpCatalog/test/ut/DpCatalogTester.hpp @@ -108,6 +108,17 @@ class DpCatalogTester : public DpCatalogGTestBase { const Fw::TextLogString& text //!< The event string ) override; + //! Handler implementation for productGet + Fw::Success::T productGet_handler(FwDpIdType id, //!< The container ID + FwSizeType dataSize, //!< The data size + Fw::Buffer& buffer //!< The buffer + ) override; + + //! Handler implementation for productSend + void productSend_handler(FwDpIdType id, //!< The container ID + const Fw::Buffer& buffer //!< The buffer + ) override; + private: // ---------------------------------------------------------------------- // Helper functions @@ -127,6 +138,9 @@ class DpCatalogTester : public DpCatalogGTestBase { //! The component under test DpCatalog component; + //! Buffer for data product storage + U8 m_dpBuff[10000]; + public: // ---------------------------------------------------------------------- // Moved Tests due to private/protected access @@ -149,6 +163,35 @@ class DpCatalogTester : public DpCatalogGTestBase { void test_PingIn(); void test_BadFileDone(); void test_ProcessFileInvalidDir(); + void test_DeleteDp_NotFound(); + void test_DeleteDp_Success(); + void test_DeleteDp_CurrentlyTransmitting(); + void test_DeleteDp_DuringTransmission(); + void test_DeleteDp_AlreadyTransmitted(); + void test_DeleteDp_ParentPointerIntegrity(); + void test_ChangeDpPriority_NotFound(); + void test_ChangeDpPriority_Success(); + void test_ChangeDpPriority_CurrentlyTransmitting(); + void test_ChangeDpPriority_SamePriority(); + void test_ChangeDpPriority_ReorderTree(); + void test_RetransmitDp_NotFound(); + void test_RetransmitDp_Success_FilePriority(); + void test_RetransmitDp_Success_OverridePriority(); + void test_RetransmitDp_AlreadyInCatalog(); + void test_RetransmitDp_AlreadyInCatalog_FilePriority(); + void test_RetransmitDp_CurrentlyTransmitting(); + void test_RetransmitDp_AfterTransmission(); + void test_ProcessDpFile_InvalidFile(); + void test_ProcessDpFile_InvalidSize(); + void test_ProcessDpFile_InvalidOp(); + void test_ProcessDpFile_DeleteOps(); + void test_ProcessDpFile_ReprioritizeOps(); + void test_ProcessDpFile_RetransmitOps(); + void test_ProcessDpFile_MixedOps(); + void test_SendCatalogDp_EmptyCatalog(); + void test_SendCatalogDp_WithEntries(); + void test_SendCatalogDp_DefaultPriority(); + void test_SendCatalogDp_CustomPriority(); }; } // namespace Svc diff --git a/Svc/DpCatalog/utils/README.md b/Svc/DpCatalog/utils/README.md new file mode 100644 index 00000000000..e61e74844f3 --- /dev/null +++ b/Svc/DpCatalog/utils/README.md @@ -0,0 +1,123 @@ +# DpCatalog Utilities + +This directory contains utilities for working with the DpCatalog component. + +## csv_to_dp_ops.py + +Converts CSV files to binary DP operations format for use with the `PROCESS_DP_FILE` command. + +### Usage + +```bash +./csv_to_dp_ops.py [-h] [-v] csv_file output_file +``` + +**Arguments:** +- `csv_file` - Input CSV file path +- `output_file` - Output binary file path +- `-v, --verbose` - Print verbose output +- `-h, --help` - Show help message + +### CSV Format + +The input CSV file must have the following columns: + +| Column | Description | Valid Values | +|-----------|------------------------------------------------|---------------------------------------| +| Operation | Operation type | DELETE, REPRIORITIZE, RETRANSMIT | +| ID | Data product ID | 0 to 4294967295 | +| tSec | Generation time in seconds | 0 to 4294967295 | +| tSub | Generation time in subseconds | 0 to 4294967295 | +| Priority | Priority value | 0 to 4294967295 (0xFFFFFFFF for RETRANSMIT means use file priority) | + +**Note:** Integer fields support decimal (123), hexadecimal (0x7B), and octal (0o173) notation. + +### Operation Types + +- **DELETE** - Removes the specified data product from the catalog and deletes the file from the filesystem. The Priority field is ignored. + +- **REPRIORITIZE** - Changes the priority of the specified data product in the catalog tree and state file. The Priority field specifies the new priority value. + +- **RETRANSMIT** - Re-adds a transmitted data product to the catalog for retransmission. If the data product is already pending transmission, its priority is updated. The Priority field specifies the priority (0xFFFFFFFF = 4294967295 means use the priority stored in the file). + +### Example CSV + +See [example_operations.csv](example_operations.csv) for a sample input file. + +```csv +Operation,ID,tSec,tSub,Priority +DELETE,123,1000,100,0 +REPRIORITIZE,234,2000,200,5 +RETRANSMIT,345,3000,300,10 +RETRANSMIT,456,4000,400,4294967295 +``` + +### Example Usage + +```bash +# Convert CSV to binary format +./csv_to_dp_ops.py example_operations.csv operations.dat + +# Convert with verbose output +./csv_to_dp_ops.py -v example_operations.csv operations.dat + +# Upload to spacecraft and execute command +# (This is deployment-specific) +``` + +### Binary Format + +Each record in the output file is exactly 17 bytes: + +| Offset | Size | Field | Description | +|--------|------|-----------|------------------------------------------------| +| 0 | 1 | Operation | 1=DELETE, 2=REPRIORITIZE, 3=RETRANSMIT | +| 1 | 4 | ID | Data product ID (U32, big-endian) | +| 5 | 4 | tSec | Time seconds (U32, big-endian) | +| 9 | 4 | tSub | Time subseconds (U32, big-endian) | +| 13 | 4 | Priority | Priority value (U32, big-endian) | + +After all records, a single 4-byte CRC32 checksum (U32, big-endian) is appended. The CRC32 is calculated over all record data and provides file integrity validation. + +### Error Handling + +The script validates: +- Required CSV columns are present +- Operation names are valid (case-insensitive) +- All numeric fields are valid 32-bit unsigned integers +- File exists and is readable + +If any validation fails, the script exits with an error message and non-zero exit code. + +### Testing + +To test the utility with the example file: + +```bash +# Create binary file +./csv_to_dp_ops.py -v example_operations.csv test_ops.dat + +# Verify file size (should be 72 bytes: 68 bytes data + 4 bytes CRC32) +ls -l test_ops.dat + +# View binary content (hexdump) +hexdump -C test_ops.dat +``` + +Expected output for example file: +``` +00000000 01 00 00 00 7b 00 00 03 e8 00 00 00 64 00 00 00 |....{.......d...| +00000010 00 02 00 00 00 ea 00 00 07 d0 00 00 00 c8 00 00 |................| +00000020 00 05 03 00 00 01 59 00 00 0b b8 00 00 01 2c 00 |......Y.......,.| +00000030 00 00 0a 03 00 00 01 c8 00 00 0f a0 00 00 01 90 |................| +00000040 ff ff ff ff |....| +``` + +## Integration with DpCatalog + +To use the generated binary file with DpCatalog: + +1. Generate the binary operations file using this utility +2. Upload the file to the spacecraft filesystem +3. Execute the `PROCESS_DP_FILE` command with the file path as an argument +4. DpCatalog will process each operation sequentially and emit events for each action diff --git a/Svc/DpCatalog/utils/csv_to_dp_ops.py b/Svc/DpCatalog/utils/csv_to_dp_ops.py new file mode 100755 index 00000000000..7902041900c --- /dev/null +++ b/Svc/DpCatalog/utils/csv_to_dp_ops.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +""" +Convert CSV file to binary DP operations file format. + +This script converts a CSV file containing data product operations into the +binary format expected by the DpCatalog PROCESS_DP_FILE command. + +CSV Format: + Operation,ID,tSec,tSub,Priority + +Where: + Operation: DELETE, REPRIORITIZE, or RETRANSMIT + ID: Data product ID (32-bit unsigned integer) + tSec: Generation time in seconds (32-bit unsigned integer) + tSub: Generation time in subseconds (32-bit unsigned integer) + Priority: Priority value (32-bit unsigned integer, 0xFFFFFFFF for RETRANSMIT means use file priority) + +Binary Format: + Each record is 17 bytes: + Offset | Size | Field | Description + -------|------|------------|------------- + 0 | 1 | Operation | 1=DELETE, 2=REPRIORITIZE, 3=RETRANSMIT + 1 | 4 | ID | Data product ID (U32, big-endian) + 5 | 4 | tSec | Time seconds (U32, big-endian) + 9 | 4 | tSub | Time subseconds (U32, big-endian) + 13 | 4 | Priority | Priority value (U32, big-endian) + + After all records, a single 4-byte CRC32 checksum (big-endian) covers all record data. + +Example CSV: + Operation,ID,tSec,tSub,Priority + DELETE,123,1000,100,0 + REPRIORITIZE,234,2000,200,5 + RETRANSMIT,345,3000,300,10 + RETRANSMIT,456,4000,400,4294967295 + +Author: Generated for F' DpCatalog component +""" + +import argparse +import csv +import struct +import sys +import zlib +from pathlib import Path +from typing import List, Tuple + +# Operation name to code mapping +OPERATION_MAP = {"DELETE": 1, "REPRIORITIZE": 2, "RETRANSMIT": 3} + +# Maximum value for U32 +MAX_U32 = 0xFFFFFFFF + + +class DpOperationError(Exception): + """Exception raised for errors in DP operations.""" + + pass + + +def parse_operation(op_str: str) -> int: + """ + Parse operation string and return operation code. + + Args: + op_str: Operation string (case-insensitive) + + Returns: + Operation code (1, 2, or 3) + + Raises: + DpOperationError: If operation is invalid + """ + op_upper = op_str.strip().upper() + if op_upper not in OPERATION_MAP: + raise DpOperationError( + f"Invalid operation '{op_str}'. Must be one of: {', '.join(OPERATION_MAP.keys())}" + ) + return OPERATION_MAP[op_upper] + + +def parse_u32(value_str: str, field_name: str) -> int: + """ + Parse a string as a 32-bit unsigned integer. + + Args: + value_str: String representation of the number + field_name: Name of the field (for error messages) + + Returns: + Parsed U32 value + + Raises: + DpOperationError: If value is invalid + """ + try: + value = int(value_str, 0) # Support decimal, hex (0x...), octal (0o...) + except ValueError: + raise DpOperationError( + f"Invalid {field_name} '{value_str}': must be an integer" + ) + + if value < 0 or value > MAX_U32: + raise DpOperationError( + f"Invalid {field_name} '{value}': must be between 0 and {MAX_U32}" + ) + + return value + + +def parse_csv_row(row: dict, line_num: int) -> Tuple[int, int, int, int, int]: + """ + Parse a CSV row into operation fields. + + Args: + row: Dictionary from CSV DictReader + line_num: Line number (for error messages) + + Returns: + Tuple of (op_code, id, tSec, tSub, priority) + + Raises: + DpOperationError: If row is invalid + """ + try: + op_code = parse_operation(row["Operation"]) + dp_id = parse_u32(row["ID"], "ID") + t_sec = parse_u32(row["tSec"], "tSec") + t_sub = parse_u32(row["tSub"], "tSub") + priority = parse_u32(row["Priority"], "Priority") + + return (op_code, dp_id, t_sec, t_sub, priority) + + except KeyError as e: + raise DpOperationError(f"Missing required column: {e}") + except DpOperationError as e: + raise DpOperationError(f"Line {line_num}: {e}") + + +def create_binary_record( + op_code: int, dp_id: int, t_sec: int, t_sub: int, priority: int +) -> bytes: + """ + Create a 17-byte binary record. + + Args: + op_code: Operation code (1-3) + dp_id: Data product ID + t_sec: Time seconds + t_sub: Time subseconds + priority: Priority value + + Returns: + 17-byte binary record + """ + # Format: B = unsigned char (1 byte), I = unsigned int (4 bytes, big-endian) + # '>' prefix means big-endian + return struct.pack(">BIIII", op_code, dp_id, t_sec, t_sub, priority) + + +def convert_csv_to_binary( + csv_path: Path, output_path: Path, verbose: bool = False +) -> None: + """ + Convert CSV file to binary operations file. + + Args: + csv_path: Path to input CSV file + output_path: Path to output binary file + verbose: Print verbose output + + Raises: + DpOperationError: If conversion fails + """ + if not csv_path.exists(): + raise DpOperationError(f"Input file not found: {csv_path}") + + records = [] + + # Read and parse CSV + with open(csv_path, "r", newline="") as csv_file: + reader = csv.DictReader(csv_file) + + # Validate header + required_fields = {"Operation", "ID", "tSec", "tSub", "Priority"} + if not required_fields.issubset(set(reader.fieldnames or [])): + raise DpOperationError( + f"CSV must have columns: {', '.join(sorted(required_fields))}" + ) + + # Parse each row + for line_num, row in enumerate( + reader, start=2 + ): # Start at 2 (header is line 1) + try: + op_code, dp_id, t_sec, t_sub, priority = parse_csv_row(row, line_num) + record = create_binary_record(op_code, dp_id, t_sec, t_sub, priority) + records.append(record) + + if verbose: + op_name = [k for k, v in OPERATION_MAP.items() if v == op_code][0] + print( + f"Line {line_num}: {op_name} ID={dp_id} tSec={t_sec} tSub={t_sub} Priority={priority}" + ) + + except DpOperationError as e: + raise DpOperationError(f"Error parsing CSV: {e}") + + if not records: + raise DpOperationError("No records found in CSV file") + + # Concatenate all records + all_data = b"".join(records) + + # Calculate CRC32 over all record data + # zlib.crc32 returns signed int on some platforms, mask to unsigned + crc32 = zlib.crc32(all_data) & 0xFFFFFFFF + + # Write binary file with records + CRC32 + with open(output_path, "wb") as bin_file: + bin_file.write(all_data) + bin_file.write(struct.pack(">I", crc32)) + + if verbose: + print(f"\nSuccessfully converted {len(records)} record(s) to {output_path}") + print( + f"Output file size: {len(all_data) + 4} bytes ({len(records)} records + 4-byte CRC32)" + ) + print(f"CRC32: 0x{crc32:08X}") + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Convert CSV file to binary DP operations format", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Example CSV file: + Operation,ID,tSec,tSub,Priority + DELETE,123,1000,100,0 + REPRIORITIZE,234,2000,200,5 + RETRANSMIT,345,3000,300,10 + +Valid operations: + DELETE - Remove DP from catalog and delete file + REPRIORITIZE - Change DP priority in catalog + RETRANSMIT - Re-add transmitted DP to catalog for retransmission + (Priority 4294967295 = use priority from file) + """, + ) + + parser.add_argument("csv_file", type=Path, help="Input CSV file path") + + parser.add_argument("output_file", type=Path, help="Output binary file path") + + parser.add_argument( + "-v", "--verbose", action="store_true", help="Print verbose output" + ) + + args = parser.parse_args() + + try: + convert_csv_to_binary(args.csv_file, args.output_file, args.verbose) + return 0 + + except DpOperationError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + except Exception as e: + print(f"Unexpected error: {e}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Svc/DpCatalog/utils/example_operations.csv b/Svc/DpCatalog/utils/example_operations.csv new file mode 100644 index 00000000000..efc678ec55c --- /dev/null +++ b/Svc/DpCatalog/utils/example_operations.csv @@ -0,0 +1,5 @@ +Operation,ID,tSec,tSub,Priority +DELETE,123,1000,100,0 +REPRIORITIZE,234,2000,200,5 +RETRANSMIT,345,3000,300,10 +RETRANSMIT,456,4000,400,4294967295