Filter/MLAgent: Add unit test cases for parsing MLAgent URIs
authorWook Song <wook16.song@samsung.com>
Thu, 14 Dec 2023 03:57:55 +0000 (12:57 +0900)
committerwooksong <wook16.song@samsung.com>
Tue, 2 Jan 2024 11:48:05 +0000 (20:48 +0900)
This patch adds test cases for unit testing of parsing the MLAgent URIs.

Signed-off-by: Wook Song <wook16.song@samsung.com>
tests/meson.build
tests/unittest_mlagent/mock_mlagent.cc [new file with mode: 0644]
tests/unittest_mlagent/mock_mlagent.h [new file with mode: 0644]
tests/unittest_mlagent/unittest_mlagent.cc [new file with mode: 0644]

index 850f2dd..f2d5aa4 100644 (file)
@@ -355,6 +355,26 @@ if gtest_dep.found()
     subdir('nnstreamer_datarepo')
   endif
 
+  if ml_agent_support_is_available
+    ml_agent_lib_common_objs = nnstreamer_single_lib.extract_objects('ml_agent.c')
+
+    lib_mlagent_mock = static_library('mock_mlagentmock',
+      join_paths('unittest_mlagent', 'mock_mlagent.cc'),
+      dependencies: [glib_dep, json_glib_dep],
+      include_directories: nnstreamer_inc,
+      install: get_option('install-test'),
+      install_dir: nnstreamer_libdir
+    )
+
+    whole_dep = declare_dependency(link_whole: lib_mlagent_mock)
+
+    filter_mlagent = executable('unittest_mlagent',
+      join_paths('unittest_mlagent', 'unittest_mlagent.cc'),
+      objects:ml_agent_lib_common_objs,
+      dependencies: [nnstreamer_unittest_deps, nnstreamer_internal_deps, whole_dep],
+    )
+  endif
+
     # ONNXRUNTIME unittest
   if onnxruntime_support_is_available
     unittest_filter_onnxruntime = executable('unittest_filter_onnxruntime',
diff --git a/tests/unittest_mlagent/mock_mlagent.cc b/tests/unittest_mlagent/mock_mlagent.cc
new file mode 100644 (file)
index 0000000..65100d8
--- /dev/null
@@ -0,0 +1,114 @@
+/* SPDX-License-Identifier: LGPL-2.1-only */
+/**
+ * @file    mlagent_mock.cc
+ * @date    30 Nov 2023
+ * @brief   A class that mocks the ML Agent instance
+ * @author  Wook Song <wook16.song@samsung.com>
+ * @see     http://github.com/nnstreamer/nnstreamer
+ * @bug     No known bugs
+ *
+ */
+
+#include <glib.h>
+#include <json-glib/json-glib.h>
+#include <nnstreamer_util.h>
+
+#include <functional>
+#include <iostream>
+#include <memory>
+
+#include "mock_mlagent.h"
+
+static std::unique_ptr<MockMLAgent> uptr_mock;
+
+/**
+ * @brief Generate C-stringified JSON
+ */
+gchar *
+MockModel::to_cstr_json ()
+{
+  using json_member_name_to_cb_t
+      = std::pair<std::string, std::function<std::string ()>>;
+
+  const std::vector<json_member_name_to_cb_t> json_mem_to_cb_map{
+    json_member_name_to_cb_t (
+        "path", [this] () -> std::string { return path (); }),
+    json_member_name_to_cb_t (
+        "description", [this] () -> std::string { return desc (); }),
+    json_member_name_to_cb_t (
+        "app_info", [this] () -> std::string { return app_info (); }),
+    json_member_name_to_cb_t ("version",
+        [this] () -> std::string { return std::to_string (version ()); }),
+    json_member_name_to_cb_t ("active",
+        [this] () -> std::string { return (is_activated () ? "T" : "F"); }),
+  };
+
+  g_autoptr (JsonBuilder) builder = json_builder_new ();
+  g_autoptr (JsonGenerator) gen = NULL;
+
+  json_builder_begin_object (builder);
+  for (auto iter : json_mem_to_cb_map) {
+    json_builder_set_member_name (builder, iter.first.c_str ());
+    json_builder_add_string_value (builder, iter.second ().c_str ());
+  }
+  json_builder_end_object (builder);
+
+  {
+    g_autoptr (JsonNode) root = json_builder_get_root (builder);
+
+    gen = json_generator_new ();
+    json_generator_set_root (gen, root);
+    json_generator_set_pretty (gen, TRUE);
+  }
+
+  return json_generator_to_data (gen, NULL);
+}
+
+/**
+ * @brief Initialize the static unique_ptr of MockMLAgent
+ */
+void
+ml_agent_mock_init ()
+{
+  uptr_mock = std::make_unique<MockMLAgent> ();
+}
+
+/**
+ * @brief C-wrapper for the MockModel's method add_model.
+ */
+bool
+ml_agent_mock_add_model (const gchar *name, const gchar *path, const gchar *app_info,
+    const bool is_activated, const char *desc, const guint version)
+{
+  MockModel model{ name, path, app_info, is_activated, desc, version };
+
+  return uptr_mock->add_model (model);
+}
+
+/**
+ * @brief C-wrapper for the MockModel's method get_model.
+ */
+MockModel *
+ml_agent_mock_model_get (const gchar *name, const guint version)
+{
+  return uptr_mock->get_model (name, version);
+}
+
+/**
+ * @brief Pass the JSON c-string generated by the ML Agent mock class to the caller.
+ */
+gint
+ml_agent_model_get (const gchar *name, const guint version, gchar **description, GError **err)
+{
+  MockModel *model_ptr = ml_agent_mock_model_get (name, version);
+
+  g_return_val_if_fail (err == NULL || *err == NULL, -EINVAL);
+
+  if (model_ptr == nullptr) {
+    return -EINVAL;
+  }
+
+  *description = model_ptr->to_cstr_json ();
+
+  return 0;
+}
diff --git a/tests/unittest_mlagent/mock_mlagent.h b/tests/unittest_mlagent/mock_mlagent.h
new file mode 100644 (file)
index 0000000..da043bd
--- /dev/null
@@ -0,0 +1,225 @@
+/**
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * @file        mlagent_mock.h
+ * @date        29 Nov 2023
+ * @brief       Mocking ML Agent APIs
+ * @see         https://github.com/nnstreamer/nnstreamer
+ * @author      Wook Song <wook.song16@samsung.com>
+ * @bug         No known bugs
+ */
+#ifndef __NNS_MLAGENT_MOCK_H__
+#define __NNS_MLAGENT_MOCK_H__
+
+#include <glib.h>
+
+#include <unordered_map>
+#include <vector>
+
+struct MockModel;
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+/**
+ * @brief Initialize the static unique_ptr of MockMLAgent
+ */
+void ml_agent_mock_init ();
+
+/**
+ * @brief C-wrapper for the MockModel's method add_model.
+ * @param[in] name The model's name to add
+ * @param[in] path The model's name to add
+ * @param[in] app_info The model's name to add
+ * @param[in] is_activated The model's name to add
+ * @param[in] desc The model's version to add
+ * @param[in] version The JSON c-string containing the information of the model matching to the given name and version
+ * @return true if there is a matching model to the given name and version, otherwise a negative integer
+ * @note ML Agent provides the original implementation of this function and
+ * the following implementation is only for testing purposes.
+ */
+bool ml_agent_mock_add_model (const gchar *name, const gchar *path, const gchar *info,
+    const bool is_activated, const char *desc, const guint version);
+
+/**
+ * @brief C-wrapper for the MockModel's method get_model.
+ * @param[in] name The model's name to retreive
+ * @param[in] version The model's version to retreive
+ * @return A pointer to the model matching the given information. If there is no model possible, NULL is returned.
+ */
+MockModel *ml_agent_mock_get_model (const gchar *name, const guint version);
+
+/**
+ * @brief Pass the JSON c-string generated by the ML Agent mock class to the caller.
+ * @param[in] name The model's name to retreive
+ * @param[in] version The model's version to retreive
+ * @param[out] description The JSON c-string, containing the information of the model matching the given name and version
+ * @param[out] err The return location for a recoverable error. This can be NULL.
+ * @return 0 if there is a matching model to the given name and version otherwise a negative integer
+ * @note ML Agent provides the original implementation of this function and
+ * the following implementation is only for testing purposes.
+ */
+gint ml_agent_model_get (
+    const gchar *name, const guint version, gchar **description, GError **err);
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+/**
+ * @brief Class for mock Model object
+ */
+struct MockModel {
+  // Constructors
+  MockModel () = delete;
+  MockModel (std::string name, std::string path = {}, std::string app_info = {},
+      bool is_activated = false, std::string desc = {}, guint version = 0U)
+      : name_{ name }, path_{ path }, app_info_{ app_info },
+        is_activated_{ is_activated }, desc_{ desc }, version_{ version } {
+
+        };
+  // Copy constructor
+  MockModel (const MockModel &other)
+      : MockModel (other.name_, other.path_, other.app_info_,
+          other.is_activated_, other.desc_, other.version_){
+
+        };
+  // Move constructor
+  MockModel (MockModel &&other) noexcept
+  {
+    *this = std::move (other);
+  }
+  MockModel &operator= (const MockModel &other) = delete;
+  MockModel &operator= (MockModel &&other) noexcept
+  {
+    if (this == &other)
+      return *this;
+
+    this->name_ = std::move (other.name_);
+    this->path_ = std::move (other.path_);
+    this->app_info_ = std::move (other.app_info_);
+    this->is_activated_ = std::exchange (other.is_activated_, false);
+    this->desc_ = std::move (other.desc_);
+    this->version_ = std::exchange (other.version_, 0U);
+
+    return *this;
+  }
+
+  bool operator== (const MockModel &model) const
+  {
+    if (name_ == model.name_ && version_ == model.version_)
+      return true;
+    return false;
+  }
+
+  // Getters
+  std::string name () const
+  {
+    return name_;
+  }
+
+  std::string path () const
+  {
+    return path_;
+  }
+  std::string app_info () const
+  {
+    return app_info_;
+  }
+  bool is_activated () const
+  {
+    return is_activated_;
+  }
+
+  std::string desc () const
+  {
+    return desc_;
+  }
+
+  guint version () const
+  {
+    return version_;
+  }
+
+  // Setters
+  void path (const std::string &path)
+  {
+    path_ = path;
+  }
+
+  void is_activated (bool flag)
+  {
+    is_activated_ = flag;
+  }
+
+  void desc (const std::string &description)
+  {
+    desc_ = description;
+  }
+
+  void app_info (const std::string &info)
+  {
+    app_info_ = info;
+  }
+
+  void version (guint ver)
+  {
+    version_ = ver;
+  }
+
+  /**
+   * @brief Generate C-stringified JSON
+   * @return C-stringified JSON
+   */
+  gchar *to_cstr_json ();
+
+  private:
+  std::string name_;
+  std::string path_{};
+  std::string app_info_{};
+  bool is_activated_{ false };
+  std::string desc_{};
+  guint version_{ 0U };
+};
+
+struct MockMLAgent {
+  MockMLAgent ()
+  {
+  }
+
+  bool add_model (MockModel model)
+  {
+    std::string key = model.name ();
+
+    {
+      auto range = models_dict_.equal_range (key);
+
+      for (auto it = range.first; it != range.second; ++it) {
+        if (model == it->second)
+          return false;
+      }
+    }
+
+    models_dict_.insert (std::make_pair (key, model));
+
+    return true;
+  }
+
+  MockModel *get_model (const gchar *name, guint version)
+  {
+    std::string key{ name };
+    auto range = models_dict_.equal_range (key);
+
+    for (auto it = range.first; it != range.second; ++it) {
+      if (version == it->second.version ())
+        return &it->second;
+    }
+
+    return nullptr;
+  }
+
+  private:
+  std::unordered_multimap<std::string, MockModel> models_dict_;
+};
+
+#endif /* __NNS_MLAGENT_MOCK_H__ */
diff --git a/tests/unittest_mlagent/unittest_mlagent.cc b/tests/unittest_mlagent/unittest_mlagent.cc
new file mode 100644 (file)
index 0000000..5a0b8d7
--- /dev/null
@@ -0,0 +1,155 @@
+/* SPDX-License-Identifier: LGPL-2.1-only */
+/**
+ * @file    unittest_mlagent.cc
+ * @date    30 Nov 2023
+ * @brief   Unit test for MLAgent URI parsing
+ * @author  Wook Song <wook.song16@samsung.com>
+ * @see     http://github.com/nnstreamer/nnstreamer
+ * @bug     No known bugs
+ *
+ */
+
+#include <gtest/gtest.h>
+#include <glib.h>
+#include <gst/gst.h>
+
+#include <iostream>
+
+#ifdef __cplusplus
+extern "C" {
+#include "ml_agent.h"
+}
+#endif //__cplusplus
+
+#include "mock_mlagent.h"
+
+static const std::vector<MockModel> default_models{
+  MockModel{ "MobileNet_v1", "/tmp/mobilenet_v1_0.tflite", "", false, "", 0U },
+  MockModel{ "MobileNet_v1", "/tmp/mobilenet_v1_1.tflite", "", false, "", 1U },
+  MockModel{ "ResNet50_v1", "/tmp/resnet50_v1_0.tflite", "", false, "", 0U },
+  MockModel{ "ResNet50_v1", "/tmp/resnet50_v1_1.tflite", "", false, "", 1U },
+};
+
+/**
+ * @brief Initialize the MockMLAgent using given MockModels
+ * @param[in] models A vector containg MockModels
+ */
+void
+_init (const std::vector<MockModel> &models = default_models)
+{
+  ml_agent_mock_init ();
+
+  for (auto iter = models.begin (); iter != models.end (); ++iter) {
+    ml_agent_mock_add_model (iter->name ().c_str (), iter->path ().c_str (),
+        iter->app_info ().c_str (), iter->is_activated (),
+        iter->desc ().c_str (), iter->version ());
+  }
+}
+
+constexpr gchar valid_uri_format_literal[] = "mlagent://model/%s/%u";
+constexpr gchar invalid_uri_format_literal[] = "ml-agent://model/%s/%u";
+
+/**
+ * @brief tests of getting model paths with valid URIs
+ */
+TEST (testMLAgent, GetModelValidURIs_p)
+{
+  _init ();
+
+  // Test the valid URI cases
+  GValue val = G_VALUE_INIT;
+  g_value_init (&val, G_TYPE_STRING);
+  const std::vector<MockModel> &models = default_models;
+
+  for (auto iter = models.begin (); iter != models.end (); ++iter) {
+    g_autofree gchar *uri = g_strdup_printf (
+        valid_uri_format_literal, iter->name ().c_str (), iter->version ());
+    g_autofree gchar *path = NULL;
+
+    g_value_set_string (&val, uri);
+
+    path = mlagent_get_model_path_from (&val);
+
+    EXPECT_STREQ (path, iter->path ().c_str ());
+
+    g_value_reset (&val);
+  }
+}
+
+/**
+ * @brief tests of getting model paths using URIs with invalid format
+ */
+TEST (testMLAgent, GetModelInvalidURIFormats_n)
+{
+  GValue val = G_VALUE_INIT;
+  g_value_init (&val, G_TYPE_STRING);
+  const std::vector<MockModel> &models = default_models;
+
+  for (auto iter = models.begin (); iter != models.end (); ++iter) {
+    g_autofree gchar *uri = g_strdup_printf (
+        invalid_uri_format_literal, iter->name ().c_str (), iter->version ());
+    g_autofree gchar *path = NULL;
+
+    g_value_set_string (&val, uri);
+
+    path = mlagent_get_model_path_from (&val);
+
+    /* In the case that invalid URIs are given, mlagent_get_model_path_from () returns
+     * the given URI as it is so that it is handled by the fallback procedure (i.e., regarding it as a file path).
+     */
+    EXPECT_STREQ (uri, path);
+
+    g_value_reset (&val);
+  }
+}
+
+/**
+ * @brief tests of getting model paths with invalid URIs
+ */
+TEST (testMLAgent, GetModelInvalidModel_n)
+{
+  // Clear the MLAgentMock instance
+  _init ();
+
+  // Test the valid URIs
+  GValue val = G_VALUE_INIT;
+  g_value_init (&val, G_TYPE_STRING);
+
+  g_autofree gchar *uri
+      = g_strdup_printf (valid_uri_format_literal, "InvalidModelName", UINT32_MAX);
+  g_autofree gchar *path = NULL;
+
+  g_value_set_string (&val, uri);
+
+  path = mlagent_get_model_path_from (&val);
+
+  /* In the case that invalid URIs are given, mlagent_get_model_path_from () returns
+   * the given URI as it is so that it is handled by the fallback procedure (i.e., regarding it as a file path).
+   */
+  EXPECT_STREQ (uri, path);
+}
+
+/**
+ * @brief Main function for this unit test
+ */
+int
+main (int argc, char *argv[])
+{
+  int ret = -1;
+
+  try {
+    testing::InitGoogleTest (&argc, argv);
+  } catch (...) {
+    g_warning ("catch 'testing::internal::<unnamed>::ClassUniqueToAlwaysTrue'");
+  }
+
+  gst_init (&argc, &argv);
+
+  try {
+    ret = RUN_ALL_TESTS ();
+  } catch (...) {
+    g_warning ("catch `testing::internal::GoogleTestFailureException`");
+  }
+
+  return ret;
+}