/** * @file tests/unit/test_publish.cpp * @brief Tests for Linux mDNS (Avahi) service publication. */ // The publisher (src/platform/linux/publish.cpp) is built on every // non-Apple Unix platform (see cmake/compile_definitions/linux.cmake), // so the stubbed-registration tests below compile wherever it is built. // The plain unit tests stay Linux-gated; only the fallback regression // runs on non-Linux, where the address-change watch is unsupported. #if !defined(_WIN32) && !defined(__APPLE__) // test includes #include "../tests_common.h" // standard includes #include #include #include #include #include #include // local includes #include #if defined(__linux__) // Linux-only includes for the interface-enumeration tests. #include #include #include #endif #if defined(__linux__) TEST(ServiceHostNameTest, IsDistinctFromDaemonHostName) { // The SRV target must not be the daemon's shared host name: the daemon // publishes both A and AAAA records for its own host name, so pointing the // service at it would advertise an IPv6 endpoint an IPv4-only // configuration does not serve (see #5860). ASSERT_EQ(platf::publish::service_host_name("myhost", 0), "myhost-sunshine.local"); } TEST(ServiceHostNameTest, SanitizesInvalidLabelCharacters) { ASSERT_EQ(platf::publish::service_host_name("my host.example", 0), "my-host-sunshine.local"); ASSERT_EQ(platf::publish::service_host_name("", 0), "sunshine-sunshine.local"); } TEST(ServiceHostNameTest, CollisionSuffixChangesName) { ASSERT_EQ(platf::publish::service_host_name("myhost", 1), "myhost-sunshine-1.local"); ASSERT_NE(platf::publish::service_host_name("myhost", 0), platf::publish::service_host_name("myhost", 1)); } TEST(ServiceHostNameTest, LabelStaysWithinDnsLimit) { const auto host_name = platf::publish::service_host_name(std::string(100, 'a'), 0); // The label (everything before ".local") must fit in 63 characters. ASSERT_TRUE(host_name.find('.') <= 63); } TEST(SelectServedAddressesTest, Ipv4OnlyPublishesNoIpv6) { // Regression core for #5860: with an IPv4-only configuration the selection // contains no IPv6 address, so no AAAA endpoint is published for the // service host name. const auto selected = platf::publish::select_served_addresses( {{1, boost::asio::ip::make_address("192.168.1.10")}, {1, boost::asio::ip::make_address("fe80::1")}, {2, boost::asio::ip::make_address("2001:db8::1")}}, net::IPV4 ); ASSERT_EQ(selected.size(), 1U); ASSERT_EQ(selected[0].interface, 1); ASSERT_EQ(selected[0].address, boost::asio::ip::make_address("192.168.1.10")); } TEST(SelectServedAddressesTest, BothPublishesBothFamilies) { const auto selected = platf::publish::select_served_addresses( {{1, boost::asio::ip::make_address("192.168.1.10")}, {2, boost::asio::ip::make_address("2001:db8::1")}}, net::BOTH ); ASSERT_EQ(selected.size(), 2U); ASSERT_EQ(selected[0].interface, 1); ASSERT_EQ(selected[1].interface, 2); } TEST(SelectServedAddressesTest, DropsUnusableAndDuplicateAddresses) { const auto selected = platf::publish::select_served_addresses( {{1, boost::asio::ip::make_address("127.0.0.1")}, {1, boost::asio::ip::make_address("0.0.0.0")}, {1, boost::asio::ip::make_address("::1")}, {1, boost::asio::ip::make_address("192.168.1.10")}, {1, boost::asio::ip::make_address("192.168.1.10")}, {1, boost::asio::ip::make_address("::ffff:192.168.1.10")}}, net::BOTH ); ASSERT_EQ(selected.size(), 1U); ASSERT_EQ(selected[0].address, boost::asio::ip::make_address("192.168.1.10")); } TEST(SelectServedAddressesTest, DeduplicatesByInterfaceAndAddress) { // The same address on two interfaces is reachable on both, so it is kept // once per interface; the same interface-plus-address pair is kept once. // A v4-mapped duplicate normalizes to the same IPv4 address. const auto selected = platf::publish::select_served_addresses( {{1, boost::asio::ip::make_address("192.168.1.10")}, {1, boost::asio::ip::make_address("192.168.1.10")}, {2, boost::asio::ip::make_address("192.168.1.10")}, {1, boost::asio::ip::make_address("::ffff:192.168.1.10")}}, net::BOTH ); ASSERT_EQ(selected.size(), 2U); ASSERT_EQ(selected[0], (platf::publish::ServedAddress {1, boost::asio::ip::make_address("192.168.1.10")})); ASSERT_EQ(selected[1], (platf::publish::ServedAddress {2, boost::asio::ip::make_address("192.168.1.10")})); } TEST(ServedAddressesTest, ExplicitBindResolvesOwningInterface) { // An explicit bind address is published with the interface index of the // interface that owns it, not IF_UNSPEC. const auto enumerated = platf::publish::enumerate_interface_addresses(); if (enumerated.empty()) { GTEST_SKIP() << "No local interface addresses to resolve a bind address against."; } const auto &entry = enumerated[0]; const auto saved_bind = config::sunshine.bind_address; config::sunshine.bind_address = entry.address.to_string(); const auto served = platf::publish::served_addresses(net::BOTH); config::sunshine.bind_address = saved_bind; ASSERT_EQ(served.size(), 1U); ASSERT_EQ(served[0], entry); } TEST(ServedAddressesTest, ExplicitBindWithoutOwnerFallsBackToUnspec) { // A bind address no local interface owns is still published exactly as // configured, with IF_UNSPEC. const auto saved_bind = config::sunshine.bind_address; // TEST-NET-1 (RFC 5737) is never assigned to a real interface, so no // local interface can own it. config::sunshine.bind_address = "192.0.2.10"; const auto served = platf::publish::served_addresses(net::BOTH); config::sunshine.bind_address = saved_bind; ASSERT_EQ(served.size(), 1U); ASSERT_EQ(served[0].interface, avahi::IF_UNSPEC); ASSERT_EQ(served[0].address, boost::asio::ip::make_address("192.0.2.10")); } TEST(ServedAddressesTest, ExplicitBindScopedLinkLocalResolvesOwningInterface) { // A scoped link-local bind must resolve to the interface that owns it: // getifaddrs() reports the zone (sin6_scope_id), and the enumeration must // preserve it, or the scoped bind never equals its enumerated owner and // the address falls back to IF_UNSPEC, advertising the link-local // endpoint on interfaces where Sunshine does not serve it. ifaddrs *interfaces = nullptr; if (getifaddrs(&interfaces) != 0) { GTEST_SKIP() << "getifaddrs() failed; cannot find a scoped IPv6 address."; } std::string scoped_bind; avahi::IfIndex expected_interface = avahi::IF_UNSPEC; for (const auto *ifa = interfaces; ifa != nullptr; ifa = ifa->ifa_next) { if (ifa->ifa_addr == nullptr || ifa->ifa_addr->sa_family != AF_INET6) { continue; } const auto *inet6_address = reinterpret_cast(ifa->ifa_addr); if (inet6_address->sin6_scope_id == 0) { continue; } boost::asio::ip::address_v6::bytes_type bytes {}; std::memcpy(bytes.data(), &inet6_address->sin6_addr, bytes.size()); const auto address = boost::asio::ip::address_v6(bytes, inet6_address->sin6_scope_id); const auto index = if_nametoindex(ifa->ifa_name); if (index == 0) { continue; } scoped_bind = address.to_string(); expected_interface = static_cast(index); break; } freeifaddrs(interfaces); if (scoped_bind.empty()) { GTEST_SKIP() << "No scoped IPv6 interface address to resolve a bind address against."; } const auto saved_bind = config::sunshine.bind_address; config::sunshine.bind_address = scoped_bind; const auto served = platf::publish::served_addresses(net::BOTH); config::sunshine.bind_address = saved_bind; ASSERT_EQ(served.size(), 1U); ASSERT_EQ(served[0].interface, expected_interface); ASSERT_EQ(served[0].address, boost::asio::ip::make_address(scoped_bind)); } TEST(ToAvahiAddressTest, Ipv4EncodesNetworkOrderBytes) { const auto address = platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.10")); ASSERT_EQ(address.proto, std::to_underlying(avahi::proto::PROTO_INET)); ASSERT_EQ(address.data, (std::array {192, 168, 1, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); } TEST(ToAvahiAddressTest, Ipv6EncodesAllSixteenBytes) { const auto address = platf::publish::to_avahi_address(boost::asio::ip::make_address("2001:db8::1")); ASSERT_EQ(address.proto, std::to_underlying(avahi::proto::PROTO_INET6)); ASSERT_EQ( address.data, (std::array {0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01}) ); } #endif // defined(__linux__) namespace { /** * @brief Captured Avahi entry-group calls for registration tests. */ struct AddressCall { avahi::IfIndex interface; ///< Interface index the record was published on. avahi::PublishFlags flags; ///< Flags the record was published with. avahi::Address address; ///< Address record published. }; std::vector g_address_calls; ///< Address-publication calls captured by the stub. std::vector g_service_hosts; ///< SRV-target host names captured by the stub. int g_reset_count = 0; ///< Entry-group resets captured by the stub. int g_commit_count = 0; ///< Entry-group commits captured by the stub. int g_quit_count = 0; ///< Poll-loop quits captured by the stub. bool g_group_empty = true; ///< Entry-group emptiness modeled by the stub: reset empties, commit fills. bool g_add_service_collide_once = false; ///< Next add-service call reports a collision. bool g_add_address_fail_once = false; ///< Next add-address call reports a failure. /** * @brief Avahi error code for a name collision (see `avahi::err_e`). */ constexpr int kErrCollision = -8; void FakeFree(void *p) { std::free(p); } void FakeClientFree(avahi::Client * /*client*/) { // The test client is a fake pointer; nothing to release. } char *FakeAlternativeServiceName(char *name) { return ::strdup(name); } char *FakeStrerror(int /*error*/) { return const_cast("stub Avahi error"); } int FakeClientErrno(avahi::Client * /*client*/) { return 0; } void FakeSimplePollQuit(avahi::SimplePoll * /*poll*/) { ++g_quit_count; } int FakeEntryGroupIsEmpty(avahi::EntryGroup * /*group*/) { return g_group_empty ? 1 : 0; } int FakeEntryGroupReset(avahi::EntryGroup * /*group*/) { ++g_reset_count; g_group_empty = true; return 0; } int FakeEntryGroupCommit(avahi::EntryGroup * /*group*/) { ++g_commit_count; g_group_empty = false; return 0; } int FakeEntryGroupAddService( avahi::EntryGroup * /*group*/, avahi::IfIndex /*interface*/, avahi::Protocol /*protocol*/, avahi::PublishFlags /*flags*/, const char * /*name*/, const char * /*type*/, const char * /*domain*/, const char *host, std::uint16_t /*port*/, ... ) { g_service_hosts.emplace_back(host ? host : ""); if (g_add_service_collide_once) { g_add_service_collide_once = false; return kErrCollision; } return 0; } /** * @brief Stub address publication that models Avahi's reverse-PTR conflict. * @details With the daemon's default `publish-addresses=yes`, the daemon * already owns the reverse PTR for each local IP. Publishing the same IP * for the service host without `PUBLISH_NO_REVERSE` creates a competing * unique reverse PTR and Avahi rejects it with `ERR_COLLISION`. */ int FakeEntryGroupAddAddress( avahi::EntryGroup * /*group*/, avahi::IfIndex interface, avahi::Protocol /*protocol*/, avahi::PublishFlags flags, const char * /*name*/, const avahi::Address *address ) { if ((flags & avahi::PUBLISH_NO_REVERSE) == 0) { return kErrCollision; } g_address_calls.push_back({interface, flags, *address}); return 0; } /** * @brief Stub address publication without the reverse-PTR conflict check. * @details The F2/F3 tests install this stub so their fail-before * evidence isolates their own finding: with the strict stub, the missing * `PUBLISH_NO_REVERSE` flag (F1) would fail those tests first. Linux-only: * only the Linux-gated registration tests install it. */ #if defined(__linux__) int FakeEntryGroupAddAddressLenient( avahi::EntryGroup * /*group*/, avahi::IfIndex interface, avahi::Protocol /*protocol*/, avahi::PublishFlags flags, const char * /*name*/, const avahi::Address *address ) { if (g_add_address_fail_once) { g_add_address_fail_once = false; return -1; } g_address_calls.push_back({interface, flags, *address}); return 0; } #endif /** * @brief Fixture driving the publisher with stubbed Avahi calls. */ class PublishRegistrationTest: public ::testing::Test { protected: void SetUp() override { saved_bind_address_ = config::sunshine.bind_address; saved_address_family_ = config::sunshine.address_family; avahi::free = FakeFree; avahi::client_free = FakeClientFree; avahi::alternative_service_name = FakeAlternativeServiceName; avahi::strerror = FakeStrerror; avahi::client_errno = FakeClientErrno; avahi::simple_poll_quit = FakeSimplePollQuit; avahi::entry_group_is_empty = FakeEntryGroupIsEmpty; avahi::entry_group_reset = FakeEntryGroupReset; avahi::entry_group_commit = FakeEntryGroupCommit; avahi::entry_group_add_service = FakeEntryGroupAddService; avahi::entry_group_add_address = FakeEntryGroupAddAddress; g_address_calls.clear(); g_service_hosts.clear(); g_reset_count = 0; g_commit_count = 0; g_quit_count = 0; g_group_empty = true; g_add_service_collide_once = false; g_add_address_fail_once = false; platf::publish::group = reinterpret_cast(0x1000); platf::publish::client.reset(reinterpret_cast(0x2000)); platf::publish::name.reset(::strdup("test-host")); platf::publish::host_name_collision_count = 0; platf::publish::published_addresses.reset(); } void TearDown() override { // Release owned state through the stubs before uninstalling them: // the deleters call the stubbed Avahi free functions. platf::publish::client.reset(); platf::publish::name.reset(); platf::publish::group = nullptr; platf::publish::host_name_collision_count = 0; platf::publish::published_addresses.reset(); avahi::free = nullptr; avahi::client_free = nullptr; avahi::alternative_service_name = nullptr; avahi::strerror = nullptr; avahi::client_errno = nullptr; avahi::simple_poll_quit = nullptr; avahi::entry_group_is_empty = nullptr; avahi::entry_group_reset = nullptr; avahi::entry_group_commit = nullptr; avahi::entry_group_add_service = nullptr; avahi::entry_group_add_address = nullptr; config::sunshine.bind_address = saved_bind_address_; config::sunshine.address_family = saved_address_family_; } std::string saved_bind_address_; std::string saved_address_family_; }; } // namespace #if defined(__linux__) TEST_F(PublishRegistrationTest, AddressPublicationSkipsReversePtr) { // The service-host alias must publish forward records only: with flags 0 // the daemon rejects the competing reverse PTR with ERR_COLLISION, the // group resets, and the poll loop quits before registration. const auto ret = platf::publish::add_service_addresses( platf::publish::group, "myhost-sunshine.local", {{1, boost::asio::ip::make_address("192.168.1.10")}} ); ASSERT_EQ(ret, 0); ASSERT_EQ(g_address_calls.size(), 1U); ASSERT_NE(g_address_calls[0].flags & avahi::PUBLISH_NO_REVERSE, 0); ASSERT_EQ(g_quit_count, 0); } TEST_F(PublishRegistrationTest, AddressPublicationUsesOwningInterface) { // Lenient stub: isolate the interface finding from the flag finding (F1). avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient; // A LAN address and a Docker-bridge address must each be published on // their owning interface, including distinct link-local IPv6 addresses: // publishing everything with IF_UNSPEC would hand LAN clients the // bridge address (and vice versa) under the same SRV target. const auto ret = platf::publish::add_service_addresses( platf::publish::group, "myhost-sunshine.local", {{1, boost::asio::ip::make_address("192.168.1.10")}, {2, boost::asio::ip::make_address("172.17.0.1")}, {1, boost::asio::ip::make_address("fe80::1")}, {2, boost::asio::ip::make_address("fe80::2")}} ); ASSERT_EQ(ret, 0); ASSERT_EQ(g_address_calls.size(), 4U); ASSERT_EQ(g_address_calls[0].interface, 1); ASSERT_EQ(g_address_calls[1].interface, 2); ASSERT_EQ(g_address_calls[2].interface, 1); ASSERT_EQ(g_address_calls[3].interface, 2); ASSERT_EQ(g_address_calls[0].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.10"))); ASSERT_EQ(g_address_calls[1].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("172.17.0.1"))); ASSERT_EQ(g_address_calls[2].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("fe80::1"))); ASSERT_EQ(g_address_calls[3].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("fe80::2"))); } TEST_F(PublishRegistrationTest, RefreshRepublishesOnAddressChange) { // Lenient stub: isolate the refresh finding from the flag finding (F1). avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient; // Register .10, change the snapshot to .20, deliver the change // notification: the group must be reset (withdrawing .10) and the new // snapshot committed, with no stale .10 record and the poll loop alive. config::sunshine.bind_address = "192.168.1.10"; config::sunshine.address_family = "both"; platf::publish::create_services(reinterpret_cast(0x2000)); ASSERT_EQ(g_commit_count, 1); ASSERT_EQ(g_quit_count, 0); ASSERT_EQ(g_address_calls.size(), 1U); ASSERT_EQ(g_address_calls[0].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.10"))); config::sunshine.bind_address = "192.168.1.20"; platf::publish::refresh_service_addresses(); ASSERT_EQ(g_reset_count, 1); ASSERT_EQ(g_commit_count, 2); ASSERT_EQ(g_quit_count, 0); ASSERT_EQ(g_address_calls.size(), 2U); ASSERT_EQ(g_address_calls[1].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.20"))); // A second refresh with an unchanged snapshot is a no-op: no churn. platf::publish::refresh_service_addresses(); ASSERT_EQ(g_reset_count, 1); ASSERT_EQ(g_commit_count, 2); } TEST_F(PublishRegistrationTest, RefreshCollisionRetryDoesNotQuitPollLoop) { // A service collision during a refresh retry must not quit the poll // loop: the retry inherits the refresh's no-quit-on-failure behavior, // and the service is re-registered under a new host name. avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient; config::sunshine.bind_address = "192.168.1.10"; config::sunshine.address_family = "both"; platf::publish::create_services(reinterpret_cast(0x2000)); ASSERT_EQ(g_commit_count, 1); ASSERT_EQ(g_service_hosts.size(), 1U); config::sunshine.bind_address = "192.168.1.20"; g_add_service_collide_once = true; platf::publish::refresh_service_addresses(); ASSERT_EQ(g_quit_count, 0); ASSERT_EQ(g_commit_count, 2); // Three service registrations: the initial one, the refresh attempt that // collided, and the retry under the regenerated host name. ASSERT_EQ(g_service_hosts.size(), 3U); ASSERT_EQ(g_service_hosts[1], g_service_hosts[0]); ASSERT_NE(g_service_hosts[2], g_service_hosts[0]); ASSERT_EQ(platf::publish::host_name_collision_count, 1U); } TEST_F(PublishRegistrationTest, RefreshFailureThenRestoreRepublishes) { // A -> failed B -> A: the failed refresh withdraws the group and must // invalidate the snapshot, so restoring A republishes instead of // early-returning on a stale snapshot while the group sits empty. avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient; config::sunshine.bind_address = "192.168.1.10"; config::sunshine.address_family = "both"; platf::publish::create_services(reinterpret_cast(0x2000)); ASSERT_EQ(g_commit_count, 1); ASSERT_EQ(g_quit_count, 0); ASSERT_TRUE(platf::publish::published_addresses.has_value()); ASSERT_EQ(avahi::entry_group_is_empty(platf::publish::group), 0); config::sunshine.bind_address = "192.168.1.20"; g_add_address_fail_once = true; platf::publish::refresh_service_addresses(); // The refresh reset the group and the registration failed: the group is // empty and no snapshot is committed. ASSERT_EQ(g_commit_count, 1); ASSERT_EQ(g_quit_count, 0); ASSERT_FALSE(platf::publish::published_addresses.has_value()); ASSERT_NE(avahi::entry_group_is_empty(platf::publish::group), 0); config::sunshine.bind_address = "192.168.1.10"; platf::publish::refresh_service_addresses(); // Restoring A must commit a second time, with a live group and the // poll loop still running. ASSERT_EQ(g_commit_count, 2); ASSERT_EQ(g_quit_count, 0); ASSERT_TRUE(platf::publish::published_addresses.has_value()); ASSERT_EQ(avahi::entry_group_is_empty(platf::publish::group), 0); ASSERT_EQ(g_address_calls.size(), 2U); ASSERT_EQ(g_address_calls[1].address, platf::publish::to_avahi_address(boost::asio::ip::make_address("192.168.1.10"))); // A further refresh with an unchanged snapshot is a no-op: no churn. platf::publish::refresh_service_addresses(); ASSERT_EQ(g_commit_count, 2); } TEST_F(PublishRegistrationTest, RefreshFailureThenRestoreEmptySnapshotRepublishes) { // Empty-A variant: a successfully published empty snapshot (the // daemon-hostname fallback) is a valid committed snapshot, not a // withdrawn group, so the failed-B invalidation must still let the // restored empty snapshot republish. avahi::entry_group_add_address = FakeEntryGroupAddAddressLenient; // An IPv6 bind under an IPv4-only configuration serves no addresses: // the service falls back to the daemon host name with no address records. config::sunshine.bind_address = "2001:db8::1"; config::sunshine.address_family = "ipv4"; platf::publish::create_services(reinterpret_cast(0x2000)); ASSERT_EQ(g_commit_count, 1); ASSERT_EQ(g_quit_count, 0); ASSERT_TRUE(platf::publish::published_addresses.has_value()); ASSERT_TRUE(platf::publish::published_addresses->empty()); ASSERT_EQ(avahi::entry_group_is_empty(platf::publish::group), 0); ASSERT_EQ(g_address_calls.size(), 0U); ASSERT_EQ(g_service_hosts.size(), 1U); config::sunshine.bind_address = "192.168.1.20"; config::sunshine.address_family = "both"; g_add_address_fail_once = true; platf::publish::refresh_service_addresses(); // The refresh reset the group and the registration failed: the group is // empty and no snapshot is committed, even though the last good snapshot // was itself empty. ASSERT_EQ(g_commit_count, 1); ASSERT_EQ(g_quit_count, 0); ASSERT_FALSE(platf::publish::published_addresses.has_value()); ASSERT_NE(avahi::entry_group_is_empty(platf::publish::group), 0); config::sunshine.bind_address = "2001:db8::1"; config::sunshine.address_family = "ipv4"; platf::publish::refresh_service_addresses(); // Restoring the empty snapshot must commit a second time, with a live // group and the poll loop still running. ASSERT_EQ(g_commit_count, 2); ASSERT_EQ(g_quit_count, 0); ASSERT_TRUE(platf::publish::published_addresses.has_value()); ASSERT_TRUE(platf::publish::published_addresses->empty()); ASSERT_EQ(avahi::entry_group_is_empty(platf::publish::group), 0); ASSERT_EQ(g_address_calls.size(), 0U); ASSERT_EQ(g_service_hosts.size(), 2U); // A further refresh with an unchanged snapshot is a no-op: no churn. platf::publish::refresh_service_addresses(); ASSERT_EQ(g_commit_count, 2); } #else TEST_F(PublishRegistrationTest, NonLinuxFallbackUsesDaemonHostName) { // On platforms without the netlink address-change watch (e.g. FreeBSD), // the service must not publish fixed address records for a // service-specific host name: with no refresh, a DHCP change would // leave clients with the stale snapshot. Instead the service uses the // daemon-managed host name (a null SRV target), and the Avahi daemon // maintains the interface address records. ASSERT_FALSE(platf::publish::install_address_change_watch()); // An explicit bind with a populated snapshot is the regression case: // the pre-fix code published the custom host plus its fixed addresses. config::sunshine.bind_address = "192.168.1.10"; config::sunshine.address_family = "both"; platf::publish::create_services(reinterpret_cast(0x2000)); ASSERT_EQ(g_commit_count, 1); ASSERT_EQ(g_quit_count, 0); // No fixed alias address records were published ... ASSERT_TRUE(g_address_calls.empty()); // ... and the service points at the daemon-managed host name. ASSERT_EQ(g_service_hosts.size(), 1U); ASSERT_TRUE(g_service_hosts[0].empty()); } #endif // defined(__linux__) #endif // !defined(_WIN32) && !defined(__APPLE__)