Repository: nasa/fprime (language: python)

Issue:
Add Fw::ExternalString and revise string implementations
This PR does the following:
1. Add a new class `Fw::ExternalString`. This class is derived from `Fw::StringBase`. It uses an externally supplied character buffer to implement a string. I added tests for `Fw::ExternalString` to the `Fw/Types` unit tests.
2. Refactor the string implementations in the framework to reduce code duplication.

Rationale:
1. `Fw::ExternalString` is needed to improve the C++ code generation strategy for strings. See fprime-community/fpp#406.
2. The refactored implementation eliminates a significant amount of copy-paste code when creating a new concrete string type. The new version should be functionally identical to the old version.

Notes:

1. Review and merge #2677 first. A lot of changes in this PR are formatting changes that will go away once #2677 is merged.
2. Static analysis is telling us that single-argument constructors should be marked `explicit`. When I tried this, I saw compilation errors in the generated C++. I think this issue will have to be addressed separately.
Tutorial Suggestions
I made a few suggestions to the tutorial to address some of the questions / roadblocks I hit while implementing MathComponent


Discussion / hints:

@rdaruwala thanks for this feedback.  A cursory glance of the changes look good.  Since it affects a tutorial, I'll do a full "from scratch" run through this weekend.  If that looks good, I'll follow-up with where to go from there.
@timcanham I recommend we merge this.  Note: the link order is wrong in the modules.mk definitions, and we'll need to update that.
Thanks for the fixes and suggestions!

Fix patch:
diff --git a/.github/workflows/pip-check.yml b/.github/workflows/pip-check.yml
index 82c5c8e928..7c67c4d37c 100644
--- a/.github/workflows/pip-check.yml
+++ b/.github/workflows/pip-check.yml
@@ -17,11 +17,19 @@ jobs:
     strategy:
       matrix:
         python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
-        runner: [macos-latest, ubuntu-latest]
+        runner: [macos-13, macos-latest, ubuntu-latest]
+        # macos-14 (ARM) currently breaks actions/setup-python with 3.8 and 3.9
+        # so we exclude them from the matrix and also test with macos-13 (x86)
+        # More info at https://github.com/actions/setup-python/issues/850
+        exclude:
+          - runner: macos-latest
+            python-version: '3.8'
+          - runner: macos-latest
+            python-version: '3.9'
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
       - name: Install dependencies
diff --git a/FppTest/dp/DpTest.cpp b/FppTest/dp/DpTest.cpp
index da812182d3..f8f10e22ff 100644
--- a/FppTest/dp/DpTest.cpp
+++ b/FppTest/dp/DpTest.cpp
@@ -15,19 +15,25 @@ namespace FppTest {
 // Construction, initialization, and destruction
 // ----------------------------------------------------------------------
 
-DpTest ::DpTest(const char* const compName,
-                U32 u32RecordData,
-                U16 dataRecordData,
-                const U8ArrayRecordData& u8ArrayRecordData,
-                const U32ArrayRecordData& u32ArrayRecordData,
-                const DataArrayRecordData& dataArrayRecordData)
+DpTest::DpTest(const char* const compName,
+               U32 u32RecordData,
+               U16 dataRecordData,
+               const U8ArrayRecordData& u8ArrayRecordData,
+               const U32ArrayRecordData& u32ArrayRecordData,
+               const DataArrayRecordData& dataArrayRecordData,
+               const Fw::StringBase& a_stringRecordData)
     : DpTestComponentBase(compName),
       u32RecordData(u32RecordData),
       dataRecordData(dataRecordData),
       u8ArrayRecordData(u8ArrayRecordData),
       u32ArrayRecordData(u32ArrayRecordData),
       dataArrayRecordData(dataArrayRecordData),
-      sendTime(Fw::ZERO_TIME) {}
+      stringRecordData(a_stringRecordData),
+      sendTime(Fw::ZERO_TIME) {
+    for (auto& elt : this->stringArrayRecordData) {
+        elt = &a_stringRecordData;
+    }
+}
 
 void DpTest ::init(const NATIVE_INT_TYPE queueDepth, const NATIVE_INT_TYPE instance) {
     DpTestComponentBase::init(queueDepth, instance);
@@ -50,6 +56,8 @@ void DpTest::schedIn_handler(const NATIVE_INT_TYPE portNum, U32 context) {
     this->dpRequest_Container4(CONTAINER_4_DATA_SIZE);
     // Request a buffer for Container 5
     this->dpRequest_Container5(CONTAINER_5_DATA_SIZE);
+    // Request a buffer for Container 6
+    this->dpRequest_Container6(CONTAINER_6_DATA_SIZE);
     // Get a buffer for Container 1
     {
         DpContainer container;
@@ -178,6 +186,37 @@ void DpTest ::dpRecv_Container5_handler(DpContainer& container, Fw::Success::T s
     }
 }
 
+void DpTest ::dpRecv_Container6_handler(DpContainer& container, Fw::Success::T status) {
+    if (status == Fw::Success::SUCCESS) {
+        auto serializeStatus = Fw::FW_SERIALIZE_OK;
+        for (FwSizeType i = 0; i < CONTAINER_6_DATA_SIZE; ++i) {
+            serializeStatus = container.serializeRecord_StringRecord(this->stringRecordData);
+            if (serializeStatus == Fw::FW_SERIALIZE_NO_ROOM_LEFT) {
+                break;
+            }
+            FW_ASSERT(serializeStatus == Fw::FW_SERIALIZE_OK, status);
+        }
+        // Use the time stamp from the time get port
+        this->dpSend(container);
+    }
+}
+
+void DpTest ::dpRecv_Container7_handler(DpContainer& container, Fw::Success::T status) {
+    if (status == Fw::Success::SUCCESS) {
+        auto serializeStatus = Fw::FW_SERIALIZE_OK;
+        for (FwSizeType i = 0; i < CONTAINER_7_DATA_SIZE; ++i) {
+            serializeStatus = container.serializeRecord_StringArrayRecord(
+                this->stringArrayRecordData, FW_NUM_ARRAY_ELEMENTS(this->stringArrayRecordData));
+            if (serializeStatus == Fw::FW_SERIALIZE_NO_ROOM_LEFT) {
+                break;
+            }
+            FW_ASSERT(serializeStatus == Fw::FW_SERIALIZE_OK, status);
+        }
+        // Use the time stamp from the time get port
+        this->dpSend(container);
+    }
+}
+
 // ----------------------------------------------------------------------
 // Private helper functions
 // ----------------------------------------------------------------------
diff --git a/FppTest/dp/DpTest.fpp b/FppTest/dp/DpTest.fpp
index 77398715ea..98c733e0d3 100644
--- a/FppTest/dp/DpTest.fpp
+++ b/FppTest/dp/DpTest.fpp
@@ -3,6 +3,12 @@ module FppTest {
   @ A component for testing data product code gen
   active component DpTest {
 
+    # ----------------------------------------------------------------------
+    # Constants
+    # ----------------------------------------------------------------------
+
+    constant stringSize = 80
+
     # ----------------------------------------------------------------------
     # Types
     # ----------------------------------------------------------------------
@@ -58,6 +64,12 @@ module FppTest {
     @ Record 5
     product record DataArrayRecord: Data array id 500
 
+    @ Record 6
+    product record StringRecord: string size stringSize id 600
+
+    @ Record 7
+    product record StringArrayRecord: string size stringSize array id 700
+
     # ----------------------------------------------------------------------
     # Containers
     # ----------------------------------------------------------------------
@@ -77,6 +89,12 @@ module FppTest {
     @ Container 5
     product container Container5 id 500 default priority 50
 
+    @ Container 6
+    product container Container6 id 600 default priority 60
+
+    @ Container 7
+    product container Container7 id 700 default priority 70
+
   }
 
 }
diff --git a/FppTest/dp/DpTest.hpp b/FppTest/dp/DpTest.hpp
index 47959a0a7f..29a3e4bc56 100644
--- a/FppTest/dp/DpTest.hpp
+++ b/FppTest/dp/DpTest.hpp
@@ -10,10 +10,15 @@
 #include <array>
 
 #include "FppTest/dp/DpTestComponentAc.hpp"
+#include "Fw/Types/String.hpp"
 
 namespace FppTest {
 
 class DpTest : public DpTestComponentBase {
+
+    // Friend class for testing
+    friend class Tester;
+
   public:
     // ----------------------------------------------------------------------
     // Constants
@@ -29,6 +34,12 @@ class DpTest : public DpTestComponentBase {
     static constexpr FwSizeType CONTAINER_4_PACKET_SIZE = DpContainer::getPacketSizeForDataSize(CONTAINER_4_DATA_SIZE);
     static constexpr FwSizeType CONTAINER_5_DATA_SIZE = 1000;
     static constexpr FwSizeType CONTAINER_5_PACKET_SIZE = DpContainer::getPacketSizeForDataSize(CONTAINER_5_DATA_SIZE);
+    static constexpr FwSizeType CONTAINER_6_DATA_SIZE = 1000;
+    static constexpr FwSizeType CONTAINER_6_PACKET_SIZE = DpContainer::getPacketSizeForDataSize(CONTAINER_6_DATA_SIZE);
+    static constexpr FwSizeType CONTAINER_7_DATA_SIZE = 1000;
+    static constexpr FwSizeType CONTAINER_7_PACKET_SIZE = DpContainer::getPacketSizeForDataSize(CONTAINER_7_DATA_SIZE);
+
+    static constexpr FwSizeType STRING_ARRAY_RECORD_ARRAY_SIZE = 100;
 
   public:
     // ----------------------------------------------------------------------
@@ -38,6 +49,7 @@ class DpTest : public DpTestComponentBase {
     using U8ArrayRecordData = std::array<U8, 256>;
     using U32ArrayRecordData = std::array<U32, 100>;
     using DataArrayRecordData = std::array<DpTest_Data, 300>;
+    using PtrToConstStringBase = const Fw::StringBase*;
 
   public:
     // ----------------------------------------------------------------------
@@ -45,12 +57,13 @@ class DpTest : public DpTestComponentBase {
     // ----------------------------------------------------------------------
 
     //! Construct object DpTest
-    DpTest(const char* const compName,                     //!< The component name
-           U32 u32RecordData,                              //!< The U32Record data
-           U16 dataRecordData,                             //!< The DataRecord data
-           const U8ArrayRecordData& u8ArrayRecordData,     //!< The U8ArrayRecord data
-           const U32ArrayRecordData& u32ArrayRecordData,   //!< The U32ArrayRecord data
-           const DataArrayRecordData& dataArrayRecordData  //!< The DataArrayRecord data
+    DpTest(const char* const compName,                      //!< The component name
+           U32 u32RecordData,                               //!< The U32Record data
+           U16 dataRecordData,                              //!< The DataRecord data
+           const U8ArrayRecordData& u8ArrayRecordData,      //!< The U8ArrayRecord data
+           const U32ArrayRecordData& u32ArrayRecordData,    //!< The U32ArrayRecord data
+           const DataArrayRecordData& dataArrayRecordData,  //!< The DataArrayRecord data
+           const Fw::StringBase& stringRecordData           //!< The StringRecord data
     );
 
     //! Initialize object DpTest
@@ -69,73 +82,82 @@ class DpTest : public DpTestComponentBase {
     //! Set the send time
     void setSendTime(Fw::Time time) { this->sendTime = time; }
 
-    PRIVATE :
-        // ----------------------------------------------------------------------
-        // Handler implementations for user-defined typed input ports
-        // ----------------------------------------------------------------------
-
-        //! Handler implementation for schedIn
-        void
-        schedIn_handler(const NATIVE_INT_TYPE portNum,  //!< The port number
-                        U32 context                     //!< The call order
-                        ) override;
-
-    PRIVATE :
-        // ----------------------------------------------------------------------
-        // Data product handler implementations
-        // ----------------------------------------------------------------------
-
-        //! Receive a data product container of type Container1
-        //! \return Serialize status
-        void
-        dpRecv_Container1_handler(DpContainer& container,  //!< The container
-                                  Fw::Success::T           //!< The container status
-                                  ) override;
+  private:
+    // ----------------------------------------------------------------------
+    // Handler implementations for user-defined typed input ports
+    // ----------------------------------------------------------------------
+
+    //! Handler implementation for schedIn
+    void schedIn_handler(const NATIVE_INT_TYPE portNum,  //!< The port number
+                         U32 context                     //!< The call order
+                         ) final;
+
+  private:
+    // ----------------------------------------------------------------------
+    // Data product handler implementations
+    // ----------------------------------------------------------------------
+
+    //! Receive a data product container of type Container1
+    //! \return Serialize status
+    void dpRecv_Container1_handler(DpContainer& container,  //!< The container
+                                   Fw::Success::T           //!< The container status
+                                   ) final;
 
     //! Receive a data product container of type Container2
     //! \return Serialize status
     void dpRecv_Container2_handler(DpContainer& container,  //!< The container
                                    Fw::Success::T           //!< The container status
-                                   ) override;
+                                   ) final;
 
     //! Receive a data product container of type Container3
     //! \return Serialize status
     void dpRecv_Container3_handler(DpContainer& container,  //!< The container
                                    Fw::Success::T           //!< The container status
-                                   ) override;
+                                   ) final;
 
     //! Receive a data product container of type Container4
     //! \return Serialize status
     void dpRecv_Container4_handler(DpContainer& container,  //!< The container
                                    Fw::Success::T           //!< The container status
-                                   ) override;
+                                   ) final;
 
     //! Receive a data product container of type Container5
     //! \return Serialize status
     void dpRecv_Container5_handler(DpContainer& container,  //!< The container
                                    Fw::Success::T           //!< The container status
-                                   ) override;
+                                   ) final;
 
-    PRIVATE :
-        // ----------------------------------------------------------------------
-        // Private helper functions
-        // ----------------------------------------------------------------------
+    //! Receive a data product container of type Container6
+    //! \return Serialize status
+    void dpRecv_Container6_handler(DpContainer& container,  //!< The container
+                                   Fw::Success::T           //!< The container status
+                                   ) final;
+
+    //! Receive a data product container of type Container7
+    //! \return Serialize status
+    void dpRecv_Container7_handler(DpContainer& container,  //!< The container
+                                   Fw::Success::T           //!< The container status
+                                   ) final;
+
+  private:
+    // ----------------------------------------------------------------------
+    // Private helper functions
+    // ----------------------------------------------------------------------
 
-        //! Check a container for validity
-        void
-        checkContainer(const DpContainer& container,  //!< The container
-                       FwDpIdType localId,            //!< The expected local id
-                       FwSizeType size,               //!< The expected size
-                       FwDpPriorityType priority      //!< The expected priority
-        ) const;
+    //! Check a container for validity
+    void checkContainer(const DpContainer& container,  //!< The container
+                        FwDpIdType localId,            //!< The expected local id
+                        FwSizeType size,               //!< The expected size
+                        FwDpPriorityType priority      //!< The expected priority
+    ) const;
 
-    PRIVATE :
-        // ----------------------------------------------------------------------
-        // Private member variables
-        // ----------------------------------------------------------------------
+  private:
+    // ----------------------------------------------------------------------
+    // Private member variables
+    // ----------------------------------------------------------------------
 
-        //! U32Record data
-        const U32 u32RecordData;
+    //! U32Record data
+    const U32 u32RecordData;
 
     //! DataRecord data
     const U16 dataRecordData;
@@ -149,6 +171,12 @@ class DpTest : public DpTestComponentBase {
     //! DataArrayRecord data
     const DataArrayRecordData& dataArrayRecordData;
 
+    //! StringRecord data
+    const Fw::StringBase& stringRecordData;
+
+    //! StringArrayRecord data
+    PtrToConstStringBase stringArrayRecordData[STRING_ARRAY_RECORD_ARRAY_SIZE];
+
     //! Send time for testing
     Fw::Time sendTime;
 };
diff --git a/Fw/Types/StringBase.cpp b/Fw/Types/StringBase.cpp
index 2a7cef41f8..f3cc9d1e0e 100644
--- a/Fw/Types/StringBase.cpp
+++ b/Fw/Types/StringBase.cpp
@@ -117,6 +117,14 @@ StringBase::SizeType StringBase::length() const {
     return static_cast<SizeType>(StringUtils::string_length(this->toChar(), this->getCapacity()));
 }
 
+StringBase::SizeType StringBase::serializedSize() const {
+    return static_cast<SizeType>(sizeof(FwSizeStoreType)) + this->length();
+}
+
+StringBase::SizeType StringBase::serializedTruncatedSize(FwSizeType maxLength) const {
+    return static_cast<SizeType>(sizeof(FwSizeStoreType)) + FW_MIN(this->length(), maxLength);
+}
+
 SerializeStatus StringBase::serialize(SerializeBufferBase& buffer) const {
     return buffer.serialize(reinterpret_cast<const U8*>(this->toChar()), this->length());
 }
diff --git a/Fw/Types/StringBase.hpp b/Fw/Types/StringBase.hpp
index 37ea190e48..4e07bc33e8 100644
--- a/Fw/Types/StringBase.hpp
+++ b/Fw/Types/StringBase.hpp
@@ -26,6 +26,9 @@ class StringBase : public Serializable {
     virtual const CHAR* toChar() const = 0;    //<! Convert to a C-style char*
     virtual SizeType getCapacity() const = 0;  //!< return size of buffer
     SizeType length() const;                   //!< Get length of string
+    SizeType serializedSize() const;           //!< Get length of string plus size of stored size
+    SizeType serializedTruncatedSize(
+        FwSizeType maxLength) const;  //!< Get truncated length of string plus size of stored size
 
     const CHAR* operator+=(const CHAR* src);              //!< Concatenate a CHAR*
     const StringBase& operator+=(const StringBase& src);  //!< Concatenate a StringBase
diff --git a/requirements.txt b/requirements.txt
index 61ae357cd4..5bff09c472 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -18,18 +18,18 @@ fprime-fpl-convert-xml==1.0.3
 fprime-fpl-extract-xml==1.0.3
 fprime-fpl-layout==1.0.3
 fprime-fpl-write-pic==1.0.3
-fprime-fpp-check==2.1.0a7
-fprime-fpp-depend==2.1.0a7
-fprime-fpp-filenames==2.1.0a7
-fprime-fpp-format==2.1.0a7
-fprime-fpp-from-xml==2.1.0a7
-fprime-fpp-locate-defs==2.1.0a7
-fprime-fpp-locate-uses==2.1.0a7
-fprime-fpp-syntax==2.1.0a7
-fprime-fpp-to-cpp==2.1.0a7
-fprime-fpp-to-dict==2.1.0a7
-fprime-fpp-to-json==2.1.0a7
-fprime-fpp-to-xml==2.1.0a7
+fprime-fpp-check==2.1.0a9
+fprime-fpp-depend==2.1.0a9
+fprime-fpp-filenames==2.1.0a9
+fprime-fpp-format==2.1.0a9
+fprime-fpp-from-xml==2.1.0a9
+fprime-fpp-locate-defs==2.1.0a9
+fprime-fpp-locate-uses==2.1.0a9
+fprime-fpp-syntax==2.1.0a9
+fprime-fpp-to-cpp==2.1.0a9
+fprime-fpp-to-dict==2.1.0a9
+fprime-fpp-to-json==2.1.0a9
+fprime-fpp-to-xml==2.1.0a9
 fprime-gds==3.4.3
 fprime-tools==3.4.4
 fprime-visual==1.0.2


Test patch:
diff --git a/FppTest/dp/test/ut/TestMain.cpp b/FppTest/dp/test/ut/TestMain.cpp
index d1034cb308..63f0dc9b46 100644
--- a/FppTest/dp/test/ut/TestMain.cpp
+++ b/FppTest/dp/test/ut/TestMain.cpp
@@ -74,6 +74,30 @@ TEST(productRecvIn, Container5_FAILURE) {
     tester.productRecvIn_Container5_FAILURE();
 }
 
+TEST(productRecvIn, Container6_SUCCESS) {
+    COMMENT("Receive Container6 SUCCESS");
+    Tester tester;
+    tester.productRecvIn_Container6_SUCCESS();
+}
+
+TEST(productRecvIn, Container6_FAILURE) {
+    COMMENT("Receive Container6 FAILURE");
+    Tester tester;
+    tester.productRecvIn_Container6_FAILURE();
+}
+
+TEST(productRecvIn, Container7_SUCCESS) {
+    COMMENT("Receive Container7 SUCCESS");
+    Tester tester;
+    tester.productRecvIn_Container7_SUCCESS();
+}
+
+TEST(productRecvIn, Container7_FAILURE) {
+    COMMENT("Receive Container7 FAILURE");
+    Tester tester;
+    tester.productRecvIn_Container7_FAILURE();
+}
+
 int main(int argc, char** argv) {
     ::testing::InitGoogleTest(&argc, argv);
     STest::Random::seed();
diff --git a/FppTest/dp/test/ut/Tester.cpp b/FppTest/dp/test/ut/Tester.cpp
index 9973b8ff02..a37f75a568 100644
--- a/FppTest/dp/test/ut/Tester.cpp
+++ b/FppTest/dp/test/ut/Tester.cpp
@@ -4,10 +4,11 @@
 // \brief  cpp file for DpTest test harness implementation class
 // ======================================================================
 
-#include <cstdio>
 #include <cstring>
 
+#include "FppTest/dp/FppConstantsAc.hpp"
 #include "FppTest/dp/test/ut/Tester.hpp"
+#include "Fw/Types/ExternalString.hpp"
 #include "STest/Pick/Pick.hpp"
 
 namespace FppTest {
@@ -28,16 +29,21 @@ Tester::Tester()
       container4Buffer(this->container4Data, sizeof this->container4Data),
       container5Data{},
       container5Buffer(this->container5Data, sizeof this->container5Data),
+      container6Data{},
+      container6Buffer(this->container6Data, sizeof this->container6Data),
+      container7Data{},
+      container7Buffer(this->container7Data, sizeof this->container7Data),
       component("DpTest",
                 STest::Pick::any(),
                 STest::Pick::any(),
                 this->u8ArrayRecordData,
                 this->u32ArrayRecordData,
-                this->dataArrayRecordData) {
+                this->dataArrayRecordData,
+                this->stringRecordData) {
     this->initComponents();
     this->connectPorts();
     this->component.setIdBase(ID_BASE);
-    // Fill in arrays with random data
+    // Fill in arrays and strings with random data
     for (U8& elt : this->u8ArrayRecordData) {
         elt = static_cast<U8>(STest::Pick::any());
     }
@@ -47,6 +53,7 @@ Tester::Tester()
     for (DpTest_Data& elt : this->dataArrayRecordData) {
         elt.set(static_cast<U16>(STest::Pick::any()));
     }
+    generateRandomString(this->stringRecordData);
 }
 
 Tester::~Tester() {}
@@ -58,12 +65,13 @@ Tester::~Tester() {}
 void Tester::schedIn_OK() {
     this->invoke_to_schedIn(0, 0);
     this->component.doDispatch();
-    ASSERT_PRODUCT_REQUEST_SIZE(5);
+    ASSERT_PRODUCT_REQUEST_SIZE(6);
     ASSERT_PRODUCT_REQUEST(0, ID_BASE + DpTest::ContainerId::Container1, FwSizeType(DpTest::CONTAINER_1_PACKET_SIZE));
     ASSERT_PRODUCT_REQUEST(1, ID_BASE + DpTest::ContainerId::Container2, FwSizeType(DpTest::CONTAINER_2_PACKET_SIZE));
     ASSERT_PRODUCT_REQUEST(2, ID_BASE + DpTest::ContainerId::Container3, FwSizeType(DpTest::CONTAINER_3_PACKET_SIZE));
     ASSERT_PRODUCT_REQUEST(3, ID_BASE + DpTest::ContainerId::Container4, FwSizeType(DpTest::CONTAINER_4_PACKET_SIZE));
     ASSERT_PRODUCT_REQUEST(4, ID_BASE + DpTest::ContainerId::Container5, FwSizeType(DpTest::CONTAINER_5_PACKET_SIZE));
+    ASSERT_PRODUCT_REQUEST(5, ID_BASE + DpTest::ContainerId::Container6, FwSizeType(DpTest::CONTAINER_6_PACKET_SIZE));
     ASSERT_PRODUCT_GET_SIZE(5);
     ASSERT_PRODUCT_GET(0, ID_BASE + DpTest::ContainerId::Container1, FwSizeType(DpTest::CONTAINER_1_PACKET_SIZE));
     ASSERT_PRODUCT_GET(1, ID_BASE + DpTest::ContainerId::Container2, FwSizeType(DpTest::CONTAINER_2_PACKET_SIZE));
@@ -129,6 +137,7 @@ void Tester::productRecvIn_Container2_FAILURE() {
 void Tester::productRecvIn_Container3_SUCCESS() {
     Fw::Buffer buffer;
     FwSizeType expectedNumElts;
+    // Compute the data element size
     const FwSizeType dataEltSize = sizeof(FwSizeStoreType) + this->u8ArrayRecordData.size();
     // Invoke the port and check the header
     this->productRecvIn_InvokeAndCheckHeader(DpTest::ContainerId::Container3, dataEltSize,
@@ -165,6 +174,7 @@ void Tester::productRecvIn_Container3_FAILURE() {
 void Tester::productRecvIn_Container4_SUCCESS() {
     Fw::Buffer buffer;
     FwSizeType expectedNumElts;
+    // Compute the data element size
     const FwSizeType dataEltSize = sizeof(FwSizeStoreType) + this->u32ArrayRecordData.size() * sizeof(U32);
     // Invoke the port and check the header
     this->productRecvIn_InvokeAndCheckHeader(DpTest::ContainerId::Container4, dataEltSize,
@@ -201,7 +211,9 @@ void Tester::productRecvIn_Container4_FAILURE() {
 void Tester::productRecvIn_Container5_SUCCESS() {
     Fw::Buffer buffer;
     FwSizeType expectedNumElts;
-    const FwSizeType dataEltSize = sizeof(FwSizeStoreType) + this->dataArrayRecordData.size() * DpTest_Data::SERIALIZED_SIZE;
+    // Compute the data element size
+    const FwSizeType dataEltSize =
+        sizeof(FwSizeStoreType) + this->dataArrayRecordData.size() * DpTest_Data::SERIALIZED_SIZE;
     // Invoke the port and check the header
     this->productRecvIn_InvokeAndCheckHeader(DpTest::ContainerId::Container5, dataEltSize,
                                              DpTest::ContainerPriority::Container5, this->container5Buffer, buffer,
@@ -234,6 +246,77 @@ void Tester::productRecvIn_Container5_FAILURE() {
     productRecvIn_CheckFailure(DpTest::ContainerId::Container5, this->container5Buffer);
 }
 
+void Tester::productRecvIn_Container6_SUCCESS() {
+    Fw::Buffer buffer;
+    FwSizeType expectedNumElts;
+    // Construct the possibly truncated string
+    // Add one to the string size to account for the null terminator
+    char esData[DpTest_stringSize+1];
+    Fw::ExternalString es(esData, sizeof esData, this->stringRecordData);
+    // Invoke the port and check the header
+    this->productRecvIn_InvokeAndCheckHeader(DpTest::ContainerId::Container6, es.serializedSize(),
+                                             DpTest::ContainerPriority::Container6, this->container6Buffer, buffer,
+                                             expectedNumElts);
+    // Check the data
+    Fw::SerializeBufferBase& serialRepr = buffer.getSerializeRepr();
+    Fw::TestUtil::DpContainerHeader::checkDeserialAtOffset(serialRepr, Fw::DpContainer::DATA_OFFSET);
+    for (FwSizeType i = 0; i < expectedNumElts; ++i) {
+        FwDpIdType id;
+        Fw::String elt;
+        auto status = serialRepr.deserialize(id);
+        ASSERT_EQ(status, Fw::FW_SERIALIZE_OK);
+        const FwDpIdType expectedId = this->component.getIdBase() + DpTest::RecordId::StringRecord;
+        ASSERT_EQ(id, expectedId);
+        status = serialRepr.deserialize(elt);
+        ASSERT_EQ(status, Fw::FW_SERIALIZE_OK);
+        ASSERT_EQ(elt, es);
+    }
+}
+
+void Tester::productRecvIn_Container6_FAILURE() {
+    productRecvIn_CheckFailure(DpTest::ContainerId::Container6, this->container6Buffer);
+}
+
+void Tester::productRecvIn_Container7_SUCCESS() {
+    Fw::Buffer buffer;
+    FwSizeType expectedNumElts;
+    // Construct the possibly truncated string
+    // Add one to the string size to account for the null terminator
+    char esData[DpTest_stringSize+1];
+    Fw::ExternalString es(esData, sizeof esData, this->stringRecordData);
+    const FwSizeType arraySize = DpTest::STRING_ARRAY_RECORD_ARRAY_SIZE;
+    const FwSizeType dataEltSize = sizeof(FwSizeStoreType) + arraySize * es.serializedSize();
+    // Invoke the port and check the header
+    this->productRecvIn_InvokeAndCheckHeader(DpTest::ContainerId::Container7, dataEltSize,
+                                             DpTest::ContainerPriority::Container7, this->container7Buffer, buffer,
+                                             expectedNumElts);
+    // Check the data
+    Fw::SerializeBufferBase& serialRepr = buffer.getSerializeRepr();
+    Fw::TestUtil::DpContainerHeader::checkDeserialAtOffset(serialRepr, Fw::DpContainer::DATA_OFFSET);
+    for (FwSizeType i = 0; i < expectedNumElts; ++i) {
+        FwDpIdType id;
+        auto status = serialRepr.deserialize(id);
+        ASSERT_EQ(status, Fw::FW_SERIALIZE_OK);
+        const FwDpIdType expectedId = this->component.getIdBase() + DpTest::RecordId::StringArrayRecord;
+        ASSERT_EQ(id, expectedId);
+        FwSizeType size;
+        status = serialRepr.deserializeSize(size);
+        ASSERT_EQ(status, Fw::FW_SERIALIZE_OK);
+        ASSERT_EQ(size, arraySize);
+        const U8* const buffAddr = serialRepr.getBuffAddr();
+        for (FwSizeType j = 0; j < size; ++j) {
+            Fw::String elt;
+            status = serialRepr.deserialize(elt);
+            ASSERT_EQ(status, Fw::FW_SERIALIZE_OK);
+            ASSERT_EQ(elt, es);
+        }
+    }
+}
+
+void Tester::productRecvIn_Container7_FAILURE() {
+    productRecvIn_CheckFailure(DpTest::ContainerId::Container7, this->container7Buffer);
+}
+
 // ----------------------------------------------------------------------
 // Helper methods
 // ----------------------------------------------------------------------
@@ -247,6 +330,23 @@ Fw::Time Tester::randomizeTestTime() {
     return time;
 }
 
+void Tester::generateRandomString(Fw::StringBase& str) {
+    // Reserve enough space for max length plus null terminator
+    char buffer[MAX_STRING_LENGTH + 1];
+    // Pick a random string length
+    const FwSizeType length = STest::Pick::lowerUpper(0, MAX_STRING_LENGTH);
+    // Fill buffer with a random null-terminated string with that length
+    FwSizeType i = 0;
+    for (; i < length; i++) {
+        U32 u32 = STest::Pick::lowerUpper(1, std::numeric_limits<char>::max());
+        buffer[i] = static_cast<char>(u32);
+    }
+    FW_ASSERT(i <= sizeof buffer, static_cast<FwAssertArgType>(i), static_cast<FwAssertArgType>(MAX_STRING_LENGTH));
+    buffer[i] = 0;
+    // Copy the contents of buffer into str
+    str.format("%s", buffer);
+}
+
 void Tester::productRecvIn_InvokeAndCheckHeader(FwDpIdType id,
                                                 FwSizeType dataEltSize,
                                                 FwDpPriorityType priority,
diff --git a/FppTest/dp/test/ut/Tester.hpp b/FppTest/dp/test/ut/Tester.hpp
index d941707881..b7021af1da 100644
--- a/FppTest/dp/test/ut/Tester.hpp
+++ b/FppTest/dp/test/ut/Tester.hpp
@@ -28,6 +28,8 @@ class Tester : public DpTestGTestBase {
     static constexpr FwSizeType TEST_INSTANCE_QUEUE_DEPTH = 10;
     // The component id base
     static constexpr FwDpIdType ID_BASE = 100;
+    // The max string length for string data
+    static constexpr FwSizeType MAX_STRING_LENGTH = 100;
 
     //! Construct object Tester
     //!
@@ -75,7 +77,19 @@ class Tester : public DpTestGTestBase {
     //! productRecvIn with Container 5 (FAILURE)
     void productRecvIn_Container5_FAILURE();
 
-  PRIVATE:
+    //! productRecvIn with Container 6 (SUCCESS)
+    void productRecvIn_Container6_SUCCESS();
+
+    //! productRecvIn with Container 6 (FAILURE)
+    void productRecvIn_Container6_FAILURE();
+
+    //! productRecvIn with Container 7 (SUCCESS)
+    void productRecvIn_Container7_SUCCESS();
+
+    //! productRecvIn with Container 7 (FAILURE)
+    void productRecvIn_Container7_FAILURE();
+
+  private:
     // ----------------------------------------------------------------------
     // Handlers for data product ports
     // ----------------------------------------------------------------------
@@ -85,7 +99,7 @@ class Tester : public DpTestGTestBase {
                                       Fw::Buffer& buffer  //!< The buffer
                                       ) override;
 
-  PRIVATE:
+  private:
     // ----------------------------------------------------------------------
     // Helper methods
     // ----------------------------------------------------------------------
@@ -102,6 +116,10 @@ class Tester : public DpTestGTestBase {
     //! \return The time
     Fw::Time randomizeTestTime();
 
+    //! Generate a random string
+    static void generateRandomString(Fw::StringBase& str //!< The string (output)
+        );
+
     //! Invoke productRecvIn and check header
     //! This sets the output buffer to the received buffer and sets the
     //! deserialization pointer to the start of the data payload
@@ -118,7 +136,7 @@ class Tester : public DpTestGTestBase {
                                     Fw::Buffer buffer  //!< The buffer
     );
 
-  PRIVATE:
+  private:
     // ----------------------------------------------------------------------
     // Variables
     // ----------------------------------------------------------------------
@@ -153,6 +171,18 @@ class Tester : public DpTestGTestBase {
     //! Buffer for Container 5
     const Fw::Buffer container5Buffer;
 
+    //! Buffer data for Container 6
+    U8 container6Data[DpTest::CONTAINER_6_PACKET_SIZE];
+
+    //! Buffer for Container 6
+    const Fw::Buffer container6Buffer;
+
+    //! Buffer data for Container 7
+    U8 container7Data[DpTest::CONTAINER_7_PACKET_SIZE];
+
+    //! Buffer for Container 7
+    const Fw::Buffer container7Buffer;
+
     //! Data for U8 array record
     DpTest::U8ArrayRecordData u8ArrayRecordData;
 
@@ -162,6 +192,9 @@ class Tester : public DpTestGTestBase {
     //! Data for Data array record
     DpTest::DataArrayRecordData dataArrayRecordData;
 
+    //! Data for String record
+    Fw::String stringRecordData;
+
     //! The component under test
     DpTest component;
 };