Add support for executing RPCs

Creating a temporary YangAccess for RPC input means I need to somehow
give the right libyang schemas. For that reason I supply a callable
which is able to fetch the schema and create a YangAccess instance for
ProxyDatastore.

The ProxyDatastore class now has a simple mechanism for deciding whether
to use the normal datastore and the temporary based on a path prefix.

Change-Id: Ib455f53237598bc2620161a44fb89c48ddfeb6e3
diff --git a/tests/cd.cpp b/tests/cd.cpp
index 0c5411b..307f287 100644
--- a/tests/cd.cpp
+++ b/tests/cd.cpp
@@ -29,6 +29,7 @@
     schema->addList("/", "example:twoKeyList", {"number", "name"});
     schema->addLeaf("/example:twoKeyList", "example:number", yang::Int32{});
     schema->addLeaf("/example:twoKeyList", "example:name", yang::String{});
+    schema->addRpc("/", "example:launch-nukes");
     Parser parser(schema);
     std::string input;
     std::ostringstream errorStream;
@@ -313,6 +314,11 @@
             input = "cd example:list  [number=10]";
         }
 
+        SECTION("cd into rpc")
+        {
+            input = "cd example:launch-nukes";
+        }
+
         REQUIRE_THROWS_AS(parser.parseCommand(input, errorStream), InvalidCommandException);
     }
 }
diff --git a/tests/command_completion.cpp b/tests/command_completion.cpp
index cf6b188..49017b0 100644
--- a/tests/command_completion.cpp
+++ b/tests/command_completion.cpp
@@ -21,7 +21,7 @@
     int expectedContextLength;
     SECTION("no prefix")
     {
-        expectedCompletions = {"cd", "copy", "create", "delete", "set", "commit", "get", "ls", "discard", "help", "describe", "move", "dump"};
+        expectedCompletions = {"cd", "copy", "create", "delete", "set", "commit", "get", "ls", "discard", "help", "describe", "move", "dump", "rpc", "exec", "cancel"};
         expectedContextLength = 0;
         SECTION("no space") {
             input = "";
@@ -34,7 +34,7 @@
     SECTION("c")
     {
         input = "c";
-        expectedCompletions = {"cd", "commit", "copy", "create"};
+        expectedCompletions = {"cd", "commit", "copy", "create", "cancel"};
         expectedContextLength = 1;
     }
 
diff --git a/tests/datastore_access.cpp b/tests/datastore_access.cpp
index b45f027..fb44018 100644
--- a/tests/datastore_access.cpp
+++ b/tests/datastore_access.cpp
@@ -8,6 +8,8 @@
 
 #include "trompeloeil_doctest.hpp"
 #include <sysrepo-cpp/Session.hpp>
+#include "yang_schema.hpp"
+#include "proxy_datastore.hpp"
 
 #ifdef sysrepo_BACKEND
 #include "sysrepo_access.hpp"
@@ -831,9 +833,11 @@
     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;
-        if (xpath == "/example-schema:noop"s) {
+        if (xpath == "/example-schema:noop"s || xpath == "/example-schema:fire"s) {
             return SR_ERR_OK;
-        } else if (xpath == nukes) {
+        }
+
+        if (xpath == nukes) {
             uint64_t kilotons = 0;
             bool hasCities = false;
             for (size_t i = 0; i < input->val_cnt(); ++i) {
@@ -879,21 +883,32 @@
     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
-    SysrepoAccess datastore("netconf-cli-test", Datastore::Running);
+    auto datastore = std::make_shared<SysrepoAccess>("netconf-cli-test", Datastore::Running);
 #elif defined(netconf_BACKEND)
-    NetconfAccess datastore(NETOPEER_SOCKET_PATH);
+    auto datastore = std::make_shared<NetconfAccess>(NETOPEER_SOCKET_PATH);
 #elif defined(yang_BACKEND)
-    YangAccess datastore;
-    datastore.addSchemaDir(schemaDir);
-    datastore.addSchemaFile(exampleSchemaFile);
+    auto datastore = std::make_shared<YangAccess>();
+    datastore->addSchemaDir(schemaDir);
+    datastore->addSchemaFile(exampleSchemaFile);
 #else
 #error "Unknown backend"
 #endif
 
+    auto createTemporaryDatastore = [](const std::shared_ptr<DatastoreAccess>& datastore) {
+        return std::make_shared<YangAccess>(std::static_pointer_cast<YangSchema>(datastore->schema()));
+    };
+
+    ProxyDatastore proxyDatastore(datastore, createTemporaryDatastore);
+
+    // ProxyDatastore cannot easily read DatastoreAccess::Tree, so we need to set the input via create/setLeaf/etc.
     SECTION("valid")
     {
         std::string rpc;
@@ -901,6 +916,7 @@
 
         SECTION("noop") {
             rpc = "/example-schema:noop";
+            proxyDatastore.initiateRpc(rpc);
         }
 
         SECTION("small nuke") {
@@ -909,6 +925,8 @@
                 {"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
         }
 
@@ -918,6 +936,9 @@
                 {"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}},
@@ -930,6 +951,9 @@
                 {"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}},
@@ -941,12 +965,25 @@
             };
         }
 
-        catching<OnRPC>([&] {REQUIRE(datastore.executeRpc(rpc, input) == output);});
+        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")
     {
-        catching<OnInvalidRpcPath>([&] {datastore.executeRpc("/example-schema:non-existing", DatastoreAccess::Tree{});});
+        catching<OnInvalidRpcPath>([&] {datastore->executeRpc("/example-schema:non-existing", DatastoreAccess::Tree{});});
     }
 
     waitForCompletionAndBitMore(seq1);
diff --git a/tests/example-schema.yang b/tests/example-schema.yang
index 792ac42..04ae22d 100644
--- a/tests/example-schema.yang
+++ b/tests/example-schema.yang
@@ -111,6 +111,16 @@
         }
     }
 
+    rpc fire {
+        input {
+            leaf whom {
+                type leafref {
+                    path '/aha:person/name';
+                }
+            }
+        }
+    }
+
     rpc launch-nukes {
         input {
             container payload {
diff --git a/tests/interpreter.cpp b/tests/interpreter.cpp
index 89fbd29..a88aa48 100644
--- a/tests/interpreter.cpp
+++ b/tests/interpreter.cpp
@@ -39,7 +39,11 @@
     auto schema = std::make_shared<MockSchema>();
     Parser parser(schema);
     auto datastore = std::make_shared<MockDatastoreAccess>();
-    ProxyDatastore proxyDatastore(datastore);
+    auto input_datastore = std::make_shared<MockDatastoreAccess>();
+    auto createTemporaryDatastore = [input_datastore]([[maybe_unused]] const std::shared_ptr<DatastoreAccess>& datastore) {
+        return input_datastore;
+    };
+    ProxyDatastore proxyDatastore(datastore, createTemporaryDatastore);
     std::vector<std::unique_ptr<trompeloeil::expectation>> expectations;
 
     std::vector<command_> toInterpret;
@@ -422,3 +426,44 @@
         boost::apply_visitor(Interpreter(parser, proxyDatastore), command);
     }
 }
+
+TEST_CASE("rpc")
+{
+    auto schema = std::make_shared<MockSchema>();
+    Parser parser(schema);
+    auto datastore = std::make_shared<MockDatastoreAccess>();
+    auto input_datastore = std::make_shared<MockDatastoreAccess>();
+    auto createTemporaryDatastore = [input_datastore]([[maybe_unused]] const std::shared_ptr<DatastoreAccess>& datastore) {
+        return input_datastore;
+    };
+    ProxyDatastore proxyDatastore(datastore, createTemporaryDatastore);
+
+    SECTION("entering/leaving rpc context")
+    {
+        dataPath_ rpcPath;
+        rpcPath.pushFragment({{"example"}, rpcNode_{"launch-nukes"}});
+        rpc_ rpcCmd;
+        rpcCmd.m_path = rpcPath;
+
+        {
+            REQUIRE_CALL(*input_datastore, createItem("/example:launch-nukes"));
+            boost::apply_visitor(Interpreter(parser, proxyDatastore), command_{rpcCmd});
+        }
+
+        REQUIRE(parser.currentPath() == rpcPath);
+
+        SECTION("exec")
+        {
+            REQUIRE_CALL(*input_datastore, getItems("/")).RETURN(DatastoreAccess::Tree{});
+            REQUIRE_CALL(*datastore, executeRpc("/example:launch-nukes", DatastoreAccess::Tree{})).RETURN(DatastoreAccess::Tree{});
+            boost::apply_visitor(Interpreter(parser, proxyDatastore), command_{exec_{}});
+        }
+
+        SECTION("cancel")
+        {
+            boost::apply_visitor(Interpreter(parser, proxyDatastore), command_{cancel_{}});
+        }
+
+        REQUIRE(parser.currentPath() == dataPath_{});
+    }
+}
diff --git a/tests/path_completion.cpp b/tests/path_completion.cpp
index 249dc8c..1c0c8f1 100644
--- a/tests/path_completion.cpp
+++ b/tests/path_completion.cpp
@@ -39,6 +39,8 @@
     schema->addLeaf("/", "example:leafInt", yang::Int32{});
     schema->addLeaf("/", "example:readonly", yang::Int32{}, yang::AccessType::ReadOnly);
     schema->addLeafList("/", "example:addresses", yang::String{});
+    schema->addRpc("/", "second:fire");
+    schema->addLeaf("/second:fire", "second:whom", yang::String{});
     auto mockDatastore = std::make_shared<MockDatastoreAccess>();
 
     // The parser will use DataQuery for key value completion, but I'm not testing that here, so I don't return anything.
@@ -68,7 +70,7 @@
         SECTION("ls ")
         {
             input = "ls ";
-            expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/", "second:amelie/"};
+            expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/", "second:amelie/", "second:fire/"};
             expectedContextLength = 0;
         }
 
@@ -103,7 +105,7 @@
         SECTION("ls /")
         {
             input = "ls /";
-            expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/", "second:amelie/"};
+            expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/", "second:amelie/", "second:fire/"};
             expectedContextLength = 0;
         }
 
@@ -131,7 +133,7 @@
         SECTION("ls /s")
         {
             input = "ls /s";
-            expectedCompletions = {"second:amelie/"};
+            expectedCompletions = {"second:amelie/", "second:fire/"};
             expectedContextLength = 1;
         }
 
@@ -334,5 +336,28 @@
         expectedContextLength = 0;
     }
 
+    SECTION("rpc input nodes NOT completed for rpc command")
+    {
+        input = "rpc example:fire/";
+        expectedCompletions = {};
+        expectedContextLength = 13;
+    }
+
+    SECTION("rpc input nodes completed for set command")
+    {
+        parser.changeNode({{}, {{module_{"second"}, rpcNode_{"fire"}}}});
+        input = "set ";
+        expectedCompletions = {"whom "};
+        expectedContextLength = 0;
+    }
+
+    SECTION("completion for other stuff while inside an rpc")
+    {
+        parser.changeNode({{}, {{module_{"second"}, rpcNode_{"fire"}}}});
+        input = "set ../";
+        expectedCompletions = {"example:addresses", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list", "example:ovoce", "example:ovocezelenina", "example:twoKeyList", "second:amelie/", "second:fire/"};
+        expectedContextLength = 0;
+    }
+
     REQUIRE(parser.completeCommand(input, errorStream) == (Completions{expectedCompletions, expectedContextLength}));
 }