Merge "Add support for `empty` YANG leaf type"
diff --git a/.zuul.CzechLight-internal.yaml b/.zuul.CzechLight-internal.yaml
new file mode 100644
index 0000000..07aa1e8
--- /dev/null
+++ b/.zuul.CzechLight-internal.yaml
@@ -0,0 +1,7 @@
+- project:
+    check:
+      jobs:
+        - czechlight-clearfog:
+            required-projects:
+              - CzechLight/br2-external
+            requires: CzechLight-br2-build-clearfog
diff --git a/.zuul.yaml b/.zuul.public.yaml
similarity index 88%
rename from .zuul.yaml
rename to .zuul.public.yaml
index ecba5d1..b470c0a 100644
--- a/.zuul.yaml
+++ b/.zuul.public.yaml
@@ -9,8 +9,6 @@
             requires: CzechLight-deps-f31-clang-asan
         - f31-clang-tsan:
             requires: CzechLight-deps-f31-clang-tsan
-        - f29-gcc:
-            requires: CzechLight-deps-f29-gcc
         - f31-cpp-coverage-diff:
             voting: false
         - clang-format:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 78e67f0..abbe97b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -274,8 +274,8 @@
     cli_test(utils)
     cli_test(path_completion)
     cli_test(command_completion)
-    cli_test(enum_completion)
-    target_link_libraries(test_enum_completion leaf_data_type)
+    cli_test(set_value_completion)
+    target_link_libraries(test_set_value_completion leaf_data_type)
     cli_test(list_manipulation)
     cli_test(ls_interpreter)
     cli_test(path_utils)
diff --git a/ci/build.sh b/ci/build.sh
index fe8d8a7..3e2cf5d 100755
--- a/ci/build.sh
+++ b/ci/build.sh
@@ -75,18 +75,16 @@
 
 if [[ -z "${ARTIFACT_URL}" ]]; then
     # fallback to a promoted artifact
-    ARTIFACT_URL="https://object-store.cloud.muni.cz/swift/v1/ci-artifacts-${ZUUL_TENANT}/${ZUUL_GERRIT_HOSTNAME}/CzechLight/dependencies/${ZUUL_JOB_NAME%%-cover?(-previous|-diff)}/${DEP_SUBMODULE_COMMIT}.tar.xz"
+    ARTIFACT_URL="https://object-store.cloud.muni.cz/swift/v1/ci-artifacts-${ZUUL_TENANT}/${ZUUL_GERRIT_HOSTNAME}/CzechLight/dependencies/${ZUUL_JOB_NAME%%-cover?(-previous|-diff)}/${DEP_SUBMODULE_COMMIT}.tar.zst"
 fi
 
 ARTIFACT_FILE=$(basename ${ARTIFACT_URL})
-DEP_HASH_FROM_ARTIFACT=$(echo "${ARTIFACT_FILE}" | sed -e 's/^czechlight-dependencies-//' -e 's/\.tar\.xz$//')
+DEP_HASH_FROM_ARTIFACT=$(echo "${ARTIFACT_FILE}" | sed -e 's/^czechlight-dependencies-//' -e 's/\.tar\.zst$//')
 if [[ "${DEP_HASH_FROM_ARTIFACT}" != "${DEP_SUBMODULE_COMMIT}" ]]; then
     echo "Mismatched artifact: HEAD of ./submodules/dependencies does not match artifact commit ref"
     exit 1
 fi
-curl ${ARTIFACT_URL} --output ${ARTIFACT_FILE}
-tar -C ${PREFIX} -xf ${ARTIFACT_FILE}
-rm ${ARTIFACT_FILE}
+curl ${ARTIFACT_URL} | unzstd --stdout | tar -C ${PREFIX} -xf -
 
 cd ${BUILD_DIR}
 cmake -GNinja -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-Debug} -DCMAKE_INSTALL_PREFIX=${PREFIX} ${CMAKE_OPTIONS} ${ZUUL_PROJECT_SRC_DIR}
diff --git a/src/ast_handlers.hpp b/src/ast_handlers.hpp
index 14d3451..ab9958a 100644
--- a/src/ast_handlers.hpp
+++ b/src/ast_handlers.hpp
@@ -172,34 +172,6 @@
 
 struct dataPathListEnd_class;
 
-struct dataPath_class {
-    template <typename Iterator, typename Exception, typename Context>
-    x3::error_handler_result on_error(Iterator&, Iterator const&, Exception const&, Context const& context)
-    {
-        auto& parserContext = x3::get<parser_context_tag>(context);
-        if (parserContext.m_errorMsg.empty()) {
-            parserContext.m_errorMsg = "Expected path.";
-            return x3::error_handler_result::fail;
-        } else {
-            return x3::error_handler_result::rethrow;
-        }
-    }
-};
-
-struct schemaPath_class {
-    template <typename Iterator, typename Exception, typename Context>
-    x3::error_handler_result on_error(Iterator&, Iterator const&, Exception const&, Context const& context)
-    {
-        auto& parserContext = x3::get<parser_context_tag>(context);
-        if (parserContext.m_errorMsg.empty()) {
-            parserContext.m_errorMsg = "Expected path.";
-            return x3::error_handler_result::fail;
-        } else {
-            return x3::error_handler_result::rethrow;
-        }
-    }
-};
-
 struct discard_class;
 
 struct ls_class;
diff --git a/src/common_parsers.hpp b/src/common_parsers.hpp
index eac8e61..8da90a6 100644
--- a/src/common_parsers.hpp
+++ b/src/common_parsers.hpp
@@ -34,6 +34,22 @@
 auto const space_separator_def =
     x3::omit[x3::no_skip[x3::space]];
 
+template <typename CoerceTo>
+struct as_type {
+    template <typename...> struct Tag{};
+
+    template <typename ParserType>
+    auto operator[](ParserType p) const {
+        return x3::rule<Tag<CoerceTo, ParserType>, CoerceTo> {"as"} = x3::as_parser(p);
+    }
+};
+
+// The `as` parser creates an ad-hoc x3::rule with the attribute specified with `CoerceTo`.
+// Example usage: as<std::string>[someParser]
+// someParser will have its attribute coerced to std::string
+// https://github.com/boostorg/spirit/issues/530#issuecomment-584836532
+template <typename CoerceTo> const as_type<CoerceTo> as{};
+
 BOOST_SPIRIT_DEFINE(node_identifier)
 BOOST_SPIRIT_DEFINE(module)
 BOOST_SPIRIT_DEFINE(module_identifier)
diff --git a/src/grammars.hpp b/src/grammars.hpp
index ae7a980..74a6a17 100644
--- a/src/grammars.hpp
+++ b/src/grammars.hpp
@@ -47,7 +47,7 @@
 } const ls_options;
 
 auto const ls_def =
-    ls_::name >> *(space_separator >> ls_options) >> -(space_separator >> (dataPathListEnd | dataPath | schemaPath | (module >> "*")));
+    ls_::name >> *(space_separator >> ls_options) >> -(space_separator >> (dataPathListEnd | anyPath | (module >> "*")));
 
 auto const cd_def =
     cd_::name >> space_separator > dataPath;
@@ -132,7 +132,7 @@
     copy_::name > space_separator > copy_args;
 
 auto const describe_def =
-    describe_::name >> space_separator > (dataPathListEnd | dataPath | schemaPath);
+    describe_::name >> space_separator > (dataPathListEnd | anyPath);
 
 auto const createCommandSuggestions_def =
     x3::eps;
diff --git a/src/leaf_data.hpp b/src/leaf_data.hpp
index 4330d5a..913de8b 100644
--- a/src/leaf_data.hpp
+++ b/src/leaf_data.hpp
@@ -22,8 +22,6 @@
 x3::rule<struct leaf_data_class<yang::Binary>, binary_> const leaf_data_binary = "leaf_data_binary";
 x3::rule<struct leaf_data_class<yang::Decimal>, double> const leaf_data_decimal = "leaf_data_decimal";
 x3::rule<struct leaf_data_class<yang::String>, std::string> const leaf_data_string = "leaf_data_string";
-x3::rule<struct leaf_data_class_binary, std::string> const leaf_data_binary_data = "leaf_data_binary_data";
-x3::rule<struct leaf_data_identityRef_data_class, identityRef_> const leaf_data_identityRef_data = "leaf_data_identityRef_data";
 
 using x3::char_;
 
@@ -43,22 +41,11 @@
     '\'' >> *(char_-'\'') >> '\'' |
     '\"' >> *(char_-'\"') >> '\"';
 
-// This intermediate rule is neccessary for coercing to std::string.
-// TODO: check if I can do the coercing right in the grammar with `as{}` from
-// https://github.com/boostorg/spirit/issues/530#issuecomment-584836532
-// This would shave off some more lines.
-auto const leaf_data_binary_data_def =
-    +(x3::alnum | char_('+') | char_('/')) >> -char_('=') >> -char_('=');
-
 auto const leaf_data_binary_def =
-    leaf_data_binary_data;
+    as<std::string>[+(x3::alnum | char_('+') | char_('/')) >> -char_('=') >> -char_('=')];
 
-auto const leaf_data_identityRef_data_def =
-    -module >> node_identifier;
-
-// TODO: get rid of this and use leaf_data_identityRef_data directly
 auto const leaf_data_identityRef_def =
-    leaf_data_identityRef_data;
+    -module >> node_identifier;
 
 template <typename It, typename Ctx, typename RCtx, typename Attr>
 struct impl_LeafData {
@@ -128,50 +115,42 @@
         std::transform(type.m_allowedValues.begin(),
                 type.m_allowedValues.end(),
                 std::inserter(parserContext.m_suggestions, parserContext.m_suggestions.end()),
-                [](auto it) { return Completion{it.m_value}; });
+                [](auto it) {
+            std::string res;
+            if constexpr (std::is_same<Type, yang::IdentityRef>()) {
+                res = it.m_prefix ? it.m_prefix->m_name + ":" : "";
+            }
+            res += it.m_value;
+            return Completion{res};
+        });
         parserContext.m_completionIterator = first;
     }
     bool operator()(const yang::Enum& type) const
     {
         createSetSuggestions(type);
-        // leaf_data_enum will advance the iterator if it succeeds, so I have
-        // to save the iterator here, to roll it back in case the enum is
-        // invalid.
-        auto saveIter = first;
-        auto pass = leaf_data_enum.parse(first, last, ctx, rctx, attr);
-        if (!pass) {
-            return false;
-        }
-        auto isValidEnum = type.m_allowedValues.count(boost::get<enum_>(attr)) != 0;
-        if (!isValidEnum) {
-            first = saveIter;
-            parserContext.m_errorMsg = "leaf data type mismatch: Expected an enum here. Allowed values:";
-            for (const auto& it : type.m_allowedValues) {
-                parserContext.m_errorMsg += " " + it.m_value;
+        auto checkValidEnum = [this, type] (auto& ctx) {
+            if (type.m_allowedValues.count(boost::get<enum_>(attr)) == 0) {
+                _pass(ctx) = false;
+                parserContext.m_errorMsg = "leaf data type mismatch: Expected an enum here. Allowed values:";
+                for (const auto& it : type.m_allowedValues) {
+                    parserContext.m_errorMsg += " " + it.m_value;
+                }
             }
-        }
-        return isValidEnum;
+        };
+        return leaf_data_enum[checkValidEnum].parse(first, last, ctx, rctx, attr);
     }
     bool operator()(const yang::IdentityRef& type) const
     {
         createSetSuggestions(type);
-        // leaf_data_identityRef will advance the iterator if it succeeds, so I have
-        // to save the iterator here, to roll it back in case the enum is
-        // invalid.
-        auto saveIter = first;
-        auto pass = leaf_data_identityRef.parse(first, last, ctx, rctx, attr);
-        if (!pass) {
-            return false;
-        }
-        identityRef_ pair{boost::get<identityRef_>(attr)};
-        if (!pair.m_prefix) {
-            pair.m_prefix = module_{parserContext.currentSchemaPath().m_nodes.front().m_prefix.get().m_name};
-        }
-        auto isValidIdentity  = type.m_allowedValues.count(pair) != 0;
-        if (!isValidIdentity) {
-            first = saveIter;
-        }
-        return isValidIdentity;
+        auto checkValidIdentity = [this, type] (auto& ctx) {
+            identityRef_ pair{boost::get<identityRef_>(_attr(ctx))};
+            if (!pair.m_prefix) {
+                pair.m_prefix = module_{parserContext.currentSchemaPath().m_nodes.front().m_prefix.get().m_name};
+            }
+            _pass(ctx) = type.m_allowedValues.count(pair) != 0;
+        };
+
+        return leaf_data_identityRef[checkValidIdentity].parse(first, last, ctx, rctx, attr);
     }
     bool operator()(const yang::LeafRef& leafRef) const
     {
@@ -211,7 +190,5 @@
 
 BOOST_SPIRIT_DEFINE(leaf_data_enum)
 BOOST_SPIRIT_DEFINE(leaf_data_string)
-BOOST_SPIRIT_DEFINE(leaf_data_binary_data)
 BOOST_SPIRIT_DEFINE(leaf_data_binary)
-BOOST_SPIRIT_DEFINE(leaf_data_identityRef_data)
 BOOST_SPIRIT_DEFINE(leaf_data_identityRef)
diff --git a/src/path_parser.hpp b/src/path_parser.hpp
index 19ab2a2..6360912 100644
--- a/src/path_parser.hpp
+++ b/src/path_parser.hpp
@@ -14,8 +14,6 @@
 
 namespace x3 = boost::spirit::x3;
 
-x3::rule<dataPath_class, dataPath_> const dataPath = "dataPath";
-x3::rule<schemaPath_class, schemaPath_> const schemaPath = "schemaPath";
 x3::rule<dataNodeList_class, decltype(dataPath_::m_nodes)::value_type> const dataNodeList = "dataNodeList";
 x3::rule<dataNodesListEnd_class, decltype(dataPath_::m_nodes)> const dataNodesListEnd = "dataNodesListEnd";
 x3::rule<dataPathListEnd_class, dataPath_> const dataPathListEnd = "dataPathListEnd";
@@ -128,6 +126,53 @@
 NodeParser<schemaNode_> schemaNode;
 NodeParser<dataNode_> dataNode;
 
+using AnyPath = boost::variant<schemaPath_, dataPath_>;
+
+template <typename PathType>
+struct PathParser : x3::parser<PathParser<PathType>> {
+    using attribute_type = PathType;
+    template <typename It, typename Ctx, typename RCtx, typename Attr>
+    bool parse(It& begin, It end, Ctx const& ctx, RCtx& rctx, Attr& attr) const
+    {
+        initializePath.parse(begin, end, ctx, rctx, x3::unused);
+        dataPath_ attrData;
+
+        // 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.
+        auto res = -absoluteStart.parse(begin, end, ctx, rctx, attrData.m_scope);
+        auto dataPath = x3::attr(attrData.m_scope) >> dataNode % '/' >> -trailingSlash;
+        res = dataPath.parse(begin, end, ctx, rctx, attrData);
+        attr = attrData;
+
+        if constexpr (std::is_same<Attr, AnyPath>()) {
+            auto pathEnd = x3::rule<class PathEnd>{"pathEnd"} = &space_separator | x3::eoi;
+            // If parsing failed, or if there's more input we try parsing schema nodes
+            if (!res || !pathEnd.parse(begin, end, ctx, rctx, x3::unused)) {
+                // If dataPath parsed some nodes, they will be saved in `attrData`. We have to keep these.
+                schemaPath_ attrSchema = dataPathToSchemaPath(attrData);
+                auto schemaPath = schemaNode % '/';
+                // The schemaPath parser continues where the dataPath parser ended.
+                res = schemaPath.parse(begin, end, ctx, rctx, attrSchema.m_nodes);
+                auto trailing = -trailingSlash >> pathEnd;
+                res = trailing.parse(begin, end, ctx, rctx, attrSchema.m_trailingSlash);
+                attr = attrSchema;
+            }
+        }
+        return res;
+    }
+};
+
+// Need to use these wrappers so that my PathParser class gets the proper
+// attribute. Otherwise, Spirit injects the attribute of the outer parser that
+// uses my PathParser.
+// Example grammar: anyPath | module.
+// The PathParser class would get a boost::variant as the attribute, but I
+// don't want to deal with that, so I use these wrappers to ensure the
+// attribute I want (and let Spirit deal with boost::variant).
+auto const anyPath = x3::rule<class anyPath_class, AnyPath>{"anyPath"} = PathParser<AnyPath>{};
+auto const dataPath = x3::rule<class dataPath_class, dataPath_>{"dataPath"} = PathParser<dataPath_>{};
+
 #if __clang__
 #pragma GCC diagnostic push
 #pragma GCC diagnostic ignored "-Woverloaded-shift-op-parentheses"
@@ -172,9 +217,6 @@
 auto const createPathSuggestions_def =
     x3::eps;
 
-// I have to insert an empty vector to the first alternative, otherwise they won't have the same attribute
-auto const dataPath_def = initializePath >> absoluteStart >> createPathSuggestions >> x3::attr(decltype(dataPath_::m_nodes)()) >> x3::attr(TrailingSlash::NonPresent) >> x3::eoi | initializePath >> -(absoluteStart >> createPathSuggestions) >> dataNode % '/' >> (-(trailingSlash >> createPathSuggestions) >> -(completing >> rest) >> (&space_separator | x3::eoi));
-
 auto const dataNodeList_def =
     createPathSuggestions >> -(module) >> list;
 
@@ -188,8 +230,6 @@
 
 auto const dataPathListEnd_def = initializePath >> absoluteStart >> createPathSuggestions >> x3::attr(decltype(dataPath_::m_nodes)()) >> x3::attr(TrailingSlash::NonPresent) >> x3::eoi | initializePath >> -(absoluteStart >> createPathSuggestions) >> dataNodesListEnd >> (-(trailingSlash >> createPathSuggestions) >> -(completing >> rest) >> (&space_separator | x3::eoi));
 
-auto const schemaPath_def = initializePath >> absoluteStart >> createPathSuggestions >> x3::attr(decltype(schemaPath_::m_nodes)()) >> x3::attr(TrailingSlash::NonPresent) >> x3::eoi | initializePath >> -(absoluteStart >> createPathSuggestions) >> schemaNode % '/' >> (-(trailingSlash >> createPathSuggestions) >> -(completing >> rest) >> (&space_separator | x3::eoi));
-
 auto const leafPath_def =
     dataPath;
 
@@ -218,8 +258,6 @@
 BOOST_SPIRIT_DEFINE(leafPath)
 BOOST_SPIRIT_DEFINE(presenceContainerPath)
 BOOST_SPIRIT_DEFINE(listInstancePath)
-BOOST_SPIRIT_DEFINE(schemaPath)
-BOOST_SPIRIT_DEFINE(dataPath)
 BOOST_SPIRIT_DEFINE(dataPathListEnd)
 BOOST_SPIRIT_DEFINE(initializePath)
 BOOST_SPIRIT_DEFINE(createKeySuggestions)
diff --git a/src/schema.hpp b/src/schema.hpp
index f11265f..0a9b2ff 100644
--- a/src/schema.hpp
+++ b/src/schema.hpp
@@ -15,8 +15,6 @@
 #include "ast_path.hpp"
 #include "leaf_data_type.hpp"
 
-using ModuleValuePair = std::pair<boost::optional<std::string>, std::string>;
-
 namespace yang {
 enum class NodeTypes {
     Container,
diff --git a/src/static_schema.cpp b/src/static_schema.cpp
index 2395aa0..aecad5a 100644
--- a/src/static_schema.cpp
+++ b/src/static_schema.cpp
@@ -73,15 +73,10 @@
 
 std::set<identityRef_> StaticSchema::validIdentities(std::string_view module, std::string_view value)
 {
-    std::set<ModuleValuePair> identities;
-    getIdentSet(ModuleNodePair{boost::optional<std::string>{module}, value}, identities);
-    std::set<identityRef_> res;
+    std::set<identityRef_> identities;
+    getIdentSet(identityRef_{std::string{module}, std::string{value}}, identities);
 
-    std::transform(identities.begin(), identities.end(), std::inserter(res, res.end()), [](const auto& identity) {
-        return identityRef_{*identity.first, identity.second};
-    });
-
-    return res;
+    return identities;
 }
 
 void StaticSchema::addLeaf(const std::string& location, const std::string& name, const yang::LeafDataType& type)
@@ -96,15 +91,15 @@
     m_modules.emplace(name);
 }
 
-void StaticSchema::addIdentity(const std::optional<ModuleValuePair>& base, const ModuleValuePair& name)
+void StaticSchema::addIdentity(const std::optional<identityRef_>& base, const identityRef_& name)
 {
     if (base)
         m_identities.at(base.value()).emplace(name);
 
-    m_identities.emplace(name, std::set<ModuleValuePair>());
+    m_identities.emplace(name, std::set<identityRef_>());
 }
 
-void StaticSchema::getIdentSet(const ModuleValuePair& ident, std::set<ModuleValuePair>& res) const
+void StaticSchema::getIdentSet(const identityRef_& ident, std::set<identityRef_>& res) const
 {
     res.insert(ident);
     auto derivedIdentities = m_identities.at(ident);
diff --git a/src/static_schema.hpp b/src/static_schema.hpp
index 67bd7b9..5184ae6 100644
--- a/src/static_schema.hpp
+++ b/src/static_schema.hpp
@@ -69,16 +69,15 @@
     void addLeaf(const std::string& location, const std::string& name, const yang::LeafDataType& type);
     void addList(const std::string& location, const std::string& name, const std::set<std::string>& keys);
     void addModule(const std::string& name);
-    void addIdentity(const std::optional<ModuleValuePair>& base, const ModuleValuePair& name);
+    void addIdentity(const std::optional<identityRef_>& base, const identityRef_& name);
 
 private:
     const std::unordered_map<std::string, NodeType>& children(const std::string& name) const;
-    void getIdentSet(const ModuleValuePair& ident, std::set<ModuleValuePair>& res) const;
+    void getIdentSet(const identityRef_& ident, std::set<identityRef_>& res) const;
     bool nodeExists(const std::string& location, const std::string& node) const;
 
     std::unordered_map<std::string, std::unordered_map<std::string, NodeType>> m_nodes;
     std::set<std::string> m_modules;
 
-    // FIXME: Change the template arguments to identityRef_
-    std::map<ModuleValuePair, std::set<ModuleValuePair>> m_identities;
+    std::map<identityRef_, std::set<identityRef_>> m_identities;
 };
diff --git a/submodules/dependencies b/submodules/dependencies
index 837e9f9..735daca 160000
--- a/submodules/dependencies
+++ b/submodules/dependencies
@@ -1 +1 @@
-Subproject commit 837e9f98f3176f02b01f7e2838fc3da0f0047c71
+Subproject commit 735daca4ccb8bb91f55d793387da50266609d538
diff --git a/tests/leaf_editing.cpp b/tests/leaf_editing.cpp
index 666c975..75433e0 100644
--- a/tests/leaf_editing.cpp
+++ b/tests/leaf_editing.cpp
@@ -37,11 +37,11 @@
     schema->addLeaf("/", "mod:leafUint32", yang::Uint32{});
     schema->addLeaf("/", "mod:leafUint64", yang::Uint64{});
     schema->addLeaf("/", "mod:leafBinary", yang::Binary{});
-    schema->addIdentity(std::nullopt, ModuleValuePair{"mod", "food"});
-    schema->addIdentity(std::nullopt, ModuleValuePair{"mod", "vehicle"});
-    schema->addIdentity(ModuleValuePair{"mod", "food"}, ModuleValuePair{"mod", "pizza"});
-    schema->addIdentity(ModuleValuePair{"mod", "food"}, ModuleValuePair{"mod", "spaghetti"});
-    schema->addIdentity(ModuleValuePair{"mod", "pizza"}, ModuleValuePair{"pizza-module", "hawaii"});
+    schema->addIdentity(std::nullopt, identityRef_{"mod", "food"});
+    schema->addIdentity(std::nullopt, identityRef_{"mod", "vehicle"});
+    schema->addIdentity(identityRef_{"mod", "food"}, identityRef_{"mod", "pizza"});
+    schema->addIdentity(identityRef_{"mod", "food"}, identityRef_{"mod", "spaghetti"});
+    schema->addIdentity(identityRef_{"mod", "pizza"}, identityRef_{"pizza-module", "hawaii"});
     schema->addLeaf("/", "mod:foodIdentRef", yang::IdentityRef{schema->validIdentities("mod", "food")});
     schema->addLeaf("/", "mod:pizzaIdentRef", yang::IdentityRef{schema->validIdentities("mod", "pizza")});
     schema->addLeaf("/mod:contA", "mod:identInCont", yang::IdentityRef{schema->validIdentities("mod", "pizza")});
diff --git a/tests/enum_completion.cpp b/tests/set_value_completion.cpp
similarity index 77%
rename from tests/enum_completion.cpp
rename to tests/set_value_completion.cpp
index 07ea32c..c3ad64a 100644
--- a/tests/enum_completion.cpp
+++ b/tests/set_value_completion.cpp
@@ -1,4 +1,3 @@
-
 /*
  * Copyright (C) 2018 CESNET, https://photonics.cesnet.cz/
  * Copyright (C) 2018 FIT CVUT, https://fit.cvut.cz/
@@ -14,7 +13,7 @@
 #include "pretty_printers.hpp"
 #include "static_schema.hpp"
 
-TEST_CASE("enum completion")
+TEST_CASE("set value completion")
 {
     auto schema = std::make_shared<StaticSchema>();
     schema->addModule("mod");
@@ -24,6 +23,12 @@
     schema->addList("/", "mod:list", {"number"});
     schema->addLeaf("/mod:list", "mod:number", yang::Int32{});
     schema->addLeaf("/mod:list", "mod:leafInList", createEnum({"ano", "anoda", "ne", "katoda"}));
+    schema->addIdentity(std::nullopt, identityRef_{"mod", "food"});
+    schema->addIdentity(std::nullopt, identityRef_{"mod", "vehicle"});
+    schema->addIdentity(identityRef_{"mod", "food"}, identityRef_{"mod", "pizza"});
+    schema->addIdentity(identityRef_{"mod", "food"}, identityRef_{"mod", "spaghetti"});
+    schema->addIdentity(identityRef_{"mod", "pizza"}, identityRef_{"pizza-module", "hawaii"});
+    schema->addLeaf("/", "mod:foodIdentRef", yang::IdentityRef{schema->validIdentities("mod", "food")});
     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.
     ALLOW_CALL(*mockDatastore, listInstances("/mod:list"))
@@ -76,5 +81,12 @@
         expectedContextLength = 0;
     }
 
+    SECTION("set mod:foodIdentRef ")
+    {
+        input = "set mod:foodIdentRef ";
+        expectedCompletions = {"mod:food", "mod:pizza", "mod:spaghetti", "pizza-module:hawaii"};
+        expectedContextLength = 0;
+    }
+
     REQUIRE(parser.completeCommand(input, errorStream) == (Completions{expectedCompletions, expectedContextLength}));
 }