packages feed

salmon-ops-recipes-0.1.0.0: test/Test/QemuShutdownSpec.hs

{-# LANGUAGE ScopedTypeVariables #-}

{- | Layer 1 (a unix socket in a temp directory, no qemu, no root): the
monitor-socket half of "Salmon.Builtin.Nodes.Qemu".'shutdown' against a fake
monitor. It shows the three answers the down hook depends on: a guest that
powers off (the socket goes away, no hard stop needed), one that ignores ACPI
(still listening at the deadline, so the caller must stop it hard) and a VM
that was never running (nothing to send to).
-}
module Test.QemuShutdownSpec (tests) where

import Control.Concurrent (forkIO, killThread, threadDelay)
import Control.Concurrent.MVar (MVar, newEmptyMVar, newMVar, modifyMVar_, readMVar, tryPutMVar)
import Control.Exception (SomeException, bracket, try)
import Control.Monad (forever, void)
import qualified Data.ByteString.Char8 as C8
import Network.Socket hiding (shutdown)
import qualified Network.Socket.ByteString as SocketBS
import Salmon.Builtin.Nodes.Qemu (reset, shutdown)
import System.Directory (removeFile)
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))

tests :: TestTree
tests =
    testGroup
        "Qemu.shutdown (monitor socket)"
        [ testCase "a guest that powers off: True, and the command was sent" powersOff
        , testCase "a guest that ignores ACPI: False at the deadline" ignoresAcpi
        , testCase "no monitor listening: True, nothing sent" neverRan
        , testCase "reset sends system_reset" sendsReset
        ]

-- | The commands received: the liveness probes (connect, hang up) send nothing and are dropped.
commands :: Fake -> IO [String]
commands fake = filter (not . null) <$> readMVar (received fake)

-- | What the fake monitor received, and whether it exits on @system_powerdown@.
data Fake = Fake {received :: MVar [String]}

withFake :: Bool -> (FilePath -> Fake -> IO a) -> IO a
withFake exitsOnPowerdown act =
    withSystemTempDirectory "qemu-mon" $ \dir -> do
        let path = dir </> "mon.sock"
        got <- newMVar []
        listener <- socket AF_UNIX Stream defaultProtocol
        bind listener (SockAddrUnix path)
        listen listener 5
        gone <- newEmptyMVar
        tid <- forkIO $ forever $ do
            (conn, _) <- accept listener
            void $ forkIO $ do
                line <- readLine conn
                modifyMVar_ got (pure . (line :))
                if exitsOnPowerdown && line == "system_powerdown"
                    then do
                        close listener
                        void (try @SomeException (removeFile path))
                        void (tryPutMVar gone ())
                    else pure ()
                close conn
        r <- act path (Fake got)
        killThread tid
        void (try @SomeException (close listener))
        pure r
  where
    readLine conn = do
        bs <- SocketBS.recv conn 4096
        pure (takeWhile (/= '\n') (C8.unpack bs))

powersOff :: IO ()
powersOff = withFake True $ \path fake -> do
    ok <- shutdown 5 path
    ok @?= True
    commands fake >>= (@?= ["system_powerdown"])

ignoresAcpi :: IO ()
ignoresAcpi = withFake False $ \path fake -> do
    ok <- shutdown 1 path
    ok @?= False
    commands fake >>= (@?= ["system_powerdown"])

neverRan :: IO ()
neverRan = withSystemTempDirectory "qemu-mon" $ \dir -> do
    ok <- shutdown 1 (dir </> "absent.sock")
    ok @?= True

sendsReset :: IO ()
sendsReset = withFake False $ \path fake -> do
    ok <- reset path
    ok @?= True
    threadDelay 100000
    commands fake >>= (@?= ["system_reset"])