From 2c6bbdbb702afc82cfb867e90b18bb401dc709a8 Mon Sep 17 00:00:00 2001
From: Trevor Shoe <trevor.schupbach@canonical.com>
Date: Tue, 17 Dec 2024 14:50:29 -0500
Subject: [PATCH] [sshfs] Add platform unix tests

---
 include/multipass/platform_unix.h |   8 +-
 src/platform/platform_unix.cpp    |  11 +--
 tests/unix/mock_signal_wrapper.h  |  42 +++++++++
 tests/unix/test_platform_unix.cpp | 136 ++++++++++++++++++++++++++++++
 4 files changed, 186 insertions(+), 11 deletions(-)
 create mode 100644 tests/unix/mock_signal_wrapper.h

diff --git a/include/multipass/platform_unix.h b/include/multipass/platform_unix.h
index a53cbfc139..981330e894 100644
--- a/include/multipass/platform_unix.h
+++ b/include/multipass/platform_unix.h
@@ -32,11 +32,11 @@ namespace multipass::platform
 class SignalWrapper : public Singleton<SignalWrapper>
 {
 public:
-  SignalWrapper(const PrivatePass&) noexcept;
+    SignalWrapper(const PrivatePass&) noexcept;
 
-  virtual int mask_signals(int how, const sigset_t* sigset, sigset_t* old_set = nullptr) const;
-  virtual int send(pthread_t target, int signal) const;
-  virtual int wait(const sigset_t& sigset, int& got) const;
+    virtual int mask_signals(int how, const sigset_t* sigset, sigset_t* old_set = nullptr) const;
+    virtual int send(pthread_t target, int signal) const;
+    virtual int wait(const sigset_t& sigset, int& got) const;
 };
 
 sigset_t make_sigset(const std::vector<int>& sigs);
diff --git a/src/platform/platform_unix.cpp b/src/platform/platform_unix.cpp
index ee7343b77d..2e7c367eb6 100644
--- a/src/platform/platform_unix.cpp
+++ b/src/platform/platform_unix.cpp
@@ -158,9 +158,9 @@ int mp::platform::symlink_attr_from(const char* path, sftp_attributes_struct* at
     return 0;
 }
 
-mp::platform::SignalWrapper::SignalWrapper(const PrivatePass& pass) noexcept
-: Singleton(pass){}
-
+mp::platform::SignalWrapper::SignalWrapper(const PrivatePass& pass) noexcept : Singleton(pass)
+{
+}
 
 int mp::platform::SignalWrapper::mask_signals(int how, const sigset_t* sigset, sigset_t* old_set) const
 {
@@ -207,11 +207,8 @@ std::function<std::optional<int>(const std::function<bool()>&)> mp::platform::ma
 {
     return [sigset = make_and_block_signals({SIGQUIT, SIGTERM, SIGHUP, SIGUSR2}),
             period](const std::function<bool()>& condition) -> std::optional<int> {
-
         // create a timer to periodically send SIGUSR2
-        utils::Timer signal_generator{period, [signalee = pthread_self()] {
-            MP_POSIX_SIGNAL.send(signalee, SIGUSR2);
-        }};
+        utils::Timer signal_generator{period, [signalee = pthread_self()] { MP_POSIX_SIGNAL.send(signalee, SIGUSR2); }};
 
         // wait on signals and condition
         int latest_signal = SIGUSR2;
diff --git a/tests/unix/mock_signal_wrapper.h b/tests/unix/mock_signal_wrapper.h
new file mode 100644
index 0000000000..9701f278a0
--- /dev/null
+++ b/tests/unix/mock_signal_wrapper.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) Canonical, Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MULTIPASS_MOCK_SIGNAL_WRAPPER_H
+#define MULTIPASS_MOCK_SIGNAL_WRAPPER_H
+
+#include "../common.h"
+#include "../mock_singleton_helpers.h"
+
+#include <multipass/platform_unix.h>
+
+namespace multipass::test
+{
+
+class MockSignalWrapper : public platform::SignalWrapper
+{
+public:
+    using SignalWrapper::SignalWrapper;
+
+    MOCK_METHOD(int, mask_signals, (int, const sigset_t*, sigset_t*), (const, override));
+    MOCK_METHOD(int, send, (pthread_t, int), (const, override));
+    MOCK_METHOD(int, wait, (const sigset_t&, int&), (const, override));
+
+    MP_MOCK_SINGLETON_BOILERPLATE(MockSignalWrapper, platform::SignalWrapper);
+};
+
+} // namespace multipass::test
+
+#endif // MULTIPASS_MOCK_SIGNAL_WRAPPER_H
diff --git a/tests/unix/test_platform_unix.cpp b/tests/unix/test_platform_unix.cpp
index 02c129e33d..bcc7215582 100644
--- a/tests/unix/test_platform_unix.cpp
+++ b/tests/unix/test_platform_unix.cpp
@@ -21,6 +21,7 @@
 #include <tests/temp_file.h>
 
 #include "mock_libc_functions.h"
+#include "mock_signal_wrapper.h"
 
 #include <multipass/constants.h>
 #include <multipass/format.h>
@@ -142,3 +143,138 @@ TEST_F(TestPlatformUnix, multipassStorageLocationNotSetReturnsEmpty)
 
     EXPECT_TRUE(storage_path.isEmpty());
 }
+
+void test_sigset_empty(const sigset_t& set)
+{
+    // there is no standard empty check to try a few different signals
+    EXPECT_EQ(sigismember(&set, SIGABRT), 0);
+    EXPECT_EQ(sigismember(&set, SIGALRM), 0);
+    EXPECT_EQ(sigismember(&set, SIGCHLD), 0);
+    EXPECT_EQ(sigismember(&set, SIGINT), 0);
+    EXPECT_EQ(sigismember(&set, SIGSEGV), 0);
+    EXPECT_EQ(sigismember(&set, SIGKILL), 0);
+    EXPECT_EQ(sigismember(&set, SIGQUIT), 0);
+    EXPECT_EQ(sigismember(&set, SIGTERM), 0);
+    EXPECT_EQ(sigismember(&set, SIGUSR1), 0);
+    EXPECT_EQ(sigismember(&set, SIGUSR2), 0);
+}
+
+bool test_sigset_has(const sigset_t& set, const std::vector<int>& sigs)
+{
+    bool good = true;
+    for (auto sig : sigs)
+    {
+        auto has_sig = sigismember(&set, sig) == 1;
+        if (!has_sig)
+            good = false;
+
+        EXPECT_TRUE(has_sig);
+    }
+
+    return good;
+}
+
+TEST_F(TestPlatformUnix, make_sigset_returns_emptyset)
+{
+    auto set = mp::platform::make_sigset({});
+    test_sigset_empty(set);
+}
+
+TEST_F(TestPlatformUnix, make_sigset_makes_sigset)
+{
+    auto set = mp::platform::make_sigset({SIGINT, SIGUSR2});
+
+    // check signals are set
+    test_sigset_has(set, {SIGINT, SIGUSR2});
+
+    // unset set signals
+    sigdelset(&set, SIGUSR2);
+    sigdelset(&set, SIGINT);
+
+    // check other signals aren't set
+    test_sigset_empty(set);
+}
+
+TEST_F(TestPlatformUnix, make_and_block_signals_works)
+{
+    auto [mock_signals, guard] = mpt::MockSignalWrapper::inject<StrictMock>();
+
+    EXPECT_CALL(
+        *mock_signals,
+        mask_signals(SIG_BLOCK, Pointee(Truly([](const auto& set) { return test_sigset_has(set, {SIGINT}); })), _));
+
+    auto set = mp::platform::make_and_block_signals({SIGINT});
+
+    test_sigset_has(set, {SIGINT});
+
+    sigdelset(&set, SIGINT);
+    test_sigset_empty(set);
+}
+
+TEST_F(TestPlatformUnix, make_quit_watchdog_blocks_signals)
+{
+    auto [mock_signals, guard] = mpt::MockSignalWrapper::inject<StrictMock>();
+
+    EXPECT_CALL(*mock_signals,
+                mask_signals(SIG_BLOCK,
+                             Pointee(Truly([](const auto& set) {
+                                 return test_sigset_has(set, {SIGQUIT, SIGTERM, SIGHUP, SIGUSR2});
+                             })),
+                             _));
+
+    mp::platform::make_quit_watchdog(std::chrono::milliseconds{1});
+}
+
+TEST_F(TestPlatformUnix, quit_watchdog_quits_on_condition)
+{
+    auto [mock_signals, guard] = mpt::MockSignalWrapper::inject<StrictMock>();
+
+    EXPECT_CALL(*mock_signals, mask_signals(SIG_BLOCK, _, _));
+    EXPECT_CALL(*mock_signals, wait(_, _)).WillRepeatedly(DoAll(SetArgReferee<1>(SIGUSR2), Return(0)));
+    ON_CALL(*mock_signals, send(pthread_self(), SIGUSR2)).WillByDefault(Return(0));
+
+    auto watchdog = mp::platform::make_quit_watchdog(std::chrono::milliseconds{1});
+    EXPECT_EQ(watchdog([] { return false; }), std::nullopt);
+}
+
+TEST_F(TestPlatformUnix, quit_watchdog_quits_on_signal)
+{
+    auto [mock_signals, guard] = mpt::MockSignalWrapper::inject<StrictMock>();
+
+    EXPECT_CALL(*mock_signals, mask_signals(SIG_BLOCK, _, _));
+    EXPECT_CALL(*mock_signals, wait(_, _))
+        .WillOnce(DoAll(SetArgReferee<1>(SIGUSR2), Return(0)))
+        .WillOnce(DoAll(SetArgReferee<1>(SIGTERM), Return(0)));
+    ON_CALL(*mock_signals, send(pthread_self(), SIGUSR2)).WillByDefault(Return(0));
+
+    auto watchdog = mp::platform::make_quit_watchdog(std::chrono::milliseconds{1});
+    EXPECT_EQ(watchdog([] { return true; }), SIGTERM);
+}
+
+TEST_F(TestPlatformUnix, quit_watchdog_signals_itself_asynchronously)
+{
+    auto [mock_signals, guard] = mpt::MockSignalWrapper::inject<StrictMock>();
+
+    std::atomic<bool> signaled = false;
+    std::atomic<int> times = 0;
+
+    EXPECT_CALL(*mock_signals, mask_signals(SIG_BLOCK, _, _));
+    EXPECT_CALL(*mock_signals, wait(_, _))
+        .WillRepeatedly(DoAll(
+            [&signaled, &times] {
+                while (!signaled.load(std::memory_order_acquire))
+                {
+                }
+                times.fetch_add(1, std::memory_order_release);
+                signaled.store(false, std::memory_order_release);
+            },
+            SetArgReferee<1>(SIGUSR2),
+            Return(0)));
+
+    EXPECT_CALL(*mock_signals, send(pthread_self(), SIGUSR2))
+        .WillRepeatedly(DoAll([&signaled] { signaled.store(true, std::memory_order_release); }, Return(0)));
+
+    auto watchdog = mp::platform::make_quit_watchdog(std::chrono::milliseconds{1});
+    EXPECT_EQ(watchdog([&times] { return times.load(std::memory_order_acquire) < 10; }), std::nullopt);
+    EXPECT_GE(times.load(std::memory_order_acquire), 10);
+}