Add datastore support for YANG actions

Change-Id: I15b96f70ce89b7bbe3ac0fefb7b018374eeabd84
diff --git a/src/datastore_access.hpp b/src/datastore_access.hpp
index 8ff7658..63df0bb 100644
--- a/src/datastore_access.hpp
+++ b/src/datastore_access.hpp
@@ -50,6 +50,7 @@
     virtual void deleteItem(const std::string& path) = 0;
     virtual void moveItem(const std::string& path, std::variant<yang::move::Absolute, yang::move::Relative> move) = 0;
     virtual Tree executeRpc(const std::string& path, const Tree& input) = 0;
+    virtual Tree executeAction(const std::string& path, const Tree& input) = 0;
 
     virtual std::shared_ptr<Schema> schema() = 0;
 
diff --git a/src/libyang_utils.cpp b/src/libyang_utils.cpp
index 756f88c..456fa62 100644
--- a/src/libyang_utils.cpp
+++ b/src/libyang_utils.cpp
@@ -55,7 +55,7 @@
 void impl_lyNodesToTree(DatastoreAccess::Tree& res, const std::vector<std::shared_ptr<libyang::Data_Node>> items, std::optional<std::string> ignoredXPathPrefix)
 {
     auto stripXPathPrefix = [&ignoredXPathPrefix] (auto path) {
-        return ignoredXPathPrefix ? path.substr(ignoredXPathPrefix->size()) : path;
+        return ignoredXPathPrefix && path.find(*ignoredXPathPrefix) != std::string::npos ? path.substr(ignoredXPathPrefix->size()) : path;
     };
 
     for (const auto& it : items) {
diff --git a/src/netconf-client.cpp b/src/netconf-client.cpp
index 63fbb20..1ff83de 100644
--- a/src/netconf-client.cpp
+++ b/src/netconf-client.cpp
@@ -345,7 +345,7 @@
     impl::do_rpc_ok(this, std::move(rpc));
 }
 
-std::shared_ptr<libyang::Data_Node> Session::rpc(const std::string& xmlData)
+std::shared_ptr<libyang::Data_Node> Session::rpc_or_action(const std::string& xmlData)
 {
     auto rpc = impl::guarded(nc_rpc_act_generic_xml(xmlData.c_str(), NC_PARAMTYPE_CONST));
     if (!rpc) {
diff --git a/src/netconf-client.hpp b/src/netconf-client.hpp
index 5949704..db004e3 100644
--- a/src/netconf-client.hpp
+++ b/src/netconf-client.hpp
@@ -44,7 +44,7 @@
                     const NC_RPC_EDIT_ERROPT errorOption,
                     const std::string& data);
     void copyConfigFromString(const NC_DATASTORE target, const std::string& data);
-    std::shared_ptr<libyang::Data_Node> rpc(const std::string& xmlData);
+    std::shared_ptr<libyang::Data_Node> rpc_or_action(const std::string& xmlData);
     void copyConfig(const NC_DATASTORE source, const NC_DATASTORE destination);
     void commit();
     void discard();
diff --git a/src/netconf_access.cpp b/src/netconf_access.cpp
index 689b2b5..4b151d3 100644
--- a/src/netconf_access.cpp
+++ b/src/netconf_access.cpp
@@ -116,7 +116,7 @@
     m_session->discard();
 }
 
-DatastoreAccess::Tree NetconfAccess::executeRpc(const std::string& path, const Tree& input)
+DatastoreAccess::Tree NetconfAccess::impl_execute(const std::string& path, const Tree& input)
 {
     auto root = m_schema->dataNodeFromPath(path);
     for (const auto& [k, v] : input) {
@@ -126,13 +126,23 @@
     auto data = root->print_mem(LYD_XML, 0);
 
     Tree res;
-    auto output = m_session->rpc(data);
+    auto output = m_session->rpc_or_action(data);
     if (output) {
         lyNodesToTree(res, output->tree_for(), joinPaths(path, "/"));
     }
     return res;
 }
 
+DatastoreAccess::Tree NetconfAccess::executeRpc(const std::string& path, const Tree& input)
+{
+    return impl_execute(path, input);
+}
+
+DatastoreAccess::Tree NetconfAccess::executeAction(const std::string& path, const Tree& input)
+{
+    return impl_execute(path, input);
+}
+
 NC_DATASTORE toNcDatastore(Datastore datastore)
 {
     switch (datastore) {
diff --git a/src/netconf_access.hpp b/src/netconf_access.hpp
index 586840f..d6100a7 100644
--- a/src/netconf_access.hpp
+++ b/src/netconf_access.hpp
@@ -41,6 +41,7 @@
     void commitChanges() override;
     void discardChanges() override;
     Tree executeRpc(const std::string& path, const Tree& input) override;
+    Tree executeAction(const std::string& path, const Tree& input) override;
     void copyConfig(const Datastore source, const Datastore destination) override;
 
     std::shared_ptr<Schema> schema() override;
@@ -49,6 +50,7 @@
 
 private:
     std::vector<ListInstance> listInstances(const std::string& path) override;
+    DatastoreAccess::Tree impl_execute(const std::string& path, const Tree& input);
 
     std::string fetchSchema(const std::string_view module, const
             std::optional<std::string_view> revision, const
diff --git a/src/sysrepo_access.cpp b/src/sysrepo_access.cpp
index 510c530..e792ed8 100644
--- a/src/sysrepo_access.cpp
+++ b/src/sysrepo_access.cpp
@@ -311,24 +311,44 @@
     }
 }
 
-DatastoreAccess::Tree SysrepoAccess::executeRpc(const std::string &path, const Tree &input)
+namespace {
+std::shared_ptr<sysrepo::Vals> toSrVals(const std::string& path, const DatastoreAccess::Tree& input)
 {
-    auto srInput = std::make_shared<sysrepo::Vals>(input.size());
+    auto res = std::make_shared<sysrepo::Vals>(input.size());
     {
         size_t i = 0;
         for (const auto& [k, v] : input) {
-            boost::apply_visitor(updateSrValFromValue(joinPaths(path, k), srInput->val(i)), v);
+            boost::apply_visitor(updateSrValFromValue(joinPaths(path, k), res->val(i)), v);
             ++i;
         }
     }
-    auto output = m_session->rpc_send(path.c_str(), srInput);
-    Tree res;
+    return res;
+}
+
+DatastoreAccess::Tree toTree(const std::string& path, const std::shared_ptr<sysrepo::Vals>& output)
+{
+    DatastoreAccess::Tree res;
     for (size_t i = 0; i < output->val_cnt(); ++i) {
         const auto& v = output->val(i);
         res.emplace_back(std::string(v->xpath()).substr(joinPaths(path, "/").size()), leafValueFromVal(v));
     }
     return res;
 }
+}
+
+DatastoreAccess::Tree SysrepoAccess::executeRpc(const std::string &path, const Tree &input)
+{
+    auto srInput = toSrVals(path, input);
+    auto output = m_session->rpc_send(path.c_str(), srInput);
+    return toTree(path, output);
+}
+
+DatastoreAccess::Tree SysrepoAccess::executeAction(const std::string& path, const Tree& input)
+{
+    auto srInput = toSrVals(path, input);
+    auto output = m_session->action_send(path.c_str(), srInput);
+    return toTree(path, output);
+}
 
 void SysrepoAccess::copyConfig(const Datastore source, const Datastore destination)
 {
diff --git a/src/sysrepo_access.hpp b/src/sysrepo_access.hpp
index a3db4c1..55b1b67 100644
--- a/src/sysrepo_access.hpp
+++ b/src/sysrepo_access.hpp
@@ -34,6 +34,7 @@
     void deleteItem(const std::string& path) override;
     void moveItem(const std::string& source, std::variant<yang::move::Absolute, yang::move::Relative> move) override;
     Tree executeRpc(const std::string& path, const Tree& input) override;
+    Tree executeAction(const std::string& path, const Tree& input) override;
 
     std::shared_ptr<Schema> schema() override;
 
diff --git a/src/yang_access.cpp b/src/yang_access.cpp
index ebe39f1..8171c89 100644
--- a/src/yang_access.cpp
+++ b/src/yang_access.cpp
@@ -229,7 +229,7 @@
 {
 }
 
-DatastoreAccess::Tree YangAccess::executeRpc(const std::string& path, const Tree& input)
+[[noreturn]] void YangAccess::impl_execute(const std::string& type, 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) {
@@ -244,7 +244,17 @@
             getErrorsAndThrow();
         }
     }
-    throw std::logic_error("in-memory datastore doesn't support executing RPCs.");
+    throw std::logic_error("in-memory datastore doesn't support executing " + type + "s");
+}
+
+DatastoreAccess::Tree YangAccess::executeRpc(const std::string& path, const Tree& input)
+{
+    impl_execute("RPC", path, input);
+}
+
+DatastoreAccess::Tree YangAccess::executeAction(const std::string& path, const Tree& input)
+{
+    impl_execute("action", path, input);
 }
 
 void YangAccess::copyConfig(const Datastore source, const Datastore dest)
diff --git a/src/yang_access.hpp b/src/yang_access.hpp
index 503a077..ab948cf 100644
--- a/src/yang_access.hpp
+++ b/src/yang_access.hpp
@@ -31,6 +31,7 @@
     void commitChanges() override;
     void discardChanges() override;
     Tree executeRpc(const std::string& path, const Tree& input) override;
+    Tree executeAction(const std::string& path, const Tree& input) override;
     void copyConfig(const Datastore source, const Datastore destination) override;
 
     std::shared_ptr<Schema> schema() override;
@@ -45,6 +46,7 @@
 
 private:
     std::vector<ListInstance> listInstances(const std::string& path) override;
+    [[noreturn]] void impl_execute(const std::string& type, const std::string& path, const Tree& input);
 
     [[noreturn]] void getErrorsAndThrow() const;
     void impl_newPath(const std::string& path, const std::optional<std::string>& value = std::nullopt);
diff --git a/src/yang_schema.cpp b/src/yang_schema.cpp
index 05195b9..681861e 100644
--- a/src/yang_schema.cpp
+++ b/src/yang_schema.cpp
@@ -512,3 +512,8 @@
 
     return std::nullopt;
 }
+
+std::string YangSchema::dataPathToSchemaPath(const std::string& path)
+{
+    return getSchemaNode(path)->path(LYS_PATH_FIRST_PREFIX);
+}
diff --git a/src/yang_schema.hpp b/src/yang_schema.hpp
index 5f3aa20..3d1dc81 100644
--- a/src/yang_schema.hpp
+++ b/src/yang_schema.hpp
@@ -69,6 +69,7 @@
     [[nodiscard]] std::shared_ptr<libyang::Data_Node> dataNodeFromPath(const std::string& path, const std::optional<const std::string> value = std::nullopt) const;
     std::shared_ptr<libyang::Module> getYangModule(const std::string& name);
 
+    [[nodiscard]] std::string dataPathToSchemaPath(const std::string& path);
 private:
     friend class YangAccess;
     template <typename NodeType>
diff --git a/tests/datastore_access.cpp b/tests/datastore_access.cpp
index fb44018..2fac222 100644
--- a/tests/datastore_access.cpp
+++ b/tests/datastore_access.cpp
@@ -18,14 +18,14 @@
 using OnInvalidSchemaPathMove = sysrepo::sysrepo_exception;
 using OnInvalidRpcPath = sysrepo::sysrepo_exception;
 using OnKeyNotFound = void;
-using OnRPC = void;
+using OnExec = 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;
+using OnExec = void;
 #include "netconf_access.hpp"
 #include "netopeer_vars.hpp"
 #elif defined(yang_BACKEND)
@@ -37,7 +37,7 @@
 using OnInvalidSchemaPathMove = DatastoreException;
 using OnInvalidRpcPath = DatastoreException;
 using OnKeyNotFound = DatastoreException;
-using OnRPC = std::logic_error;
+using OnExec = std::logic_error;
 #else
 #error "Unknown backend"
 #endif
@@ -830,6 +830,17 @@
 }
 
 class RpcCb: public sysrepo::Callback {
+    int action(const char *xpath, [[maybe_unused]] const ::sysrepo::S_Vals input, ::sysrepo::S_Vals_Holder output, void* priv) override
+    {
+        auto schema = reinterpret_cast<YangSchema*>(priv);
+        if (schema->dataPathToSchemaPath(xpath) == "/example-schema:ports/shutdown") {
+            auto buf = output->allocate(1);
+            buf->val(0)->set(joinPaths(xpath, "success").c_str(), true);
+            return SR_ERR_OK;
+        }
+        throw std::runtime_error("unrecognized RPC");
+    }
+
     int rpc(const char *xpath, const ::sysrepo::S_Vals input, ::sysrepo::S_Vals_Holder output, void *) override
     {
         const auto nukes = "/example-schema:launch-nukes"s;
@@ -876,19 +887,8 @@
     }
 };
 
-TEST_CASE("rpc") {
+TEST_CASE("rpc/action") {
     trompeloeil::sequence seq1;
-    auto srConn = std::make_shared<sysrepo::Connection>("netconf-cli-test-rpc");
-    auto srSession = std::make_shared<sysrepo::Session>(srConn);
-    auto srSubscription = std::make_shared<sysrepo::Subscribe>(srSession);
-    auto cb = std::make_shared<RpcCb>();
-    sysrepo::Logs{}.set_stderr(SR_LL_INF);
-    auto doNothingCb = std::make_shared<sysrepo::Callback>();
-    srSubscription->module_change_subscribe("example-schema", doNothingCb, nullptr, SR_SUBSCR_CTX_REUSE);
-    // careful here, sysrepo insists on module_change CBs being registered before RPC CBs, otherwise there's a memleak
-    srSubscription->rpc_subscribe("/example-schema:noop", cb, nullptr, SR_SUBSCR_CTX_REUSE);
-    srSubscription->rpc_subscribe("/example-schema:launch-nukes", cb, nullptr, SR_SUBSCR_CTX_REUSE);
-    srSubscription->rpc_subscribe("/example-schema:fire", cb, nullptr, SR_SUBSCR_CTX_REUSE);
 
 #ifdef sysrepo_BACKEND
     auto datastore = std::make_shared<SysrepoAccess>("netconf-cli-test", Datastore::Running);
@@ -902,88 +902,124 @@
 #error "Unknown backend"
 #endif
 
-    auto createTemporaryDatastore = [](const std::shared_ptr<DatastoreAccess>& datastore) {
-        return std::make_shared<YangAccess>(std::static_pointer_cast<YangSchema>(datastore->schema()));
-    };
+    auto srConn = std::make_shared<sysrepo::Connection>("netconf-cli-test-rpc");
+    auto srSession = std::make_shared<sysrepo::Session>(srConn);
+    auto srSubscription = std::make_shared<sysrepo::Subscribe>(srSession);
+    auto cb = std::make_shared<RpcCb>();
+    sysrepo::Logs{}.set_stderr(SR_LL_INF);
+    auto doNothingCb = std::make_shared<sysrepo::Callback>();
+    srSubscription->module_change_subscribe("example-schema", doNothingCb, nullptr, SR_SUBSCR_CTX_REUSE);
+    // careful here, sysrepo insists on module_change CBs being registered before RPC CBs, otherwise there's a memleak
+    srSubscription->rpc_subscribe("/example-schema:noop", cb, nullptr, SR_SUBSCR_CTX_REUSE);
+    srSubscription->rpc_subscribe("/example-schema:launch-nukes", cb, nullptr, SR_SUBSCR_CTX_REUSE);
+    srSubscription->rpc_subscribe("/example-schema:fire", cb, nullptr, SR_SUBSCR_CTX_REUSE);
+    srSubscription->action_subscribe("/example-schema:ports/shutdown", cb, datastore->schema().get(), SR_SUBSCR_CTX_REUSE);
 
-    ProxyDatastore proxyDatastore(datastore, createTemporaryDatastore);
-
-    // ProxyDatastore cannot easily read DatastoreAccess::Tree, so we need to set the input via create/setLeaf/etc.
-    SECTION("valid")
+    SECTION("rpc")
     {
-        std::string rpc;
-        DatastoreAccess::Tree input, output;
+        auto createTemporaryDatastore = [](const std::shared_ptr<DatastoreAccess>& datastore) {
+            return std::make_shared<YangAccess>(std::static_pointer_cast<YangSchema>(datastore->schema()));
+        };
+        ProxyDatastore proxyDatastore(datastore, createTemporaryDatastore);
 
-        SECTION("noop") {
-            rpc = "/example-schema:noop";
-            proxyDatastore.initiateRpc(rpc);
+        // ProxyDatastore cannot easily read DatastoreAccess::Tree, so we need to set the input via create/setLeaf/etc.
+        SECTION("valid")
+        {
+            std::string rpc;
+            DatastoreAccess::Tree input, output;
+
+            SECTION("noop") {
+                rpc = "/example-schema:noop";
+                proxyDatastore.initiateRpc(rpc);
+            }
+
+            SECTION("small nuke") {
+                rpc = "/example-schema:launch-nukes";
+                input = {
+                    {"description", "dummy"s},
+                    {"payload/kilotons", uint64_t{333'666}},
+                };
+                proxyDatastore.initiateRpc(rpc);
+                proxyDatastore.setLeaf("/example-schema:launch-nukes/example-schema:payload/example-schema:kilotons", uint64_t{333'666});
+                // no data are returned
+            }
+
+            SECTION("small nuke") {
+                rpc = "/example-schema:launch-nukes";
+                input = {
+                    {"description", "dummy"s},
+                    {"payload/kilotons", uint64_t{4}},
+                };
+                proxyDatastore.initiateRpc(rpc);
+                proxyDatastore.setLeaf("/example-schema:launch-nukes/example-schema:payload/example-schema:kilotons", uint64_t{4});
+
+                output = {
+                    {"blast-radius", uint32_t{33'666}},
+                    {"actual-yield", uint64_t{5}},
+                };
+            }
+
+            SECTION("with lists") {
+                rpc = "/example-schema:launch-nukes";
+                input = {
+                    {"payload/kilotons", uint64_t{6}},
+                    {"cities/targets[city='Prague']/city", "Prague"s},
+                };
+                proxyDatastore.initiateRpc(rpc);
+                proxyDatastore.setLeaf("/example-schema:launch-nukes/example-schema:payload/example-schema:kilotons", uint64_t{6});
+                proxyDatastore.createItem("/example-schema:launch-nukes/example-schema:cities/example-schema:targets[city='Prague']");
+                output = {
+                    {"blast-radius", uint32_t{33'666}},
+                    {"actual-yield", uint64_t{7}},
+                    {"damaged-places", special_{SpecialValue::PresenceContainer}},
+                    {"damaged-places/targets[city='London']", special_{SpecialValue::List}},
+                    {"damaged-places/targets[city='London']/city", "London"s},
+                    {"damaged-places/targets[city='Berlin']", special_{SpecialValue::List}},
+                    {"damaged-places/targets[city='Berlin']/city", "Berlin"s},
+                };
+            }
+
+            SECTION("with leafref") {
+                datastore->createItem("/example-schema:person[name='Colton']");
+                datastore->commitChanges();
+
+                rpc = "/example-schema:fire";
+                input = {
+                    {"whom", "Colton"s}
+                };
+                proxyDatastore.initiateRpc(rpc);
+                proxyDatastore.setLeaf("/example-schema:fire/example-schema:whom", "Colton"s);
+            }
+
+            catching<OnExec>([&] {REQUIRE(datastore->executeRpc(rpc, input) == output);});
+            catching<OnExec>([&] {REQUIRE(proxyDatastore.executeRpc() == output);});
         }
 
-        SECTION("small nuke") {
-            rpc = "/example-schema:launch-nukes";
-            input = {
-                {"description", "dummy"s},
-                {"payload/kilotons", uint64_t{333'666}},
-            };
-            proxyDatastore.initiateRpc(rpc);
-            proxyDatastore.setLeaf("/example-schema:launch-nukes/example-schema:payload/example-schema:kilotons", uint64_t{333'666});
-            // no data are returned
+        SECTION("non-existing RPC")
+        {
+            catching<OnInvalidRpcPath>([&] {datastore->executeRpc("/example-schema:non-existing", DatastoreAccess::Tree{});});
         }
-
-        SECTION("small nuke") {
-            rpc = "/example-schema:launch-nukes";
-            input = {
-                {"description", "dummy"s},
-                {"payload/kilotons", uint64_t{4}},
-            };
-            proxyDatastore.initiateRpc(rpc);
-            proxyDatastore.setLeaf("/example-schema:launch-nukes/example-schema:payload/example-schema:kilotons", uint64_t{4});
-
-            output = {
-                {"blast-radius", uint32_t{33'666}},
-                {"actual-yield", uint64_t{5}},
-            };
-        }
-
-        SECTION("with lists") {
-            rpc = "/example-schema:launch-nukes";
-            input = {
-                {"payload/kilotons", uint64_t{6}},
-                {"cities/targets[city='Prague']/city", "Prague"s},
-            };
-            proxyDatastore.initiateRpc(rpc);
-            proxyDatastore.setLeaf("/example-schema:launch-nukes/example-schema:payload/example-schema:kilotons", uint64_t{6});
-            proxyDatastore.createItem("/example-schema:launch-nukes/example-schema:cities/example-schema:targets[city='Prague']");
-            output = {
-                {"blast-radius", uint32_t{33'666}},
-                {"actual-yield", uint64_t{7}},
-                {"damaged-places", special_{SpecialValue::PresenceContainer}},
-                {"damaged-places/targets[city='London']", special_{SpecialValue::List}},
-                {"damaged-places/targets[city='London']/city", "London"s},
-                {"damaged-places/targets[city='Berlin']", special_{SpecialValue::List}},
-                {"damaged-places/targets[city='Berlin']/city", "Berlin"s},
-            };
-        }
-
-        SECTION("with leafref") {
-            datastore->createItem("/example-schema:person[name='Colton']");
-            datastore->commitChanges();
-
-            rpc = "/example-schema:fire";
-            input = {
-                {"whom", "Colton"s}
-            };
-            proxyDatastore.initiateRpc(rpc);
-            proxyDatastore.setLeaf("/example-schema:fire/example-schema:whom", "Colton"s);
-        }
-
-        catching<OnRPC>([&] {REQUIRE(datastore->executeRpc(rpc, input) == output);});
-        catching<OnRPC>([&] {REQUIRE(proxyDatastore.executeRpc() == output);});
     }
 
-    SECTION("non-existing RPC")
+    SECTION("action")
     {
-        catching<OnInvalidRpcPath>([&] {datastore->executeRpc("/example-schema:non-existing", DatastoreAccess::Tree{});});
+        std::string path;
+        DatastoreAccess::Tree input, output;
+
+        output = {
+#ifdef netconf_BACKEND
+            {"/example-schema:ports[name='A']", special_{SpecialValue::List}},
+            {"/example-schema:ports[name='A']/name", enum_{"A"}},
+#endif
+            {"success", true}
+        };
+        datastore->createItem("/example-schema:ports[name='A']");
+        datastore->commitChanges();
+        SECTION("shutdown") {
+            path = "/example-schema:ports[name='A']/shutdown";
+        }
+
+        catching<OnExec>([&] {REQUIRE(datastore->executeAction(path, input) == output);});
     }
 
     waitForCompletionAndBitMore(seq1);
diff --git a/tests/datastoreaccess_mock.hpp b/tests/datastoreaccess_mock.hpp
index e1d365b..b8ec1b0 100644
--- a/tests/datastoreaccess_mock.hpp
+++ b/tests/datastoreaccess_mock.hpp
@@ -25,6 +25,7 @@
     IMPLEMENT_MOCK1(deleteItem);
     IMPLEMENT_MOCK2(moveItem);
     IMPLEMENT_MOCK2(executeRpc);
+    IMPLEMENT_MOCK2(executeAction);
 
     // Can't use IMPLEMENT_MOCK for private methods - IMPLEMENT_MOCK needs full visibility of the method
     MAKE_MOCK1(listInstances, std::vector<ListInstance>(const std::string&), override);
diff --git a/tests/example-schema.yang b/tests/example-schema.yang
index 04ae22d..da45afd 100644
--- a/tests/example-schema.yang
+++ b/tests/example-schema.yang
@@ -1,4 +1,5 @@
 module example-schema {
+    yang-version 1.1;
     prefix aha;
     namespace "http://example.com";
 
@@ -184,6 +185,15 @@
                 enum E;
             }
         }
+
+        action shutdown {
+            output {
+                leaf success {
+                    mandatory true;
+                    type boolean;
+                }
+            }
+        }
     }
 
     list org {
diff --git a/tests/pretty_printers.hpp b/tests/pretty_printers.hpp
index 65e28a6..6864443 100644
--- a/tests/pretty_printers.hpp
+++ b/tests/pretty_printers.hpp
@@ -42,7 +42,7 @@
 {
     s << "DatastoreAccess::Tree {\n";
     for (const auto& [xpath, value] : tree) {
-        s << "    {" << xpath << ", " << leafDataToString(value) << "}\n";
+        s << "    {" << xpath << ", " << leafDataToString(value) << "},\n";
     }
     s << "}\n";
     return s;
diff --git a/tests/yang.cpp b/tests/yang.cpp
index cee9e9a..f226778 100644
--- a/tests/yang.cpp
+++ b/tests/yang.cpp
@@ -334,6 +334,13 @@
                 enum eth2;
             }
         }
+        action shutdown {
+            output {
+                leaf success {
+                    type boolean;
+                }
+            }
+        }
     }
 
     feature weirdPortNames;
@@ -904,6 +911,10 @@
                         {boost::none, "/example-schema:portMapping/port"},
                         {boost::none, "/example-schema:portSettings"},
                         {boost::none, "/example-schema:portSettings/port"},
+                        {boost::none, "/example-schema:portSettings/shutdown"},
+                        {boost::none, "/example-schema:portSettings/shutdown/input"},
+                        {boost::none, "/example-schema:portSettings/shutdown/output"},
+                        {boost::none, "/example-schema:portSettings/shutdown/output/success"},
                         {boost::none, "/example-schema:systemStats"},
                         {boost::none, "/example-schema:systemStats/upTime"},
                         {boost::none, "/example-schema:subLeaf"},
@@ -1099,6 +1110,12 @@
             REQUIRE(ys.leafTypeName("/example-schema:leafEnumTypedefRestricted") == "enumTypedef");
             REQUIRE(ys.leafTypeName("/example-schema:leafInt32") == std::nullopt);
         }
+
+        SECTION("dataPathToSchemaPath")
+        {
+            REQUIRE(ys.dataPathToSchemaPath("/example-schema:portSettings[port='eth0']") == "/example-schema:portSettings");
+            REQUIRE(ys.dataPathToSchemaPath("/example-schema:portSettings[port='eth0']/shutdown") == "/example-schema:portSettings/shutdown");
+        }
     }
 
     SECTION("negative")