base-stats,tests: Add unit test for Stats::Group

Add a unit test for Stats::Group.

Three bugs were found: groups are able to add
themselves/null groups as their sub-groups, and
one can create a cyclic dependency of sub-groups.

The ADD_STAT macro is not being tested.

Change-Id: I52326994b3f75e313024f872d214e8c45943f44d
Signed-off-by: Daniel R. Carvalho <odanrc@yahoo.com.br>
Reviewed-on: https://gem5-review.googlesource.com/c/public/gem5/+/43010
Maintainer: Bobby R. Bruce <bbruce@ucdavis.edu>
Reviewed-by: Hoa Nguyen <hoanguyen@ucdavis.edu>
Tested-by: kokoro <noreply+kokoro@google.com>
diff --git a/src/base/stats/SConscript b/src/base/stats/SConscript
index f44ef03..882ac13 100644
--- a/src/base/stats/SConscript
+++ b/src/base/stats/SConscript
@@ -40,6 +40,8 @@
     else:
         Source('hdf5.cc')
 
+GTest('group.test', 'group.test.cc', 'group.cc', 'info.cc',
+    with_tag('gem5 trace'))
 GTest('info.test', 'info.test.cc', 'info.cc', '../debug.cc', '../str.cc')
 GTest('storage.test', 'storage.test.cc', '../debug.cc', '../str.cc', 'info.cc',
     'storage.cc', '../../sim/cur_tick.cc')
diff --git a/src/base/stats/group.test.cc b/src/base/stats/group.test.cc
new file mode 100644
index 0000000..92f125a
--- /dev/null
+++ b/src/base/stats/group.test.cc
@@ -0,0 +1,659 @@
+/*
+ * Copyright (c) 2021 Daniel R. Carvalho
+ * All rights reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met: redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer;
+ * redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution;
+ * neither the name of the copyright holders nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <gtest/gtest-spi.h>
+#include <gtest/gtest.h>
+
+#include "base/stats/group.hh"
+#include "base/stats/info.hh"
+#include "base/stats/output.hh"
+
+using namespace gem5;
+
+/** Test that the constructor without a parent doesn't do anything. */
+TEST(StatsGroupTest, ConstructNoParent)
+{
+    Stats::Group root(nullptr);
+    ASSERT_EQ(root.getStatGroups().size(), 0);
+}
+
+/** Test adding a single stat group to a root node. */
+TEST(StatsGroupTest, AddGetSingleStatGroup)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(nullptr);
+    root.addStatGroup("Node1", &node1);
+
+    const auto root_map = root.getStatGroups();
+    ASSERT_EQ(root_map.size(), 1);
+    ASSERT_NE(root_map.find("Node1"), root_map.end());
+
+    ASSERT_EQ(node1.getStatGroups().size(), 0);
+}
+
+/** Test that group names are unique within a node's stat group. */
+TEST(StatsGroupDeathTest, AddUniqueNameStatGroup)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(nullptr);
+    Stats::Group node2(nullptr);
+    root.addStatGroup("Node1", &node1);
+    ASSERT_ANY_THROW(root.addStatGroup("Node1", &node2));
+}
+
+/** Test that group names are not unique among two nodes' stat groups. */
+TEST(StatsGroupTest, AddNotUniqueNameAmongGroups)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(nullptr);
+    Stats::Group node2(nullptr);
+    Stats::Group node1_1(nullptr);
+    root.addStatGroup("Node1", &node1);
+    node1.addStatGroup("Node1_1", &node1_1);
+    ASSERT_NO_THROW(node1.addStatGroup("Node1", &node2));
+}
+
+/** Test that a group cannot add a non-existent group. */
+TEST(StatsGroupDeathTest, AddNull)
+{
+    Stats::Group root(nullptr);
+    ASSERT_ANY_THROW(root.addStatGroup("Node1", nullptr));
+}
+
+/** Test that a group cannot add itself. */
+TEST(StatsGroupDeathTest, AddItself)
+{
+    Stats::Group root(nullptr);
+    ASSERT_ANY_THROW(root.addStatGroup("Node1", &root));
+}
+
+/** @todo Test that a group cannot be added in a cycle. */
+TEST(StatsGroupDeathTest, DISABLED_AddCycle)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(nullptr);
+    Stats::Group node1_1(nullptr);
+    root.addStatGroup("Node1", &node1);
+    node1.addStatGroup("Node1_1", &node1_1);
+    ASSERT_ANY_THROW(node1_1.addStatGroup("Root", &root));
+}
+
+/** Test adding multiple stat groups to a root node. */
+TEST(StatsGroupTest, AddGetMultipleStatGroup)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(nullptr);
+    Stats::Group node2(nullptr);
+    root.addStatGroup("Node1", &node1);
+    root.addStatGroup("Node2", &node2);
+
+    const auto root_map = root.getStatGroups();
+    ASSERT_EQ(root_map.size(), 2);
+    ASSERT_NE(root_map.find("Node1"), root_map.end());
+    ASSERT_NE(root_map.find("Node2"), root_map.end());
+
+    ASSERT_EQ(node1.getStatGroups().size(), 0);
+    ASSERT_EQ(node2.getStatGroups().size(), 0);
+}
+
+/** Make sure that the groups are correctly assigned in the map. */
+TEST(StatsGroupTest, ConstructCorrectlyAssigned)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(nullptr);
+    Stats::Group node1_1(nullptr);
+    Stats::Group node1_1_1(nullptr);
+    root.addStatGroup("Node1", &node1);
+    node1.addStatGroup("Node1_1", &node1_1);
+    node1_1.addStatGroup("Node1_1_1", &node1_1_1);
+
+    ASSERT_EQ(node1.getStatGroups().find("Node1_1")->second->getStatGroups(),
+        node1_1.getStatGroups());
+}
+
+/**
+ * Test that the constructor, when provided both the parent and the nodes'
+ * name, creates the following tree:
+ *
+ * root
+ *   |
+ * node1
+ */
+TEST(StatsGroupTest, ConstructOneLevelLinear)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(&root, "Node1");
+
+    const auto root_map = root.getStatGroups();
+    ASSERT_EQ(root_map.size(), 1);
+    ASSERT_NE(root_map.find("Node1"), root_map.end());
+
+    ASSERT_EQ(node1.getStatGroups().size(), 0);
+}
+
+/**
+ * Test that the constructor, when provided both the parent and the nodes'
+ * name, creates the following tree:
+ *
+ *    root
+ *   /    \
+ * node1 node2
+ */
+TEST(StatsGroupTest, ConstructOneLevelOfTwoNodes)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(&root, "Node1");
+    Stats::Group node2(&root, "Node2");
+
+    const auto root_map = root.getStatGroups();
+    ASSERT_EQ(root_map.size(), 2);
+    ASSERT_NE(root_map.find("Node1"), root_map.end());
+    ASSERT_NE(root_map.find("Node2"), root_map.end());
+
+    ASSERT_EQ(node1.getStatGroups().size(), 0);
+    ASSERT_EQ(node2.getStatGroups().size(), 0);
+}
+
+/**
+ * Test that the constructor, when provided both the parent and the nodes'
+ * name, creates the following tree:
+ *
+ * root
+ *   |
+ * node1
+ *   |
+ * node1_1
+ */
+TEST(StatsGroupTest, ConstructTwoLevelsLinear)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(&root, "Node1");
+    Stats::Group node1_1(&node1, "Node1_1");
+
+    const auto root_map = root.getStatGroups();
+    ASSERT_EQ(root_map.size(), 1);
+    ASSERT_NE(root_map.find("Node1"), root_map.end());
+    ASSERT_EQ(root_map.find("Node1_1"), root_map.end());
+
+    ASSERT_EQ(node1.getStatGroups().size(), 1);
+    ASSERT_NE(node1.getStatGroups().find("Node1_1"),
+        node1.getStatGroups().end());
+
+    ASSERT_EQ(node1_1.getStatGroups().size(), 0);
+}
+
+/**
+ * Test that the constructor, when provided both the parent and the nodes'
+ * name, creates the following tree:
+ *
+ *        root
+ *       /     \
+ *  node1       node2
+ *    |        /     \
+ * node1_1  node2_1 node2_2
+ */
+TEST(StatsGroupTest, ConstructTwoLevelsUnbalancedTree)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(&root, "Node1");
+    Stats::Group node2(&root, "Node2");
+    Stats::Group node1_1(&node1, "Node1_1");
+    Stats::Group node2_1(&node2, "Node2_1");
+    Stats::Group node2_2(&node2, "Node2_2");
+
+    const auto root_map = root.getStatGroups();
+    ASSERT_EQ(root_map.size(), 2);
+    ASSERT_NE(root_map.find("Node1"), root_map.end());
+    ASSERT_NE(root_map.find("Node2"), root_map.end());
+    ASSERT_EQ(root_map.find("Node1_1"), root_map.end());
+    ASSERT_EQ(root_map.find("Node2_1"), root_map.end());
+    ASSERT_EQ(root_map.find("Node2_2"), root_map.end());
+
+    ASSERT_EQ(node1.getStatGroups().size(), 1);
+    ASSERT_NE(node1.getStatGroups().find("Node1_1"),
+        node1.getStatGroups().end());
+    ASSERT_EQ(node1.getStatGroups().find("Node2_1"),
+        node1.getStatGroups().end());
+    ASSERT_EQ(node1.getStatGroups().find("Node2_2"),
+        node1.getStatGroups().end());
+
+    ASSERT_EQ(node2.getStatGroups().size(), 2);
+    ASSERT_EQ(node2.getStatGroups().find("Node1_1"),
+        node2.getStatGroups().end());
+    ASSERT_NE(node2.getStatGroups().find("Node2_1"),
+        node2.getStatGroups().end());
+    ASSERT_NE(node2.getStatGroups().find("Node2_2"),
+        node2.getStatGroups().end());
+
+    ASSERT_EQ(node1_1.getStatGroups().size(), 0);
+    ASSERT_EQ(node2_1.getStatGroups().size(), 0);
+    ASSERT_EQ(node2_2.getStatGroups().size(), 0);
+}
+
+class DummyInfo : public Stats::Info
+{
+  public:
+    using Stats::Info::Info;
+
+    int value = 0;
+
+    bool check() const override { return true; }
+    void prepare() override {}
+    void reset() override { value = 0; }
+    bool zero() const override { return false; }
+    void visit(Stats::Output &visitor) override {}
+};
+
+/** Test adding stats to a group. */
+TEST(StatsGroupTest, AddGetStat)
+{
+    Stats::Group root(nullptr);
+    auto info_vec = root.getStats();
+    ASSERT_EQ(info_vec.size(), 0);
+
+    DummyInfo info;
+    info.setName("InfoAddGetStat");
+    root.addStat(&info);
+    info_vec = root.getStats();
+    ASSERT_EQ(info_vec.size(), 1);
+    ASSERT_EQ(info_vec[0]->name, "InfoAddGetStat");
+
+    DummyInfo info2;
+    info2.setName("InfoAddGetStat2");
+    root.addStat(&info2);
+    info_vec = root.getStats();
+    ASSERT_EQ(info_vec.size(), 2);
+    ASSERT_EQ(info_vec[0]->name, "InfoAddGetStat");
+    ASSERT_EQ(info_vec[1]->name, "InfoAddGetStat2");
+}
+
+/** Test that a group cannot merge if another group is not provided. */
+TEST(StatsGroupDeathTest, MergeStatGroupNoGroup)
+{
+    Stats::Group root(nullptr);
+    ASSERT_ANY_THROW(root.mergeStatGroup(nullptr));
+}
+
+/** Test that a group cannot merge with itself. */
+TEST(StatsGroupDeathTest, MergeStatGroupItself)
+{
+    Stats::Group root(nullptr);
+    ASSERT_ANY_THROW(root.mergeStatGroup(&root));
+}
+
+/** Test merging groups. */
+TEST(StatsGroupTest, MergeStatGroup)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(nullptr);
+
+    DummyInfo info;
+    info.setName("InfoMergeStatGroup");
+    node1.addStat(&info);
+    DummyInfo info2;
+    info2.setName("InfoMergeStatGroup2");
+    node1.addStat(&info2);
+
+    root.mergeStatGroup(&node1);
+    auto info_vec = root.getStats();
+    ASSERT_EQ(info_vec.size(), 2);
+    ASSERT_EQ(info_vec[0]->name, "InfoMergeStatGroup");
+    ASSERT_EQ(info_vec[1]->name, "InfoMergeStatGroup2");
+}
+
+/** Test that a group that has already been merged cannot be merged again. */
+TEST(StatsGroupDeathTest, MergeStatGroupMergedParent)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(nullptr);
+    Stats::Group node2(nullptr);
+    root.mergeStatGroup(&node2);
+    ASSERT_ANY_THROW(node1.mergeStatGroup(&node2));
+}
+
+/**
+ * Test that after a group has been merged, adding stats to it will add
+ * stats to the group it was merged to too.
+ */
+TEST(StatsGroupTest, AddStatMergedParent)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(nullptr);
+    Stats::Group node1_1(nullptr);
+
+    root.mergeStatGroup(&node1);
+    root.mergeStatGroup(&node1_1);
+
+    DummyInfo info;
+    info.setName("AddStatMergedParent");
+    node1_1.addStat(&info);
+
+    auto info_vec = root.getStats();
+    ASSERT_EQ(info_vec.size(), 1);
+    ASSERT_EQ(info_vec[0]->name, "AddStatMergedParent");
+    info_vec = node1.getStats();
+    ASSERT_EQ(info_vec.size(), 0);
+    info_vec = node1_1.getStats();
+    ASSERT_EQ(info_vec.size(), 1);
+    ASSERT_EQ(info_vec[0]->name, "AddStatMergedParent");
+}
+
+/**
+ * Test that after a group has been merged, adding stats to the "main" group
+ * does not add stats to the group it was merged to.
+ */
+TEST(StatsGroupTest, AddStatMergedParentMain)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(nullptr);
+
+    root.mergeStatGroup(&node1);
+
+    DummyInfo info;
+    info.setName("AddStatMergedParentMain");
+    root.addStat(&info);
+
+    auto info_vec = root.getStats();
+    ASSERT_EQ(info_vec.size(), 1);
+    ASSERT_EQ(info_vec[0]->name, "AddStatMergedParentMain");
+    info_vec = node1.getStats();
+    ASSERT_EQ(info_vec.size(), 0);
+}
+
+/**
+ * Test that calling the constructor with a parent, but no name merges the
+ * groups.
+ */
+TEST(StatsGroupTest, ConstructNoName)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(&root);
+
+    DummyInfo info;
+    info.setName("InfoConstructNoName");
+    node1.addStat(&info);
+
+    auto info_vec = root.getStats();
+    ASSERT_EQ(info_vec.size(), 1);
+    ASSERT_EQ(info_vec[0]->name, "InfoConstructNoName");
+}
+
+/**
+ * Test that calling regStats calls the respective function of all sub-groups
+ * and merged groups.
+ */
+TEST(StatsGroupTest, RegStats)
+{
+    class TestGroup : public Stats::Group
+    {
+      public:
+        using Stats::Group::Group;
+
+        int value = 0;
+
+        void
+        regStats() override
+        {
+            value++;
+            Stats::Group::regStats();
+        }
+    };
+
+    TestGroup root(nullptr);
+    root.value = 1;
+    TestGroup node1(&root, "Node1");
+    node1.value = 2;
+    TestGroup node1_1(&node1, "Node1_1");
+    node1_1.value = 3;
+    TestGroup node1_2(&node1_1);
+    node1_2.value = 4;
+
+    node1.regStats();
+    ASSERT_EQ(root.value, 1);
+    ASSERT_EQ(node1.value, 3);
+    ASSERT_EQ(node1_1.value, 4);
+    ASSERT_EQ(node1_2.value, 5);
+}
+
+/**
+ * Test that resetting a stat from a specific node reset the stats of all its
+ * sub-groups and merged groups, and that it does not reset the stats of its
+ * parents.
+ */
+TEST(StatsGroupTest, ResetStats)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(&root, "Node1");
+    Stats::Group node1_1(&node1, "Node1_1");
+    Stats::Group node1_2(&node1_1);
+
+    DummyInfo info;
+    info.setName("InfoResetStats");
+    info.value = 1;
+    root.addStat(&info);
+
+    DummyInfo info2;
+    info2.setName("InfoResetStats2");
+    info2.value = 2;
+    node1.addStat(&info2);
+
+    DummyInfo info3;
+    info3.setName("InfoResetStats3");
+    info3.value = 3;
+    node1_1.addStat(&info3);
+
+    DummyInfo info4;
+    info4.setName("InfoResetStats4");
+    info4.value = 4;
+    node1_1.addStat(&info4);
+
+    DummyInfo info5;
+    info5.setName("InfoResetStats5");
+    info5.value = 5;
+    node1_2.addStat(&info5);
+
+    node1.resetStats();
+    ASSERT_EQ(info.value, 1);
+    ASSERT_EQ(info2.value, 0);
+    ASSERT_EQ(info3.value, 0);
+    ASSERT_EQ(info4.value, 0);
+    ASSERT_EQ(info5.value, 0);
+}
+
+/**
+ * Test that calling preDumpStats calls the respective function of all sub-
+ * groups and merged groups.
+ */
+TEST(StatsGroupTest, PreDumpStats)
+{
+    class TestGroup : public Stats::Group
+    {
+      public:
+        using Stats::Group::Group;
+
+        int value = 0;
+
+        void
+        preDumpStats() override
+        {
+            value++;
+            Stats::Group::preDumpStats();
+        }
+    };
+
+    TestGroup root(nullptr);
+    root.value = 1;
+    TestGroup node1(&root, "Node1");
+    node1.value = 2;
+    TestGroup node1_1(&node1, "Node1_1");
+    node1_1.value = 3;
+    TestGroup node1_2(&node1_1);
+    node1_2.value = 4;
+
+    node1.preDumpStats();
+    ASSERT_EQ(root.value, 1);
+    ASSERT_EQ(node1.value, 3);
+    ASSERT_EQ(node1_1.value, 4);
+    ASSERT_EQ(node1_2.value, 5);
+}
+
+/** Test that resolving a non-existent stat returns a nullptr. */
+TEST(StatsGroupTest, ResolveStatNone)
+{
+    Stats::Group root(nullptr);
+
+    DummyInfo info;
+    info.setName("InfoResolveStatNone");
+    root.addStat(&info);
+
+    auto info_found = root.resolveStat("InfoResolveStatAny");
+    ASSERT_EQ(info_found, nullptr);
+}
+
+/** Test resolving a stat belonging to the caller group. */
+TEST(StatsGroupTest, ResolveStatSelf)
+{
+    Stats::Group root(nullptr);
+
+    DummyInfo info;
+    info.setName("InfoResolveStatSelf");
+    root.addStat(&info);
+
+    DummyInfo info2;
+    info2.setName("InfoResolveStatSelf2");
+    root.addStat(&info2);
+
+    DummyInfo info3;
+    info3.setName("InfoResolveStatSelf3");
+    root.addStat(&info3);
+
+    auto info_found = root.resolveStat("InfoResolveStatSelf");
+    ASSERT_NE(info_found, nullptr);
+    ASSERT_EQ(info_found->name, "InfoResolveStatSelf");
+
+    info_found = root.resolveStat("InfoResolveStatSelf2");
+    ASSERT_NE(info_found, nullptr);
+    ASSERT_EQ(info_found->name, "InfoResolveStatSelf2");
+
+    info_found = root.resolveStat("InfoResolveStatSelf3");
+    ASSERT_NE(info_found, nullptr);
+    ASSERT_EQ(info_found->name, "InfoResolveStatSelf3");
+}
+
+/** Test that resolving stats from sub-groups is possible. */
+TEST(StatsGroupTest, ResolveSubGroupStatFromParent)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(&root, "Node1");
+    Stats::Group node1_1(&node1, "Node1_1");
+    Stats::Group node1_1_1(&node1_1, "Node1_1_1");
+
+    DummyInfo info;
+    info.setName("InfoResolveSubGroupStatFromParent");
+    node1.addStat(&info);
+
+    DummyInfo info2;
+    info2.setName("InfoResolveSubGroupStatFromParent2");
+    node1_1.addStat(&info2);
+
+    DummyInfo info3;
+    info3.setName("InfoResolveSubGroupStatFromParent3");
+    node1_1_1.addStat(&info3);
+
+    auto info_found =
+        root.resolveStat("Node1.InfoResolveSubGroupStatFromParent");
+    ASSERT_NE(info_found, nullptr);
+    ASSERT_EQ(info_found->name, "InfoResolveSubGroupStatFromParent");
+
+    info_found = root.resolveStat(
+        "Node1.Node1_1.InfoResolveSubGroupStatFromParent2");
+    ASSERT_NE(info_found, nullptr);
+    ASSERT_EQ(info_found->name, "InfoResolveSubGroupStatFromParent2");
+
+    info_found = root.resolveStat(
+        "Node1.Node1_1.Node1_1_1.InfoResolveSubGroupStatFromParent3");
+    ASSERT_NE(info_found, nullptr);
+    ASSERT_EQ(info_found->name, "InfoResolveSubGroupStatFromParent3");
+}
+
+/** Test that resolving a stat from the parent is not possible. */
+TEST(StatsGroupTest, ResolveStatSubGroupOnSubGroup)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(&root, "Node1");
+
+    DummyInfo info;
+    info.setName("InfoResolveStatSubGroupOnSubGroup");
+    root.addStat(&info);
+
+    auto info_found = node1.resolveStat("InfoResolveStatSubGroupOnSubGroup");
+    ASSERT_EQ(info_found, nullptr);
+}
+
+/** Test that resolving a merged stat is possible. */
+TEST(StatsGroupTest, ResolveStatMerged)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(nullptr);
+
+    DummyInfo info;
+    info.setName("InfoResolveStatMerged");
+    node1.addStat(&info);
+    DummyInfo info2;
+    info2.setName("InfoResolveStatMerged2");
+    node1.addStat(&info2);
+
+    root.mergeStatGroup(&node1);
+
+    auto info_found = root.resolveStat("InfoResolveStatMerged");
+    ASSERT_NE(info_found, nullptr);
+    ASSERT_EQ(info_found->name, "InfoResolveStatMerged");
+
+    info_found = root.resolveStat("InfoResolveStatMerged2");
+    ASSERT_NE(info_found, nullptr);
+    ASSERT_EQ(info_found->name, "InfoResolveStatMerged2");
+}
+
+/** Test that resolving a stat belonging to a merged sub-group is possible. */
+TEST(StatsGroupTest, ResolveStatMergedSubGroup)
+{
+    Stats::Group root(nullptr);
+    Stats::Group node1(nullptr);
+    Stats::Group node2(nullptr);
+
+    DummyInfo info;
+    info.setName("InfoResolveStatMergedSubGroup");
+    node2.addStat(&info);
+
+    root.addStatGroup("Node1", &node1);
+    node1.mergeStatGroup(&node2);
+
+    auto info_found = root.resolveStat("Node1.InfoResolveStatMergedSubGroup");
+    ASSERT_NE(info_found, nullptr);
+    ASSERT_EQ(info_found->name, "InfoResolveStatMergedSubGroup");
+}