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_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