packages feed

ghc-tags-plugin-1.0.0.0: test/Test/AtomicFile.hs

module Test.AtomicFile (tests) where

import Control.Concurrent
import Control.Exception
import System.Directory
import System.FilePath ((</>))
import System.IO

import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit

import Plugin.GhcTags.AtomicFile

tests :: TestTree
tests = testGroup "Atomic file updates"
    [ testCase "aborted initial update leaves no files" testAbortedInitialUpdate
    , testCase "successful update publishes replacement" testSuccessfulUpdate
    , testCase "aborted update preserves existing tags" testAbortedUpdate
    , testCase "write exception preserves existing tags" testWriteException
    , testCase "cancellation preserves tags and permits the next update" testCancellation
    ]


testAbortedInitialUpdate :: Assertion
testAbortedInitialUpdate = withTestDirectory $ \dir -> do
    withAtomicFile (dir </> "tags") $ \h ->
      hPutStr h "discarded\n" >> pure False
    listDirectory dir >>= (@?= [])


testSuccessfulUpdate :: Assertion
testSuccessfulUpdate = withTagsFile $ \dir path -> do
    withAtomicFile path $ \h -> hPutStr h "replacement\n" >> pure True
    checkTagsFile dir path "replacement\n"


testAbortedUpdate :: Assertion
testAbortedUpdate = withTagsFile $ \dir path -> do
    withAtomicFile path $ \h -> hPutStr h "partial\n" >> pure False
    checkTagsFile dir path original


testWriteException :: Assertion
testWriteException = withTagsFile $ \dir path -> do
    result <- try $ withAtomicFile path $ \h -> do
      hPutStr h "partial\n"
      hFlush h
      throwIO (userError "write failed")
    case result :: Either IOException () of
      Left _ -> pure ()
      Right _ -> assertFailure "write exception swallowed"
    checkTagsFile dir path original


testCancellation :: Assertion
testCancellation = withTagsFile $ \dir path -> do
    -- Cancel after partial output has reached the temporary file.
    started <- newEmptyMVar
    blocker <- newEmptyMVar :: IO (MVar ())
    finished <- newEmptyMVar
    let writer = do
          outcome <- try $ withAtomicFile path $ \h -> do
            hPutStr h "partial\n"
            hFlush h
            putMVar started ()
            takeMVar blocker
            pure True
          putMVar finished (outcome :: Either SomeException ())
    bracket (forkIO writer) killThread $ \tid -> do
      takeMVar started
      -- Readers see complete old contents while the update is in progress.
      readFile path >>= (@?= original)
      throwTo tid ThreadKilled
      outcome <- takeMVar finished
      case outcome of
        Left err -> fromException err @?= Just ThreadKilled
        Right _ -> assertFailure "cancellation swallowed"
      checkTagsFile dir path original
      withAtomicFile path $ \h -> hPutStr h "recovered\n" >> pure True
      checkTagsFile dir path "recovered\n"


--
-- Utils
--

original :: String
original = "existing tags\n"


withTestDirectory :: (FilePath -> IO a) -> IO a
withTestDirectory action = do
    tmp <- getTemporaryDirectory
    bracket (do (path, h) <- openTempFile tmp "ghc-tags-atomic-test"
                hClose h
                removeFile path
                createDirectory path
                pure path)
            removeDirectoryRecursive
            action

withTagsFile :: (FilePath -> FilePath -> IO a) -> IO a
withTagsFile action = withTestDirectory $ \dir -> do
    let path = dir </> "tags"
    writeFile path original
    action dir path

checkTagsFile :: FilePath -> FilePath -> String -> Assertion
checkTagsFile dir path expected = do
    readFile path >>= (@?= expected)
    listDirectory dir >>= (@?= ["tags"])