Add libbej encoder wrapper for C++

Add unit tests and the test data files needed for unit tests.
Move APIs from bej_decoder_test.cpp to a common file, to share those
with bej_encoder_test.cpp

Tested:
Verified that encoding using the wrapper works well at runtime.
Verified that the unit tests pass.

Change-Id: I61b7c17690eb7e7fefd1973a22d4316c6214267e
Signed-off-by: Nikhil Namjoshi <nikhilnamjoshi@google.com>
diff --git a/test/bej_decoder_test.cpp b/test/bej_decoder_test.cpp
index b390017..16cd19a 100644
--- a/test/bej_decoder_test.cpp
+++ b/test/bej_decoder_test.cpp
@@ -1,11 +1,7 @@
+#include "bej_common_test.hpp"
 #include "bej_decoder_json.hpp"
-#include "nlohmann/json.hpp"
 
-#include <fstream>
-#include <iostream>
 #include <memory>
-#include <optional>
-#include <span>
 #include <string_view>
 
 #include <gmock/gmock-matchers.h>
@@ -15,24 +11,6 @@
 namespace libbej
 {
 
-struct BejTestInputFiles
-{
-    const char* jsonFile;
-    const char* schemaDictionaryFile;
-    const char* annotationDictionaryFile;
-    const char* errorDictionaryFile;
-    const char* encodedStreamFile;
-};
-
-struct BejTestInputs
-{
-    const nlohmann::json expectedJson;
-    const uint8_t* schemaDictionary;
-    const uint8_t* annotationDictionary;
-    const uint8_t* errorDictionary;
-    std::span<const uint8_t> encodedStream;
-};
-
 struct BejDecoderTestParams
 {
     const std::string testName;
@@ -73,82 +51,6 @@
     .encodedStreamFile = "../test/encoded/dummy_simple_enc.bin",
 };
 
-// Buffer size for storing a single binary file data.
-constexpr uint32_t maxBufferSize = 16 * 1024;
-
-std::streamsize readBinaryFile(const char* fileName, std::span<uint8_t> buffer)
-{
-    std::ifstream inputStream(fileName, std::ios::binary);
-    if (!inputStream.is_open())
-    {
-        std::cerr << "Cannot open file: " << fileName << "\n";
-        return 0;
-    }
-    auto readLength = inputStream.readsome(
-        reinterpret_cast<char*>(buffer.data()), buffer.size_bytes());
-    if (inputStream.peek() != EOF)
-    {
-        std::cerr << "Failed to read the complete file: " << fileName
-                  << "  read length: " << readLength << "\n";
-        return 0;
-    }
-    return readLength;
-}
-
-std::optional<BejTestInputs> loadInputs(const BejTestInputFiles& files,
-                                        bool readErrorDictionary = false)
-{
-    std::ifstream jsonInput(files.jsonFile);
-    if (!jsonInput.is_open())
-    {
-        std::cerr << "Cannot open file: " << files.jsonFile << "\n";
-        return std::nullopt;
-    }
-    nlohmann::json expJson;
-    jsonInput >> expJson;
-
-    static uint8_t schemaDictBuffer[maxBufferSize];
-    if (readBinaryFile(files.schemaDictionaryFile,
-                       std::span(schemaDictBuffer, maxBufferSize)) == 0)
-    {
-        return std::nullopt;
-    }
-
-    static uint8_t annoDictBuffer[maxBufferSize];
-    if (readBinaryFile(files.annotationDictionaryFile,
-                       std::span(annoDictBuffer, maxBufferSize)) == 0)
-    {
-        return std::nullopt;
-    }
-
-    static uint8_t encBuffer[maxBufferSize];
-    auto encLen = readBinaryFile(files.encodedStreamFile,
-                                 std::span(encBuffer, maxBufferSize));
-    if (encLen == 0)
-    {
-        return std::nullopt;
-    }
-
-    static uint8_t errorDict[maxBufferSize];
-    if (readErrorDictionary)
-    {
-        if (readBinaryFile(files.errorDictionaryFile,
-                           std::span(errorDict, maxBufferSize)) == 0)
-        {
-            return std::nullopt;
-        }
-    }
-
-    BejTestInputs inputs = {
-        .expectedJson = expJson,
-        .schemaDictionary = schemaDictBuffer,
-        .annotationDictionary = annoDictBuffer,
-        .errorDictionary = errorDict,
-        .encodedStream = std::span(encBuffer, encLen),
-    };
-    return inputs;
-}
-
 TEST_P(BejDecoderTest, Decode)
 {
     const BejDecoderTestParams& test_case = GetParam();
diff --git a/test/bej_encoder_test.cpp b/test/bej_encoder_test.cpp
new file mode 100644
index 0000000..d258b4b
--- /dev/null
+++ b/test/bej_encoder_test.cpp
@@ -0,0 +1,442 @@
+#include "bej_dictionary.h"
+#include "bej_encoder_core.h"
+#include "bej_tree.h"
+
+#include "bej_common_test.hpp"
+#include "bej_decoder_json.hpp"
+#include "bej_encoder_json.hpp"
+
+#include <vector>
+
+#include <gmock/gmock-matchers.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace libbej
+{
+
+struct BejEncoderTestParams
+{
+    const std::string testName;
+    const BejTestInputFiles inputFiles;
+    std::string expectedJson;
+    struct RedfishPropertyParent* (*createResource)();
+};
+
+using BejEncoderTest = testing::TestWithParam<BejEncoderTestParams>;
+
+const BejTestInputFiles dummySimpleTestFiles = {
+    .jsonFile = "../test/json/dummysimple.json",
+    .schemaDictionaryFile = "../test/dictionaries/dummy_simple_dict.bin",
+    .annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
+    .errorDictionaryFile = "",
+    .encodedStreamFile = "../test/encoded/dummy_simple_enc.bin",
+};
+
+const BejTestInputFiles driveOemTestFiles = {
+    .jsonFile = "../test/json/drive_oem.json",
+    .schemaDictionaryFile = "../test/dictionaries/drive_oem_dict.bin",
+    .annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
+    .errorDictionaryFile = "",
+    .encodedStreamFile = "../test/encoded/drive_oem_enc.bin",
+};
+
+const BejTestInputFiles chassisTestFiles = {
+    .jsonFile = "../test/json/chassis.json",
+    .schemaDictionaryFile = "../test/dictionaries/chassis_dict.bin",
+    .annotationDictionaryFile = "../test/dictionaries/annotation_dict.bin",
+    .errorDictionaryFile = "",
+    .encodedStreamFile = "../test/encoded/chassis_enc.bin",
+};
+
+struct RedfishPropertyParent* createDummyResource()
+{
+    static struct RedfishPropertyParent root;
+    bejTreeInitSet(&root, "DummySimple");
+
+    static struct RedfishPropertyLeafString id;
+    bejTreeAddString(&root, &id, "Id", "Dummy ID");
+
+    static struct RedfishPropertyLeafInt intProp;
+    bejTreeAddInteger(&root, &intProp, "SampleIntegerProperty", -5);
+
+    static struct RedfishPropertyLeafReal real;
+    bejTreeAddReal(&root, &real, "SampleRealProperty", -5576.90001);
+
+    static struct RedfishPropertyLeafNull enProp;
+    bejTreeAddNull(&root, &enProp, "SampleEnabledProperty");
+
+    static struct RedfishPropertyParent chArraySet1;
+    bejTreeInitSet(&chArraySet1, nullptr);
+
+    static struct RedfishPropertyLeafBool chArraySet1bool;
+    bejTreeAddBool(&chArraySet1, &chArraySet1bool, "AnotherBoolean", true);
+
+    static struct RedfishPropertyLeafEnum chArraySet1Ls;
+    bejTreeAddEnum(&chArraySet1, &chArraySet1Ls, "LinkStatus", "NoLink");
+
+    static struct RedfishPropertyParent chArraySet2;
+    bejTreeInitSet(&chArraySet2, nullptr);
+
+    static struct RedfishPropertyLeafEnum chArraySet2Ls;
+    bejTreeAddEnum(&chArraySet2, &chArraySet2Ls, "LinkStatus", "LinkDown");
+
+    static struct RedfishPropertyParent chArray;
+    bejTreeInitArray(&chArray, "ChildArrayProperty");
+
+    bejTreeLinkChildToParent(&chArray, &chArraySet1);
+    bejTreeLinkChildToParent(&chArray, &chArraySet2);
+
+    bejTreeLinkChildToParent(&root, &chArray);
+    return &root;
+}
+
+const std::string driveOemJson = R"(
+      {
+        "@odata.id": "/redfish/v1/drives/1",
+        "@odata.type": "#Drive.v1_5_0.Drive",
+        "Id": "Drive1",
+        "Actions": {
+            "#Drive.Reset": {
+                "target": "/redfish/v1/drives/1/Actions/Drive.Reset",
+                "title": "Reset a Drive",
+                "ResetType@Redfish.AllowableValues": [
+                        "On",
+                        "ForceOff",
+                        "ForceRestart",
+                        "Nmi",
+                        "ForceOn",
+                        "PushPowerButton"
+                    ]
+            }
+        },
+        "Status@Message.ExtendedInfo": [
+            {
+                "MessageId": "PredictiveFailure",
+                "RelatedProperties": ["FailurePredicted", "MediaType"]
+            }
+        ],
+        "Identifiers": [],
+        "Links": {}
+    }
+)";
+
+struct RedfishPropertyParent* createDriveOem()
+{
+    static struct RedfishPropertyParent root;
+    bejTreeInitSet(&root, "Drive");
+
+    static struct RedfishPropertyLeafString odataId;
+    bejTreeAddString(&root, &odataId, "@odata.id", "/redfish/v1/drives/1");
+
+    static struct RedfishPropertyLeafString odataType;
+    bejTreeAddString(&root, &odataType, "@odata.type", "#Drive.v1_5_0.Drive");
+
+    static struct RedfishPropertyLeafString id;
+    bejTreeAddString(&root, &id, "Id", "Drive1");
+
+    static struct RedfishPropertyParent actions;
+    bejTreeInitSet(&actions, "Actions");
+
+    static struct RedfishPropertyParent drRst;
+    bejTreeInitSet(&drRst, "#Drive.Reset");
+
+    static struct RedfishPropertyLeafString drRstTarget;
+    bejTreeAddString(&drRst, &drRstTarget, "target",
+                     "/redfish/v1/drives/1/Actions/Drive.Reset");
+
+    static struct RedfishPropertyLeafString drRstTitle;
+    bejTreeAddString(&drRst, &drRstTitle, "title", "Reset a Drive");
+
+    static struct RedfishPropertyParent drRstType;
+    bejTreeInitPropertyAnnotated(&drRstType, "ResetType");
+
+    static struct RedfishPropertyParent drRstTypeAllowable;
+    bejTreeInitArray(&drRstTypeAllowable, "@Redfish.AllowableValues");
+
+    static struct RedfishPropertyLeafString drRstTypeAllowableS1;
+    bejTreeAddString(&drRstTypeAllowable, &drRstTypeAllowableS1, "", "On");
+
+    static struct RedfishPropertyLeafString drRstTypeAllowableS2;
+    bejTreeAddString(&drRstTypeAllowable, &drRstTypeAllowableS2, "",
+                     "ForceOff");
+
+    static struct RedfishPropertyLeafString drRstTypeAllowableS3;
+    bejTreeAddString(&drRstTypeAllowable, &drRstTypeAllowableS3, "",
+                     "ForceRestart");
+
+    static struct RedfishPropertyLeafString drRstTypeAllowableS4;
+    bejTreeAddString(&drRstTypeAllowable, &drRstTypeAllowableS4, "", "Nmi");
+
+    static struct RedfishPropertyLeafString drRstTypeAllowableS5;
+    bejTreeAddString(&drRstTypeAllowable, &drRstTypeAllowableS5, "", "ForceOn");
+
+    static struct RedfishPropertyLeafString drRstTypeAllowableS6;
+    bejTreeAddString(&drRstTypeAllowable, &drRstTypeAllowableS6, "",
+                     "PushPowerButton");
+
+    bejTreeLinkChildToParent(&drRstType, &drRstTypeAllowable);
+    bejTreeLinkChildToParent(&drRst, &drRstType);
+    bejTreeLinkChildToParent(&actions, &drRst);
+    bejTreeLinkChildToParent(&root, &actions);
+
+    static struct RedfishPropertyParent statusAnt;
+    bejTreeInitPropertyAnnotated(&statusAnt, "Status");
+
+    static struct RedfishPropertyParent statusAntMsgExtInfo;
+    bejTreeInitArray(&statusAntMsgExtInfo, "@Message.ExtendedInfo");
+
+    static struct RedfishPropertyParent statusAntMsgExtInfoSet1;
+    bejTreeInitSet(&statusAntMsgExtInfoSet1, nullptr);
+
+    static struct RedfishPropertyLeafString statusAntMsgExtInfoSet1P1;
+    bejTreeAddString(&statusAntMsgExtInfoSet1, &statusAntMsgExtInfoSet1P1,
+                     "MessageId", "PredictiveFailure");
+
+    static struct RedfishPropertyParent statusAntMsgExtInfoSet1P2;
+    bejTreeInitArray(&statusAntMsgExtInfoSet1P2, "RelatedProperties");
+    bejTreeLinkChildToParent(&statusAntMsgExtInfoSet1,
+                             &statusAntMsgExtInfoSet1P2);
+
+    static struct RedfishPropertyLeafString statusAntMsgExtInfoSet1P2Ele1;
+    bejTreeAddString(&statusAntMsgExtInfoSet1P2, &statusAntMsgExtInfoSet1P2Ele1,
+                     "", "FailurePredicted");
+
+    static struct RedfishPropertyLeafString statusAntMsgExtInfoSet1P2Ele2;
+    bejTreeAddString(&statusAntMsgExtInfoSet1P2, &statusAntMsgExtInfoSet1P2Ele2,
+                     "", "MediaType");
+
+    bejTreeLinkChildToParent(&statusAntMsgExtInfo, &statusAntMsgExtInfoSet1);
+    bejTreeLinkChildToParent(&statusAnt, &statusAntMsgExtInfo);
+    bejTreeLinkChildToParent(&root, &statusAnt);
+
+    static struct RedfishPropertyParent identifiers;
+    bejTreeInitArray(&identifiers, "Identifiers");
+    bejTreeLinkChildToParent(&root, &identifiers);
+
+    static struct RedfishPropertyParent links;
+    bejTreeInitSet(&links, "Links");
+    bejTreeLinkChildToParent(&root, &links);
+
+    return &root;
+}
+
+/**
+ * @brief Storage for an array of links with an annotated odata.count.
+ *
+ * This doesn't contain storage for the link itself.
+ *
+ * Eg:
+ * "Contains": [],
+ * "Contains@odata.count": 0,
+ */
+struct RedfishArrayOfLinksJson
+{
+    struct RedfishPropertyParent array;
+    struct RedfishPropertyParent annotatedProperty;
+    struct RedfishPropertyLeafInt count;
+};
+
+/**
+ * @brief Storage for a single odata.id link inside a JSON "Set" object.
+ *
+ * Eg: FieldName: {
+ *   "@odata.id": "/redfish/v1/Chassis/Something"
+ * }
+ */
+struct RedfishLinkJson
+{
+    struct RedfishPropertyParent set;
+    struct RedfishPropertyLeafString odataId;
+};
+
+void addLinkToTree(struct RedfishPropertyParent* parent,
+                   struct RedfishPropertyParent* linkSet,
+                   const char* linkSetLabel,
+                   struct RedfishPropertyLeafString* odataId,
+                   const char* linkValue)
+{
+    bejTreeInitSet(linkSet, linkSetLabel);
+    bejTreeAddString(linkSet, odataId, "@odata.id", linkValue);
+    bejTreeLinkChildToParent(parent, linkSet);
+}
+
+void redfishCreateArrayOfLinksJson(struct RedfishPropertyParent* parent,
+                                   const char* arrayName, int linkCount,
+                                   const char* const links[],
+                                   struct RedfishArrayOfLinksJson* linksInfo,
+                                   struct RedfishLinkJson* linkJsonArray)
+{
+    bejTreeInitArray(&linksInfo->array, arrayName);
+    bejTreeLinkChildToParent(parent, &linksInfo->array);
+
+    bejTreeInitPropertyAnnotated(&linksInfo->annotatedProperty, arrayName);
+    bejTreeAddInteger(&linksInfo->annotatedProperty, &linksInfo->count,
+                      "@odata.count", linkCount);
+    bejTreeLinkChildToParent(parent, &linksInfo->annotatedProperty);
+
+    for (int i = 0; i < linkCount; ++i)
+    {
+        addLinkToTree(&linksInfo->array, &linkJsonArray[i].set, NULL,
+                      &linkJsonArray[i].odataId, links[i]);
+    }
+}
+
+struct RedfishPropertyParent* createChassisResource()
+{
+    constexpr int containsLinkCount = 2;
+    const char* contains[containsLinkCount] = {"/redfish/v1/Chassis/Disk_0",
+                                               "/redfish/v1/Chassis/Disk_1"};
+    const char* storage[1] = {"/redfish/v1/Systems/system/Storage/SATA"};
+    const char* drives[1] = {"/redfish/v1/Chassis/SomeChassis/Drives/SATA_0"};
+    static struct RedfishPropertyParent root;
+    static struct RedfishPropertyLeafString odataId;
+    static struct RedfishPropertyParent links;
+    static struct RedfishPropertyParent computerSystemsArray;
+    static struct RedfishPropertyParent computerSystemsLinkSet;
+    static struct RedfishPropertyLeafString computerSystemsLinkOdataId;
+    static struct RedfishPropertyParent containedBySet;
+    static struct RedfishPropertyLeafString containedByOdataId;
+    static struct RedfishArrayOfLinksJson containsArray;
+    static struct RedfishLinkJson containsLinks[containsLinkCount];
+    static struct RedfishArrayOfLinksJson storageArray;
+    static struct RedfishLinkJson storageLink;
+    static struct RedfishArrayOfLinksJson drives_array;
+    static struct RedfishLinkJson drive_link;
+
+    bejTreeInitSet(&root, "Chassis");
+    bejTreeAddString(&root, &odataId, "@odata.id",
+                     "/redfish/v1/Chassis/SomeChassis");
+
+    bejTreeInitSet(&links, "Links");
+    bejTreeLinkChildToParent(&root, &links);
+
+    bejTreeInitArray(&computerSystemsArray, "ComputerSystems");
+    bejTreeLinkChildToParent(&links, &computerSystemsArray);
+
+    addLinkToTree(&computerSystemsArray, &computerSystemsLinkSet, "",
+                  &computerSystemsLinkOdataId, "/redfish/v1/Systems/system");
+
+    addLinkToTree(&links, &containedBySet, "ContainedBy", &containedByOdataId,
+                  "/redfish/v1/Chassis/SomeOtherChassis");
+
+    redfishCreateArrayOfLinksJson(&links, "Contains", containsLinkCount,
+                                  contains, &containsArray, containsLinks);
+
+    redfishCreateArrayOfLinksJson(&links, "Storage", /*linkCount=*/1, storage,
+                                  &storageArray, &storageLink);
+
+    redfishCreateArrayOfLinksJson(&links, "Drives", /*linkCount=*/1, drives,
+                                  &drives_array, &drive_link);
+
+    return &root;
+}
+
+TEST_P(BejEncoderTest, Encode)
+{
+    const BejEncoderTestParams& test_case = GetParam();
+    auto inputsOrErr = loadInputs(test_case.inputFiles);
+    EXPECT_TRUE(inputsOrErr);
+
+    BejDictionaries dictionaries = {
+        .schemaDictionary = inputsOrErr->schemaDictionary,
+        .annotationDictionary = inputsOrErr->annotationDictionary,
+        .errorDictionary = inputsOrErr->errorDictionary,
+    };
+
+    std::vector<uint8_t> outputBuffer;
+    struct BejEncoderOutputHandler output = {
+        .handlerContext = &outputBuffer,
+        .recvOutput = &getBejEncodedBuffer,
+    };
+
+    std::vector<void*> pointerStack;
+    struct BejPointerStackCallback stackCallbacks = {
+        .stackContext = &pointerStack,
+        .stackEmpty = stackEmpty,
+        .stackPeek = stackPeek,
+        .stackPop = stackPop,
+        .stackPush = stackPush,
+        .deleteStack = NULL,
+    };
+
+    bejEncode(&dictionaries, BEJ_DICTIONARY_START_AT_HEAD, bejMajorSchemaClass,
+              test_case.createResource(), &output, &stackCallbacks);
+
+    BejDecoderJson decoder;
+    EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)), 0);
+    std::string decoded = decoder.getOutput();
+    nlohmann::json jsonDecoded = nlohmann::json::parse(decoded);
+
+    if (!test_case.expectedJson.empty())
+    {
+        inputsOrErr->expectedJson =
+            nlohmann::json::parse(test_case.expectedJson);
+    }
+    EXPECT_TRUE(jsonDecoded.dump() == inputsOrErr->expectedJson.dump());
+}
+
+TEST_P(BejEncoderTest, EncodeWrapper)
+{
+    const BejEncoderTestParams& test_case = GetParam();
+    auto inputsOrErr = loadInputs(test_case.inputFiles);
+    EXPECT_TRUE(inputsOrErr);
+
+    BejDictionaries dictionaries = {
+        .schemaDictionary = inputsOrErr->schemaDictionary,
+        .annotationDictionary = inputsOrErr->annotationDictionary,
+        .errorDictionary = inputsOrErr->errorDictionary,
+    };
+
+    libbej::BejEncoderJson encoder;
+    encoder.encode(&dictionaries, bejMajorSchemaClass,
+                   test_case.createResource());
+
+    std::vector<uint8_t> outputBuffer = encoder.getOutput();
+    BejDecoderJson decoder;
+    EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)), 0);
+    std::string decoded = decoder.getOutput();
+    nlohmann::json jsonDecoded = nlohmann::json::parse(decoded);
+
+    if (!test_case.expectedJson.empty())
+    {
+        inputsOrErr->expectedJson =
+            nlohmann::json::parse(test_case.expectedJson);
+    }
+    EXPECT_TRUE(jsonDecoded.dump() == inputsOrErr->expectedJson.dump());
+
+    // Try using the same encoder object again to ensure that the same object
+    // does the encoding correctly
+    encoder.encode(&dictionaries, bejMajorSchemaClass,
+                   test_case.createResource());
+
+    outputBuffer = encoder.getOutput();
+
+    EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)), 0);
+    decoded = decoder.getOutput();
+    jsonDecoded = nlohmann::json::parse(decoded);
+
+    if (!test_case.expectedJson.empty())
+    {
+        inputsOrErr->expectedJson =
+            nlohmann::json::parse(test_case.expectedJson);
+    }
+    EXPECT_TRUE(jsonDecoded.dump() == inputsOrErr->expectedJson.dump());
+}
+
+/**
+ * TODO: Add more test cases.
+ */
+
+INSTANTIATE_TEST_SUITE_P(
+    , BejEncoderTest,
+    testing::ValuesIn<BejEncoderTestParams>({
+        {"DriveOEM", driveOemTestFiles, driveOemJson, &createDriveOem},
+        {"DummySimple", dummySimpleTestFiles, "", &createDummyResource},
+        {"Chassis", chassisTestFiles, "", &createChassisResource},
+    }),
+    [](const testing::TestParamInfo<BejEncoderTest::ParamType>& info) {
+    return info.param.testName;
+});
+
+} // namespace libbej
diff --git a/test/dictionaries/chassis_dict.bin b/test/dictionaries/chassis_dict.bin
new file mode 100644
index 0000000..88b3b4d
--- /dev/null
+++ b/test/dictionaries/chassis_dict.bin
Binary files differ
diff --git a/test/encoded/chassis_enc.bin b/test/encoded/chassis_enc.bin
new file mode 100644
index 0000000..a4d4172
--- /dev/null
+++ b/test/encoded/chassis_enc.bin
Binary files differ
diff --git a/test/include/bej_common_test.hpp b/test/include/bej_common_test.hpp
new file mode 100644
index 0000000..74d8c83
--- /dev/null
+++ b/test/include/bej_common_test.hpp
@@ -0,0 +1,109 @@
+#include "bej_dictionary.h"
+#include "bej_encoder_core.h"
+#include "bej_tree.h"
+
+#include "nlohmann/json.hpp"
+
+#include <fstream>
+#include <iostream>
+#include <optional>
+#include <span>
+
+namespace libbej
+{
+
+// Buffer size for storing a single binary file data.
+constexpr uint32_t maxBufferSize = 16 * 1024;
+
+struct BejTestInputFiles
+{
+    const char* jsonFile;
+    const char* schemaDictionaryFile;
+    const char* annotationDictionaryFile;
+    const char* errorDictionaryFile;
+    const char* encodedStreamFile;
+};
+
+struct BejTestInputs
+{
+    nlohmann::json expectedJson;
+    const uint8_t* schemaDictionary;
+    const uint8_t* annotationDictionary;
+    const uint8_t* errorDictionary;
+    std::span<const uint8_t> encodedStream;
+};
+
+std::streamsize readBinaryFile(const char* fileName, std::span<uint8_t> buffer)
+{
+    std::ifstream inputStream(fileName, std::ios::binary);
+    if (!inputStream.is_open())
+    {
+        std::cerr << "Cannot open file: " << fileName << "\n";
+        return 0;
+    }
+    auto readLength = inputStream.readsome(
+        reinterpret_cast<char*>(buffer.data()), buffer.size_bytes());
+    if (inputStream.peek() != EOF)
+    {
+        std::cerr << "Failed to read the complete file: " << fileName
+                  << "  read length: " << readLength << "\n";
+        return 0;
+    }
+    return readLength;
+}
+
+std::optional<BejTestInputs> loadInputs(const BejTestInputFiles& files,
+                                        bool readErrorDictionary = false)
+{
+    std::ifstream jsonInput(files.jsonFile);
+    if (!jsonInput.is_open())
+    {
+        std::cerr << "Cannot open file: " << files.jsonFile << "\n";
+        return std::nullopt;
+    }
+    nlohmann::json expJson;
+    jsonInput >> expJson;
+
+    static uint8_t schemaDictBuffer[maxBufferSize];
+    if (readBinaryFile(files.schemaDictionaryFile,
+                       std::span(schemaDictBuffer, maxBufferSize)) == 0)
+    {
+        return std::nullopt;
+    }
+
+    static uint8_t annoDictBuffer[maxBufferSize];
+    if (readBinaryFile(files.annotationDictionaryFile,
+                       std::span(annoDictBuffer, maxBufferSize)) == 0)
+    {
+        return std::nullopt;
+    }
+
+    static uint8_t encBuffer[maxBufferSize];
+    auto encLen = readBinaryFile(files.encodedStreamFile,
+                                 std::span(encBuffer, maxBufferSize));
+    if (encLen == 0)
+    {
+        return std::nullopt;
+    }
+
+    static uint8_t errorDict[maxBufferSize];
+    if (readErrorDictionary)
+    {
+        if (readBinaryFile(files.errorDictionaryFile,
+                           std::span(errorDict, maxBufferSize)) == 0)
+        {
+            return std::nullopt;
+        }
+    }
+
+    BejTestInputs inputs = {
+        .expectedJson = expJson,
+        .schemaDictionary = schemaDictBuffer,
+        .annotationDictionary = annoDictBuffer,
+        .errorDictionary = errorDict,
+        .encodedStream = std::span(encBuffer, encLen),
+    };
+    return inputs;
+}
+
+} // namespace libbej
diff --git a/test/json/chassis.json b/test/json/chassis.json
new file mode 100644
index 0000000..40cc88d
--- /dev/null
+++ b/test/json/chassis.json
@@ -0,0 +1,34 @@
+{
+    "@odata.id": "/redfish/v1/Chassis/SomeChassis",
+    "Links": {
+        "ComputerSystems": [
+            {
+                "@odata.id": "/redfish/v1/Systems/system"
+            }
+        ],
+        "ContainedBy": {
+            "@odata.id": "/redfish/v1/Chassis/SomeOtherChassis"
+        },
+        "Contains": [
+            {
+                "@odata.id": "/redfish/v1/Chassis/Disk_0"
+            },
+            {
+                "@odata.id": "/redfish/v1/Chassis/Disk_1"
+            }
+        ],
+        "Contains@odata.count": 2,
+        "Storage": [
+            {
+                "@odata.id": "/redfish/v1/Systems/system/Storage/SATA"
+            }
+        ],
+        "Storage@odata.count": 1,
+        "Drives": [
+            {
+                "@odata.id": "/redfish/v1/Chassis/SomeChassis/Drives/SATA_0"
+            }
+        ],
+        "Drives@odata.count": 1
+    }
+}
diff --git a/test/meson.build b/test/meson.build
index a4ddfe1..c61ab8c 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -20,10 +20,14 @@
   'bej_common',
   'bej_dictionary',
   'bej_tree',
+  'bej_encoder'
 ]
+
+libbej_test_incs = include_directories('include')
 foreach t : gtests
   test(t, executable(t.underscorify(), t + '_test.cpp',
                      build_by_default: false,
                      implicit_include_directories: false,
+                     include_directories: libbej_test_incs,
                      dependencies: [libbej, gtest, gmock]))
 endforeach