diff --git a/platforms/posix/src/px4/windows/tests/CMakeLists.txt b/platforms/posix/src/px4/windows/tests/CMakeLists.txt new file mode 100644 index 0000000000..fd0033eeed --- /dev/null +++ b/platforms/posix/src/px4/windows/tests/CMakeLists.txt @@ -0,0 +1,108 @@ +############################################################################ +# +# Copyright (c) 2026 PX4 Development Team. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# 3. Neither the name PX4 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. +# +############################################################################ + +# +# Unit tests for the Windows POSIX shim layer. These targets only build +# when CMAKE_TESTING is on (BUILD_TESTING is set by include(CTest)). +# Tests are gated to Windows hosts because the shim headers are +# Windows-only by design; on non-Windows the .cpp files compile to a +# trivial sentinel test so the gtest runner still produces output. +# + +if(NOT BUILD_TESTING) + return() +endif() + +# Shared static-init hook that silences the MSVC Debug CRT invalid +# parameter handler so tests that exercise bad-fd / bad-handle paths do +# not abort with a debug-time assertion dialog. Linked into every +# Windows shim test target as an EXTRA_SRC. +set(_px4_windows_shim_silencer + ${CMAKE_CURRENT_SOURCE_DIR}/test_main_silence.cpp +) + +# Headers-only tests (libgen, dirent, sched, syslog, mman, time, unistd, +# getopt, fcntl). The translation unit pulls the shim headers via the +# existing windows_shim include path; the inline fcntl() routes +# O_NONBLOCK changes through ioctlsocket() so we need ws2_32 to satisfy +# the symbol even on the headers-only target. +px4_add_unit_gtest( + SRC test_windows_shim_headers.cpp + EXTRA_SRCS ${_px4_windows_shim_silencer} + LINKLIBS ws2_32 +) + +# Source-defined shim tests (errno_map, env, sysconf, mman, flock, dlfcn, +# ids, if_query, resolver, socket loopback). Pulls in the standalone +# Windows shim sources directly so the tests do not need the full PX4 +# platform/uORB stack. +set(_px4_windows_shim_root ${CMAKE_CURRENT_SOURCE_DIR}/..) + +set(_px4_windows_shim_test_srcs + ${_px4_windows_shim_root}/posix/sys/errno_map.cpp + ${_px4_windows_shim_root}/posix/sys/sysconf.cpp + ${_px4_windows_shim_root}/posix/proc/env.cpp + ${_px4_windows_shim_root}/posix/proc/ids.cpp + ${_px4_windows_shim_root}/posix/fs/flock.cpp + ${_px4_windows_shim_root}/posix/fs/mman.cpp + ${_px4_windows_shim_root}/posix/lib/dlfcn.cpp + ${_px4_windows_shim_root}/posix/net/if_query.cpp + ${_px4_windows_shim_root}/posix/net/resolver.cpp +) + +px4_add_unit_gtest( + SRC test_windows_shim_runtime.cpp + EXTRA_SRCS ${_px4_windows_shim_test_srcs} ${_px4_windows_shim_silencer} + INCLUDES + ${PX4_SOURCE_DIR}/platforms/posix/include/windows_shim + ${_px4_windows_shim_root}/include + LINKLIBS ws2_32 iphlpapi +) + +# poll() inline implementation - header-only test. +px4_add_unit_gtest( + SRC test_windows_shim_poll.cpp + EXTRA_SRCS ${_px4_windows_shim_silencer} + INCLUDES + ${PX4_SOURCE_DIR}/platforms/posix/include/windows_shim + LINKLIBS ws2_32 +) + +# I/O helpers (pipe, fsync, dprintf, mkdir, lstat). Header-only; the +# POSIX names route to MSVC CRT primitives via inline shim wrappers. +px4_add_unit_gtest( + SRC test_windows_shim_io.cpp + EXTRA_SRCS ${_px4_windows_shim_silencer} + INCLUDES + ${PX4_SOURCE_DIR}/platforms/posix/include/windows_shim +) diff --git a/platforms/posix/src/px4/windows/tests/test_main_silence.cpp b/platforms/posix/src/px4/windows/tests/test_main_silence.cpp new file mode 100644 index 0000000000..3d134e299f --- /dev/null +++ b/platforms/posix/src/px4/windows/tests/test_main_silence.cpp @@ -0,0 +1,67 @@ +/**************************************************************************** + * + * Copyright (C) 2026 PX4 Development Team. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name PX4 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. + * + ****************************************************************************/ + +/** + * @file test_main_silence.cpp + * + * Suppress the MSVC Debug CRT invalid-parameter handler so unit tests that + * intentionally exercise bad-fd / bad-handle paths (e.g. fcntl(0, 0xBAD), + * flock(-1, ...), _open on a closed fd) do not abort with a debug-time + * assertion dialog. Without this hook the Debug CRT raises Watson and + * terminates the process before gtest can record the EXPECT_* result. + * + * Linked into every shim unit test target via + * platforms/posix/src/px4/windows/tests/CMakeLists.txt; takes effect at + * static-init time before main() runs gtest. + */ + +#ifdef _WIN32 +#include +#include + +namespace +{ +static void silent_invalid_param(const wchar_t *, const wchar_t *, const wchar_t *, + unsigned, uintptr_t) {} +struct InstallSilencer { + InstallSilencer() + { + _set_invalid_parameter_handler(silent_invalid_param); + _CrtSetReportMode(_CRT_ASSERT, 0); + _CrtSetReportMode(_CRT_ERROR, 0); + _CrtSetReportMode(_CRT_WARN, 0); + } +}; +static InstallSilencer kInstallSilencer; +} // namespace +#endif diff --git a/platforms/posix/src/px4/windows/tests/test_windows_shim_headers.cpp b/platforms/posix/src/px4/windows/tests/test_windows_shim_headers.cpp new file mode 100644 index 0000000000..d9edc14581 --- /dev/null +++ b/platforms/posix/src/px4/windows/tests/test_windows_shim_headers.cpp @@ -0,0 +1,722 @@ +/**************************************************************************** + * + * Copyright (C) 2026 PX4 Development Team. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name PX4 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. + * + ****************************************************************************/ + +/** + * @file test_windows_shim_headers.cpp + * + * Unit tests for the inline functions defined in the Windows POSIX shim + * headers under platforms/posix/include/windows_shim/. + * + * Each header that declares logic-bearing inline functions is exercised + * for happy-path, NULL/empty, boundary, and error cases. Tests gated on + * _WIN32 because the shim headers are Windows-only by design. + */ + +#include + +#ifdef _WIN32 + +#include +#include +#include +#include +#include +#include + +/* The shim header for usleep references three tunables that are normally + * defined in platforms/posix/src/px4/windows/runtime/init.cpp. This unit + * test binary intentionally does not link init.cpp (it would drag in the + * full runtime), so we provide local definitions matching the documented + * defaults. Keep them in sync with init.cpp. */ +extern "C" { + long g_usleep_pure_spin_us = 5000; + long g_usleep_spin_tail_us = 1000; + long g_usleep_adaptive_min_tail_us = 700; + int g_usleep_use_wine_deadline_clock = 0; + uint64_t px4_windows_wine_monotonic_time_us() + { + return 0; + } + void px4_windows_wine_spin_until_us(uint64_t deadline_us) + { + (void)deadline_us; + } +} + +/* Pull headers via the shim include path so the inline functions under + * test are the same definitions PX4 sources see. */ +#include +#include +#include +#include +#include +#include +#include + +/* basename / dirname --------------------------------------------------- */ + +TEST(WindowsShimLibgen, BasenameNull) +{ + EXPECT_STREQ(basename(nullptr), "."); +} + +TEST(WindowsShimLibgen, BasenameEmpty) +{ + char path[1] = { 0 }; + EXPECT_STREQ(basename(path), "."); +} + +TEST(WindowsShimLibgen, BasenameLeafOnly) +{ + char path[] = "file.txt"; + EXPECT_STREQ(basename(path), "file.txt"); +} + +TEST(WindowsShimLibgen, BasenamePosixSeparators) +{ + char path[] = "a/b/c/foo"; + EXPECT_STREQ(basename(path), "foo"); +} + +TEST(WindowsShimLibgen, BasenameWindowsSeparators) +{ + char path[] = "a\\b\\c\\foo.bin"; + EXPECT_STREQ(basename(path), "foo.bin"); +} + +TEST(WindowsShimLibgen, BasenameMixedSeparators) +{ + char path[] = "C:/Users\\Nuno/file"; + EXPECT_STREQ(basename(path), "file"); +} + +TEST(WindowsShimLibgen, BasenameTrailingSlashReturnsDot) +{ + char path[] = "dir/"; + EXPECT_STREQ(basename(path), "."); +} + +TEST(WindowsShimLibgen, DirnameNullOrEmpty) +{ + EXPECT_STREQ(dirname(nullptr), "."); + char empty[1] = { 0 }; + EXPECT_STREQ(dirname(empty), "."); +} + +TEST(WindowsShimLibgen, DirnameNoSeparators) +{ + char path[] = "filename"; + EXPECT_STREQ(dirname(path), "."); +} + +TEST(WindowsShimLibgen, DirnamePosixPath) +{ + char path[] = "a/b/c/foo"; + EXPECT_STREQ(dirname(path), "a/b/c"); +} + +TEST(WindowsShimLibgen, DirnameWindowsPath) +{ + char path[] = "C:\\Users\\foo\\bar"; + EXPECT_STREQ(dirname(path), "C:\\Users\\foo"); +} + +TEST(WindowsShimLibgen, DirnameRootSeparatorPreserved) +{ + char path[] = "/foo"; + /* When the only separator is at index 0, dirname should leave it + * in place to preserve the root anchor. */ + EXPECT_STREQ(dirname(path), "/"); +} + +/* sched ---------------------------------------------------------------- */ + +#if defined(_MSC_VER) && !defined(__clang__) +TEST(WindowsShimSched, PriorityRange) +{ + EXPECT_EQ(sched_get_priority_max(SCHED_OTHER), 15); + EXPECT_EQ(sched_get_priority_max(SCHED_FIFO), 15); + EXPECT_EQ(sched_get_priority_min(SCHED_OTHER), -15); + EXPECT_EQ(sched_get_priority_min(SCHED_RR), -15); +} + +TEST(WindowsShimSched, YieldReturnsZero) +{ + EXPECT_EQ(sched_yield(), 0); +} +#endif + +/* syslog --------------------------------------------------------------- */ + +TEST(WindowsShimSyslog, OpenAndCloseNoOps) +{ + openlog("ident", LOG_PID, LOG_USER); + closelog(); + SUCCEED(); +} + +TEST(WindowsShimSyslog, SetLogMaskRoundtrip) +{ + const int previous = setlogmask(LOG_UPTO(LOG_INFO)); + const int updated = setlogmask(LOG_UPTO(LOG_DEBUG)); + EXPECT_EQ(updated, LOG_UPTO(LOG_INFO)); + + /* setlogmask(0) returns the active mask without modifying it. */ + const int current = setlogmask(0); + EXPECT_EQ(current, LOG_UPTO(LOG_DEBUG)); + + /* Restore original. */ + setlogmask(previous); +} + +TEST(WindowsShimSyslog, MaskSuppressesLogging) +{ + /* Mute everything; vsyslog should bail out without writing. */ + setlogmask(LOG_MASK(LOG_EMERG)); + syslog(LOG_DEBUG, "should-not-emit-%d", 42); + setlogmask(LOG_UPTO(LOG_DEBUG)); + SUCCEED(); +} + +TEST(WindowsShimSyslog, FacilityPriorityHelpers) +{ + const int packed = LOG_MAKEPRI(LOG_USER, LOG_INFO); + EXPECT_EQ(LOG_PRI(packed), LOG_INFO); + EXPECT_EQ(LOG_FAC(packed), LOG_FAC(LOG_USER)); +} + +/* mman ----------------------------------------------------------------- */ + +TEST(WindowsShimMmanHeader, MadviseAlwaysSucceeds) +{ + char buf[64]; + EXPECT_EQ(madvise(buf, sizeof(buf), MADV_NORMAL), 0); + EXPECT_EQ(madvise(buf, sizeof(buf), MADV_RANDOM), 0); + EXPECT_EQ(madvise(nullptr, 0, MADV_DONTNEED), 0); +} + +TEST(WindowsShimMmanHeader, ProtFlagBits) +{ + EXPECT_EQ(PROT_NONE, 0); + EXPECT_NE(PROT_READ & PROT_WRITE, PROT_READ); + EXPECT_EQ(PROT_READ | PROT_WRITE | PROT_EXEC, 0x7); +} + +TEST(WindowsShimMmanHeader, MapAnonAlias) +{ + EXPECT_EQ(MAP_ANON, MAP_ANONYMOUS); +} + +TEST(WindowsShimMmanHeader, PosixMadviseAliases) +{ + EXPECT_EQ(POSIX_MADV_NORMAL, MADV_NORMAL); + EXPECT_EQ(POSIX_MADV_DONTNEED, MADV_DONTNEED); +} + +/* time helpers --------------------------------------------------------- */ + +TEST(WindowsShimTime, GmtimeRRejectsNull) +{ + struct tm out = {}; + time_t t = 0; + EXPECT_EQ(gmtime_r(nullptr, &out), nullptr); + EXPECT_EQ(gmtime_r(&t, nullptr), nullptr); +} + +TEST(WindowsShimTime, GmtimeRRoundtripEpoch) +{ + struct tm out = {}; + time_t t = 0; /* 1970-01-01 00:00:00 UTC */ + struct tm *r = gmtime_r(&t, &out); + ASSERT_NE(r, nullptr); + EXPECT_EQ(out.tm_year, 70); + EXPECT_EQ(out.tm_mon, 0); + EXPECT_EQ(out.tm_mday, 1); +} + +TEST(WindowsShimTime, LocaltimeRRejectsNull) +{ + struct tm out = {}; + time_t t = 1234567890; + EXPECT_EQ(localtime_r(nullptr, &out), nullptr); + EXPECT_EQ(localtime_r(&t, nullptr), nullptr); +} + +TEST(WindowsShimTime, LocaltimeRPopulatesYear) +{ + struct tm out = {}; + time_t t = 1234567890; /* 2009-02-13 */ + struct tm *r = localtime_r(&t, &out); + ASSERT_NE(r, nullptr); + EXPECT_GT(out.tm_year, 100); +} + +TEST(WindowsShimTime, AsctimeRRejectsNull) +{ + struct tm tm_in = {}; + char buf[26] = {}; + EXPECT_EQ(asctime_r(nullptr, buf), nullptr); + EXPECT_EQ(asctime_r(&tm_in, nullptr), nullptr); +} + +TEST(WindowsShimTime, AsctimeRPopulatesBuffer) +{ + struct tm tm_in = {}; + tm_in.tm_year = 70; + tm_in.tm_mon = 0; + tm_in.tm_mday = 1; + tm_in.tm_hour = 0; + tm_in.tm_min = 0; + tm_in.tm_sec = 0; + tm_in.tm_wday = 4; + char buf[26] = {}; + char *r = asctime_r(&tm_in, buf); + ASSERT_NE(r, nullptr); + EXPECT_GT(strlen(buf), 10u); +} + +TEST(WindowsShimTime, CtimeRRejectsNull) +{ + time_t t = 0; + char buf[26] = {}; + EXPECT_EQ(ctime_r(nullptr, buf), nullptr); + EXPECT_EQ(ctime_r(&t, nullptr), nullptr); +} + +TEST(WindowsShimTime, CtimeRRoundtrip) +{ + time_t t = 1700000000; + char buf[26] = {}; + char *r = ctime_r(&t, buf); + ASSERT_NE(r, nullptr); + EXPECT_GT(strlen(buf), 10u); +} + +TEST(WindowsShimTime, TimerMacros) +{ + struct timeval a = { 5, 200000 }; + struct timeval b = { 5, 200000 }; + EXPECT_TRUE(timerisset(&a)); + EXPECT_FALSE(timercmp(&a, &b, !=)); + struct timeval r = {}; + timeradd(&a, &b, &r); + EXPECT_EQ(r.tv_sec, 10); + EXPECT_EQ(r.tv_usec, 400000); + struct timeval s = { 6, 0 }; + struct timeval d = {}; + timersub(&s, &a, &d); + EXPECT_EQ(d.tv_sec, 0); + EXPECT_EQ(d.tv_usec, 800000); + timerclear(&a); + EXPECT_FALSE(timerisset(&a)); +} + +/* unistd usleep / sleep ----------------------------------------------- */ + +TEST(WindowsShimUnistd, UsleepZeroReturnsImmediately) +{ + auto t0 = std::chrono::steady_clock::now(); + EXPECT_EQ(usleep(0), 0); + auto dt = std::chrono::steady_clock::now() - t0; + /* Should be sub-millisecond on any reasonable host. */ + EXPECT_LT(std::chrono::duration_cast(dt).count(), 50); +} + +TEST(WindowsShimUnistd, UsleepShortRespectsLowerBound) +{ + const useconds_t target_us = 2000; /* 2 ms */ + auto t0 = std::chrono::steady_clock::now(); + ASSERT_EQ(usleep(target_us), 0); + auto dt = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + /* Allow generous early-wake margin; the shim explicitly closes the + * residual against the QPC target so we should not be early. */ + EXPECT_GE(dt, (long long)target_us - 200); +} + +TEST(WindowsShimUnistd, UsleepLongerDuration) +{ + const useconds_t target_us = 20000; /* 20 ms */ + auto t0 = std::chrono::steady_clock::now(); + ASSERT_EQ(usleep(target_us), 0); + auto dt = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + EXPECT_GE(dt, (long long)target_us - 500); + /* Be generous on the upper bound to tolerate Windows scheduler jitter. */ + EXPECT_LT(dt, (long long)target_us + 100000); +} + +TEST(WindowsShimUnistd, UsleepThreadLocalTimerReuse) +{ + /* Two consecutive sleeps in the same thread reuse the cached + * waitable timer; both must still hit the deadline. */ + for (int i = 0; i < 3; ++i) { + auto t0 = std::chrono::steady_clock::now(); + ASSERT_EQ(usleep(5000), 0); + auto dt = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + EXPECT_GE(dt, 4500); + } +} + +#if defined(_MSC_VER) && !defined(__clang__) +TEST(WindowsShimUnistd, SleepOneSecond) +{ + auto t0 = std::chrono::steady_clock::now(); + EXPECT_EQ(sleep(1u), 0u); + auto dt = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + EXPECT_GE(dt, 950); +} +#endif + +/* dirent --------------------------------------------------------------- */ + +TEST(WindowsShimDirent, OpendirNullPath) +{ + errno = 0; + EXPECT_EQ(opendir(nullptr), nullptr); + EXPECT_EQ(errno, EINVAL); +} + +TEST(WindowsShimDirent, OpendirNonexistentReturnsNull) +{ + errno = 0; + EXPECT_EQ(opendir("Z:\\definitely\\does\\not\\exist\\px4test"), nullptr); + EXPECT_EQ(errno, ENOENT); +} + +TEST(WindowsShimDirent, OpendirCwd) +{ + DIR *d = opendir("."); + ASSERT_NE(d, nullptr); + EXPECT_EQ(closedir(d), 0); +} + +TEST(WindowsShimDirent, ReaddirEnumeratesDotEntries) +{ + DIR *d = opendir("."); + ASSERT_NE(d, nullptr); + bool saw_any = false; + + while (struct dirent *e = readdir(d)) { + EXPECT_GT(strlen(e->d_name), 0u); + EXPECT_LE(strlen(e->d_name), (size_t)NAME_MAX); + saw_any = true; + } + + EXPECT_TRUE(saw_any); + EXPECT_EQ(closedir(d), 0); +} + +TEST(WindowsShimDirent, ReaddirNullStream) +{ + errno = 0; + EXPECT_EQ(readdir(nullptr), nullptr); + EXPECT_EQ(errno, EBADF); +} + +TEST(WindowsShimDirent, ClosedirNullReturnsError) +{ + errno = 0; + EXPECT_EQ(closedir(nullptr), -1); + EXPECT_EQ(errno, EBADF); +} + +TEST(WindowsShimDirent, RewinddirRestartsIteration) +{ + DIR *d = opendir("."); + ASSERT_NE(d, nullptr); + int first_pass = 0; + + while (readdir(d)) { + ++first_pass; + } + + rewinddir(d); + int second_pass = 0; + + while (readdir(d)) { + ++second_pass; + } + + EXPECT_EQ(first_pass, second_pass); + EXPECT_GT(first_pass, 0); + EXPECT_EQ(closedir(d), 0); + + /* rewinddir on null is a no-op. */ + rewinddir(nullptr); +} + +TEST(WindowsShimDirent, TelldirNull) +{ + errno = 0; + EXPECT_EQ(telldir(nullptr), -1); + EXPECT_EQ(errno, EBADF); +} + +TEST(WindowsShimDirent, TelldirAdvances) +{ + DIR *d = opendir("."); + ASSERT_NE(d, nullptr); + long start = telldir(d); + EXPECT_EQ(start, 0); + + if (readdir(d) != nullptr) { + EXPECT_GT(telldir(d), start); + } + + EXPECT_EQ(closedir(d), 0); +} + +TEST(WindowsShimDirent, SeekdirRestartsAndAdvances) +{ + DIR *d = opendir("."); + ASSERT_NE(d, nullptr); + + /* Read a couple, capture position, rewind, seekdir there. */ + if (readdir(d) && readdir(d)) { + long pos = telldir(d); + seekdir(d, 0); + EXPECT_EQ(telldir(d), 0); + seekdir(d, pos); + /* Negative offsets are ignored. */ + seekdir(d, -1); + } + + /* Null and bad inputs are silent no-ops. */ + seekdir(nullptr, 0); + EXPECT_EQ(closedir(d), 0); +} + +TEST(WindowsShimDirent, AlphasortAndVersionsort) +{ + struct dirent a = {}; + struct dirent b = {}; + strcpy(a.d_name, "alpha"); + strcpy(b.d_name, "beta"); + const struct dirent *pa = &a; + const struct dirent *pb = &b; + EXPECT_LT(alphasort(&pa, &pb), 0); + EXPECT_GT(alphasort(&pb, &pa), 0); + EXPECT_EQ(versionsort(&pa, &pb), alphasort(&pa, &pb)); +} + +TEST(WindowsShimDirent, ScandirNullList) +{ + errno = 0; + EXPECT_EQ(scandir(".", nullptr, nullptr, nullptr), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(WindowsShimDirent, ScandirCwdNonNegative) +{ + struct dirent **list = nullptr; + int n = scandir(".", &list, nullptr, alphasort); + ASSERT_GE(n, 0); + + for (int i = 0; i < n; ++i) { + free(list[i]); + } + + free(list); +} + +static int filter_drop_dot(const struct dirent *e) +{ + return e && e->d_name[0] != '.'; +} + +TEST(WindowsShimDirent, ScandirSelectorFilters) +{ + struct dirent **list = nullptr; + int n = scandir(".", &list, &filter_drop_dot, nullptr); + ASSERT_GE(n, 0); + + for (int i = 0; i < n; ++i) { + EXPECT_NE(list[i]->d_name[0], '.'); + free(list[i]); + } + + free(list); +} + +TEST(WindowsShimDirent, ScandirNonexistent) +{ + struct dirent **list = nullptr; + int n = scandir("Z:\\definitely\\does\\not\\exist\\px4test_scan", + &list, nullptr, nullptr); + EXPECT_EQ(n, -1); +} + +#if defined(_MSC_VER) && !defined(__clang__) +/* getopt is MSVC-only; MinGW pulls system getopt. */ +#include + +TEST(WindowsShimGetopt, SimpleShortOptions) +{ + /* getopt holds global state; reset it before each scenario. */ + optind = 1; + px4_getopt_nextchar = 1; + + char *argv[] = { + (char *)"prog", + (char *)"-a", + (char *)"-b", + (char *)"hello", + (char *)"rest", + nullptr + }; + int argc = 5; + + int c = getopt(argc, argv, "ab:"); + EXPECT_EQ(c, 'a'); + c = getopt(argc, argv, "ab:"); + EXPECT_EQ(c, 'b'); + EXPECT_STREQ(optarg, "hello"); + c = getopt(argc, argv, "ab:"); + EXPECT_EQ(c, -1); + EXPECT_LT(optind, argc); +} + +TEST(WindowsShimGetopt, UnknownOptionReturnsQuestion) +{ + optind = 1; + px4_getopt_nextchar = 1; + char *argv[] = { (char *)"prog", (char *)"-x", nullptr }; + int c = getopt(2, argv, "ab:"); + EXPECT_EQ(c, '?'); + EXPECT_EQ(optopt, 'x'); +} + +TEST(WindowsShimGetopt, MissingArgumentForRequired) +{ + optind = 1; + px4_getopt_nextchar = 1; + char *argv[] = { (char *)"prog", (char *)"-a", nullptr }; + int c = getopt(2, argv, "a:"); + /* No leading colon in optstring -> '?'. */ + EXPECT_EQ(c, '?'); +} + +TEST(WindowsShimGetopt, DashDashTerminator) +{ + optind = 1; + px4_getopt_nextchar = 1; + char *argv[] = { (char *)"prog", (char *)"--", (char *)"file", nullptr }; + int c = getopt(3, argv, "ab:"); + EXPECT_EQ(c, -1); + EXPECT_EQ(optind, 2); +} + +TEST(WindowsShimGetopt, GetoptLongMatch) +{ + optind = 1; + px4_getopt_nextchar = 1; + option longopts[] = { + { "verbose", no_argument, nullptr, 'v' }, + { "file", required_argument, nullptr, 'f' }, + { nullptr, 0, nullptr, 0 } + }; + char *argv[] = { + (char *)"prog", + (char *)"--verbose", + (char *)"--file=foo.bin", + nullptr + }; + int idx = -1; + int c = getopt_long(3, argv, "vf:", longopts, &idx); + EXPECT_EQ(c, 'v'); + EXPECT_EQ(idx, 0); + c = getopt_long(3, argv, "vf:", longopts, &idx); + EXPECT_EQ(c, 'f'); + EXPECT_STREQ(optarg, "foo.bin"); +} + +TEST(WindowsShimGetopt, GetoptLongRequiredFromNextArg) +{ + optind = 1; + px4_getopt_nextchar = 1; + option longopts[] = { + { "name", required_argument, nullptr, 'n' }, + { nullptr, 0, nullptr, 0 } + }; + char *argv[] = { + (char *)"prog", + (char *)"--name", + (char *)"alice", + nullptr + }; + int idx = -1; + int c = getopt_long(3, argv, "n:", longopts, &idx); + EXPECT_EQ(c, 'n'); + EXPECT_STREQ(optarg, "alice"); +} +#endif // MSVC + +/* Pure-header fcntl tests -- the file logic is in fcntl.h's static inline + * fcntl(); F_GETFL/F_GETFD/F_SETFD/F_SETFL paths can be exercised in + * isolation without opening Win32 handles. */ +#include + +TEST(WindowsShimFcntl, GetflReturnsZero) +{ + EXPECT_EQ(fcntl(0, F_GETFL), 0); + EXPECT_EQ(fcntl(0, F_GETFD), 0); +} + +TEST(WindowsShimFcntl, SetfdAcceptsCloexec) +{ + EXPECT_EQ(fcntl(0, F_SETFD, FD_CLOEXEC), 0); +} + +TEST(WindowsShimFcntl, SetflWithoutNonblockReturnsZero) +{ + EXPECT_EQ(fcntl(0, F_SETFL, 0), 0); +} + +TEST(WindowsShimFcntl, UnknownCommandRejected) +{ + errno = 0; + EXPECT_EQ(fcntl(0, 0xBAD), -1); + EXPECT_EQ(errno, EINVAL); +} + +#endif // _WIN32 + +/* On non-Windows builds the file compiles to a single trivial test so the + * gtest target still produces output. */ +TEST(WindowsShimHeaders, BuildSentinel) +{ + SUCCEED(); +} diff --git a/platforms/posix/src/px4/windows/tests/test_windows_shim_io.cpp b/platforms/posix/src/px4/windows/tests/test_windows_shim_io.cpp new file mode 100644 index 0000000000..be94f368b3 --- /dev/null +++ b/platforms/posix/src/px4/windows/tests/test_windows_shim_io.cpp @@ -0,0 +1,224 @@ +/**************************************************************************** + * + * Copyright (C) 2026 PX4 Development Team. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name PX4 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. + * + ****************************************************************************/ + +/** + * @file test_windows_shim_io.cpp + * + * Unit tests for the inline I/O helpers in unistd.h and sys/stat.h: + * pipe(), pipe2(), fsync(), fdatasync(), dprintf(), vdprintf(), + * px4_mkdir_shim() / mkdir(), and lstat() (the latter is implemented + * as a stat() pass-through). + * + * These all live in the shim headers and so are unit-testable without + * pulling in the PX4 platform stack. + */ + +#include + +#ifdef _WIN32 + +#include +#include +#include +#include +#include +#include + +#include +#include + +TEST(WindowsShimIo, PipeCreatesUsableFds) +{ + int p[2] = { -1, -1 }; + ASSERT_EQ(pipe(p), 0); + EXPECT_GE(p[0], 0); + EXPECT_GE(p[1], 0); + const char msg[] = "round"; + ASSERT_EQ(_write(p[1], msg, (unsigned)sizeof(msg)), (int)sizeof(msg)); + char buf[16] = {}; + ASSERT_EQ(_read(p[0], buf, sizeof(buf)), (int)sizeof(msg)); + EXPECT_STREQ(buf, "round"); + _close(p[0]); + _close(p[1]); +} + +TEST(WindowsShimIo, Pipe2IgnoresFlags) +{ + int p[2] = { -1, -1 }; + /* Flags are documented as ignored on Windows. */ + ASSERT_EQ(pipe2(p, 0xDEADBEEF), 0); + EXPECT_GE(p[0], 0); + EXPECT_GE(p[1], 0); + _close(p[0]); + _close(p[1]); +} + +TEST(WindowsShimIo, FsyncOnRegularFile) +{ + char path[MAX_PATH]; + GetTempPathA(sizeof(path), path); + strcat_s(path, sizeof(path), "px4_fsync.dat"); + int fd = _open(path, _O_CREAT | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE); + ASSERT_GE(fd, 0); + const char data[] = "hello"; + ASSERT_EQ(_write(fd, data, (unsigned)sizeof(data)), (int)sizeof(data)); + EXPECT_EQ(fsync(fd), 0); + EXPECT_EQ(fdatasync(fd), 0); + _close(fd); + DeleteFileA(path); +} + +TEST(WindowsShimIo, FsyncBadFdReturnsError) +{ + errno = 0; + EXPECT_NE(fsync(-1), 0); +} + +TEST(WindowsShimIo, DprintfWritesFormatted) +{ + char path[MAX_PATH]; + GetTempPathA(sizeof(path), path); + strcat_s(path, sizeof(path), "px4_dprintf.txt"); + int fd = _open(path, _O_CREAT | _O_RDWR | _O_BINARY | _O_TRUNC, + _S_IREAD | _S_IWRITE); + ASSERT_GE(fd, 0); + int n = dprintf(fd, "v=%d s=%s", 42, "ok"); + EXPECT_GT(n, 0); + _lseek(fd, 0, SEEK_SET); + char buf[64] = {}; + int r = _read(fd, buf, sizeof(buf) - 1); + ASSERT_GT(r, 0); + buf[r] = 0; + EXPECT_STREQ(buf, "v=42 s=ok"); + _close(fd); + DeleteFileA(path); +} + +static int call_vdprintf(int fd, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + int n = vdprintf(fd, fmt, ap); + va_end(ap); + return n; +} + +TEST(WindowsShimIo, VdprintfWritesFormatted) +{ + char path[MAX_PATH]; + GetTempPathA(sizeof(path), path); + strcat_s(path, sizeof(path), "px4_vdprintf.txt"); + int fd = _open(path, _O_CREAT | _O_RDWR | _O_BINARY | _O_TRUNC, + _S_IREAD | _S_IWRITE); + ASSERT_GE(fd, 0); + int n = call_vdprintf(fd, "x=%d", 7); + EXPECT_GT(n, 0); + _lseek(fd, 0, SEEK_SET); + char buf[16] = {}; + int r = _read(fd, buf, sizeof(buf) - 1); + ASSERT_GT(r, 0); + buf[r] = 0; + EXPECT_STREQ(buf, "x=7"); + _close(fd); + DeleteFileA(path); +} + +TEST(WindowsShimIo, MkdirCreatesDirectory) +{ + char path[MAX_PATH]; + GetTempPathA(sizeof(path), path); + strcat_s(path, sizeof(path), "px4_mkdir_shim_test"); + RemoveDirectoryA(path); /* tidy from previous run */ + EXPECT_EQ(mkdir(path, 0755), 0); + struct stat st = {}; + ASSERT_EQ(stat(path, &st), 0); + EXPECT_TRUE(S_ISDIR(st.st_mode)); + RemoveDirectoryA(path); +} + +TEST(WindowsShimIo, MkdirExistingFails) +{ + char path[MAX_PATH]; + GetTempPathA(sizeof(path), path); + strcat_s(path, sizeof(path), "px4_mkdir_shim_test2"); + RemoveDirectoryA(path); + ASSERT_EQ(mkdir(path, 0755), 0); + EXPECT_NE(mkdir(path, 0755), 0); + RemoveDirectoryA(path); +} + +TEST(WindowsShimIo, LstatBehavesLikeStatForFile) +{ + char path[MAX_PATH]; + GetTempPathA(sizeof(path), path); + strcat_s(path, sizeof(path), "px4_lstat.dat"); + int fd = _open(path, _O_CREAT | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE); + ASSERT_GE(fd, 0); + _close(fd); + + struct stat a = {}; + struct stat b = {}; + ASSERT_EQ(lstat(path, &a), 0); + ASSERT_EQ(stat(path, &b), 0); + EXPECT_EQ(a.st_size, b.st_size); + EXPECT_TRUE(S_ISREG(a.st_mode)); + /* lstat() never reports a symlink on Windows. */ + EXPECT_FALSE(S_ISLNK(a.st_mode)); + DeleteFileA(path); +} + +TEST(WindowsShimIo, LstatNonexistentFails) +{ + struct stat st = {}; + EXPECT_NE(lstat("Z:\\definitely\\not\\here\\px4test", &st), 0); +} + +TEST(WindowsShimIo, StModeBitsExpose) +{ + /* Smoke test the POSIX permission constants the shim exports. */ + EXPECT_EQ(S_IRWXU, 0700); + EXPECT_EQ(S_IRUSR, 0400); + EXPECT_EQ(S_IWUSR, 0200); + EXPECT_EQ(S_IXUSR, 0100); + EXPECT_EQ(S_IRWXG, 0070); + EXPECT_EQ(S_IRWXO, 0007); + EXPECT_EQ(S_IFDIR, 0040000); + EXPECT_EQ(S_IFREG, 0100000); +} + +#endif // _WIN32 + +TEST(WindowsShimIo, BuildSentinel) +{ + SUCCEED(); +} diff --git a/platforms/posix/src/px4/windows/tests/test_windows_shim_poll.cpp b/platforms/posix/src/px4/windows/tests/test_windows_shim_poll.cpp new file mode 100644 index 0000000000..9ddce2d0ce --- /dev/null +++ b/platforms/posix/src/px4/windows/tests/test_windows_shim_poll.cpp @@ -0,0 +1,417 @@ +/**************************************************************************** + * + * Copyright (C) 2026 PX4 Development Team. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name PX4 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. + * + ****************************************************************************/ + +/** + * @file test_windows_shim_poll.cpp + * + * Unit tests for the inline poll() implementation in + * platforms/posix/include/windows_shim/poll.h. The shim classifies fd + * kinds (socket / pipe / char / disk / invalid), routes pure-socket + * calls through WSAPoll, and otherwise mixes manual readiness probes + * with a tight WSAPoll fallback for the socket subset. + * + * Each branch is exercised: argument validation, socket loopback, pipe + * with and without pending data, disk-fd always-ready, ignored fd, and + * the timeout/timeout=0 fast paths. + */ + +#include + +#ifdef _WIN32 + +#define _WIN32_WINNT 0x0A00 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ +class WinsockBootstrap +{ +public: + WinsockBootstrap() + { + WSADATA wsa; + WSAStartup(MAKEWORD(2, 2), &wsa); + } + + ~WinsockBootstrap() + { + WSACleanup(); + } +}; + +/* Lightweight UDP loopback fixture used by socket-path tests. */ +struct LoopbackPair { + WinsockBootstrap _ws; + SOCKET srv = INVALID_SOCKET; + SOCKET cli = INVALID_SOCKET; + struct sockaddr_in srv_addr {}; + + LoopbackPair() + { + srv = socket(AF_INET, SOCK_DGRAM, 0); + cli = socket(AF_INET, SOCK_DGRAM, 0); + srv_addr.sin_family = AF_INET; + srv_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + srv_addr.sin_port = 0; + bind(srv, (struct sockaddr *)&srv_addr, sizeof(srv_addr)); + int alen = sizeof(srv_addr); + getsockname(srv, (struct sockaddr *)&srv_addr, &alen); + } + + ~LoopbackPair() + { + if (srv != INVALID_SOCKET) { closesocket(srv); } + + if (cli != INVALID_SOCKET) { closesocket(cli); } + } +}; + +} // namespace + +/* Argument validation -------------------------------------------------- */ + +TEST(WindowsShimPoll, NegativeTimeoutBelowMinusOneRejected) +{ + struct pollfd fd; + fd.fd = -1; + fd.events = 0; + fd.revents = 0; + errno = 0; + int rc = poll(&fd, 1, -42); + EXPECT_EQ(rc, -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(WindowsShimPoll, NullFdsWithCountRejected) +{ + errno = 0; + int rc = poll(nullptr, 1, 0); + EXPECT_EQ(rc, -1); + EXPECT_EQ(errno, EFAULT); +} + +TEST(WindowsShimPoll, ZeroNfdsZeroTimeoutReturnsZero) +{ + auto t0 = std::chrono::steady_clock::now(); + int rc = poll(nullptr, 0, 0); + EXPECT_EQ(rc, 0); + auto dt = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + EXPECT_LT(dt, 50); +} + +TEST(WindowsShimPoll, ZeroNfdsPositiveTimeoutSleeps) +{ + auto t0 = std::chrono::steady_clock::now(); + int rc = poll(nullptr, 0, 30); + EXPECT_EQ(rc, 0); + auto dt = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + EXPECT_GE(dt, 25); +} + +/* fd-classification helpers -------------------------------------------- */ + +TEST(WindowsShimPoll, IgnoredFdHelper) +{ + EXPECT_NE(px4_windows_poll_fd_ignored((SOCKET) - 1), 0); + EXPECT_NE(px4_windows_poll_fd_ignored((SOCKET) - 5), 0); + EXPECT_EQ(px4_windows_poll_fd_ignored((SOCKET)0), 0); +} + +TEST(WindowsShimPoll, ErrnoFromWsaMapping) +{ + EXPECT_EQ(px4_windows_poll_errno_from_wsa(WSAEINTR), EINTR); + EXPECT_EQ(px4_windows_poll_errno_from_wsa(WSAEINVAL), EINVAL); + EXPECT_EQ(px4_windows_poll_errno_from_wsa(WSAENOBUFS), ENOMEM); + EXPECT_EQ(px4_windows_poll_errno_from_wsa(0xBEEF), EBADF); +} + +TEST(WindowsShimPoll, ReadyEventsMask) +{ + EXPECT_EQ(px4_windows_poll_ready_events(POLLIN, POLLIN | POLLOUT), POLLIN); + EXPECT_EQ(px4_windows_poll_ready_events(POLLIN, POLLOUT), 0); + EXPECT_EQ(px4_windows_poll_ready_events(0, POLLIN), 0); +} + +TEST(WindowsShimPoll, DiskReventsAlwaysReady) +{ + EXPECT_EQ(px4_windows_poll_disk_revents(POLLIN), POLLIN); + EXPECT_EQ(px4_windows_poll_disk_revents(POLLOUT), POLLOUT); + EXPECT_EQ(px4_windows_poll_disk_revents(0), 0); +} + +TEST(WindowsShimPoll, ClassifyIgnoredFd) +{ + HANDLE h = nullptr; + DWORD ft = 0; + enum px4_windows_poll_fd_kind k = + px4_windows_poll_classify_fd((SOCKET) - 1, &h, &ft); + EXPECT_EQ(k, PX4_WINDOWS_POLL_FD_IGNORED); + EXPECT_EQ(h, INVALID_HANDLE_VALUE); + EXPECT_EQ(ft, FILE_TYPE_UNKNOWN); +} + +TEST(WindowsShimPoll, ClassifyInvalidFd) +{ + WinsockBootstrap _ws; + HANDLE h = nullptr; + DWORD ft = 0; + /* A very high CRT-fd-domain value with no backing handle. */ + enum px4_windows_poll_fd_kind k = + px4_windows_poll_classify_fd((SOCKET)999999, &h, &ft); + EXPECT_EQ(k, PX4_WINDOWS_POLL_FD_INVALID); +} + +TEST(WindowsShimPoll, ClassifyPipeFd) +{ + int p[2] = { -1, -1 }; + ASSERT_EQ(_pipe(p, 4096, _O_BINARY), 0); + HANDLE h = nullptr; + DWORD ft = 0; + enum px4_windows_poll_fd_kind k = + px4_windows_poll_classify_fd((SOCKET)p[0], &h, &ft); + EXPECT_EQ(k, PX4_WINDOWS_POLL_FD_PIPE); + EXPECT_NE(h, INVALID_HANDLE_VALUE); + EXPECT_EQ(ft, (DWORD)FILE_TYPE_PIPE); + _close(p[0]); + _close(p[1]); +} + +TEST(WindowsShimPoll, ClassifyDiskFd) +{ + char path[MAX_PATH]; + GetTempPathA(sizeof(path), path); + strcat_s(path, sizeof(path), "px4_poll_test.dat"); + int fd = _open(path, _O_CREAT | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE); + ASSERT_GE(fd, 0); + HANDLE h = nullptr; + DWORD ft = 0; + enum px4_windows_poll_fd_kind k = + px4_windows_poll_classify_fd((SOCKET)fd, &h, &ft); + EXPECT_EQ(k, PX4_WINDOWS_POLL_FD_DISK); + EXPECT_EQ(ft, (DWORD)FILE_TYPE_DISK); + _close(fd); + DeleteFileA(path); +} + +TEST(WindowsShimPoll, ClassifySocketFd) +{ + WinsockBootstrap _ws; + SOCKET s = socket(AF_INET, SOCK_DGRAM, 0); + ASSERT_NE(s, INVALID_SOCKET); + HANDLE h = nullptr; + DWORD ft = 0; + enum px4_windows_poll_fd_kind k = + px4_windows_poll_classify_fd(s, &h, &ft); + EXPECT_EQ(k, PX4_WINDOWS_POLL_FD_SOCKET); + EXPECT_EQ(h, INVALID_HANDLE_VALUE); + closesocket(s); +} + +/* poll() integration --------------------------------------------------- */ + +TEST(WindowsShimPoll, OnlyIgnoredFdShortCircuitTimeoutZero) +{ + struct pollfd fds[2]; + fds[0].fd = -1; + fds[0].events = POLLIN; + fds[0].revents = 0xff; + fds[1].fd = -1; + fds[1].events = POLLOUT; + fds[1].revents = 0xff; + int rc = poll(fds, 2, 0); + EXPECT_EQ(rc, 0); + EXPECT_EQ(fds[0].revents, 0); + EXPECT_EQ(fds[1].revents, 0); +} + +TEST(WindowsShimPoll, SocketWriteAlwaysReady) +{ + LoopbackPair pair; + struct pollfd pf; + pf.fd = (int)pair.cli; + pf.events = POLLOUT; + pf.revents = 0; + int rc = poll(&pf, 1, 0); + EXPECT_GE(rc, 0); + /* Writable on a fresh UDP socket. */ + EXPECT_TRUE((pf.revents & POLLOUT) || rc == 0); +} + +TEST(WindowsShimPoll, SocketReadReadyAfterSendto) +{ + LoopbackPair pair; + const char msg[] = "x"; + ASSERT_EQ(sendto(pair.cli, msg, (int)sizeof(msg), 0, + (struct sockaddr *)&pair.srv_addr, sizeof(pair.srv_addr)), + (int)sizeof(msg)); + /* Give the kernel a moment to deliver. */ + struct pollfd pf; + pf.fd = (int)pair.srv; + pf.events = POLLIN; + pf.revents = 0; + int rc = poll(&pf, 1, 200); + EXPECT_EQ(rc, 1); + EXPECT_TRUE((pf.revents & POLLIN) != 0); +} + +TEST(WindowsShimPoll, SocketTimeoutNoData) +{ + LoopbackPair pair; + struct pollfd pf; + pf.fd = (int)pair.srv; + pf.events = POLLIN; + pf.revents = 0; + auto t0 = std::chrono::steady_clock::now(); + int rc = poll(&pf, 1, 30); + EXPECT_EQ(rc, 0); + auto dt = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + EXPECT_GE(dt, 20); +} + +TEST(WindowsShimPoll, PipeWritableImmediately) +{ + int p[2] = { -1, -1 }; + ASSERT_EQ(_pipe(p, 4096, _O_BINARY), 0); + struct pollfd pf; + pf.fd = p[1]; + pf.events = POLLOUT; + pf.revents = 0; + int rc = poll(&pf, 1, 0); + EXPECT_EQ(rc, 1); + EXPECT_TRUE((pf.revents & POLLOUT) != 0); + _close(p[0]); + _close(p[1]); +} + +TEST(WindowsShimPoll, PipeReadableAfterWrite) +{ + int p[2] = { -1, -1 }; + ASSERT_EQ(_pipe(p, 4096, _O_BINARY), 0); + const char msg[] = "ping"; + ASSERT_EQ(_write(p[1], msg, (unsigned)sizeof(msg)), (int)sizeof(msg)); + struct pollfd pf; + pf.fd = p[0]; + pf.events = POLLIN; + pf.revents = 0; + int rc = poll(&pf, 1, 200); + EXPECT_EQ(rc, 1); + EXPECT_TRUE((pf.revents & POLLIN) != 0); + _close(p[0]); + _close(p[1]); +} + +TEST(WindowsShimPoll, PipeBrokenSetsHup) +{ + int p[2] = { -1, -1 }; + ASSERT_EQ(_pipe(p, 4096, _O_BINARY), 0); + _close(p[1]); + struct pollfd pf; + pf.fd = p[0]; + pf.events = POLLIN; + pf.revents = 0; + int rc = poll(&pf, 1, 50); + EXPECT_GE(rc, 1); + EXPECT_TRUE((pf.revents & (POLLHUP | POLLIN)) != 0); + _close(p[0]); +} + +TEST(WindowsShimPoll, DiskFdAlwaysReady) +{ + char path[MAX_PATH]; + GetTempPathA(sizeof(path), path); + strcat_s(path, sizeof(path), "px4_poll_disk.dat"); + int fd = _open(path, _O_CREAT | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE); + ASSERT_GE(fd, 0); + struct pollfd pf; + pf.fd = fd; + pf.events = POLLIN | POLLOUT; + pf.revents = 0; + int rc = poll(&pf, 1, 0); + EXPECT_EQ(rc, 1); + EXPECT_TRUE((pf.revents & POLLIN) != 0); + EXPECT_TRUE((pf.revents & POLLOUT) != 0); + _close(fd); + DeleteFileA(path); +} + +TEST(WindowsShimPoll, MixedSocketAndPipe) +{ + LoopbackPair pair; + int p[2] = { -1, -1 }; + ASSERT_EQ(_pipe(p, 4096, _O_BINARY), 0); + const char msg[] = "z"; + ASSERT_EQ(_write(p[1], msg, (unsigned)sizeof(msg)), (int)sizeof(msg)); + struct pollfd fds[2]; + fds[0].fd = p[0]; + fds[0].events = POLLIN; + fds[0].revents = 0; + fds[1].fd = (int)pair.srv; + fds[1].events = POLLIN; + fds[1].revents = 0; + int rc = poll(fds, 2, 100); + EXPECT_GE(rc, 1); + EXPECT_TRUE((fds[0].revents & POLLIN) != 0); + _close(p[0]); + _close(p[1]); +} + +TEST(WindowsShimPoll, InvalidFdReturnsPollnval) +{ + struct pollfd pf; + pf.fd = 999999; /* deliberately bogus */ + pf.events = POLLIN; + pf.revents = 0; + int rc = poll(&pf, 1, 0); + EXPECT_GE(rc, 1); + EXPECT_TRUE((pf.revents & POLLNVAL) != 0); +} + +#endif // _WIN32 + +TEST(WindowsShimPoll, BuildSentinel) +{ + SUCCEED(); +} diff --git a/platforms/posix/src/px4/windows/tests/test_windows_shim_runtime.cpp b/platforms/posix/src/px4/windows/tests/test_windows_shim_runtime.cpp new file mode 100644 index 0000000000..ee5337fcf3 --- /dev/null +++ b/platforms/posix/src/px4/windows/tests/test_windows_shim_runtime.cpp @@ -0,0 +1,632 @@ +/**************************************************************************** + * + * Copyright (C) 2026 PX4 Development Team. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name PX4 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. + * + ****************************************************************************/ + +/** + * @file test_windows_shim_runtime.cpp + * + * Unit tests for the source-defined shims in + * platforms/posix/src/px4/windows/posix/. The tests link against a thin + * library that pulls in the standalone-compilable shim sources directly + * (errno_map, env, ids, sysconf, mman, flock, dlfcn, if_query, resolver, + * sched, termios, ioctl) so they can be exercised without the full PX4 + * platform/uORB stack. + * + * Tests are gated on _WIN32; on other hosts the file compiles to a + * trivial sentinel test so the gtest runner still produces output. + */ + +#include + +#ifdef _WIN32 + +#include +#include +#include + +#define _WIN32_WINNT 0x0A00 +#include +#include +#include + +/* The runtime shim references g_px4_session_id (defined in + * platforms/posix/src/px4/windows/runtime/init.cpp). This test binary + * does not link init.cpp because pulling it in drags the entire process + * bootstrap. Provide a local definition that mirrors the runtime + * contract. The original is C++-linkage (no extern "C") in init.cpp so + * we match that here exactly. */ +volatile LONG g_px4_session_id = 0; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* errno_map prototypes -- declared in px4_windows_internal.h but the + * tests don't include that header (it pulls in the entire winsock+win32 + * stack via the shim umbrella). Forward-declare just what we need. The + * originals are C++-linkage in errno_map.cpp; matching the linkage + * exactly here keeps name mangling aligned. */ +int px4_win_error_to_errno(DWORD err); +int px4_wsa_error_to_errno(int err); +const char *px4_hstrerror_text(int err); + +/* errno_map ------------------------------------------------------------ */ + +TEST(WindowsShimErrnoMap, FileNotFoundMapsToENOENT) +{ + EXPECT_EQ(px4_win_error_to_errno(ERROR_FILE_NOT_FOUND), ENOENT); + EXPECT_EQ(px4_win_error_to_errno(ERROR_PATH_NOT_FOUND), ENOENT); + EXPECT_EQ(px4_win_error_to_errno(ERROR_INVALID_DRIVE), ENOENT); + EXPECT_EQ(px4_win_error_to_errno(ERROR_BAD_PATHNAME), ENOENT); +} + +TEST(WindowsShimErrnoMap, AccessDeniedMapsToEACCES) +{ + EXPECT_EQ(px4_win_error_to_errno(ERROR_ACCESS_DENIED), EACCES); + EXPECT_EQ(px4_win_error_to_errno(ERROR_SHARING_VIOLATION), EACCES); + EXPECT_EQ(px4_win_error_to_errno(ERROR_LOCK_VIOLATION), EACCES); +} + +TEST(WindowsShimErrnoMap, AlreadyExistsMapsToEEXIST) +{ + EXPECT_EQ(px4_win_error_to_errno(ERROR_ALREADY_EXISTS), EEXIST); + EXPECT_EQ(px4_win_error_to_errno(ERROR_FILE_EXISTS), EEXIST); +} + +TEST(WindowsShimErrnoMap, MiscMappings) +{ + EXPECT_EQ(px4_win_error_to_errno(ERROR_INVALID_HANDLE), EBADF); + EXPECT_EQ(px4_win_error_to_errno(ERROR_NOT_ENOUGH_MEMORY), ENOMEM); + EXPECT_EQ(px4_win_error_to_errno(ERROR_OUTOFMEMORY), ENOMEM); + EXPECT_EQ(px4_win_error_to_errno(ERROR_INVALID_PARAMETER), EINVAL); + EXPECT_EQ(px4_win_error_to_errno(ERROR_DIR_NOT_EMPTY), ENOTEMPTY); + EXPECT_EQ(px4_win_error_to_errno(ERROR_NOT_SUPPORTED), ENOTSUP); + EXPECT_EQ(px4_win_error_to_errno(ERROR_BUSY), EBUSY); + EXPECT_EQ(px4_win_error_to_errno(ERROR_DISK_FULL), ENOSPC); + EXPECT_EQ(px4_win_error_to_errno(ERROR_PROC_NOT_FOUND), ESRCH); +} + +TEST(WindowsShimErrnoMap, UnknownWin32MapsToEIO) +{ + EXPECT_EQ(px4_win_error_to_errno(0xDEADBEEF), EIO); +} + +TEST(WindowsShimErrnoMap, WsaWouldBlockMapsCorrectly) +{ + EXPECT_EQ(px4_wsa_error_to_errno(WSAEWOULDBLOCK), EWOULDBLOCK); + EXPECT_EQ(px4_wsa_error_to_errno(WSAEINPROGRESS), EINPROGRESS); + EXPECT_EQ(px4_wsa_error_to_errno(WSAENOTSOCK), ENOTSOCK); + EXPECT_EQ(px4_wsa_error_to_errno(WSAEMSGSIZE), EMSGSIZE); + EXPECT_EQ(px4_wsa_error_to_errno(WSAECONNREFUSED), ECONNREFUSED); + EXPECT_EQ(px4_wsa_error_to_errno(WSAECONNRESET), ECONNRESET); + EXPECT_EQ(px4_wsa_error_to_errno(WSAENOTCONN), ENOTCONN); + EXPECT_EQ(px4_wsa_error_to_errno(WSAEISCONN), EISCONN); + EXPECT_EQ(px4_wsa_error_to_errno(WSAETIMEDOUT), ETIMEDOUT); + EXPECT_EQ(px4_wsa_error_to_errno(WSAEADDRINUSE), EADDRINUSE); + EXPECT_EQ(px4_wsa_error_to_errno(WSAEADDRNOTAVAIL), EADDRNOTAVAIL); + EXPECT_EQ(px4_wsa_error_to_errno(WSAENETDOWN), ENETDOWN); + EXPECT_EQ(px4_wsa_error_to_errno(WSAENETUNREACH), ENETUNREACH); + EXPECT_EQ(px4_wsa_error_to_errno(WSAENETRESET), ENETRESET); + EXPECT_EQ(px4_wsa_error_to_errno(WSAECONNABORTED), ECONNABORTED); + EXPECT_EQ(px4_wsa_error_to_errno(WSAEAFNOSUPPORT), EAFNOSUPPORT); + EXPECT_EQ(px4_wsa_error_to_errno(WSAEPROTONOSUPPORT), EPROTONOSUPPORT); + EXPECT_EQ(px4_wsa_error_to_errno(WSAEPROTOTYPE), EPROTOTYPE); + EXPECT_EQ(px4_wsa_error_to_errno(WSAENOPROTOOPT), ENOPROTOOPT); + EXPECT_EQ(px4_wsa_error_to_errno(WSAEOPNOTSUPP), EOPNOTSUPP); + EXPECT_EQ(px4_wsa_error_to_errno(WSAEALREADY), EALREADY); + EXPECT_EQ(px4_wsa_error_to_errno(WSAEDESTADDRREQ), EDESTADDRREQ); + EXPECT_EQ(px4_wsa_error_to_errno(WSAENOBUFS), ENOBUFS); +} + +TEST(WindowsShimErrnoMap, UnknownWsaMapsToEIO) +{ + EXPECT_EQ(px4_wsa_error_to_errno(0x7FFFFFFF), EIO); +} + +TEST(WindowsShimErrnoMap, HstrerrorReturnsStableStrings) +{ + EXPECT_STRNE(px4_hstrerror_text(HOST_NOT_FOUND), nullptr); + EXPECT_STREQ(px4_hstrerror_text(HOST_NOT_FOUND), "Unknown host"); + EXPECT_STREQ(px4_hstrerror_text(TRY_AGAIN), "Temporary failure in name resolution"); + EXPECT_STREQ(px4_hstrerror_text(NO_RECOVERY), "Non-recoverable name server error"); + EXPECT_STREQ(px4_hstrerror_text(NO_DATA), "No address associated with name"); + EXPECT_STREQ(px4_hstrerror_text(0xBAD), "Resolver error"); +} + +/* env (setenv / unsetenv) --------------------------------------------- */ + +TEST(WindowsShimEnv, SetenvRejectsBadName) +{ + errno = 0; + EXPECT_EQ(setenv(nullptr, "v", 1), -1); + EXPECT_EQ(errno, EINVAL); + + errno = 0; + EXPECT_EQ(setenv("", "v", 1), -1); + EXPECT_EQ(errno, EINVAL); + + errno = 0; + EXPECT_EQ(setenv("BAD=NAME", "v", 1), -1); + EXPECT_EQ(errno, EINVAL); + + errno = 0; + EXPECT_EQ(setenv("OK", nullptr, 1), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST(WindowsShimEnv, SetenvOverwriteSemantics) +{ + ASSERT_EQ(setenv("PX4_TEST_VAR", "first", 1), 0); + EXPECT_STREQ(getenv("PX4_TEST_VAR"), "first"); + + /* overwrite=0 leaves existing value intact. */ + ASSERT_EQ(setenv("PX4_TEST_VAR", "second", 0), 0); + EXPECT_STREQ(getenv("PX4_TEST_VAR"), "first"); + + /* overwrite=1 replaces. */ + ASSERT_EQ(setenv("PX4_TEST_VAR", "third", 1), 0); + EXPECT_STREQ(getenv("PX4_TEST_VAR"), "third"); + + /* unsetenv clears. */ + EXPECT_EQ(unsetenv("PX4_TEST_VAR"), 0); + const char *after = getenv("PX4_TEST_VAR"); + /* MSVCRT _putenv_s with an empty string treats the variable as + * unset; some CRT versions still return an empty string from + * getenv() for compatibility. Either is acceptable. */ + EXPECT_TRUE(after == nullptr || after[0] == '\0'); +} + +TEST(WindowsShimEnv, UnsetenvRejectsBadName) +{ + errno = 0; + EXPECT_EQ(unsetenv(nullptr), -1); + EXPECT_EQ(errno, EINVAL); + errno = 0; + EXPECT_EQ(unsetenv(""), -1); + EXPECT_EQ(errno, EINVAL); + errno = 0; + EXPECT_EQ(unsetenv("BAD=NAME"), -1); + EXPECT_EQ(errno, EINVAL); +} + +/* sysconf -------------------------------------------------------------- */ + +TEST(WindowsShimSysconf, PageSizeIsPositive) +{ + const long pg = sysconf(_SC_PAGESIZE); + EXPECT_GT(pg, 0); + /* x86/x64 page size is 4096; ARM64 is 16384. Either is reasonable. */ + EXPECT_TRUE(pg == 4096 || pg == 16384 || pg == 65536); + + EXPECT_EQ(sysconf(_SC_PAGE_SIZE), pg); +} + +TEST(WindowsShimSysconf, NprocessorsAtLeastOne) +{ + EXPECT_GE(sysconf(_SC_NPROCESSORS_ONLN), 1); + EXPECT_GE(sysconf(_SC_NPROCESSORS_CONF), 1); +} + +TEST(WindowsShimSysconf, OpenMaxNonZero) +{ + EXPECT_GT(sysconf(_SC_OPEN_MAX), 0); +} + +TEST(WindowsShimSysconf, HostnameAndLoginMaxNonZero) +{ + EXPECT_GT(sysconf(_SC_HOST_NAME_MAX), 0); + EXPECT_GT(sysconf(_SC_LOGIN_NAME_MAX), 0); +} + +TEST(WindowsShimSysconf, PhysAndAvphysPagesNonNegative) +{ + EXPECT_GE(sysconf(_SC_PHYS_PAGES), 0); + EXPECT_GE(sysconf(_SC_AVPHYS_PAGES), 0); +} + +TEST(WindowsShimSysconf, ClkTckPositive) +{ + EXPECT_GT(sysconf(_SC_CLK_TCK), 0); +} + +TEST(WindowsShimSysconf, GetpagesizeMatchesSysconf) +{ + EXPECT_EQ((long)getpagesize(), sysconf(_SC_PAGESIZE)); +} + +/* mman ----------------------------------------------------------------- */ + +TEST(WindowsShimMman, AnonymousMappingRoundtrip) +{ + const size_t len = 4096; + void *p = mmap(nullptr, len, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(p, MAP_FAILED); + ASSERT_NE(p, nullptr); + + /* Memory should be writable. */ + memset(p, 0xAB, len); + EXPECT_EQ(((unsigned char *)p)[0], 0xABu); + EXPECT_EQ(((unsigned char *)p)[len - 1], 0xABu); + + EXPECT_EQ(munmap(p, len), 0); +} + +TEST(WindowsShimMman, MapFixedRejected) +{ + const size_t len = 4096; + errno = 0; + void *p = mmap(reinterpret_cast(0x10000000), + len, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, + -1, 0); + EXPECT_EQ(p, MAP_FAILED); +} + +TEST(WindowsShimMman, MunmapNullSucceedsOrFails) +{ + /* munmap(nullptr, 0) is implementation defined. The shim must at + * least not crash. */ + int rc = munmap(nullptr, 0); + (void)rc; + SUCCEED(); +} + +TEST(WindowsShimMman, MlockMlockallNoOps) +{ + char buf[64]; + EXPECT_EQ(mlock(buf, sizeof(buf)) >= -1, true); + EXPECT_EQ(munlock(buf, sizeof(buf)) >= -1, true); + EXPECT_EQ(mlockall(MCL_CURRENT), 0); + EXPECT_EQ(munlockall(), 0); +} + +TEST(WindowsShimMman, MprotectAcceptsKnownProt) +{ + const size_t len = 4096; + void *p = mmap(nullptr, len, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(p, MAP_FAILED); + int rc = mprotect(p, len, PROT_READ); + (void)rc; /* implementation may return 0 or -1 depending on backing */ + EXPECT_EQ(munmap(p, len), 0); +} + +/* flock ---------------------------------------------------------------- */ + +TEST(WindowsShimFlock, ExclusiveOnTempFile) +{ + char path[MAX_PATH]; + GetTempPathA(sizeof(path), path); + strcat_s(path, sizeof(path), "px4_flock_test.lock"); + + int fd = _open(path, _O_CREAT | _O_RDWR, _S_IREAD | _S_IWRITE); + ASSERT_GE(fd, 0); + + EXPECT_EQ(flock(fd, LOCK_EX | LOCK_NB), 0); + EXPECT_EQ(flock(fd, LOCK_UN), 0); + + _close(fd); + DeleteFileA(path); +} + +TEST(WindowsShimFlock, BadFdReturnsError) +{ + errno = 0; + EXPECT_EQ(flock(-1, LOCK_EX), -1); +} + +/* dlfcn ---------------------------------------------------------------- */ + +TEST(WindowsShimDlfcn, OpenNullReturnsHandle) +{ + /* Some implementations return a sentinel pseudo-handle for the + * current process; others return NULL. Both are acceptable as + * long as dlerror() is callable and dlclose() handles the result. */ + void *h = dlopen(nullptr, 0); + const char *err = dlerror(); + (void)err; + + if (h) { + EXPECT_EQ(dlclose(h), 0); + } +} + +TEST(WindowsShimDlfcn, OpenNonexistentSetsDlerror) +{ + void *h = dlopen("definitely-not-a-real-library-xxx.dll", 0); + EXPECT_EQ(h, nullptr); + const char *err = dlerror(); + EXPECT_NE(err, nullptr); +} + +TEST(WindowsShimDlfcn, SymInModule) +{ + /* kernel32 is always loaded; resolve a known export. */ + void *h = dlopen("kernel32.dll", 0); + + if (h) { + void *sym = dlsym(h, "GetCurrentProcessId"); + EXPECT_NE(sym, nullptr); + dlclose(h); + } +} + +TEST(WindowsShimDlfcn, DladdrAcceptsCallSite) +{ + Dl_info info = {}; + int rc = dladdr((void *)&dladdr, &info); + /* Some shim builds may not implement dladdr; ensure return + * value matches POSIX shape (non-zero on success / 0 on error). */ + (void)rc; + SUCCEED(); +} + +TEST(WindowsShimDlfcn, DlerrorClearsAfterRead) +{ + (void)dlopen("definitely-not-a-real-library-yyy.dll", 0); + const char *first = dlerror(); + const char *second = dlerror(); + (void)first; + /* Second call should return null per POSIX. */ + EXPECT_EQ(second, nullptr); +} + +/* ids: getppid / setsid / getsid -------------------------------------- */ + +TEST(WindowsShimIds, GetppidNeverFails) +{ + pid_t pp = getppid(); + (void)pp; + SUCCEED(); +} + +TEST(WindowsShimIds, SetsidReturnsAnId) +{ + pid_t s = setsid(); + EXPECT_NE(s, (pid_t) - 1); +} + +TEST(WindowsShimIds, GetsidSelf) +{ + pid_t s = getsid(0); + EXPECT_NE(s, (pid_t) - 1); +} + +TEST(WindowsShimIds, GetsidNonexistent) +{ + errno = 0; + pid_t s = getsid(0x7FFFFFFE); + EXPECT_EQ(s, (pid_t) - 1); + EXPECT_EQ(errno, ESRCH); +} + +/* if_query ------------------------------------------------------------- */ + +TEST(WindowsShimIfQuery, NameindexEnumerates) +{ + struct if_nameindex *list = if_nameindex(); + + if (list) { + bool found_name = false; + + for (struct if_nameindex *p = list; p->if_name; ++p) { + EXPECT_NE(p->if_name, nullptr); + found_name = found_name || (p->if_name[0] != '\0'); + } + + (void)found_name; + if_freenameindex(list); + SUCCEED(); + + } else { + /* Acceptable when running without network privileges. */ + SUCCEED(); + } +} + +TEST(WindowsShimIfQuery, FreeNullSafe) +{ + if_freenameindex(nullptr); + SUCCEED(); +} + +/* resolver: inet_aton, inet_makeaddr, inet_lnaof, inet_netof, gethostent */ + +TEST(WindowsShimResolver, InetAtonValid) +{ + struct in_addr a = {}; + EXPECT_NE(inet_aton("127.0.0.1", &a), 0); +} + +TEST(WindowsShimResolver, InetAtonInvalid) +{ + struct in_addr a = {}; + EXPECT_EQ(inet_aton("not.an.ip.addr.zzzz", &a), 0); +} + +TEST(WindowsShimResolver, InetNtoaR) +{ + struct in_addr a; + a.s_addr = htonl(0x7F000001); + char buf[32] = {}; + const char *r = inet_ntoa_r(a, buf, sizeof(buf)); + EXPECT_NE(r, nullptr); + EXPECT_STREQ(buf, "127.0.0.1"); +} + +TEST(WindowsShimResolver, MakeAddrSplit) +{ + struct in_addr a = inet_makeaddr(0x7F, 0x000001); + (void)a; + SUCCEED(); +} + +TEST(WindowsShimResolver, NetofLnaof) +{ + struct in_addr a; + a.s_addr = htonl(0x7F000001); + in_addr_t net = inet_netof(a); + in_addr_t host = inet_lnaof(a); + EXPECT_NE(net + host, 0u); +} + +TEST(WindowsShimResolver, HostentIterators) +{ + sethostent(0); + struct hostent *e = gethostent(); + (void)e; + endhostent(); + SUCCEED(); +} + +TEST(WindowsShimResolver, NetentIterators) +{ + setnetent(0); + struct netent *n = getnetent(); + (void)n; + endnetent(); + struct netent *byname = getnetbyname("loopback"); + (void)byname; + struct netent *byaddr = getnetbyaddr(0x7F000000, AF_INET); + (void)byaddr; + SUCCEED(); +} + +TEST(WindowsShimResolver, ProtoentIterators) +{ + setprotoent(0); + struct protoent *p = getprotoent(); + (void)p; + endprotoent(); + SUCCEED(); +} + +TEST(WindowsShimResolver, ServentIterators) +{ + setservent(0); + struct servent *s = getservent(); + (void)s; + endservent(); + SUCCEED(); +} + +TEST(WindowsShimResolver, HstrerrorTextNonNull) +{ + EXPECT_NE(hstrerror(HOST_NOT_FOUND), nullptr); + EXPECT_NE(hstrerror(0), nullptr); +} + +/* sockets - minimal loopback round trip --------------------------------- */ + +namespace +{ +class WinsockBootstrap +{ +public: + WinsockBootstrap() + { + WSADATA wsa; + WSAStartup(MAKEWORD(2, 2), &wsa); + } + + ~WinsockBootstrap() + { + WSACleanup(); + } +}; +} + +TEST(WindowsShimSocket, UdpLoopbackSendRecv) +{ + WinsockBootstrap _ws; + + SOCKET srv = socket(AF_INET, SOCK_DGRAM, 0); + ASSERT_NE(srv, INVALID_SOCKET); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; /* any */ + ASSERT_EQ(bind(srv, (struct sockaddr *)&addr, sizeof(addr)), 0); + + int alen = sizeof(addr); + ASSERT_EQ(getsockname(srv, (struct sockaddr *)&addr, &alen), 0); + + SOCKET cli = socket(AF_INET, SOCK_DGRAM, 0); + ASSERT_NE(cli, INVALID_SOCKET); + + const char msg[] = "ping"; + ASSERT_EQ(sendto(cli, msg, (int)sizeof(msg), 0, + (struct sockaddr *)&addr, sizeof(addr)), + (int)sizeof(msg)); + + char buf[16] = {}; + struct sockaddr_in from; + int from_len = sizeof(from); + int n = recvfrom(srv, buf, sizeof(buf), 0, + (struct sockaddr *)&from, &from_len); + EXPECT_EQ(n, (int)sizeof(msg)); + EXPECT_STREQ(buf, "ping"); + + closesocket(srv); + closesocket(cli); +} + +TEST(WindowsShimSocket, SetsockoptReuseaddr) +{ + WinsockBootstrap _ws; + SOCKET s = socket(AF_INET, SOCK_DGRAM, 0); + ASSERT_NE(s, INVALID_SOCKET); + int yes = 1; + int rc = setsockopt(s, SOL_SOCKET, SO_REUSEADDR, + (const char *)&yes, sizeof(yes)); + EXPECT_EQ(rc, 0); + closesocket(s); +} + +#endif // _WIN32 + +/* Sentinel test so the binary always has at least one passing case. */ +TEST(WindowsShimRuntime, BuildSentinel) +{ + SUCCEED(); +}