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}));
}