diff --git a/Fw/Types/Serializable.hpp b/Fw/Types/Serializable.hpp index b9f06d9d354..bce2b8b1880 100644 --- a/Fw/Types/Serializable.hpp +++ b/Fw/Types/Serializable.hpp @@ -12,14 +12,15 @@ namespace Fw { class StringBase; //!< forward declaration for string typedef enum { - FW_SERIALIZE_OK, //!< Serialization/Deserialization operation was successful - FW_SERIALIZE_FORMAT_ERROR, //!< Data was the wrong format (e.g. wrong packet type) - FW_SERIALIZE_NO_ROOM_LEFT, //!< No room left in the buffer to serialize data - FW_DESERIALIZE_BUFFER_EMPTY, //!< Deserialization buffer was empty when trying to read more data - FW_DESERIALIZE_FORMAT_ERROR, //!< Deserialization data had incorrect values (unexpected data types) - FW_DESERIALIZE_SIZE_MISMATCH, //!< Data was left in the buffer, but not enough to deserialize - FW_DESERIALIZE_TYPE_MISMATCH, //!< Deserialized type ID didn't match - FW_DESERIALIZE_IMMUTABLE, //!< Attempted to deserialize into an immutable buffer + FW_SERIALIZE_OK, //!< Serialization/Deserialization operation was successful + FW_SERIALIZE_FORMAT_ERROR, //!< Data was the wrong format (e.g. wrong packet type) + FW_SERIALIZE_NO_ROOM_LEFT, //!< No room left in the buffer to serialize data + FW_DESERIALIZE_BUFFER_EMPTY, //!< Deserialization buffer was empty when trying to read more data + FW_DESERIALIZE_FORMAT_ERROR, //!< Deserialization data had incorrect values (unexpected data types) + FW_DESERIALIZE_SIZE_MISMATCH, //!< Data was left in the buffer, but not enough to deserialize + FW_DESERIALIZE_TYPE_MISMATCH, //!< Deserialized type ID didn't match + FW_DESERIALIZE_IMMUTABLE, //!< Attempted to deserialize into an immutable buffer + FW_SERIALIZE_DISCARDED_EXISTING, //!< Serialization succeeded, but deleted old data } SerializeStatus; class SerialBufferBase; //!< forward declaration diff --git a/Svc/ComQueue/ComQueue.cpp b/Svc/ComQueue/ComQueue.cpp index 8420230140e..68f21b02695 100644 --- a/Svc/ComQueue/ComQueue.cpp +++ b/Svc/ComQueue/ComQueue.cpp @@ -25,6 +25,8 @@ ComQueue ::QueueConfigurationTable ::QueueConfigurationTable() { for (FwIndexType i = 0; i < static_cast(FW_NUM_ARRAY_ELEMENTS(this->entries)); i++) { this->entries[i].priority = 0; this->entries[i].depth = 0; + this->entries[i].mode = Types::QUEUE_FIFO; + this->entries[i].overflowMode = Types::QUEUE_DROP_NEWEST; } } @@ -39,6 +41,8 @@ ComQueue ::ComQueue(const char* const compName) for (FwIndexType i = 0; i < TOTAL_PORT_COUNT; i++) { this->m_throttle[i] = false; } + + static_assert(TOTAL_PORT_COUNT >= 1, "ComQueue must have more than one port"); } ComQueue ::~ComQueue() {} @@ -86,6 +90,8 @@ void ComQueue::configure(QueueConfigurationTable queueConfig, QueueMetadata& entry = this->m_prioritizedList[currentPriorityIndex]; entry.priority = queueConfig.entries[entryIndex].priority; entry.depth = queueConfig.entries[entryIndex].depth; + entry.mode = queueConfig.entries[entryIndex].mode; + entry.overflowMode = queueConfig.entries[entryIndex].overflowMode; entry.index = entryIndex; // Message size is determined by the type of object being stored, which in turn is determined by the // index of the entry. Those lower than COM_PORT_COUNT are Fw::ComBuffers and those larger Fw::Buffer. @@ -119,7 +125,8 @@ void ComQueue::configure(QueueConfigurationTable queueConfig, if (allocationSize > 0) { this->m_queues[this->m_prioritizedList[i].index].setup( reinterpret_cast(this->m_allocation) + allocationOffset, allocationSize, - this->m_prioritizedList[i].depth, this->m_prioritizedList[i].msgSize); + this->m_prioritizedList[i].depth, this->m_prioritizedList[i].msgSize, this->m_prioritizedList[i].mode, + this->m_prioritizedList[i].overflowMode); } allocationOffset += allocationSize; } @@ -134,8 +141,14 @@ void ComQueue::configure(QueueConfigurationTable queueConfig, void ComQueue ::FLUSH_QUEUE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, Svc::QueueType queueType, FwIndexType index) { // Acquire the queue that we need to drain - FwIndexType queueIndex = - (queueType == QueueType::COM_QUEUE) ? index : static_cast(index + COM_PORT_COUNT); + FwIndexType queueIndex = this->getQueueNum(queueType, index); + + // Validate queue index + if (queueIndex < 0 || queueIndex >= TOTAL_PORT_COUNT) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; + } + this->drainQueue(queueIndex); this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } @@ -147,6 +160,55 @@ void ComQueue ::FLUSH_ALL_QUEUES_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } +void ComQueue::SET_QUEUE_PRIORITY_cmdHandler(FwOpcodeType opCode, + U32 cmdSeq, + Svc::QueueType queueType, + FwIndexType index, + FwIndexType newPriority) { + // Acquire the queue we are to reprioritize + FwIndexType queueIndex = this->getQueueNum(queueType, index); + + // Validate queue index + if (queueIndex < 0 || queueIndex >= TOTAL_PORT_COUNT) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; + } + + // Validate priority range + if (newPriority < 0 || newPriority >= TOTAL_PORT_COUNT) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; + } + + // Find our queue in the prioritized list & update the priority + for (FwIndexType prioIndex = 0; prioIndex < TOTAL_PORT_COUNT; prioIndex++) { + // If the port based index matches, then update + if (m_prioritizedList[prioIndex].index == queueIndex) { + m_prioritizedList[prioIndex].priority = newPriority; + break; // Since we shouldn't find more than one queue at this port index + } + } + + // Re-sort the prioritized list to maintain priority ordering + // Using simple bubble sort since TOTAL_PORT_COUNT is typically small + for (FwIndexType i = 0; i < TOTAL_PORT_COUNT - 1; i++) { + for (FwIndexType j = 0; j < TOTAL_PORT_COUNT - i - 1; j++) { + if (m_prioritizedList[j].priority > m_prioritizedList[j + 1].priority) { + // Swap metadata + QueueMetadata temp = m_prioritizedList[j]; + m_prioritizedList[j] = m_prioritizedList[j + 1]; + m_prioritizedList[j + 1] = temp; + } + } + } + + // Emit event for successful priority change + this->log_ACTIVITY_HI_QueuePriorityChanged(queueType, queueIndex, newPriority); + + // Send command response + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); +} + // ---------------------------------------------------------------------- // Handler implementations for user-defined typed input ports // ---------------------------------------------------------------------- @@ -251,24 +313,23 @@ bool ComQueue::enqueue(const FwIndexType queueNum, QueueType queueType, const U8 static_cast(queueType), static_cast(queueNum)); const FwIndexType portNum = static_cast(queueNum - ((queueType == QueueType::COM_QUEUE) ? 0 : COM_PORT_COUNT)); - bool rvStatus = true; FW_ASSERT(expectedSize == size, static_cast(size), static_cast(expectedSize)); FW_ASSERT(portNum >= 0, static_cast(portNum)); Fw::SerializeStatus status = this->m_queues[queueNum].enqueue(data, size); - if (status == Fw::FW_SERIALIZE_NO_ROOM_LEFT) { + if (status == Fw::FW_SERIALIZE_NO_ROOM_LEFT || status == Fw::FW_SERIALIZE_DISCARDED_EXISTING) { if (!this->m_throttle[queueNum]) { - this->log_WARNING_HI_QueueOverflow(queueType, static_cast(portNum)); + this->log_WARNING_HI_QueueOverflow(queueType, portNum); this->m_throttle[queueNum] = true; } - - rvStatus = false; } + // When the component is already in READY state process the queue to send out the next available message immediately if (this->m_state == READY) { this->processQueue(); } - return rvStatus; + // Check if the buffer was accepted or must be returned + return status != Fw::FW_SERIALIZE_NO_ROOM_LEFT; } void ComQueue::sendComBuffer(Fw::ComBuffer& comBuffer, FwIndexType queueIndex) { @@ -385,4 +446,9 @@ void ComQueue::processQueue() { this->m_prioritizedList[priorityIndex - 1] = temp; } } + +FwIndexType ComQueue::getQueueNum(Svc::QueueType queueType, FwIndexType portNum) { + // Acquire the queue that we need to drain + return static_cast(portNum + ((queueType == QueueType::COM_QUEUE) ? 0 : COM_PORT_COUNT)); +} } // end namespace Svc diff --git a/Svc/ComQueue/ComQueue.fpp b/Svc/ComQueue/ComQueue.fpp index 5da8f0335b3..10924c1a85a 100644 --- a/Svc/ComQueue/ComQueue.fpp +++ b/Svc/ComQueue/ComQueue.fpp @@ -36,30 +36,10 @@ module Svc { @ Port for scheduling telemetry output async input port run: Svc.Sched drop - @ Flush a specific queue. This will discard all queued data in the specified queue removing it from eventual - @ downlink. Buffers requiring ownership return will be returned via the bufferReturnOut port. - async command FLUSH_QUEUE(queueType: QueueType @< The Queue data type - indexType: FwIndexType @< The index of the queue (within the supplied type) to flush - ) - @ Flush all queues. This will discard all queued data removing it from eventual downlink. Buffers requiring - @ ownership return will be returned via the bufferReturnOut port. - async command FLUSH_ALL_QUEUES() # ---------------------------------------------------------------------- # Special ports # ---------------------------------------------------------------------- - @ Port for emitting events - event port Log - - @ Port for emitting text events - text event port LogText - - @ Port for getting the time - time get port Time - - @ Port for emitting telemetry - telemetry port Tlm - @ Command receive port command recv port CmdDisp @@ -69,26 +49,20 @@ module Svc { @ Command response port command resp port CmdStatus - # ---------------------------------------------------------------------- - # Events - # ---------------------------------------------------------------------- + @ Port for emitting events + event port Log - @ Queue overflow event - event QueueOverflow( - queueType: QueueType @< The Queue data type - index: U32 @< index of overflowed queue - ) \ - severity warning high \ - format "The {} queue at index {} overflowed" + @ Port for emitting text events + text event port LogText - # ---------------------------------------------------------------------- - # Telemetry - # ---------------------------------------------------------------------- + @ Port for getting the time + time get port Time - @ Depth of queues of Fw::ComBuffer type - telemetry comQueueDepth: ComQueueDepth id 0 + @ Port for emitting telemetry + telemetry port Tlm - @ Depth of queues of Fw::Buffer type - telemetry buffQueueDepth: BuffQueueDepth id 1 + include "ComQueueCommands.fppi" + include "ComQueueEvents.fppi" + include "ComQueueTelemetry.fppi" } } diff --git a/Svc/ComQueue/ComQueue.hpp b/Svc/ComQueue/ComQueue.hpp index 0d45957cb51..875ed3aee81 100644 --- a/Svc/ComQueue/ComQueue.hpp +++ b/Svc/ComQueue/ComQueue.hpp @@ -22,6 +22,9 @@ namespace Svc { // ---------------------------------------------------------------------- class ComQueue final : public ComQueueComponentBase { + // Added to enable easy testing of set queue priority command + friend class ComQueueTester; + //! State of the currently transmitted buffer enum BufferState { OWNED, UNOWNED }; @@ -47,10 +50,16 @@ class ComQueue final : public ComQueueComponentBase { * Priority is an integer between 0 (inclusive) and TOTAL_PORT_COUNT (exclusive). Queues with lower priority values * will be serviced first. Priorities may be repeated and queues sharing priorities will be serviced in a balanced * manner. + * + * Queue mode determines whether messages are dequeued in FIFO or LIFO order. + * + * Overflow mode determines whether the newest or oldest message is dropped when the queue is full. */ struct QueueConfigurationEntry { - FwSizeType depth; //!< Depth of the queue [0, infinity) - FwIndexType priority; //!< Priority of the queue [0, TOTAL_PORT_COUNT) + FwSizeType depth; //!< Depth of the queue [0, infinity) + FwIndexType priority; //!< Priority of the queue [0, TOTAL_PORT_COUNT) + Types::QueueMode mode; //!< Queue mode (FIFO or LIFO) + Types::QueueOverflowMode overflowMode; //!< Overflow handling mode (DROP_NEWEST or DROP_OLDEST) }; /** @@ -81,10 +90,12 @@ class ComQueue final : public ComQueueComponentBase { * method. Index and message size are calculated by the configuration call. */ struct QueueMetadata { - FwSizeType depth; //!< Depth of the queue in messages - FwIndexType priority; //!< Priority of the queue - FwIndexType index; //!< Index of this queue in the prioritized list - FwSizeType msgSize; //!< Message size of messages in this queue + FwSizeType depth; //!< Depth of the queue in messages + FwIndexType priority; //!< Priority of the queue + Types::QueueMode mode; //!< Queue mode (FIFO or LIFO) + Types::QueueOverflowMode overflowMode; //!< Overflow handling mode + FwIndexType index; //!< Index of this queue in m_queues + FwSizeType msgSize; //!< Message size of messages in this queue }; /** @@ -145,6 +156,16 @@ class ComQueue final : public ComQueueComponentBase { U32 cmdSeq //!< The command sequence number ) override; + //! Handler for SET_QUEUE_PRIORITY command + //! + void SET_QUEUE_PRIORITY_cmdHandler( + FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + Svc::QueueType queueType, //!< The Queue data type + FwIndexType indexType, //!< The index of the queue (within the supplied type) to modify + FwIndexType newPriority //!< New priority value for the queue + ) override; + private: // ---------------------------------------------------------------------- // Handler implementations for user-defined typed input ports @@ -222,6 +243,9 @@ class ComQueue final : public ComQueueComponentBase { //! void processQueue(); + //! Convert Queue Type & Index into single queueIndex + FwIndexType getQueueNum(Svc::QueueType queueType, FwIndexType portNum); + private: // ---------------------------------------------------------------------- // Member variables diff --git a/Svc/ComQueue/ComQueueCommands.fppi b/Svc/ComQueue/ComQueueCommands.fppi new file mode 100644 index 00000000000..921051aece4 --- /dev/null +++ b/Svc/ComQueue/ComQueueCommands.fppi @@ -0,0 +1,17 @@ +@ Flush a specific queue. This will discard all queued data in the specified queue removing it from eventual +@ downlink. Buffers requiring ownership return will be returned via the bufferReturnOut port. +async command FLUSH_QUEUE(queueType: QueueType @< The Queue data type + indexType: FwIndexType @< The index of the queue (within the supplied type) to flush + ) + +@ Flush all queues. This will discard all queued data removing it from eventual downlink. Buffers requiring +@ ownership return will be returned via the bufferReturnOut port. +async command FLUSH_ALL_QUEUES() + +@ Set the priority of a specific queue at runtime +async command SET_QUEUE_PRIORITY( + queueType: QueueType @< The Queue data type + indexType: FwIndexType @< The index of the queue (within the supplied type) to modify + newPriority: FwIndexType @< New priority value for the queue + ) + diff --git a/Svc/ComQueue/ComQueueEvents.fppi b/Svc/ComQueue/ComQueueEvents.fppi new file mode 100644 index 00000000000..643d257c428 --- /dev/null +++ b/Svc/ComQueue/ComQueueEvents.fppi @@ -0,0 +1,16 @@ +@ Queue overflow event +event QueueOverflow( + queueType: QueueType @< The Queue data type + index: FwIndexType @< index of overflowed queue +) \ +severity warning high \ +format "The {} queue at index {} overflowed" + +@ Queue priority changed event +event QueuePriorityChanged( + queueType: QueueType @< The Queue data type + indexType: FwIndexType @< The index of the queue (within the supplied type) that was modified + newPriority: FwIndexType @< New priority value +) \ +severity activity high \ +format "{} {} priority changed to {}" \ No newline at end of file diff --git a/Svc/ComQueue/ComQueueTelemetry.fppi b/Svc/ComQueue/ComQueueTelemetry.fppi new file mode 100644 index 00000000000..d3266d86b8e --- /dev/null +++ b/Svc/ComQueue/ComQueueTelemetry.fppi @@ -0,0 +1,5 @@ +@ Depth of queues of Fw::ComBuffer type +telemetry comQueueDepth: ComQueueDepth id 0 + +@ Depth of queues of Fw::Buffer type +telemetry buffQueueDepth: BuffQueueDepth id 1 \ No newline at end of file diff --git a/Svc/ComQueue/docs/sdd.md b/Svc/ComQueue/docs/sdd.md index 34a4584d4bf..a2a775326ba 100644 --- a/Svc/ComQueue/docs/sdd.md +++ b/Svc/ComQueue/docs/sdd.md @@ -136,16 +136,18 @@ The `run` port handler does the following: ### 4.7 Events -| Name | Description | -|----------------|---------------------------------------------------------------------------------| -| QueueOverflow | WARNING_HI event triggered when a queue can no longer hold the incoming message | +| Name | Description | +|-----------------------|--------------------------------------------------------------------| +| QueueOverflow | WARNING_HI event triggered when a queue discards data | +| QueuePriorityChanged | ACTIVITY_HI event triggered when a user changes a queue's priority | ### 4.8 Commands -| Name | Description | -|------------------|-------------------------------------------------------------------------------------------------| -| FLUSH_QUEUE | Flushes all queued items from the specified queue type and index, returning ownership of any buffers. | -| FLUSH_ALL_QUEUES | Flushes all queued items from all queues, returning ownership of any buffers. | +| Name | Description | +|--------------------|-------------------------------------------------------------------------------------------------------| +| FLUSH_QUEUE | Flushes all queued items from the specified queue type and index, returning ownership of any buffers. | +| FLUSH_ALL_QUEUES | Flushes all queued items from all queues, returning ownership of any buffers. | +| SET_QUEUE_PRIORITY | Changes a queue's priority and re-sorts all queues | ### 4.9 Helper Functions @@ -157,13 +159,25 @@ Stores the com buffer message, sends the com buffer message on the output port, Stores the buffer message, sends the buffer message on the output port, and then sets the send state to waiting. #### 4.9.3 processQueue -In a bounded loop that is constrained by the total size of the queue that contains both +In a bounded loop that is constrained by the total size of the queue that contains both buffer and com buffer data, do: - 1. Check if there are any items on the queue, and continue with the loop if there are none. + 1. Check if there are any items on the queue, and continue with the loop if there are none. 2. Store the entry point of the queue based on the index of the array that contains the prioritized data. 3. Compare the entry index with the value of the size of the queue that contains com buffer data. 1. If it is less than the size value, then invoke the sendComBuffer function. - 2. If it is greater than the size value, then invoke the sendBuffer function. + 2. If it is greater than the size value, then invoke the sendBuffer function. 4. Break out of the loop, but enter a new loop that starts at the next entry and linearly swap the remaining items in the prioritized list. + +#### 4.9.4 enqueue + +Attempts to enqueue the buffer onto the queue index, logs a (throttled) warning if data is discarded, and immediately processes the queue if state is `READY`. + +#### 4.9.5 drainQueue + +Pops all messages out of the queue at queueIndex, `index`. + +#### 4.9.6 getQueueNum + +Converts a `queueType` & `portNum` into an index into the `m_queues` array--translates between user facing index system & internal one. diff --git a/Svc/ComQueue/test/ut/ComQueueTestMain.cpp b/Svc/ComQueue/test/ut/ComQueueTestMain.cpp index 6a8eb2231d0..a9d1189b2b1 100644 --- a/Svc/ComQueue/test/ut/ComQueueTestMain.cpp +++ b/Svc/ComQueue/test/ut/ComQueueTestMain.cpp @@ -49,6 +49,71 @@ TEST(Nominal, ContextData) { tester.testContextData(); } +TEST(QueueModes, FIFOMode) { + Svc::ComQueueTester tester; + tester.testFIFOMode(); +} + +TEST(QueueModes, LIFOMode) { + Svc::ComQueueTester tester; + tester.testLIFOMode(); +} + +TEST(OverflowModes, DropNewest) { + Svc::ComQueueTester tester; + tester.testDropNewestMode(); +} + +TEST(OverflowModes, DropOldest) { + Svc::ComQueueTester tester; + tester.testDropOldestMode(); +} + +TEST(CombinedModes, LIFOWithDropOldest) { + Svc::ComQueueTester tester; + tester.testLIFOWithDropOldest(); +} + +TEST(BufferQueueModes, FIFOMode) { + Svc::ComQueueTester tester; + tester.testBufferQueueFIFOMode(); +} + +TEST(BufferQueueModes, LIFOMode) { + Svc::ComQueueTester tester; + tester.testBufferQueueLIFOMode(); +} + +TEST(BufferQueueOverflowModes, DropOldest) { + Svc::ComQueueTester tester; + tester.testBufferQueueDropOldestMode(); +} + +TEST(Commands, SetQueuePriority) { + Svc::ComQueueTester tester; + tester.testSetQueuePriorityCommand(); +} + +TEST(Commands, SetQueuePriorityInvalidIndex) { + Svc::ComQueueTester tester; + tester.testSetQueuePriorityInvalidIndex(); +} + +TEST(Commands, SetQueuePriorityNegativeIndex) { + Svc::ComQueueTester tester; + tester.testSetQueuePriorityNegativeIndex(); +} + +TEST(Commands, SetQueuePriorityInvalidPriority) { + Svc::ComQueueTester tester; + tester.testSetQueuePriorityInvalidPriority(); +} + +TEST(Commands, SetQueuePriorityNegativePriority) { + Svc::ComQueueTester tester; + tester.testSetQueuePriorityNegativePriority(); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/Svc/ComQueue/test/ut/ComQueueTester.cpp b/Svc/ComQueue/test/ut/ComQueueTester.cpp index c6e102d7147..c44c7e2f1c2 100644 --- a/Svc/ComQueue/test/ut/ComQueueTester.cpp +++ b/Svc/ComQueue/test/ut/ComQueueTester.cpp @@ -416,4 +416,534 @@ void ComQueueTester ::from_dataOut_handler(FwIndexType portNum, Fw::Buffer& data this->invoke_to_dataReturnIn(0, data, context); } +// ---------------------------------------------------------------------- +// Queue Mode and Overflow Mode Tests +// ---------------------------------------------------------------------- + +void ComQueueTester ::testFIFOMode() { + // Test that FIFO mode dequeues in the correct order + ComQueue::QueueConfigurationTable configurationTable; + + // Configure first COM queue as FIFO (default) with sufficient depth + configurationTable.entries[0].priority = 0; + configurationTable.entries[0].depth = 5; + configurationTable.entries[0].mode = Types::QUEUE_FIFO; + configurationTable.entries[0].overflowMode = Types::QUEUE_DROP_NEWEST; + + // Configure other queues with minimal settings + for (FwIndexType i = 1; i < ComQueue::TOTAL_PORT_COUNT; i++) { + configurationTable.entries[i].priority = i; + configurationTable.entries[i].depth = 1; + } + + component.configure(configurationTable, 0, mallocAllocator); + + // Create unique buffers for each message + U8 data1[BUFFER_LENGTH] = BUFFER_DATA; + U8 data2[BUFFER_LENGTH] = BUFFER_DATA; + U8 data3[BUFFER_LENGTH] = BUFFER_DATA; + data1[BUFFER_DATA_OFFSET] = 1; + data2[BUFFER_DATA_OFFSET] = 2; + data3[BUFFER_DATA_OFFSET] = 3; + + Fw::ComBuffer comBuffer1(&data1[0], sizeof(data1)); + Fw::ComBuffer comBuffer2(&data2[0], sizeof(data2)); + Fw::ComBuffer comBuffer3(&data3[0], sizeof(data3)); + + // Enqueue three messages + invoke_to_comPacketQueueIn(0, comBuffer1, 0); + invoke_to_comPacketQueueIn(0, comBuffer2, 0); + invoke_to_comPacketQueueIn(0, comBuffer3, 0); + dispatchAll(); + + // Dequeue and verify FIFO order: 1, 2, 3 + emitOneAndCheck(0, data1, BUFFER_LENGTH); + emitOneAndCheck(1, data2, BUFFER_LENGTH); + emitOneAndCheck(2, data3, BUFFER_LENGTH); + + component.cleanup(); +} + +void ComQueueTester ::testLIFOMode() { + // Test that LIFO mode dequeues in reverse order + ComQueue::QueueConfigurationTable configurationTable; + + // Configure first COM queue as LIFO with sufficient depth + configurationTable.entries[0].priority = 0; + configurationTable.entries[0].depth = 5; + configurationTable.entries[0].mode = Types::QUEUE_LIFO; + configurationTable.entries[0].overflowMode = Types::QUEUE_DROP_NEWEST; + + // Configure other queues with minimal settings + for (FwIndexType i = 1; i < ComQueue::TOTAL_PORT_COUNT; i++) { + configurationTable.entries[i].priority = i; + configurationTable.entries[i].depth = 1; + } + + component.configure(configurationTable, 0, mallocAllocator); + + // Create unique buffers for each message + U8 data1[BUFFER_LENGTH] = BUFFER_DATA; + U8 data2[BUFFER_LENGTH] = BUFFER_DATA; + U8 data3[BUFFER_LENGTH] = BUFFER_DATA; + data1[BUFFER_DATA_OFFSET] = 1; + data2[BUFFER_DATA_OFFSET] = 2; + data3[BUFFER_DATA_OFFSET] = 3; + + Fw::ComBuffer comBuffer1(&data1[0], sizeof(data1)); + Fw::ComBuffer comBuffer2(&data2[0], sizeof(data2)); + Fw::ComBuffer comBuffer3(&data3[0], sizeof(data3)); + + // Enqueue three messages + invoke_to_comPacketQueueIn(0, comBuffer1, 0); + invoke_to_comPacketQueueIn(0, comBuffer2, 0); + invoke_to_comPacketQueueIn(0, comBuffer3, 0); + dispatchAll(); + + // Dequeue and verify LIFO order: 3, 2, 1 (newest first) + emitOneAndCheck(0, data3, BUFFER_LENGTH); + emitOneAndCheck(1, data2, BUFFER_LENGTH); + emitOneAndCheck(2, data1, BUFFER_LENGTH); + + component.cleanup(); +} + +void ComQueueTester ::testDropNewestMode() { + // Test that DROP_NEWEST rejects new messages when queue is full + ComQueue::QueueConfigurationTable configurationTable; + + // Configure first COM queue with DROP_NEWEST and small depth + configurationTable.entries[0].priority = 0; + configurationTable.entries[0].depth = 2; + configurationTable.entries[0].mode = Types::QUEUE_FIFO; + configurationTable.entries[0].overflowMode = Types::QUEUE_DROP_NEWEST; + + // Configure other queues with minimal settings + for (FwIndexType i = 1; i < ComQueue::TOTAL_PORT_COUNT; i++) { + configurationTable.entries[i].priority = i; + configurationTable.entries[i].depth = 1; + } + + component.configure(configurationTable, 0, mallocAllocator); + + // Create unique buffers + U8 data1[BUFFER_LENGTH] = BUFFER_DATA; + U8 data2[BUFFER_LENGTH] = BUFFER_DATA; + U8 data3[BUFFER_LENGTH] = BUFFER_DATA; + data1[BUFFER_DATA_OFFSET] = 1; + data2[BUFFER_DATA_OFFSET] = 2; + data3[BUFFER_DATA_OFFSET] = 3; + + Fw::ComBuffer comBuffer1(&data1[0], sizeof(data1)); + Fw::ComBuffer comBuffer2(&data2[0], sizeof(data2)); + Fw::ComBuffer comBuffer3(&data3[0], sizeof(data3)); + + // Fill the queue (depth = 2) + invoke_to_comPacketQueueIn(0, comBuffer1, 0); + invoke_to_comPacketQueueIn(0, comBuffer2, 0); + dispatchAll(); + + // Try to enqueue when full - should cause overflow event + invoke_to_comPacketQueueIn(0, comBuffer3, 0); + dispatchAll(); + + // Verify overflow event was emitted + ASSERT_EVENTS_QueueOverflow_SIZE(1); + ASSERT_EVENTS_QueueOverflow(0, QueueType::COM_QUEUE, 0); + + // Dequeue and verify original messages are intact (newest was dropped) + emitOneAndCheck(0, data1, BUFFER_LENGTH); + emitOneAndCheck(1, data2, BUFFER_LENGTH); + + // Verify queue is now empty (no third message) + ASSERT_from_dataOut_SIZE(2); + + component.cleanup(); +} + +void ComQueueTester ::testDropOldestMode() { + // Test that DROP_OLDEST removes oldest message when queue is full + ComQueue::QueueConfigurationTable configurationTable; + + // Configure first COM queue with DROP_OLDEST and small depth + configurationTable.entries[0].priority = 0; + configurationTable.entries[0].depth = 2; + configurationTable.entries[0].mode = Types::QUEUE_FIFO; + configurationTable.entries[0].overflowMode = Types::QUEUE_DROP_OLDEST; + + // Configure other queues with minimal settings + for (FwIndexType i = 1; i < ComQueue::TOTAL_PORT_COUNT; i++) { + configurationTable.entries[i].priority = i; + configurationTable.entries[i].depth = 1; + } + + component.configure(configurationTable, 0, mallocAllocator); + + // Create unique buffers + U8 data1[BUFFER_LENGTH] = BUFFER_DATA; + U8 data2[BUFFER_LENGTH] = BUFFER_DATA; + U8 data3[BUFFER_LENGTH] = BUFFER_DATA; + data1[BUFFER_DATA_OFFSET] = 1; + data2[BUFFER_DATA_OFFSET] = 2; + data3[BUFFER_DATA_OFFSET] = 3; + + Fw::ComBuffer comBuffer1(&data1[0], sizeof(data1)); + Fw::ComBuffer comBuffer2(&data2[0], sizeof(data2)); + Fw::ComBuffer comBuffer3(&data3[0], sizeof(data3)); + + // Fill the queue (depth = 2) + invoke_to_comPacketQueueIn(0, comBuffer1, 0); + invoke_to_comPacketQueueIn(0, comBuffer2, 0); + dispatchAll(); + + // Enqueue when full - should succeed by dropping oldest (message 1) + invoke_to_comPacketQueueIn(0, comBuffer3, 0); + dispatchAll(); + + // Verify overflow event was emitted + ASSERT_EVENTS_QueueOverflow_SIZE(1); + ASSERT_EVENTS_QueueOverflow(0, QueueType::COM_QUEUE, 0); + + // Dequeue and verify we have messages 2 and 3 (oldest was dropped) + emitOneAndCheck(0, data2, BUFFER_LENGTH); + emitOneAndCheck(1, data3, BUFFER_LENGTH); + + // Verify we only have 2 messages + ASSERT_from_dataOut_SIZE(2); + + component.cleanup(); +} + +void ComQueueTester ::testLIFOWithDropOldest() { + // Test combination of LIFO mode with DROP_OLDEST overflow behavior + ComQueue::QueueConfigurationTable configurationTable; + + // Configure first COM queue as LIFO with DROP_OLDEST + configurationTable.entries[0].priority = 0; + configurationTable.entries[0].depth = 2; + configurationTable.entries[0].mode = Types::QUEUE_LIFO; + configurationTable.entries[0].overflowMode = Types::QUEUE_DROP_OLDEST; + + // Configure other queues with minimal settings + for (FwIndexType i = 1; i < ComQueue::TOTAL_PORT_COUNT; i++) { + configurationTable.entries[i].priority = i; + configurationTable.entries[i].depth = 1; + } + + component.configure(configurationTable, 0, mallocAllocator); + + // Create unique buffers + U8 data1[BUFFER_LENGTH] = BUFFER_DATA; + U8 data2[BUFFER_LENGTH] = BUFFER_DATA; + U8 data3[BUFFER_LENGTH] = BUFFER_DATA; + data1[BUFFER_DATA_OFFSET] = 1; + data2[BUFFER_DATA_OFFSET] = 2; + data3[BUFFER_DATA_OFFSET] = 3; + + Fw::ComBuffer comBuffer1(&data1[0], sizeof(data1)); + Fw::ComBuffer comBuffer2(&data2[0], sizeof(data2)); + Fw::ComBuffer comBuffer3(&data3[0], sizeof(data3)); + + // Fill the queue (depth = 2) + invoke_to_comPacketQueueIn(0, comBuffer1, 0); + invoke_to_comPacketQueueIn(0, comBuffer2, 0); + dispatchAll(); + + // Enqueue when full - should drop oldest (message 1) + invoke_to_comPacketQueueIn(0, comBuffer3, 0); + dispatchAll(); + + // Verify overflow event was emitted + ASSERT_EVENTS_QueueOverflow_SIZE(1); + ASSERT_EVENTS_QueueOverflow(0, QueueType::COM_QUEUE, 0); + + // Dequeue in LIFO order from remaining messages: 3, 2 + emitOneAndCheck(0, data3, BUFFER_LENGTH); + emitOneAndCheck(1, data2, BUFFER_LENGTH); + + // Verify we only have 2 messages + ASSERT_from_dataOut_SIZE(2); + + component.cleanup(); +} + +void ComQueueTester ::testBufferQueueFIFOMode() { + // Test FIFO mode with Fw::Buffer queues + ComQueue::QueueConfigurationTable configurationTable; + + // Configure all COM queues with priority 1, minimal depth + for (FwIndexType i = 0; i < ComQueue::COM_PORT_COUNT; i++) { + configurationTable.entries[i].priority = 1; + configurationTable.entries[i].depth = 1; + } + + // Configure first BUFFER queue as FIFO with higher priority (0) + configurationTable.entries[ComQueue::COM_PORT_COUNT].priority = 0; + configurationTable.entries[ComQueue::COM_PORT_COUNT].depth = 5; + configurationTable.entries[ComQueue::COM_PORT_COUNT].mode = Types::QUEUE_FIFO; + configurationTable.entries[ComQueue::COM_PORT_COUNT].overflowMode = Types::QUEUE_DROP_NEWEST; + + // Configure remaining buffer queues + for (FwIndexType i = ComQueue::COM_PORT_COUNT + 1; i < ComQueue::TOTAL_PORT_COUNT; i++) { + configurationTable.entries[i].priority = i; + configurationTable.entries[i].depth = 1; + } + + component.configure(configurationTable, 0, mallocAllocator); + + // Create unique buffers for each message + U8 data1[BUFFER_LENGTH] = BUFFER_DATA; + U8 data2[BUFFER_LENGTH] = BUFFER_DATA; + U8 data3[BUFFER_LENGTH] = BUFFER_DATA; + data1[BUFFER_DATA_OFFSET] = 1; + data2[BUFFER_DATA_OFFSET] = 2; + data3[BUFFER_DATA_OFFSET] = 3; + + Fw::Buffer buffer1(&data1[0], sizeof(data1)); + Fw::Buffer buffer2(&data2[0], sizeof(data2)); + Fw::Buffer buffer3(&data3[0], sizeof(data3)); + + // Enqueue three buffer messages + invoke_to_bufferQueueIn(0, buffer1); + invoke_to_bufferQueueIn(0, buffer2); + invoke_to_bufferQueueIn(0, buffer3); + dispatchAll(); + + // Dequeue and verify FIFO order: 1, 2, 3 + emitOneAndCheck(0, data1, BUFFER_LENGTH); + emitOneAndCheck(1, data2, BUFFER_LENGTH); + emitOneAndCheck(2, data3, BUFFER_LENGTH); + + // Verify buffers were returned + ASSERT_from_bufferReturnOut_SIZE(3); + + component.cleanup(); +} + +void ComQueueTester ::testBufferQueueLIFOMode() { + // Test LIFO mode with Fw::Buffer queues + ComQueue::QueueConfigurationTable configurationTable; + + // Configure all COM queues with priority 1, minimal depth + for (FwIndexType i = 0; i < ComQueue::COM_PORT_COUNT; i++) { + configurationTable.entries[i].priority = 1; + configurationTable.entries[i].depth = 1; + } + + // Configure first BUFFER queue as LIFO with higher priority (0) + configurationTable.entries[ComQueue::COM_PORT_COUNT].priority = 0; + configurationTable.entries[ComQueue::COM_PORT_COUNT].depth = 5; + configurationTable.entries[ComQueue::COM_PORT_COUNT].mode = Types::QUEUE_LIFO; + configurationTable.entries[ComQueue::COM_PORT_COUNT].overflowMode = Types::QUEUE_DROP_NEWEST; + + // Configure remaining buffer queues + for (FwIndexType i = ComQueue::COM_PORT_COUNT + 1; i < ComQueue::TOTAL_PORT_COUNT; i++) { + configurationTable.entries[i].priority = i; + configurationTable.entries[i].depth = 1; + } + + component.configure(configurationTable, 0, mallocAllocator); + + // Create unique buffers for each message + U8 data1[BUFFER_LENGTH] = BUFFER_DATA; + U8 data2[BUFFER_LENGTH] = BUFFER_DATA; + U8 data3[BUFFER_LENGTH] = BUFFER_DATA; + data1[BUFFER_DATA_OFFSET] = 1; + data2[BUFFER_DATA_OFFSET] = 2; + data3[BUFFER_DATA_OFFSET] = 3; + + Fw::Buffer buffer1(&data1[0], sizeof(data1)); + Fw::Buffer buffer2(&data2[0], sizeof(data2)); + Fw::Buffer buffer3(&data3[0], sizeof(data3)); + + // Enqueue three buffer messages + invoke_to_bufferQueueIn(0, buffer1); + invoke_to_bufferQueueIn(0, buffer2); + invoke_to_bufferQueueIn(0, buffer3); + dispatchAll(); + + // Dequeue and verify LIFO order: 3, 2, 1 + emitOneAndCheck(0, data3, BUFFER_LENGTH); + emitOneAndCheck(1, data2, BUFFER_LENGTH); + emitOneAndCheck(2, data1, BUFFER_LENGTH); + + // Verify buffers were returned + ASSERT_from_bufferReturnOut_SIZE(3); + + component.cleanup(); +} + +void ComQueueTester ::testBufferQueueDropOldestMode() { + // Test DROP_OLDEST mode with Fw::Buffer queues + ComQueue::QueueConfigurationTable configurationTable; + + // Configure all COM queues with priority 1, minimal depth + for (FwIndexType i = 0; i < ComQueue::COM_PORT_COUNT; i++) { + configurationTable.entries[i].priority = 1; + configurationTable.entries[i].depth = 1; + } + + // Configure first BUFFER queue with DROP_OLDEST + configurationTable.entries[ComQueue::COM_PORT_COUNT].priority = 0; + configurationTable.entries[ComQueue::COM_PORT_COUNT].depth = 2; + configurationTable.entries[ComQueue::COM_PORT_COUNT].mode = Types::QUEUE_FIFO; + configurationTable.entries[ComQueue::COM_PORT_COUNT].overflowMode = Types::QUEUE_DROP_OLDEST; + + // Configure remaining buffer queues + for (FwIndexType i = ComQueue::COM_PORT_COUNT + 1; i < ComQueue::TOTAL_PORT_COUNT; i++) { + configurationTable.entries[i].priority = i; + configurationTable.entries[i].depth = 1; + } + + component.configure(configurationTable, 0, mallocAllocator); + + // Create unique buffers + U8 data1[BUFFER_LENGTH] = BUFFER_DATA; + U8 data2[BUFFER_LENGTH] = BUFFER_DATA; + U8 data3[BUFFER_LENGTH] = BUFFER_DATA; + data1[BUFFER_DATA_OFFSET] = 1; + data2[BUFFER_DATA_OFFSET] = 2; + data3[BUFFER_DATA_OFFSET] = 3; + + Fw::Buffer buffer1(&data1[0], sizeof(data1)); + Fw::Buffer buffer2(&data2[0], sizeof(data2)); + Fw::Buffer buffer3(&data3[0], sizeof(data3)); + + // Fill the buffer queue (depth = 2) + invoke_to_bufferQueueIn(0, buffer1); + invoke_to_bufferQueueIn(0, buffer2); + dispatchAll(); + + // Enqueue when full - should drop oldest (buffer1) + invoke_to_bufferQueueIn(0, buffer3); + dispatchAll(); + + // Verify overflow event was emitted + ASSERT_EVENTS_QueueOverflow_SIZE(1); + ASSERT_EVENTS_QueueOverflow(0, QueueType::BUFFER_QUEUE, 0); + + // Dequeue and verify we have buffers 2 and 3 + emitOneAndCheck(0, data2, BUFFER_LENGTH); + emitOneAndCheck(1, data3, BUFFER_LENGTH); + + // Verify we only have 2 messages + ASSERT_from_dataOut_SIZE(2); + + // Verify buffers were returned (2 sent + dropped buffer1) + // Note: When DROP_OLDEST happens, the dropped buffer should NOT be returned + // since it was consumed by the queue + ASSERT_from_bufferReturnOut_SIZE(2); + + component.cleanup(); +} + +void ComQueueTester::testSetQueuePriorityCommand() { + // Configure the component + configure(); + + for (FwIndexType queueIndex = 0; queueIndex < 3; queueIndex++) { + ASSERT_EQ(queueIndex, this->component.m_prioritizedList[queueIndex].index); + } + + // Send the SET_QUEUE_PRIORITY command for queue 0, setting priority to 2 + // Priority must be < TOTAL_PORT_COUNT, so we use a small value that's guaranteed to be valid + const FwIndexType newPriority = 2; + this->sendCmd_SET_QUEUE_PRIORITY(0, 0, Svc::QueueType::COM_QUEUE, 0, newPriority); + this->component.doDispatch(); + + // Verify command response was OK + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, ComQueue::OPCODE_SET_QUEUE_PRIORITY, 0, Fw::CmdResponse::OK); + + // Verify the event was emitted + ASSERT_EVENTS_QueuePriorityChanged_SIZE(1); + ASSERT_EVENTS_QueuePriorityChanged(0, Svc::QueueType::COM_QUEUE, 0, newPriority); + + // Ensure the prioritizedList is sorted (monotonic) + FwIndexType currentPriority = 0; + + for (FwIndexType queueIndex = 0; queueIndex < 3; queueIndex++) { + ASSERT_LE(currentPriority, this->component.m_prioritizedList[queueIndex].priority); + currentPriority = this->component.m_prioritizedList[queueIndex].priority; + } + + component.cleanup(); +} + +void ComQueueTester::testSetQueuePriorityInvalidIndex() { + // Configure the component + configure(); + + // Send command with invalid queue index (beyond TOTAL_PORT_COUNT) + const FwIndexType invalidIndex = ComQueue::TOTAL_PORT_COUNT + 1; + this->sendCmd_SET_QUEUE_PRIORITY(0, 0, Svc::QueueType::COM_QUEUE, invalidIndex, 1); + this->component.doDispatch(); + + // Verify command response was VALIDATION_ERROR + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, ComQueue::OPCODE_SET_QUEUE_PRIORITY, 0, Fw::CmdResponse::VALIDATION_ERROR); + + // Verify no priority changed event was emitted + ASSERT_EVENTS_QueuePriorityChanged_SIZE(0); + + component.cleanup(); +} + +void ComQueueTester::testSetQueuePriorityNegativeIndex() { + // Configure the component + configure(); + + // Send command with invalid queue index (beyond TOTAL_PORT_COUNT) + const FwIndexType invalidIndex = -1; + this->sendCmd_SET_QUEUE_PRIORITY(0, 0, Svc::QueueType::COM_QUEUE, invalidIndex, 1); + this->component.doDispatch(); + + // Verify command response was VALIDATION_ERROR + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, ComQueue::OPCODE_SET_QUEUE_PRIORITY, 0, Fw::CmdResponse::VALIDATION_ERROR); + + // Verify no priority changed event was emitted + ASSERT_EVENTS_QueuePriorityChanged_SIZE(0); + + component.cleanup(); +} + +void ComQueueTester::testSetQueuePriorityInvalidPriority() { + // Configure the component + configure(); + + // Send command with invalid priority value (beyond TOTAL_PORT_COUNT) + const FwIndexType invalidPriority = ComQueue::TOTAL_PORT_COUNT + 1; + this->sendCmd_SET_QUEUE_PRIORITY(0, 0, Svc::QueueType::BUFFER_QUEUE, 0, invalidPriority); + this->component.doDispatch(); + + // Verify command response was VALIDATION_ERROR + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, ComQueue::OPCODE_SET_QUEUE_PRIORITY, 0, Fw::CmdResponse::VALIDATION_ERROR); + + // Verify no priority changed event was emitted + ASSERT_EVENTS_QueuePriorityChanged_SIZE(0); + + component.cleanup(); +} + +void ComQueueTester::testSetQueuePriorityNegativePriority() { + // Configure the component + configure(); + + // Send command with invalid priority value (< 0) + const FwIndexType invalidPriority = -1; + this->sendCmd_SET_QUEUE_PRIORITY(0, 0, Svc::QueueType::COM_QUEUE, 0, invalidPriority); + this->component.doDispatch(); + + // Verify command response was VALIDATION_ERROR + ASSERT_CMD_RESPONSE_SIZE(1); + ASSERT_CMD_RESPONSE(0, ComQueue::OPCODE_SET_QUEUE_PRIORITY, 0, Fw::CmdResponse::VALIDATION_ERROR); + + // Verify no priority changed event was emitted + ASSERT_EVENTS_QueuePriorityChanged_SIZE(0); + + component.cleanup(); +} + } // end namespace Svc diff --git a/Svc/ComQueue/test/ut/ComQueueTester.hpp b/Svc/ComQueue/test/ut/ComQueueTester.hpp index 7d3b1c09977..9356aa7cd39 100644 --- a/Svc/ComQueue/test/ut/ComQueueTester.hpp +++ b/Svc/ComQueue/test/ut/ComQueueTester.hpp @@ -81,6 +81,32 @@ class ComQueueTester : public ComQueueGTestBase { void testContextData(); + void testFIFOMode(); + + void testLIFOMode(); + + void testDropNewestMode(); + + void testDropOldestMode(); + + void testLIFOWithDropOldest(); + + void testBufferQueueFIFOMode(); + + void testBufferQueueLIFOMode(); + + void testBufferQueueDropOldestMode(); + + void testSetQueuePriorityCommand(); + + void testSetQueuePriorityInvalidIndex(); + + void testSetQueuePriorityNegativeIndex(); + + void testSetQueuePriorityInvalidPriority(); + + void testSetQueuePriorityNegativePriority(); + private: // ---------------------------------------------------------------------- // Helper methods diff --git a/Utils/Types/CMakeLists.txt b/Utils/Types/CMakeLists.txt index e28ab0b902c..008bdefc3b6 100644 --- a/Utils/Types/CMakeLists.txt +++ b/Utils/Types/CMakeLists.txt @@ -1,34 +1,37 @@ #### # F prime CMakeLists.txt: -# -# SOURCE_FILES: combined list of source and autocoding files -# MOD_DEPS: (optional) module dependencies -# #### -set(SOURCE_FILES - "${CMAKE_CURRENT_LIST_DIR}/CircularBuffer.cpp" - "${CMAKE_CURRENT_LIST_DIR}/Queue.cpp" -) -set(MOD_DEPS - "Fw/Types" -) -register_fprime_module() +register_fprime_module( + SOURCES + "${CMAKE_CURRENT_LIST_DIR}/CircularBuffer.cpp" + "${CMAKE_CURRENT_LIST_DIR}/Queue.cpp" + DEPENDS + Fw_Types -# Rules based unit testing -set(UT_MOD_DEPS - STest - Fw/Types -) -set(UT_SOURCE_FILES - "${CMAKE_CURRENT_LIST_DIR}/test/ut/CircularBuffer/CircularState.cpp" - "${CMAKE_CURRENT_LIST_DIR}/test/ut/CircularBuffer/CircularRules.cpp" - "${CMAKE_CURRENT_LIST_DIR}/test/ut/CircularBuffer/CircularBufferTester.cpp" - "${CMAKE_CURRENT_LIST_DIR}/test/ut/CircularBuffer/Main.cpp" ) -# STest Includes for this UT + set (UT_TARGET_NAME "Types_Circular_Buffer_ut_exe") -register_fprime_ut("${UT_TARGET_NAME}") +register_fprime_ut("${UT_TARGET_NAME}" + SOURCES + "${CMAKE_CURRENT_LIST_DIR}/test/ut/CircularBuffer/CircularState.cpp" + "${CMAKE_CURRENT_LIST_DIR}/test/ut/CircularBuffer/CircularRules.cpp" + "${CMAKE_CURRENT_LIST_DIR}/test/ut/CircularBuffer/CircularBufferTester.cpp" + "${CMAKE_CURRENT_LIST_DIR}/test/ut/CircularBuffer/Main.cpp" + DEPENDS + STest + Fw_Types + ) if (TARGET "${UT_TARGET_NAME}") target_compile_options("${UT_TARGET_NAME}" PRIVATE -Wno-conversion) endif() + +# Queue unit tests +register_fprime_ut( + "Utils_Types_Queue_ut_exe" + SOURCES + "${CMAKE_CURRENT_LIST_DIR}/test/ut/Queue/QueueTest.cpp" + DEPENDS + STest + Fw_Types +) \ No newline at end of file diff --git a/Utils/Types/CircularBuffer.cpp b/Utils/Types/CircularBuffer.cpp index 51076c00a21..03e26863aa1 100644 --- a/Utils/Types/CircularBuffer.cpp +++ b/Utils/Types/CircularBuffer.cpp @@ -137,6 +137,17 @@ Fw::SerializeStatus CircularBuffer ::rotate(FwSizeType amount) { return Fw::FW_SERIALIZE_OK; } +Fw::SerializeStatus CircularBuffer ::trim(FwSizeType amount) { + FW_ASSERT(m_store != nullptr && m_store_size != 0); // setup method was called + // Check there is sufficient data + if (amount > m_allocated_size) { + return Fw::FW_DESERIALIZE_BUFFER_EMPTY; + } + // Simply reduce the allocated size without moving the head + m_allocated_size -= amount; + return Fw::FW_SERIALIZE_OK; +} + FwSizeType CircularBuffer ::get_capacity() const { FW_ASSERT(m_store != nullptr && m_store_size != 0); // setup method was called return m_store_size; diff --git a/Utils/Types/CircularBuffer.hpp b/Utils/Types/CircularBuffer.hpp index fc1c662a2a0..2501d5a3f08 100644 --- a/Utils/Types/CircularBuffer.hpp +++ b/Utils/Types/CircularBuffer.hpp @@ -105,6 +105,14 @@ class CircularBuffer { */ Fw::SerializeStatus rotate(FwSizeType amount); + /** + * Remove data from the back of the circular buffer (most recently added data). + * This is the opposite of rotate, which removes from the front. + * \param amount: amount to remove from the back (in bytes) + * \return Fw::FW_SERIALIZE_OK on success or something else on error + */ + Fw::SerializeStatus trim(FwSizeType amount); + /** * Get the number of bytes allocated in the buffer * \return number of bytes diff --git a/Utils/Types/Queue.cpp b/Utils/Types/Queue.cpp index da5903f25ec..afdfd230d42 100644 --- a/Utils/Types/Queue.cpp +++ b/Utils/Types/Queue.cpp @@ -12,36 +12,79 @@ namespace Types { -Queue::Queue() : m_internal(), m_message_size(0) {} +Queue::Queue() : m_internal(), m_message_size(0), m_mode(QUEUE_FIFO), m_overflow_mode(QUEUE_DROP_NEWEST) {} void Queue::setup(U8* const storage, const FwSizeType storage_size, const FwSizeType depth, - const FwSizeType message_size) { + const FwSizeType message_size, + const QueueMode mode, + const QueueOverflowMode overflow_mode) { // Ensure that enough storage was supplied const FwSizeType total_needed_size = depth * message_size; FW_ASSERT(storage_size >= total_needed_size, static_cast(storage_size), static_cast(depth), static_cast(message_size)); m_internal.setup(storage, total_needed_size); m_message_size = message_size; + m_mode = mode; + m_overflow_mode = overflow_mode; } Fw::SerializeStatus Queue::enqueue(const U8* const message, const FwSizeType size) { FW_ASSERT(m_message_size > 0, static_cast(m_message_size)); // Ensure initialization FW_ASSERT(m_message_size == size, static_cast(size), static_cast(m_message_size)); // Message size is as expected - return m_internal.serialize(message, m_message_size); + + Fw::SerializeStatus status = m_internal.serialize(message, m_message_size); + + // If queue is full and we're in DROP_OLDEST mode, remove the oldest and try again + if (status == Fw::FW_SERIALIZE_NO_ROOM_LEFT && m_overflow_mode == QUEUE_DROP_OLDEST) { + // Remove the oldest message by rotating + Fw::SerializeStatus rotate_status = m_internal.rotate(m_message_size); + if (rotate_status != Fw::FW_SERIALIZE_OK) { + return rotate_status; + } + + // Now enqueue the new message (should succeed since we just freed space) + status = m_internal.serialize(message, m_message_size); + if (status != Fw::FW_SERIALIZE_OK) { + return status; + } else { + // Let the caller know we deleted data + return Fw::FW_SERIALIZE_DISCARDED_EXISTING; + } + } + + return status; } Fw::SerializeStatus Queue::dequeue(U8* const message, const FwSizeType size) { FW_ASSERT(m_message_size > 0); // Ensure initialization FW_ASSERT(m_message_size <= size, static_cast(size), static_cast(m_message_size)); // Sufficient storage space for read message - Fw::SerializeStatus result = m_internal.peek(message, m_message_size, 0); - if (result != Fw::FW_SERIALIZE_OK) { - return result; + + Fw::SerializeStatus result; + + if (m_mode == QUEUE_FIFO) { + // FIFO: Dequeue from the front (oldest message) + result = m_internal.peek(message, m_message_size, 0); + if (result != Fw::FW_SERIALIZE_OK) { + return result; + } + return m_internal.rotate(m_message_size); + } else { + // LIFO: Dequeue from the back (newest message) + FwSizeType current_size = m_internal.get_allocated_size(); + if (current_size < m_message_size) { + return Fw::FW_DESERIALIZE_BUFFER_EMPTY; + } + FwSizeType offset = current_size - m_message_size; + result = m_internal.peek(message, m_message_size, offset); + if (result != Fw::FW_SERIALIZE_OK) { + return result; + } + return m_internal.trim(m_message_size); } - return m_internal.rotate(m_message_size); } FwSizeType Queue::get_high_water_mark() const { diff --git a/Utils/Types/Queue.hpp b/Utils/Types/Queue.hpp index 4af54008284..1f5610e5759 100644 --- a/Utils/Types/Queue.hpp +++ b/Utils/Types/Queue.hpp @@ -1,7 +1,7 @@ /* * Queue.hpp: * - * FIFO queue of fixed size messages. For use generally where non-concurrent, non-OS backed based FIFO queues are + * FIFO/LIFO queue of fixed size messages. For use generally where non-concurrent, non-OS backed based queues are * necessary. Message size is defined at construction time and all messages enqueued and dequeued must be of that fixed * size. Wraps circular buffer to perform actual storage of messages. This implementation is not thread safe and the * expectation is that the user will wrap it in concurrency constructs where necessary. @@ -19,6 +19,22 @@ namespace Types { +/** + * \brief Queue ordering mode + */ +enum QueueMode { + QUEUE_FIFO, //!< First-In-First-Out: dequeue from front + QUEUE_LIFO //!< Last-In-First-Out: dequeue from back +}; + +/** + * \brief Queue overflow behavior mode + */ +enum QueueOverflowMode { + QUEUE_DROP_NEWEST, //!< Drop the newest (incoming) message on overflow + QUEUE_DROP_OLDEST //!< Drop the oldest (front) message on overflow +}; + class Queue { public: /** @@ -30,38 +46,53 @@ class Queue { * \brief setup the queue object to setup storage * * The queue must be configured before use to setup storage parameters. This function supplies those parameters - * including depth, and message size. Storage size must be greater than or equal to the depth x message size. + * including depth, message size, queue mode, and overflow mode. Storage size must be greater than or equal to the + * depth x message size. * * \param storage: storage memory allocation * \param storage_size: size of the provided allocation * \param depth: depth of the queue * \param message_size: size of individual messages + * \param mode: queue ordering mode (FIFO or LIFO), defaults to FIFO + * \param overflow_mode: overflow handling mode, defaults to DROP_NEWEST */ - void setup(U8* const storage, const FwSizeType storage_size, const FwSizeType depth, const FwSizeType message_size); + void setup(U8* const storage, + const FwSizeType storage_size, + const FwSizeType depth, + const FwSizeType message_size, + const QueueMode mode = QUEUE_FIFO, + const QueueOverflowMode overflow_mode = QUEUE_DROP_NEWEST); /** - * \brief pushes a fixed-size message onto the back of the queue + * \brief pushes a fixed-size message onto the queue * * Pushes a fixed-size message onto the queue. This performs a copy of the data onto the queue so the user is free * to dispose the message data as soon as the call returns. Note: message is required to be of the size message_size * as defined by the construction of the queue. Size is provided as a safety check to ensure the sent size is * consistent with the expected size of the queue. * - * This will return a non-Fw::SERIALIZE_OK status when the queue is full. + * When the queue is full, behavior depends on the overflow mode: + * - DROP_NEWEST: Returns FW_SERIALIZE_NO_ROOM_LEFT without modifying the queue + * - DROP_OLDEST: Removes the oldest message and adds the new one, returns FW_SERIALIZE_DISCARDED_EXISTING * * \param message: message of size m_message_size to enqueue * \param size: size of the message being sent. Must be equivalent to queue's message size. - * \return: Fw::SERIALIZE_OK on success, something else on failure + * \return: Fw::SERIALIZE_OK on success, FW_SERIALIZE_NO_ROOM_LEFT when full with DROP_NEWEST mode, + * FW_SERIALIZE_DISCARDED_EXISTING when full with DROP_OLDEST mode */ Fw::SerializeStatus enqueue(const U8* const message, const FwSizeType size); /** - * \brief pops a fixed-size message off the front of the queue + * \brief pops a fixed-size message off the queue * - * Pops a fixed-size message off the front of the queue. This performs a copy of the data into the provided message + * Pops a fixed-size message off the queue. This performs a copy of the data into the provided message * buffer. Note: message is required to be of the size message_size as defined by the construction of the queue. The * size must be greater or equal to message size, although only message size bytes will be used. * + * Dequeue location depends on the queue mode: + * - FIFO: Removes and returns the oldest (front) message + * - LIFO: Removes and returns the newest (back) message + * * This will return a non-Fw::SERIALIZE_OK status when the queue is empty. * * \param message: message of size m_message_size to dequeue @@ -85,6 +116,8 @@ class Queue { private: CircularBuffer m_internal; FwSizeType m_message_size; + QueueMode m_mode; + QueueOverflowMode m_overflow_mode; }; } // namespace Types #endif // _UTILS_TYPES_QUEUE_HPP diff --git a/Utils/Types/test/ut/CircularBuffer/CircularRules.cpp b/Utils/Types/test/ut/CircularBuffer/CircularRules.cpp index 4236e1681e7..7a410215b76 100644 --- a/Utils/Types/test/ut/CircularBuffer/CircularRules.cpp +++ b/Utils/Types/test/ut/CircularBuffer/CircularRules.cpp @@ -166,4 +166,30 @@ bool RotateBadRule::precondition(const MockTypes::CircularState& state) { void RotateBadRule::action(MockTypes::CircularState& state) { ASSERT_EQ(state.getTestBuffer().rotate(state.getRandomSize()), Fw::FW_DESERIALIZE_BUFFER_EMPTY); } + +TrimOkRule::TrimOkRule(const char* const name) : STest::Rule(name) {} + +bool TrimOkRule::precondition(const MockTypes::CircularState& state) { + FwSizeType trim_available = (MAX_BUFFER_SIZE - state.getRemainingSize()); + return trim_available >= state.getRandomSize(); +} + +void TrimOkRule::action(MockTypes::CircularState& state) { + state.checkSizes(); + ASSERT_EQ(state.getTestBuffer().trim(state.getRandomSize()), Fw::FW_SERIALIZE_OK); + ASSERT_TRUE(state.trim(state.getRandomSize())); + state.setRemainingSize(state.getRemainingSize() + state.getRandomSize()); + state.checkSizes(); +} + +TrimBadRule::TrimBadRule(const char* const name) : STest::Rule(name) {} + +bool TrimBadRule::precondition(const MockTypes::CircularState& state) { + FwSizeType trim_available = (MAX_BUFFER_SIZE - state.getRemainingSize()); + return trim_available < state.getRandomSize(); +} + +void TrimBadRule::action(MockTypes::CircularState& state) { + ASSERT_EQ(state.getTestBuffer().trim(state.getRandomSize()), Fw::FW_DESERIALIZE_BUFFER_EMPTY); +} } // namespace Types diff --git a/Utils/Types/test/ut/CircularBuffer/CircularRules.hpp b/Utils/Types/test/ut/CircularBuffer/CircularRules.hpp index b05b70fe9fd..8f6b12491e1 100644 --- a/Utils/Types/test/ut/CircularBuffer/CircularRules.hpp +++ b/Utils/Types/test/ut/CircularBuffer/CircularRules.hpp @@ -138,5 +138,37 @@ struct RotateBadRule : public STest::Rule { // Action that tests the buffer's ability to rotate void action(MockTypes::CircularState& state); }; + +/** + * TrimOkRule: + * + * This rule tests that the circular buffer can trim correctly (remove from the back). + */ +struct TrimOkRule : public STest::Rule { + // Constructor + TrimOkRule(const char* const name); + + // Trim is ok when there is more data then trim size + bool precondition(const MockTypes::CircularState& state); + + // Action that tests the buffer's ability to trim + void action(MockTypes::CircularState& state); +}; + +/** + * TrimBadRule: + * + * This rule tests that the circular buffer cannot trim when it should not trim. + */ +struct TrimBadRule : public STest::Rule { + // Constructor + TrimBadRule(const char* const name); + + // Trim is bad when there is less data then trim size + bool precondition(const MockTypes::CircularState& state); + + // Action that tests the buffer's inability to trim + void action(MockTypes::CircularState& state); +}; } // namespace Types #endif // FPRIME_GROUNDINTERFACERULES_HPP diff --git a/Utils/Types/test/ut/CircularBuffer/CircularState.cpp b/Utils/Types/test/ut/CircularBuffer/CircularState.cpp index 37bf199bf6b..6ec753d7ee8 100644 --- a/Utils/Types/test/ut/CircularBuffer/CircularState.cpp +++ b/Utils/Types/test/ut/CircularBuffer/CircularState.cpp @@ -94,6 +94,15 @@ bool CircularState::rotate(FwSizeType size) { return true; } +bool CircularState::trim(FwSizeType size) { + // Fail if we try to trim too much + if ((m_infinite_write - size) < m_infinite_read) { + return false; + } + m_infinite_write -= size; + return true; +} + FwSizeType CircularState::getRandomSize() const { return m_random_size; } diff --git a/Utils/Types/test/ut/CircularBuffer/CircularState.hpp b/Utils/Types/test/ut/CircularBuffer/CircularState.hpp index 90072ac99d7..70dd1a827e6 100644 --- a/Utils/Types/test/ut/CircularBuffer/CircularState.hpp +++ b/Utils/Types/test/ut/CircularBuffer/CircularState.hpp @@ -51,6 +51,12 @@ class CircularState { * @return true if successful, false otherwise */ bool rotate(FwSizeType size); + /** + * Trim the circular buffer (remove from the back). + * @param size: size to trim + * @return true if successful, false otherwise + */ + bool trim(FwSizeType size); /** * Get the size of the random buffer data. * @return size of the buffer diff --git a/Utils/Types/test/ut/CircularBuffer/Main.cpp b/Utils/Types/test/ut/CircularBuffer/Main.cpp index 0ac88d468e9..41bbc76d472 100644 --- a/Utils/Types/test/ut/CircularBuffer/Main.cpp +++ b/Utils/Types/test/ut/CircularBuffer/Main.cpp @@ -37,12 +37,14 @@ TEST(CircularBufferTests, RandomCircularTests) { Types::SerializeOverflowRule serializeOverflow("serializeOverflow"); Types::PeekOkRule peekOk("peekOk"); Types::PeekBadRule peekBad("peekBad"); - Types::PeekOkRule rotateOk("rotateOk"); - Types::PeekBadRule rotateBad("rotateBad"); + Types::RotateOkRule rotateOk("rotateOk"); + Types::RotateBadRule rotateBad("rotateBad"); + Types::TrimOkRule trimOk("trimOk"); + Types::TrimBadRule trimBad("trimBad"); // Setup a list of rules to choose from - STest::Rule* rules[] = {&randomize, &serializeOk, &serializeOverflow, &peekOk, - &peekBad, &rotateOk, &rotateBad}; + STest::Rule* rules[] = { + &randomize, &serializeOk, &serializeOverflow, &peekOk, &peekBad, &rotateOk, &rotateBad, &trimOk, &trimBad}; // Construct the random scenario and run it with the defined bounds STest::RandomScenario random("Random Rules", rules, FW_NUM_ARRAY_ELEMENTS(rules)); @@ -163,6 +165,33 @@ TEST(CircularBufferTests, BasicRotateBadTest) { rotateBad.apply(state); } +/** + * Test that the most basic trim work. + */ +TEST(CircularBufferTests, BasicTrimTest) { + // Setup and register state + MockTypes::CircularState state; + + // Create rules, and assign them into the array + Types::RandomizeRule randomGo("randomGo"); + Types::SerializeOkRule serializeOk("SerializeOk"); + Types::TrimOkRule trimOk("trimOk"); + randomGo.apply(state); + serializeOk.apply(state); + trimOk.apply(state); +} + +/** + * Test that the most basic bad-trim work. + */ +TEST(CircularBufferTests, BasicTrimBadTest) { + // Setup all circular state + MockTypes::CircularState state; + // Run trim bad rule + Types::TrimBadRule trimBad("trimBad"); + trimBad.apply(state); +} + /** * Test boundary cases */ diff --git a/Utils/Types/test/ut/Queue/QueueTest.cpp b/Utils/Types/test/ut/Queue/QueueTest.cpp new file mode 100644 index 00000000000..2235bf647d9 --- /dev/null +++ b/Utils/Types/test/ut/Queue/QueueTest.cpp @@ -0,0 +1,227 @@ +// ====================================================================== +// \title QueueTest.cpp +// \author Auto-generated +// \brief cpp file for Queue unit tests +// +// Test the FIFO/LIFO and DROP_NEWEST/DROP_OLDEST functionality +// ====================================================================== + +#include +#include +#include + +class QueueTest : public ::testing::Test { + protected: + static const FwSizeType MSG_SIZE = sizeof(U32); + static const FwSizeType QUEUE_DEPTH = 5; + static const FwSizeType BUFFER_SIZE = MSG_SIZE * QUEUE_DEPTH; + + void enqueueValue(Types::Queue& queue, U32 value) { + Fw::SerializeStatus status = queue.enqueue(reinterpret_cast(&value), MSG_SIZE); + ASSERT_EQ(Fw::FW_SERIALIZE_OK, status); + } + + U32 dequeueValue(Types::Queue& queue) { + U32 value; + Fw::SerializeStatus status = queue.dequeue(reinterpret_cast(&value), MSG_SIZE); + EXPECT_EQ(Fw::FW_SERIALIZE_OK, status); + return value; + } +}; + +// Test FIFO mode (default) +TEST_F(QueueTest, FIFOMode) { + U8 storage[BUFFER_SIZE]; + Types::Queue queue; + queue.setup(storage, BUFFER_SIZE, QUEUE_DEPTH, MSG_SIZE); + + // Enqueue values 1, 2, 3, 4, 5 + for (U32 i = 1; i <= 5; i++) { + enqueueValue(queue, i); + } + + // Dequeue should return in order: 1, 2, 3, 4, 5 + for (U32 i = 1; i <= 5; i++) { + EXPECT_EQ(i, dequeueValue(queue)); + } +} + +// Test LIFO mode +TEST_F(QueueTest, LIFOMode) { + U8 storage[BUFFER_SIZE]; + Types::Queue queue; + queue.setup(storage, BUFFER_SIZE, QUEUE_DEPTH, MSG_SIZE, Types::QUEUE_LIFO); + + // Enqueue values 1, 2, 3, 4, 5 + for (U32 i = 1; i <= 5; i++) { + enqueueValue(queue, i); + } + + // Dequeue should return in reverse order: 5, 4, 3, 2, 1 + for (U32 i = 5; i >= 1; i--) { + EXPECT_EQ(i, dequeueValue(queue)); + } +} + +// Test DROP_NEWEST mode (default) - queue full should reject new items +TEST_F(QueueTest, DropNewestMode) { + U8 storage[BUFFER_SIZE]; + Types::Queue queue; + queue.setup(storage, BUFFER_SIZE, QUEUE_DEPTH, MSG_SIZE); + + // Fill the queue completely + for (U32 i = 1; i <= 5; i++) { + enqueueValue(queue, i); + } + + // Try to enqueue when full - should fail + U32 newValue = 99; + Fw::SerializeStatus status = queue.enqueue(reinterpret_cast(&newValue), MSG_SIZE); + EXPECT_EQ(Fw::FW_SERIALIZE_NO_ROOM_LEFT, status); + + // Verify original values still intact + for (U32 i = 1; i <= 5; i++) { + EXPECT_EQ(i, dequeueValue(queue)); + } +} + +// Test DROP_OLDEST mode - queue full should drop oldest and add new +TEST_F(QueueTest, DropOldestMode) { + U8 storage[BUFFER_SIZE]; + Types::Queue queue; + queue.setup(storage, BUFFER_SIZE, QUEUE_DEPTH, MSG_SIZE, Types::QUEUE_FIFO, Types::QUEUE_DROP_OLDEST); + + // Fill the queue with values 1, 2, 3, 4, 5 + for (U32 i = 1; i <= 5; i++) { + enqueueValue(queue, i); + } + + // Enqueue 99 when full - should succeed and drop oldest (1) + U32 newValue = 99; + Fw::SerializeStatus status = queue.enqueue(reinterpret_cast(&newValue), MSG_SIZE); + EXPECT_EQ(Fw::FW_SERIALIZE_DISCARDED_EXISTING, status); + + // Should now have: 2, 3, 4, 5, 99 + EXPECT_EQ(2, dequeueValue(queue)); + EXPECT_EQ(3, dequeueValue(queue)); + EXPECT_EQ(4, dequeueValue(queue)); + EXPECT_EQ(5, dequeueValue(queue)); + EXPECT_EQ(99, dequeueValue(queue)); +} + +// Test LIFO with DROP_OLDEST +TEST_F(QueueTest, LIFOWithDropOldest) { + U8 storage[BUFFER_SIZE]; + Types::Queue queue; + queue.setup(storage, BUFFER_SIZE, QUEUE_DEPTH, MSG_SIZE, Types::QUEUE_LIFO, Types::QUEUE_DROP_OLDEST); + + // Fill the queue with values 1, 2, 3, 4, 5 + for (U32 i = 1; i <= 5; i++) { + enqueueValue(queue, i); + } + + // Enqueue 99 when full - should succeed and drop oldest (1) + U32 newValue = 99; + Fw::SerializeStatus status = queue.enqueue(reinterpret_cast(&newValue), MSG_SIZE); + EXPECT_EQ(Fw::FW_SERIALIZE_DISCARDED_EXISTING, status); + + // LIFO should return newest first: 99, 5, 4, 3, 2 + EXPECT_EQ(99, dequeueValue(queue)); + EXPECT_EQ(5, dequeueValue(queue)); + EXPECT_EQ(4, dequeueValue(queue)); + EXPECT_EQ(3, dequeueValue(queue)); + EXPECT_EQ(2, dequeueValue(queue)); +} + +// Test empty queue dequeue +TEST_F(QueueTest, DequeueEmpty) { + U8 storage[BUFFER_SIZE]; + Types::Queue queue; + queue.setup(storage, BUFFER_SIZE, QUEUE_DEPTH, MSG_SIZE); + + U32 value; + Fw::SerializeStatus status = queue.dequeue(reinterpret_cast(&value), MSG_SIZE); + EXPECT_EQ(Fw::FW_DESERIALIZE_BUFFER_EMPTY, status); +} + +// Test queue size tracking +TEST_F(QueueTest, QueueSize) { + U8 storage[BUFFER_SIZE]; + Types::Queue queue; + queue.setup(storage, BUFFER_SIZE, QUEUE_DEPTH, MSG_SIZE); + + EXPECT_EQ(0, queue.getQueueSize()); + + enqueueValue(queue, 1); + EXPECT_EQ(1, queue.getQueueSize()); + + enqueueValue(queue, 2); + enqueueValue(queue, 3); + EXPECT_EQ(3, queue.getQueueSize()); + + dequeueValue(queue); + EXPECT_EQ(2, queue.getQueueSize()); + + dequeueValue(queue); + dequeueValue(queue); + EXPECT_EQ(0, queue.getQueueSize()); +} + +// Test high water mark +TEST_F(QueueTest, HighWaterMark) { + U8 storage[BUFFER_SIZE]; + Types::Queue queue; + queue.setup(storage, BUFFER_SIZE, QUEUE_DEPTH, MSG_SIZE); + + EXPECT_EQ(0, queue.get_high_water_mark()); + + enqueueValue(queue, 1); + EXPECT_EQ(1, queue.get_high_water_mark()); + + enqueueValue(queue, 2); + enqueueValue(queue, 3); + EXPECT_EQ(3, queue.get_high_water_mark()); + + // Dequeue doesn't lower high water mark + dequeueValue(queue); + EXPECT_EQ(3, queue.get_high_water_mark()); + + // Clear and verify + queue.clear_high_water_mark(); + EXPECT_EQ(0, queue.get_high_water_mark()); +} + +// Test alternating enqueue/dequeue with FIFO +TEST_F(QueueTest, AlternatingFIFO) { + U8 storage[BUFFER_SIZE]; + Types::Queue queue; + queue.setup(storage, BUFFER_SIZE, QUEUE_DEPTH, MSG_SIZE); + + enqueueValue(queue, 1); + enqueueValue(queue, 2); + EXPECT_EQ(1, dequeueValue(queue)); + + enqueueValue(queue, 3); + EXPECT_EQ(2, dequeueValue(queue)); + EXPECT_EQ(3, dequeueValue(queue)); +} + +// Test alternating enqueue/dequeue with LIFO +TEST_F(QueueTest, AlternatingLIFO) { + U8 storage[BUFFER_SIZE]; + Types::Queue queue; + queue.setup(storage, BUFFER_SIZE, QUEUE_DEPTH, MSG_SIZE, Types::QUEUE_LIFO); + + enqueueValue(queue, 1); + enqueueValue(queue, 2); + EXPECT_EQ(2, dequeueValue(queue)); // LIFO returns newest + + enqueueValue(queue, 3); + EXPECT_EQ(3, dequeueValue(queue)); + EXPECT_EQ(1, dequeueValue(queue)); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}