neighbor: Refactor out netlink code

This will allow us to re-use the generic netlink bits for other netlink
request operations.

Adds test coverage to the netlink message parsing routines for sanity
and regression prevention.

Tested:
    Neighbor population still works when static neighbors are created on
    the BMC.

Change-Id: I755e86eb76a8f6f825616c13279328134de87da9
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/test/Makefile.am b/test/Makefile.am
index ffa4655..388f679 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -7,6 +7,8 @@
 test_SOURCES = \
 	test_util.cpp \
 	mock_syscall.cpp \
+	test_neighbor.cpp \
+	test_netlink.cpp \
 	test_network_manager.cpp \
 	test_ethernet_interface.cpp \
 	test_rtnetlink.cpp \
@@ -50,6 +52,7 @@
 			$(top_builddir)/network_config.o \
 			$(top_builddir)/ipaddress.o \
 			$(top_builddir)/neighbor.o \
+			$(top_builddir)/netlink.o \
 			$(top_builddir)/routing_table.o \
 			$(top_builddir)/util.o \
 			$(top_builddir)/rtnetlink_server.o \
diff --git a/test/test_neighbor.cpp b/test/test_neighbor.cpp
new file mode 100644
index 0000000..a76a7d5
--- /dev/null
+++ b/test/test_neighbor.cpp
@@ -0,0 +1,173 @@
+#include "neighbor.hpp"
+#include "util.hpp"
+
+#include <arpa/inet.h>
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+#include <net/ethernet.h>
+#include <net/if.h>
+
+#include <cstring>
+#include <stdexcept>
+#include <string>
+#include <system_error>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace phosphor
+{
+namespace network
+{
+namespace detail
+{
+
+TEST(ParseNeighbor, NotNeighborType)
+{
+    nlmsghdr hdr{};
+    hdr.nlmsg_type = RTM_NEWLINK;
+
+    std::vector<NeighborInfo> neighbors;
+    EXPECT_THROW(parseNeighbor(hdr, "", neighbors), std::runtime_error);
+    EXPECT_EQ(0, neighbors.size());
+}
+
+TEST(ParseNeighbor, SmallMsg)
+{
+    nlmsghdr hdr{};
+    hdr.nlmsg_type = RTM_NEWNEIGH;
+    std::string data = "1";
+
+    std::vector<NeighborInfo> neighbors;
+    EXPECT_THROW(parseNeighbor(hdr, data, neighbors), std::runtime_error);
+    EXPECT_EQ(0, neighbors.size());
+}
+
+TEST(ParseNeighbor, BadIf)
+{
+    nlmsghdr hdr{};
+    hdr.nlmsg_type = RTM_NEWNEIGH;
+    ndmsg msg{};
+    std::string data;
+    data.append(reinterpret_cast<char*>(&msg), sizeof(msg));
+
+    std::vector<NeighborInfo> neighbors;
+    EXPECT_THROW(parseNeighbor(hdr, data, neighbors), std::system_error);
+    EXPECT_EQ(0, neighbors.size());
+}
+
+TEST(ParseNeighbor, NoAttrs)
+{
+    nlmsghdr hdr{};
+    hdr.nlmsg_type = RTM_NEWNEIGH;
+    ndmsg msg{};
+    msg.ndm_ifindex = if_nametoindex("lo");
+    ASSERT_NE(0, msg.ndm_ifindex);
+    std::string data;
+    data.append(reinterpret_cast<char*>(&msg), sizeof(msg));
+
+    std::vector<NeighborInfo> neighbors;
+    EXPECT_THROW(parseNeighbor(hdr, data, neighbors), std::runtime_error);
+    EXPECT_EQ(0, neighbors.size());
+}
+
+TEST(ParseNeighbor, NoAddress)
+{
+    nlmsghdr hdr{};
+    hdr.nlmsg_type = RTM_NEWNEIGH;
+    ndmsg msg{};
+    msg.ndm_ifindex = if_nametoindex("lo");
+    ASSERT_NE(0, msg.ndm_ifindex);
+    ether_addr mac = {{0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa}};
+    rtattr lladdr{};
+    lladdr.rta_len = RTA_LENGTH(sizeof(mac));
+    lladdr.rta_type = NDA_LLADDR;
+    char lladdrbuf[RTA_ALIGN(lladdr.rta_len)];
+    std::memset(lladdrbuf, '\0', sizeof(lladdrbuf));
+    std::memcpy(lladdrbuf, &lladdr, sizeof(lladdr));
+    std::memcpy(RTA_DATA(lladdrbuf), &mac, sizeof(mac));
+    std::string data;
+    data.append(reinterpret_cast<char*>(&msg), sizeof(msg));
+    data.append(reinterpret_cast<char*>(&lladdrbuf), sizeof(lladdrbuf));
+
+    std::vector<NeighborInfo> neighbors;
+    EXPECT_THROW(parseNeighbor(hdr, data, neighbors), std::runtime_error);
+    EXPECT_EQ(0, neighbors.size());
+}
+
+TEST(ParseNeighbor, NoMAC)
+{
+    constexpr auto ifstr = "lo";
+    nlmsghdr hdr{};
+    hdr.nlmsg_type = RTM_NEWNEIGH;
+    ndmsg msg{};
+    msg.ndm_family = AF_INET;
+    msg.ndm_state = NUD_PERMANENT;
+    msg.ndm_ifindex = if_nametoindex(ifstr);
+    ASSERT_NE(0, msg.ndm_ifindex);
+    in_addr addr;
+    ASSERT_EQ(1, inet_pton(msg.ndm_family, "192.168.10.1", &addr));
+    rtattr dst{};
+    dst.rta_len = RTA_LENGTH(sizeof(addr));
+    dst.rta_type = NDA_DST;
+    char dstbuf[RTA_ALIGN(dst.rta_len)];
+    std::memset(dstbuf, '\0', sizeof(dstbuf));
+    std::memcpy(dstbuf, &dst, sizeof(dst));
+    std::memcpy(RTA_DATA(dstbuf), &addr, sizeof(addr));
+    std::string data;
+    data.append(reinterpret_cast<char*>(&msg), sizeof(msg));
+    data.append(reinterpret_cast<char*>(&dstbuf), sizeof(dstbuf));
+
+    std::vector<NeighborInfo> neighbors;
+    parseNeighbor(hdr, data, neighbors);
+    EXPECT_EQ(1, neighbors.size());
+    EXPECT_EQ(ifstr, neighbors[0].interface);
+    EXPECT_TRUE(neighbors[0].permanent);
+    EXPECT_FALSE(neighbors[0].mac);
+    EXPECT_TRUE(equal(addr, std::get<in_addr>(neighbors[0].address)));
+}
+
+TEST(ParseNeighbor, Full)
+{
+    constexpr auto ifstr = "lo";
+    nlmsghdr hdr{};
+    hdr.nlmsg_type = RTM_NEWNEIGH;
+    ndmsg msg{};
+    msg.ndm_family = AF_INET6;
+    msg.ndm_state = NUD_NOARP;
+    msg.ndm_ifindex = if_nametoindex(ifstr);
+    ether_addr mac = {{0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa}};
+    rtattr lladdr{};
+    lladdr.rta_len = RTA_LENGTH(sizeof(mac));
+    lladdr.rta_type = NDA_LLADDR;
+    char lladdrbuf[RTA_ALIGN(lladdr.rta_len)];
+    std::memset(lladdrbuf, '\0', sizeof(lladdrbuf));
+    std::memcpy(lladdrbuf, &lladdr, sizeof(lladdr));
+    std::memcpy(RTA_DATA(lladdrbuf), &mac, sizeof(mac));
+    in6_addr addr;
+    ASSERT_EQ(1, inet_pton(msg.ndm_family, "fd00::1", &addr));
+    rtattr dst{};
+    dst.rta_len = RTA_LENGTH(sizeof(addr));
+    dst.rta_type = NDA_DST;
+    char dstbuf[RTA_ALIGN(dst.rta_len)];
+    std::memset(dstbuf, '\0', sizeof(dstbuf));
+    std::memcpy(dstbuf, &dst, sizeof(dst));
+    std::memcpy(RTA_DATA(dstbuf), &addr, sizeof(addr));
+    std::string data;
+    data.append(reinterpret_cast<char*>(&msg), sizeof(msg));
+    data.append(reinterpret_cast<char*>(&lladdrbuf), sizeof(lladdrbuf));
+    data.append(reinterpret_cast<char*>(&dstbuf), sizeof(dstbuf));
+
+    std::vector<NeighborInfo> neighbors;
+    parseNeighbor(hdr, data, neighbors);
+    EXPECT_EQ(1, neighbors.size());
+    EXPECT_EQ(ifstr, neighbors[0].interface);
+    EXPECT_FALSE(neighbors[0].permanent);
+    EXPECT_TRUE(neighbors[0].mac);
+    EXPECT_EQ(0, std::memcmp(&mac, neighbors[0].mac->data(), sizeof(mac)));
+    EXPECT_TRUE(equal(addr, std::get<in6_addr>(neighbors[0].address)));
+}
+
+} // namespace detail
+} // namespace network
+} // namespace phosphor
diff --git a/test/test_netlink.cpp b/test/test_netlink.cpp
new file mode 100644
index 0000000..b788dd1
--- /dev/null
+++ b/test/test_netlink.cpp
@@ -0,0 +1,290 @@
+#include "netlink.hpp"
+#include "util.hpp"
+
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+
+#include <cstring>
+#include <stdexcept>
+#include <string_view>
+
+#include <gtest/gtest.h>
+
+namespace phosphor
+{
+namespace network
+{
+namespace netlink
+{
+namespace detail
+{
+
+TEST(ExtractMsgs, TooSmall)
+{
+    const char buf[] = {'1'};
+    static_assert(sizeof(buf) < sizeof(nlmsghdr));
+    std::string_view data(buf, sizeof(buf));
+
+    size_t cbCalls = 0;
+    auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+    bool done = true;
+    EXPECT_THROW(processMsg(data, done, cb), std::runtime_error);
+    EXPECT_EQ(1, data.size());
+    EXPECT_EQ(0, cbCalls);
+    EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsgs, SmallAttrLen)
+{
+    nlmsghdr hdr{};
+    hdr.nlmsg_len = NLMSG_LENGTH(0) - 1;
+    std::string_view data(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+
+    size_t cbCalls = 0;
+    auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+    bool done = true;
+    EXPECT_THROW(processMsg(data, done, cb), std::runtime_error);
+    EXPECT_EQ(NLMSG_SPACE(0), data.size());
+    EXPECT_EQ(0, cbCalls);
+    EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsgs, LargeAttrLen)
+{
+    nlmsghdr hdr{};
+    hdr.nlmsg_len = NLMSG_LENGTH(0) + 1;
+    std::string_view data(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+
+    size_t cbCalls = 0;
+    auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+    bool done = true;
+    EXPECT_THROW(processMsg(data, done, cb), std::runtime_error);
+    EXPECT_EQ(NLMSG_SPACE(0), data.size());
+    EXPECT_EQ(0, cbCalls);
+    EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsgs, NoopMsg)
+{
+    nlmsghdr hdr{};
+    hdr.nlmsg_len = NLMSG_LENGTH(0);
+    hdr.nlmsg_type = NLMSG_NOOP;
+    std::string_view data(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+
+    size_t cbCalls = 0;
+    auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+    bool done = true;
+    processMsg(data, done, cb);
+    EXPECT_EQ(0, data.size());
+    EXPECT_EQ(0, cbCalls);
+    EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsgs, AckMsg)
+{
+    nlmsgerr ack{};
+    nlmsghdr hdr{};
+    hdr.nlmsg_len = NLMSG_LENGTH(sizeof(ack));
+    hdr.nlmsg_type = NLMSG_ERROR;
+    char buf[NLMSG_ALIGN(hdr.nlmsg_len)];
+    std::memcpy(buf, &hdr, sizeof(hdr));
+    std::memcpy(NLMSG_DATA(buf), &ack, sizeof(ack));
+    std::string_view data(reinterpret_cast<char*>(&buf), sizeof(buf));
+
+    size_t cbCalls = 0;
+    auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+    bool done = true;
+    processMsg(data, done, cb);
+    EXPECT_EQ(0, data.size());
+    EXPECT_EQ(0, cbCalls);
+    EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsgs, ErrMsg)
+{
+    nlmsgerr err{};
+    err.error = EINVAL;
+    nlmsghdr hdr{};
+    hdr.nlmsg_len = NLMSG_LENGTH(sizeof(err));
+    hdr.nlmsg_type = NLMSG_ERROR;
+    char buf[NLMSG_ALIGN(hdr.nlmsg_len)];
+    std::memcpy(buf, &hdr, sizeof(hdr));
+    std::memcpy(NLMSG_DATA(buf), &err, sizeof(err));
+    std::string_view data(reinterpret_cast<char*>(&buf), sizeof(buf));
+
+    size_t cbCalls = 0;
+    nlmsghdr hdrOut;
+    std::string_view dataOut;
+    auto cb = [&](const nlmsghdr& hdr, std::string_view data) {
+        hdrOut = hdr;
+        dataOut = data;
+        cbCalls++;
+    };
+    bool done = true;
+    processMsg(data, done, cb);
+    EXPECT_EQ(0, data.size());
+    EXPECT_EQ(1, cbCalls);
+    EXPECT_TRUE(equal(hdr, hdrOut));
+    EXPECT_TRUE(equal(err, extract<nlmsgerr>(dataOut)));
+    EXPECT_EQ(0, dataOut.size());
+    EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsgs, DoneNoMulti)
+{
+    nlmsghdr hdr{};
+    hdr.nlmsg_len = NLMSG_LENGTH(0);
+    hdr.nlmsg_type = NLMSG_DONE;
+    std::string_view data(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+
+    size_t cbCalls = 0;
+    auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+    bool done = true;
+    EXPECT_THROW(processMsg(data, done, cb), std::runtime_error);
+    EXPECT_EQ(0, data.size());
+    EXPECT_EQ(0, cbCalls);
+    EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsg, TwoMultiMsgs)
+{
+    nlmsghdr hdr{};
+    hdr.nlmsg_len = NLMSG_LENGTH(0);
+    hdr.nlmsg_type = RTM_NEWLINK;
+    hdr.nlmsg_flags = NLM_F_MULTI;
+    std::string buf;
+    buf.append(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+    buf.append(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+
+    std::string_view data = buf;
+    size_t cbCalls = 0;
+    auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+    bool done = true;
+    processMsg(data, done, cb);
+    EXPECT_EQ(NLMSG_SPACE(0), data.size());
+    EXPECT_EQ(1, cbCalls);
+    EXPECT_FALSE(done);
+
+    processMsg(data, done, cb);
+    EXPECT_EQ(0, data.size());
+    EXPECT_EQ(2, cbCalls);
+    EXPECT_FALSE(done);
+}
+
+TEST(ExtractMsgs, MultiMsgValid)
+{
+    nlmsghdr hdr{};
+    hdr.nlmsg_len = NLMSG_LENGTH(0);
+    hdr.nlmsg_type = RTM_NEWLINK;
+    hdr.nlmsg_flags = NLM_F_MULTI;
+    std::string_view data(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+
+    size_t cbCalls = 0;
+    auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+    bool done = true;
+    processMsg(data, done, cb);
+    EXPECT_EQ(0, data.size());
+    EXPECT_EQ(1, cbCalls);
+    EXPECT_FALSE(done);
+
+    hdr.nlmsg_type = NLMSG_DONE;
+    hdr.nlmsg_flags = 0;
+    data = std::string_view(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+    processMsg(data, done, cb);
+    EXPECT_EQ(0, data.size());
+    EXPECT_EQ(1, cbCalls);
+    EXPECT_TRUE(done);
+}
+
+TEST(ExtractMsgs, MultiMsgInvalid)
+{
+    nlmsghdr hdr{};
+    hdr.nlmsg_len = NLMSG_LENGTH(0);
+    hdr.nlmsg_type = RTM_NEWLINK;
+    hdr.nlmsg_flags = NLM_F_MULTI;
+    std::string_view data(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+
+    size_t cbCalls = 0;
+    auto cb = [&](const nlmsghdr&, std::string_view) { cbCalls++; };
+    bool done = true;
+    processMsg(data, done, cb);
+    EXPECT_EQ(0, data.size());
+    EXPECT_EQ(1, cbCalls);
+    EXPECT_FALSE(done);
+
+    hdr.nlmsg_flags = 0;
+    data = std::string_view(reinterpret_cast<char*>(&hdr), NLMSG_SPACE(0));
+    EXPECT_THROW(processMsg(data, done, cb), std::runtime_error);
+    EXPECT_EQ(0, data.size());
+    EXPECT_EQ(1, cbCalls);
+    EXPECT_FALSE(done);
+}
+
+} // namespace detail
+
+TEST(ExtractRtAttr, TooSmall)
+{
+    const char buf[] = {'1'};
+    static_assert(sizeof(buf) < sizeof(rtattr));
+    std::string_view data(buf, sizeof(buf));
+
+    EXPECT_THROW(extractRtAttr(data), std::runtime_error);
+    EXPECT_EQ(1, data.size());
+}
+
+TEST(ExtractRtAttr, SmallAttrLen)
+{
+    rtattr rta{};
+    rta.rta_len = RTA_LENGTH(0) - 1;
+    std::string_view data(reinterpret_cast<char*>(&rta), RTA_SPACE(0));
+
+    EXPECT_THROW(extractRtAttr(data), std::runtime_error);
+    EXPECT_EQ(RTA_SPACE(0), data.size());
+}
+
+TEST(ExtractRtAttr, LargeAttrLen)
+{
+    rtattr rta{};
+    rta.rta_len = RTA_LENGTH(0) + 1;
+    std::string_view data(reinterpret_cast<char*>(&rta), RTA_SPACE(0));
+
+    EXPECT_THROW(extractRtAttr(data), std::runtime_error);
+    EXPECT_EQ(RTA_SPACE(0), data.size());
+}
+
+TEST(ExtractRtAttr, NoData)
+{
+    rtattr rta{};
+    rta.rta_len = RTA_LENGTH(0);
+    std::string_view data(reinterpret_cast<char*>(&rta), RTA_SPACE(0));
+
+    auto [hdr, attr] = extractRtAttr(data);
+    EXPECT_EQ(0, data.size());
+    EXPECT_EQ(0, attr.size());
+    EXPECT_EQ(0, std::memcmp(&rta, &hdr, sizeof(rta)));
+}
+
+TEST(ExtractRtAttr, SomeData)
+{
+    const char attrbuf[] = "abcd";
+    const char nextbuf[] = "efgh";
+    rtattr rta{};
+    rta.rta_len = RTA_LENGTH(sizeof(attrbuf));
+
+    char buf[RTA_SPACE(sizeof(attrbuf)) + sizeof(nextbuf)];
+    memcpy(buf, &rta, sizeof(rta));
+    memcpy(RTA_DATA(buf), &attrbuf, sizeof(attrbuf));
+    memcpy(buf + RTA_SPACE(sizeof(attrbuf)), &nextbuf, sizeof(nextbuf));
+    std::string_view data(buf, sizeof(buf));
+
+    auto [hdr, attr] = extractRtAttr(data);
+    EXPECT_EQ(0, memcmp(&rta, &hdr, sizeof(rta)));
+    EXPECT_EQ(sizeof(attrbuf), attr.size());
+    EXPECT_EQ(0, memcmp(&attrbuf, attr.data(), sizeof(attrbuf)));
+    EXPECT_EQ(sizeof(nextbuf), data.size());
+    EXPECT_EQ(0, memcmp(&nextbuf, data.data(), sizeof(nextbuf)));
+}
+
+} // namespace netlink
+} // namespace network
+} // namespace phosphor
diff --git a/test/test_util.cpp b/test/test_util.cpp
index 4e79394..3cf28e3 100644
--- a/test/test_util.cpp
+++ b/test/test_util.cpp
@@ -4,6 +4,9 @@
 #include <netinet/in.h>
 
 #include <cstddef>
+#include <cstring>
+#include <string>
+#include <string_view>
 #include <xyz/openbmc_project/Common/error.hpp>
 
 #include <gtest/gtest.h>
@@ -13,6 +16,7 @@
 namespace network
 {
 
+using namespace std::literals;
 using InternalFailure =
     sdbusplus::xyz::openbmc_project::Common::Error::InternalFailure;
 class TestUtil : public testing::Test
@@ -268,5 +272,93 @@
     EXPECT_EQ("fe80::", address);
 }
 
+TEST_F(TestUtil, CopyFromTooSmall)
+{
+    constexpr auto expected = "abcde"sv;
+    struct
+    {
+        uint8_t data[10];
+    } data;
+    static_assert(sizeof(data) > expected.size());
+    EXPECT_THROW(copyFrom<decltype(data)>(expected), std::runtime_error);
+}
+
+TEST_F(TestUtil, CopyFromSome)
+{
+    constexpr auto expected = "abcde"sv;
+    struct
+    {
+        uint8_t data[2];
+    } data;
+    static_assert(sizeof(data) < expected.size());
+    data = copyFrom<decltype(data)>(expected);
+    EXPECT_EQ(0, memcmp(&data, expected.data(), sizeof(data)));
+}
+
+TEST_F(TestUtil, CopyFromAll)
+{
+    constexpr auto expected = "abcde"sv;
+    struct
+    {
+        uint8_t data[5];
+    } data;
+    static_assert(sizeof(data) == expected.size());
+    data = copyFrom<decltype(data)>(expected);
+    EXPECT_EQ(0, memcmp(&data, expected.data(), sizeof(data)));
+}
+
+TEST_F(TestUtil, ExtractSome)
+{
+    constexpr auto expected = "abcde"sv;
+    auto buf = expected;
+    struct
+    {
+        uint8_t data[2];
+    } data;
+    static_assert(sizeof(data) < expected.size());
+    data = extract<decltype(data)>(buf);
+    EXPECT_EQ(0, memcmp(&data, expected.data(), sizeof(data)));
+    EXPECT_EQ(3, buf.size());
+    EXPECT_EQ(expected.substr(2), buf);
+}
+
+TEST_F(TestUtil, ExtractAll)
+{
+    constexpr auto expected = "abcde"sv;
+    auto buf = expected;
+    struct
+    {
+        uint8_t data[5];
+    } data;
+    static_assert(sizeof(data) == expected.size());
+    data = extract<decltype(data)>(buf);
+    EXPECT_EQ(0, memcmp(&data, expected.data(), sizeof(data)));
+    EXPECT_EQ(0, buf.size());
+}
+
+TEST_F(TestUtil, Equal)
+{
+    struct
+    {
+        int i;
+    } a, b{};
+    a.i = 4;
+    b.i = 4;
+
+    EXPECT_TRUE(equal(a, b));
+}
+
+TEST_F(TestUtil, NotEqual)
+{
+    struct
+    {
+        int i;
+    } a, b{};
+    a.i = 2;
+    b.i = 4;
+
+    EXPECT_FALSE(equal(a, b));
+}
+
 } // namespace network
 } // namespace phosphor