Add yang-cli

The original idea for this was that I would libyang C++ bindings for
this. Unfortunately, there have been problems with them as explained
here: https://github.com/CESNET/libyang/issues/1106. The easiest
solution to this was to just use the C api of libyang. After creating
some safe wrappers around pointers, it wasn't too difficult.

Change-Id: I0421cb64df66c640956501e56ffc4122eef0b9b7
diff --git a/CMakeLists.txt b/CMakeLists.txt
index baec077..4b2f0bc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -111,6 +111,12 @@
 
 target_link_libraries(netconfaccess PUBLIC datastoreaccess yangschema ast_values utils PRIVATE PkgConfig::LIBNETCONF2)
 
+add_library(yangaccess STATIC
+    src/yang_access.cpp
+    )
+
+target_link_libraries(yangaccess PUBLIC datastoreaccess yangschema)
+
 add_library(yangschema STATIC
     src/yang_schema.cpp
     src/libyang_utils.cpp
@@ -127,15 +133,28 @@
     )
 target_link_libraries(parser schemas utils ast_values)
 
+# Links libraries, that aren't specific to a datastore type
+function(cli_link_required cli_target)
+    target_include_directories(${cli_target} PRIVATE ${REPLXX_PATH})
+    target_link_libraries(${cli_target} yangschema docopt parser ${REPLXX_LIBRARY})
+    add_dependencies(${cli_target} target-NETCONF_CLI_VERSION)
+    target_include_directories(${cli_target} PRIVATE ${PROJECT_BINARY_DIR})
+endfunction()
+
 add_executable(sysrepo-cli
     src/cli.cpp
     )
 target_compile_definitions(sysrepo-cli PRIVATE SYSREPO_CLI)
-target_include_directories(sysrepo-cli PRIVATE ${REPLXX_PATH})
-target_link_libraries(sysrepo-cli sysrepoaccess yangschema docopt parser ${REPLXX_LIBRARY})
+target_link_libraries(sysrepo-cli sysrepoaccess)
+cli_link_required(sysrepo-cli)
 
-add_dependencies(sysrepo-cli target-NETCONF_CLI_VERSION)
-target_include_directories(sysrepo-cli PRIVATE ${PROJECT_BINARY_DIR})
+add_executable(yang-cli
+    src/cli.cpp
+    )
+target_compile_definitions(yang-cli PRIVATE YANG_CLI)
+cli_link_required(yang-cli)
+target_link_libraries(yang-cli yangaccess)
+
 
 include(CTest)
 if(BUILD_TESTING)
@@ -201,6 +220,7 @@
     set(NETOPEER_SOCKET_PATH "${CMAKE_CURRENT_BINARY_DIR}/netopeer2-server.sock")
     configure_file(${CMAKE_CURRENT_SOURCE_DIR}/tests/start_daemons.sh.in ${CMAKE_CURRENT_BINARY_DIR}/start_daemons.sh @ONLY)
     configure_file(${CMAKE_CURRENT_SOURCE_DIR}/tests/netopeer_vars.hpp.in ${CMAKE_CURRENT_BINARY_DIR}/netopeer_vars.hpp @ONLY)
+    configure_file(${CMAKE_CURRENT_SOURCE_DIR}/tests/yang_access_test_vars.hpp.in ${CMAKE_CURRENT_BINARY_DIR}/yang_access_test_vars.hpp @ONLY)
 
     function(setup_datastore_tests)
         add_test(NAME setup_netopeer COMMAND ${SYSREPOCFG_EXECUTABLE} ietf-netconf-server -i ${CMAKE_CURRENT_SOURCE_DIR}/tests/netopeer-test-config.xml --datastore=startup --format=xml)
@@ -246,6 +266,8 @@
             target_link_libraries(${TESTNAME} sysrepoaccess)
         elseif (${backend} STREQUAL "netconf")
             target_link_libraries(${TESTNAME} netconfaccess)
+        elseif (${backend} STREQUAL "yang")
+            target_link_libraries(${TESTNAME} yangaccess)
         else()
             message(FATAL_ERROR "Unknown backend ${backend}")
         endif()
@@ -257,6 +279,7 @@
     function(datastore_test name model)
         datastore_test_impl(${name} ${model} sysrepo)
         datastore_test_impl(${name} ${model} netconf)
+        datastore_test_impl(${name} ${model} yang)
     endfunction()
 
     cli_test(cd)
diff --git a/src/cli.cpp b/src/cli.cpp
index c142850..b036ebc 100644
--- a/src/cli.cpp
+++ b/src/cli.cpp
@@ -24,6 +24,20 @@
 
 Options:
   -d <datastore>   can be "running" or "startup" [default: running])";
+#elif defined(YANG_CLI)
+#include <boost/spirit/home/x3.hpp>
+#include "yang_access.hpp"
+#define PROGRAM_NAME "yang-cli"
+static const auto usage = R"(CLI interface for creating local YANG data instances
+
+Usage:
+  yang-cli [-s <search_dir>] [-e enable_features]... <schema_file>...
+  yang-cli (-h | --help)
+  yang-cli --version
+
+Options:
+  -s <search_dir>       Set search for schema lookup
+  -e <enable_features>  Feature to enable after modules are loaded. This option can be supplied more than once. Format: <module_name>:<feature>)";
 #else
 #error "Unknown CLI backend"
 #endif
@@ -52,6 +66,34 @@
     }
     SysrepoAccess datastore(PROGRAM_NAME, datastoreType);
     std::cout << "Connected to sysrepo [datastore: " << (datastoreType == Datastore::Startup ? "startup" : "running") << "]" << std::endl;
+#elif defined(YANG_CLI)
+    YangAccess datastore;
+    if (const auto& search_dir = args["-s"]) {
+        datastore.addSchemaDir(search_dir.asString());
+    }
+    for (const auto& schemaFile : args["<schema_file>"].asStringList()) {
+        datastore.addSchemaFile(schemaFile);
+    }
+    if (const auto& enableFeatures = args["-e"]) {
+        namespace x3 = boost::spirit::x3;
+        auto grammar = +(x3::char_-":") >> ":" >> +(x3::char_-":");
+        for (const auto& enableFeature : enableFeatures.asStringList()) {
+            std::pair<std::string, std::string> parsed;
+            auto it = enableFeature.begin();
+            auto res = x3::parse(it, enableFeature.cend(), grammar, parsed);
+            if (!res || it != enableFeature.cend()) {
+                std::cerr << "Error parsing feature enable flags: " << enableFeature << "\n";
+                return 1;
+            }
+            try {
+                datastore.enableFeature(parsed.first, parsed.second);
+            } catch (std::runtime_error& ex) {
+                std::cerr << ex.what() << "\n";
+                return 1;
+            }
+
+        }
+    }
 #else
 #error "Unknown CLI backend"
 #endif
diff --git a/src/datastore_access.hpp b/src/datastore_access.hpp
index 0856d41..29253ce 100644
--- a/src/datastore_access.hpp
+++ b/src/datastore_access.hpp
@@ -23,7 +23,7 @@
     std::string message;
     std::optional<std::string> xpath;
 
-    DatastoreError(const std::string& message, const std::optional<std::string>& xpath);
+    DatastoreError(const std::string& message, const std::optional<std::string>& xpath = std::nullopt);
 };
 
 class DatastoreException : std::exception {
diff --git a/src/yang_access.cpp b/src/yang_access.cpp
new file mode 100644
index 0000000..71aebf1
--- /dev/null
+++ b/src/yang_access.cpp
@@ -0,0 +1,304 @@
+#include <boost/algorithm/string/predicate.hpp>
+#include <experimental/iterator>
+#include <iostream>
+#include <libyang/Tree_Data.hpp>
+#include <libyang/libyang.h>
+#include "UniqueResource.hpp"
+#include "libyang_utils.hpp"
+#include "utils.hpp"
+#include "yang_access.hpp"
+#include "yang_schema.hpp"
+
+namespace {
+template <typename Type> using lyPtrDeleter_type = void (*)(Type*);
+template <typename Type> const lyPtrDeleter_type<Type> lyPtrDeleter;
+template <> const auto lyPtrDeleter<ly_set> = ly_set_free;
+template <> const auto lyPtrDeleter<ly_ctx> = static_cast<lyPtrDeleter_type<ly_ctx>>([] (auto* ptr) {ly_ctx_destroy(ptr, nullptr);});
+template <> const auto lyPtrDeleter<lyd_node> = lyd_free_withsiblings;
+
+template <typename Type>
+auto lyWrap(Type* ptr)
+{
+    return std::unique_ptr<Type, lyPtrDeleter_type<Type>>{ptr, lyPtrDeleter<Type>};
+}
+
+// Convenient for functions that take m_datastore as an argument
+using DatastoreType = std::unique_ptr<lyd_node, lyPtrDeleter_type<lyd_node>>;
+}
+
+YangAccess::YangAccess()
+    : m_ctx(lyWrap(ly_ctx_new(nullptr, LY_CTX_DISABLE_SEARCHDIR_CWD)))
+    , m_datastore(lyWrap<lyd_node>(nullptr))
+    , m_schema(std::make_shared<YangSchema>(libyang::create_new_Context(m_ctx.get())))
+{
+}
+
+YangAccess::~YangAccess()
+{
+}
+
+[[noreturn]] void YangAccess::getErrorsAndThrow() const
+{
+    auto errors = libyang::get_ly_errors(libyang::create_new_Context(m_ctx.get()));
+    std::vector<DatastoreError> errorsRes;
+    for (const auto& error : errors) {
+        using namespace std::string_view_literals;
+        errorsRes.emplace_back(error->errmsg(), error->errpath() != ""sv ? std::optional{error->errpath()} : std::nullopt);
+    }
+
+    throw DatastoreException(errorsRes);
+}
+
+void YangAccess::impl_newPath(const std::string& path, const std::optional<std::string>& value)
+{
+    auto newNode = lyd_new_path(m_datastore.get(), m_ctx.get(), path.c_str(), value ? (void*)value->c_str() : nullptr, LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_UPDATE);
+    if (!newNode) {
+        getErrorsAndThrow();
+    }
+    if (!m_datastore) {
+        m_datastore = lyWrap(newNode);
+    }
+}
+
+namespace {
+void impl_unlink(DatastoreType& datastore, lyd_node* what)
+{
+    // If the node to be unlinked is the one our datastore variable points to, we need to find a new one to point to (one of its siblings)
+    if (datastore.get() == what) {
+        auto oldDatastore = datastore.release();
+        if (oldDatastore->prev != oldDatastore) {
+            datastore = lyWrap(oldDatastore->prev);
+        } else {
+            datastore = lyWrap(oldDatastore->next);
+        }
+    }
+
+    lyd_unlink(what);
+}
+}
+
+void YangAccess::impl_removeNode(const std::string& path)
+{
+    auto set = lyWrap(lyd_find_path(m_datastore.get(), path.c_str()));
+    if (!set || set->number == 0) {
+        // Check if schema node exists - lyd_find_path first checks if the first argument is non-null before checking for path validity
+        if (!ly_ctx_get_node(m_ctx.get(), nullptr, path.c_str(), 0)) {
+            throw DatastoreException{{DatastoreError{"Schema node doesn't exist.", path}}};
+        }
+        // Check if libyang found another error
+        if (ly_err_first(m_ctx.get())) {
+            getErrorsAndThrow();
+        }
+
+        // Otherwise the datastore just doesn't contain the wanted node.
+        throw DatastoreException{{DatastoreError{"Data node doesn't exist.", path}}};
+    }
+
+    auto toRemove = set->set.d[0];
+
+    impl_unlink(m_datastore, toRemove);
+
+    lyd_free(toRemove);
+}
+
+void YangAccess::validate()
+{
+    auto datastore = m_datastore.release();
+    lyd_validate(&datastore, LYD_OPT_DATA | LYD_OPT_DATA_NO_YANGLIB, m_ctx.get());
+    m_datastore = lyWrap(datastore);
+}
+
+DatastoreAccess::Tree YangAccess::getItems(const std::string& path)
+{
+    DatastoreAccess::Tree res;
+    if (!m_datastore) {
+        return res;
+    }
+
+    auto set = lyWrap(lyd_find_path(m_datastore.get(), path == "/" ? "/*" : path.c_str()));
+    auto setWrapper = libyang::Set(set.get(), nullptr);
+
+    lyNodesToTree(res, setWrapper.data());
+    return res;
+}
+
+void YangAccess::setLeaf(const std::string& path, leaf_data_ value)
+{
+    auto lyValue = value.type() == typeid(empty_) ? std::nullopt : std::optional(leafDataToString(value));
+    impl_newPath(path, lyValue);
+}
+
+void YangAccess::createItem(const std::string& path)
+{
+    impl_newPath(path);
+}
+
+void YangAccess::deleteItem(const std::string& path)
+{
+    impl_removeNode(path);
+}
+
+namespace {
+struct impl_moveItem {
+    DatastoreType& m_datastore;
+    lyd_node* m_sourceNode;
+
+    void operator()(yang::move::Absolute absolute) const
+    {
+        auto set = lyWrap(lyd_find_instance(m_sourceNode, m_sourceNode->schema));
+        if (set->number == 1) { // m_sourceNode is the sole instance, do nothing
+            return;
+        }
+
+        doUnlink();
+        switch (absolute) {
+        case yang::move::Absolute::Begin:
+            if (set->set.d[0] == m_sourceNode) { // List is already at the beginning, do nothing
+                return;
+            }
+            lyd_insert_before(set->set.d[0], m_sourceNode);
+            return;
+        case yang::move::Absolute::End:
+            if (set->set.d[set->number - 1] == m_sourceNode) { // List is already at the end, do nothing
+                return;
+            }
+            lyd_insert_after(set->set.d[set->number - 1], m_sourceNode);
+            return;
+        }
+    }
+
+    void operator()(const yang::move::Relative& relative) const
+    {
+        auto keySuffix = m_sourceNode->schema->nodetype == LYS_LIST ? instanceToString(relative.m_path)
+                                                                    : leafDataToString(relative.m_path.at("."));
+        lyd_node* destNode;
+        lyd_find_sibling_val(m_sourceNode, m_sourceNode->schema, keySuffix.c_str(), &destNode);
+
+        doUnlink();
+        if (relative.m_position == yang::move::Relative::Position::After) {
+            lyd_insert_after(destNode, m_sourceNode);
+        } else {
+            lyd_insert_before(destNode, m_sourceNode);
+        }
+    }
+
+private:
+    void doUnlink() const
+    {
+        impl_unlink(m_datastore, m_sourceNode);
+    }
+};
+}
+
+void YangAccess::moveItem(const std::string& source, std::variant<yang::move::Absolute, yang::move::Relative> move)
+{
+    auto set = lyWrap(lyd_find_path(m_datastore.get(), source.c_str()));
+    if (!set) { // Error, the node probably doesn't exist in the schema
+        getErrorsAndThrow();
+    }
+    if (set->number == 0) {
+        return;
+    }
+    auto sourceNode = set->set.d[0];
+    std::visit(impl_moveItem{m_datastore, sourceNode}, move);
+}
+
+void YangAccess::commitChanges()
+{
+    validate();
+}
+
+void YangAccess::discardChanges()
+{
+}
+
+DatastoreAccess::Tree YangAccess::executeRpc(const std::string& path, const Tree& input)
+{
+    auto root = lyWrap(lyd_new_path(nullptr, m_ctx.get(), path.c_str(), nullptr, LYD_ANYDATA_CONSTSTRING, 0));
+    if (!root) {
+        getErrorsAndThrow();
+    }
+    for (const auto& [k, v] : input) {
+        auto node = lyd_new_path(root.get(), m_ctx.get(), joinPaths(path, k).c_str(), (void*)leafDataToString(v).c_str(), LYD_ANYDATA_CONSTSTRING, 0);
+        if (!node) {
+            getErrorsAndThrow();
+        }
+    }
+    throw std::logic_error("in-memory datastore doesn't support executing RPCs.");
+}
+
+void YangAccess::copyConfig(const Datastore source, const Datastore dest)
+{
+    if (source == Datastore::Startup && dest == Datastore::Running) {
+        m_datastore = nullptr;
+    }
+}
+
+std::shared_ptr<Schema> YangAccess::schema()
+{
+    return m_schema;
+}
+
+std::vector<ListInstance> YangAccess::listInstances(const std::string& path)
+{
+    std::vector<ListInstance> res;
+    if (!m_datastore) {
+        return res;
+    }
+
+    auto instances = lyWrap(lyd_find_path(m_datastore.get(), path.c_str()));
+    auto instancesWrapper = libyang::Set(instances.get(), nullptr);
+    for (const auto& list : instancesWrapper.data()) {
+        ListInstance instance;
+        for (const auto& child : list->child()->tree_for()) {
+            if (child->schema()->nodetype() == LYS_LEAF) {
+                libyang::Schema_Node_Leaf leafSchema(child->schema());
+                if (leafSchema.is_key()) {
+                    libyang::Data_Node_Leaf_List leafData(child);
+                    instance.insert({leafSchema.name(), leafValueFromValue(leafData.value(), leafSchema.type()->base())});
+                }
+            }
+        }
+        res.push_back(instance);
+    }
+    return res;
+}
+
+std::string impl_dumpConfig(const lyd_node* datastore, LYD_FORMAT format)
+{
+    char* output;
+    lyd_print_mem(&output, datastore, format, LYP_WITHSIBLINGS);
+
+    if (output) {
+        std::string res = output;
+        free(output);
+        return res;
+    }
+
+    return "";
+}
+
+std::string YangAccess::dumpXML() const
+{
+    return impl_dumpConfig(m_datastore.get(), LYD_XML);
+}
+
+std::string YangAccess::dumpJSON() const
+{
+    return impl_dumpConfig(m_datastore.get(), LYD_JSON);
+}
+
+void YangAccess::addSchemaFile(const std::string& path)
+{
+    m_schema->addSchemaFile(path.c_str());
+}
+
+void YangAccess::addSchemaDir(const std::string& path)
+{
+    m_schema->addSchemaDirectory(path.c_str());
+}
+
+void YangAccess::enableFeature(const std::string& module, const std::string& feature)
+{
+    m_schema->enableFeature(module, feature);
+}
diff --git a/src/yang_access.hpp b/src/yang_access.hpp
new file mode 100644
index 0000000..9f20b51
--- /dev/null
+++ b/src/yang_access.hpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Václav Kubernát <kubernat@cesnet.cz>
+ *
+*/
+
+#pragma once
+
+#include "datastore_access.hpp"
+
+/*! \class YangAccess
+ *     \brief Implementation of DatastoreAccess with a local libyang data node instance
+ */
+
+class YangSchema;
+
+struct ly_ctx;
+struct lyd_node;
+
+class YangAccess : public DatastoreAccess {
+public:
+    YangAccess();
+    ~YangAccess() override;
+    Tree getItems(const std::string& path) override;
+    void setLeaf(const std::string& path, leaf_data_ value) override;
+    void createItem(const std::string& path) override;
+    void deleteItem(const std::string& path) override;
+    void moveItem(const std::string& source, std::variant<yang::move::Absolute, yang::move::Relative> move) override;
+    void commitChanges() override;
+    void discardChanges() override;
+    Tree executeRpc(const std::string& path, const Tree& input) override;
+    void copyConfig(const Datastore source, const Datastore destination) override;
+
+    std::shared_ptr<Schema> schema() override;
+
+    void enableFeature(const std::string& module, const std::string& feature);
+    std::string dumpXML() const;
+    std::string dumpJSON() const;
+
+    void addSchemaFile(const std::string& path);
+    void addSchemaDir(const std::string& path);
+
+private:
+    std::vector<ListInstance> listInstances(const std::string& path) override;
+
+    [[noreturn]] void getErrorsAndThrow() const;
+    void impl_newPath(const std::string& path, const std::optional<std::string>& value = std::nullopt);
+    void impl_removeNode(const std::string& path);
+    void validate();
+
+    std::unique_ptr<ly_ctx, void(*)(ly_ctx*)> m_ctx;
+    std::unique_ptr<lyd_node, void(*)(lyd_node*)> m_datastore;
+    std::shared_ptr<YangSchema> m_schema;
+};
diff --git a/src/yang_schema.cpp b/src/yang_schema.cpp
index 2b69dc7..96b7e6e 100644
--- a/src/yang_schema.cpp
+++ b/src/yang_schema.cpp
@@ -40,7 +40,6 @@
 YangSchema::YangSchema(std::shared_ptr<libyang::Context> lyCtx)
     : m_context(lyCtx)
 {
-
 }
 
 YangSchema::~YangSchema() = default;
diff --git a/tests/data_query.cpp b/tests/data_query.cpp
index 50de180..f6f7343 100644
--- a/tests/data_query.cpp
+++ b/tests/data_query.cpp
@@ -13,6 +13,9 @@
 #elif defined(netconf_BACKEND)
 #include "netconf_access.hpp"
 #include "netopeer_vars.hpp"
+#elif defined(yang_BACKEND)
+#include "yang_access.hpp"
+#include "yang_access_test_vars.hpp"
 #else
 #error "Unknown backend"
 #endif
@@ -32,6 +35,10 @@
     SysrepoAccess datastore("netconf-cli-test", Datastore::Running);
 #elif defined(netconf_BACKEND)
     NetconfAccess datastore(NETOPEER_SOCKET_PATH);
+#elif defined(yang_BACKEND)
+    YangAccess datastore;
+    datastore.addSchemaDir(schemaDir);
+    datastore.addSchemaFile(exampleSchemaFile);
 #else
 #error "Unknown backend"
 #endif
diff --git a/tests/datastore_access.cpp b/tests/datastore_access.cpp
index 48b8f78..57ecf24 100644
--- a/tests/datastore_access.cpp
+++ b/tests/datastore_access.cpp
@@ -14,14 +14,28 @@
 using OnInvalidSchemaPathCreate = DatastoreException;
 using OnInvalidSchemaPathDelete = void;
 using OnInvalidSchemaPathMove = sysrepo::sysrepo_exception;
+using OnInvalidRpcPath = sysrepo::sysrepo_exception;
 using OnKeyNotFound = void;
+using OnRPC = void;
 #elif defined(netconf_BACKEND)
 using OnInvalidSchemaPathCreate = std::runtime_error;
 using OnInvalidSchemaPathDelete = std::runtime_error;
 using OnInvalidSchemaPathMove = std::runtime_error;
+using OnInvalidRpcPath = std::runtime_error;
 using OnKeyNotFound = std::runtime_error;
+using OnRPC = void;
 #include "netconf_access.hpp"
 #include "netopeer_vars.hpp"
+#elif defined(yang_BACKEND)
+#include <fstream>
+#include "yang_access.hpp"
+#include "yang_access_test_vars.hpp"
+using OnInvalidSchemaPathCreate = DatastoreException;
+using OnInvalidSchemaPathDelete = DatastoreException;
+using OnInvalidSchemaPathMove = DatastoreException;
+using OnInvalidRpcPath = DatastoreException;
+using OnKeyNotFound = DatastoreException;
+using OnRPC = std::logic_error;
 #else
 #error "Unknown backend"
 #endif
@@ -51,6 +65,8 @@
         // cannot use REQUIRE_THROWS_AS(..., Exception) directly because that one
         // needs an extra `typename` deep in the bowels of doctest
         REQUIRE_THROWS_AS(what(), std::runtime_error);
+    } else if constexpr (std::is_same<Exception, std::logic_error>()) {
+        REQUIRE_THROWS_AS(what(), std::logic_error);
     } else if constexpr (std::is_same<Exception, DatastoreException>()) {
         REQUIRE_THROWS_AS(what(), DatastoreException);
     } else if constexpr (std::is_same<Exception, sysrepo::sysrepo_exception>()) {
@@ -61,6 +77,34 @@
 }
 }
 
+#if defined(yang_BACKEND)
+class TestYangAccess : public YangAccess {
+public:
+    void commitChanges() override
+    {
+        YangAccess::commitChanges();
+        dumpToSysrepo();
+    }
+
+    void copyConfig(const Datastore source, const Datastore destination) override
+    {
+        YangAccess::copyConfig(source, destination);
+        dumpToSysrepo();
+    }
+
+private:
+    void dumpToSysrepo()
+    {
+        {
+            std::ofstream of(testConfigFile);
+            of << dumpXML();
+        }
+        auto command = std::string(sysrepocfgExecutable) + " --import=" + testConfigFile + " --format=xml --datastore=running example-schema";
+        REQUIRE(std::system(command.c_str()) == 0);
+    }
+};
+#endif
+
 TEST_CASE("setting/getting values")
 {
     trompeloeil::sequence seq1;
@@ -71,6 +115,10 @@
     SysrepoAccess datastore("netconf-cli-test", Datastore::Running);
 #elif defined(netconf_BACKEND)
     NetconfAccess datastore(NETOPEER_SOCKET_PATH);
+#elif defined(yang_BACKEND)
+    TestYangAccess datastore;
+    datastore.addSchemaDir(schemaDir);
+    datastore.addSchemaFile(exampleSchemaFile);
 #else
 #error "Unknown backend"
 #endif
@@ -457,6 +505,7 @@
         REQUIRE(datastore.getItems("/example-schema:dummy") == expected);
     }
 
+#if not defined(yang_BACKEND)
     SECTION("operational data")
     {
         MockDataSupplier mockOpsData;
@@ -472,6 +521,7 @@
         REQUIRE_CALL(mockOpsData, get_data(xpath)).RETURN(expected);
         REQUIRE(datastore.getItems(xpath) == expected);
     }
+#endif
 
     SECTION("leaf list")
     {
@@ -822,6 +872,10 @@
     SysrepoAccess datastore("netconf-cli-test", Datastore::Running);
 #elif defined(netconf_BACKEND)
     NetconfAccess datastore(NETOPEER_SOCKET_PATH);
+#elif defined(yang_BACKEND)
+    YangAccess datastore;
+    datastore.addSchemaDir(schemaDir);
+    datastore.addSchemaFile(exampleSchemaFile);
 #else
 #error "Unknown backend"
 #endif
@@ -873,12 +927,12 @@
             };
         }
 
-        REQUIRE(datastore.executeRpc(rpc, input) == output);
+        catching<OnRPC>([&] {REQUIRE(datastore.executeRpc(rpc, input) == output);});
     }
 
     SECTION("non-existing RPC")
     {
-        REQUIRE_THROWS_AS(datastore.executeRpc("/example-schema:non-existing", DatastoreAccess::Tree{}), std::runtime_error);
+        catching<OnInvalidRpcPath>([&] {datastore.executeRpc("/example-schema:non-existing", DatastoreAccess::Tree{});});
     }
 
     waitForCompletionAndBitMore(seq1);
diff --git a/tests/yang_access_test_vars.hpp.in b/tests/yang_access_test_vars.hpp.in
new file mode 100644
index 0000000..a23f515
--- /dev/null
+++ b/tests/yang_access_test_vars.hpp.in
@@ -0,0 +1,4 @@
+const auto exampleSchemaFile = "@CMAKE_CURRENT_SOURCE_DIR@/tests/example-schema.yang";
+const auto schemaDir = "@CMAKE_CURRENT_SOURCE_DIR@/tests";
+const auto sysrepocfgExecutable = "@SYSREPOCFG_EXECUTABLE@";
+const auto testConfigFile = "@CMAKE_CURRENT_BINARY_DIR@/test_yang_access_config";