Fix completion bug

The reason why completion didn't work is that the parser didn't reject
"/ietf-system:system-" properly. It was able to parse the
ietf-system:system part but didn't care about the dash. In the end the
parser still failed, but completions did get created as if we were
already inside ietf-system:system (which were not because of the dash).
The fix is this: after a path fragment there can only be two things, a
slash or a "pathEnd" (which is a space or EOI). So, when I parse
ietf-system:system I check if that's the case. Otherwise, the parsing
failed, because we didn't parse the whole path fragment (because of the
dash in this case).

Issue: https://tree.taiga.io/project/jktjkt-netconf-cli/issue/208
Change-Id: I180308658af41e0ae119fcb17c75be9cce6aa764
diff --git a/src/ast_path.cpp b/src/ast_path.cpp
index 7ab9b18..9c77f35 100644
--- a/src/ast_path.cpp
+++ b/src/ast_path.cpp
@@ -272,6 +272,9 @@
             res = joinPaths(res, (prefixes == Prefixes::Always ? path.m_nodes.at(0).m_prefix.value().m_name + ":" : "") + std::visit(nodeToDataStringVisitor(), it.m_suffix));
         }
     }
+    if (path.m_trailingSlash == TrailingSlash::Present) {
+        res += "/";
+    }
 
     return res;
 }
diff --git a/src/path_parser.hpp b/src/path_parser.hpp
index c3df931..29dbd04 100644
--- a/src/path_parser.hpp
+++ b/src/path_parser.hpp
@@ -30,6 +30,7 @@
 x3::rule<createValueSuggestions_class, x3::unused_type> const createValueSuggestions = "createValueSuggestions";
 x3::rule<suggestKeysEnd_class, x3::unused_type> const suggestKeysEnd = "suggestKeysEnd";
 x3::rule<class leafListValue_class, leaf_data_> const leafListValue = "leafListValue";
+auto pathEnd = x3::rule<class PathEnd>{"pathEnd"} = &space_separator | x3::eoi;
 
 enum class NodeParserMode {
     CompleteDataNode,
@@ -218,7 +219,15 @@
             }
 
             if (res) {
-                parserContext.pushPathFragment(attr);
+                // After a path fragment, there can only be a slash or a "pathEnd". If this is not the case
+                // then that means there are other unparsed characters after the fragment. In that case the parsing
+                // needs to fail.
+                res = (pathEnd | &char_('/')).parse(begin, end, ctx, rctx, x3::unused);
+                if (!res) {
+                    begin = saveIter;
+                } else {
+                    parserContext.pushPathFragment(attr);
+                }
             }
 
             return res;
@@ -270,7 +279,6 @@
         initializePath.parse(begin, end, ctx, rctx, x3::unused);
         dataPath_ attrData;
 
-        auto pathEnd = x3::rule<class PathEnd>{"pathEnd"} = &space_separator | x3::eoi;
         // absoluteStart has to be separate from the dataPath parser,
         // otherwise, if the "dataNode % '/'" parser fails, the begin iterator
         // gets reverted to before the starting slash.
diff --git a/tests/cd.cpp b/tests/cd.cpp
index 1091bae..ee056ae 100644
--- a/tests/cd.cpp
+++ b/tests/cd.cpp
@@ -10,6 +10,7 @@
 #include "ast_commands.hpp"
 #include "leaf_data_helpers.hpp"
 #include "parser.hpp"
+#include "pretty_printers.hpp"
 #include "static_schema.hpp"
 
 TEST_CASE("cd")
diff --git a/tests/path_completion.cpp b/tests/path_completion.cpp
index 6c7a8e6..511d922 100644
--- a/tests/path_completion.cpp
+++ b/tests/path_completion.cpp
@@ -41,6 +41,9 @@
     schema->addLeafList("/", "example:addresses", yang::String{});
     schema->addRpc("/", "second:fire");
     schema->addLeaf("/second:fire", "second:whom", yang::String{});
+    schema->addContainer("/", "example:system");
+    schema->addContainer("/example:system", "example:thing");
+    schema->addContainer("/", "example:system-state");
     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.
@@ -70,14 +73,14 @@
         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/", "second:fire/"};
+            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/", "example:system/", "example:system-state/"};
             expectedContextLength = 0;
         }
 
         SECTION("ls e")
         {
             input = "ls e";
-            expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/"};
+            expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/", "example:system/", "example:system-state/"};
             expectedContextLength = 1;
         }
 
@@ -105,14 +108,14 @@
         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/", "second:fire/"};
+            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/", "example:system/", "example:system-state/"};
             expectedContextLength = 0;
         }
 
         SECTION("ls /e")
         {
             input = "ls /e";
-            expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/"};
+            expectedCompletions = {"example:addresses/", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list/", "example:ovoce/", "example:readonly ", "example:ovocezelenina/", "example:twoKeyList/", "example:system/", "example:system-state/"};
             expectedContextLength = 1;
         }
 
@@ -185,6 +188,13 @@
             expectedCompletions = {};
             expectedContextLength = 0;
         }
+
+        SECTION("ls /example:system-")
+        {
+            input = "ls /example:system-";
+            expectedCompletions = {"example:system-state/"};
+            expectedContextLength = 15;
+        }
     }
 
     SECTION("get completion")
@@ -383,21 +393,21 @@
     {
         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/"};
+        expectedCompletions = {"example:addresses", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list", "example:ovoce", "example:ovocezelenina", "example:twoKeyList", "second:amelie/", "second:fire/", "example:system/", "example:system-state/"};
         expectedContextLength = 0;
     }
 
     SECTION("rpc nodes not completed for the get command")
     {
         input = "get ";
-        expectedCompletions = {"example:addresses", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list", "example:ovoce", "example:ovocezelenina", "example:readonly ", "example:twoKeyList", "second:amelie/"};
+        expectedCompletions = {"example:addresses", "example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list", "example:ovoce", "example:ovocezelenina", "example:readonly ", "example:twoKeyList", "second:amelie/", "example:system/", "example:system-state/"};
         expectedContextLength = 0;
     }
 
     SECTION("leafs not completed for cd")
     {
         input = "cd ";
-        expectedCompletions = {"example:addresses", "example:ano/", "example:anoda/", "example:bota/","example:list", "example:ovoce", "example:ovocezelenina", "example:twoKeyList", "second:amelie/"};
+        expectedCompletions = {"example:addresses", "example:ano/", "example:anoda/", "example:bota/","example:list", "example:ovoce", "example:ovocezelenina", "example:twoKeyList", "second:amelie/", "example:system/", "example:system-state/"};
         expectedContextLength = 0;
     }
 
diff --git a/tests/pretty_printers.hpp b/tests/pretty_printers.hpp
index 7f19a8e..cb9962e 100644
--- a/tests/pretty_printers.hpp
+++ b/tests/pretty_printers.hpp
@@ -181,6 +181,12 @@
     return s;
 }
 
+std::ostream& operator<<(std::ostream& s, const cd_& cd)
+{
+    s << "\ncd_ {\n    " << cd.m_path << "}\n";
+    return s;
+}
+
 std::ostream& operator<<(std::ostream& s, const move_& move)
 {
     s << "\nmove_ {\n";