halfs-0.2: HighLevelTests.hs
{-# LANGUAGE PatternSignatures #-}
-----------------------------------------------------------------------------
-- |
-- Module : HighLevelTests
--
-- Maintainer : Isaac Jones <ijones@galois.com>
-- Stability : alpha
-- Portability : GHC
--
-- Explanation: Implements higher-level interfaces like copying whole
-- files. Implements high-level tests that tie together multiple modules.
module HighLevelTests where
import Halfs (withFSWrite, unmountFS, unmountWriteFS, withFSRead, mountFSMV,
rename, mkdir, getDirectoryContents, openFileAtPathWrite,
syncPeriodicallyWrite,
openFileAtPathRead, withNewFSWrite, unlink, openAppend, seek,
closeWrite, makeFiles, write,
fsckWrite, closeWrite, closeRead, openRead, readReadRef)
import qualified Halfs as H (read)
import Halfs.TestFramework (Test(..), UnitTests, hunitToUnitTest,
assertCmd, assertEqual, assertBool)
import Halfs.BasicIO (devBufferReadHost, devBufferWriteHost,
getInodeWrite, getMemoryBlock)
import Binary(openBinMem, resetBin, put, get)
import Halfs.Inode (Inode(..), InodeMetadata(..), getPointerAt)
import Halfs.FSRoot (FSRoot(..))
import Halfs.FileHandle (FileHandle(..), FileMode(WriteMode), fhCloseWrite,
fhRead, fhWrite, fhSize)
import Halfs.FSState (FSWrite, FSRW, StateHandle(..), readToWrite, unsafeLiftIOWrite,
putStrLnWriteRead, unsafeLiftIORW, evalFSWriteIOMV,
unsafeLiftIOWriteWithRoot, unsafeReadGet, unsafeLiftIORead,
runFSWriteIO, throwError, runFSReadIO, unsafeWriteGet)
import Halfs.Buffer (strToNewBuff, newBuff, buffToStrSize)
import Halfs.Blocks (getBlockFromCacheOrDeviceWrite, getDiskAddrOfBlockWrite)
import Halfs.BufferBlock(mkDiskAddressBlock, getPartBufferBlock)
import Halfs.TheBlockMap(TheBlockMap(..))
import System.RawDevice(RawDevice, devBufferWrite, devBufferRead, newDevice)
import qualified Binary (get)
import Halfs.Utils (FileType(..), bytesPerBlock, mRemoveFile )
import Data.Integral ( INInt, inIntToInt, intToINLong, INLong
, fromIntegral', fromIntegral'')
import Data.Word (Word8)
-- base
import System.IO(openFile, hClose, hFileSize, IOMode(ReadMode))
import System.IO.Error (isFullError)
import System.Posix.IO (OpenMode(..), openFd, closeFd, defaultFileFlags)
import System.Posix.Files(unionFileModes, ownerReadMode, ownerWriteMode, groupReadMode)
import Control.Monad(when)
import Control.Monad.Error(throwError, catchError)
import Control.Concurrent( MVar, modifyMVar_, newEmptyMVar
, takeMVar, newMVar, putMVar
, forkIO, threadDelay)
import System.Random(randomR, getStdRandom)
import qualified Control.Exception as CE (finally)
--UNUSED:import qualified Control.Exception as CE (catch, throwIO)
import Control.Exception (finally)
import Data.Queue(queueToList)
-- ------------------------------------------------------------
-- * Interacting with host filesystem
-- ------------------------------------------------------------
-- |FIX: For now only works with file size < bufferSize. overwrites
-- existing file.
copyFromHost :: FSRoot
-> FilePath -- ^From (host)
-> FilePath -- ^To (fs)
-> IO ((FileHandle, INInt), FSRoot)
copyFromHost fsroot fromPath toPath = do
fileSize <- (do h <- openFile fromPath ReadMode
s <- hFileSize h
hClose h
return s)
hFrom <- openFd fromPath ReadWrite Nothing defaultFileFlags
putStrLn $ "copyFromHost fileSize: " ++ (show fileSize)
buffer <- openBinMem $ fromIntegral'' fileSize
devBufferReadHost hFrom 0 buffer (fromIntegral'' fileSize)
closeFd hFrom
resetBin buffer
(toFsHandle, newRoot)
<- runFSWriteIO (openFileAtPathWrite Halfs.FileHandle.WriteMode toPath File False) fsroot
runFSWriteIO (do r <- fhWrite toFsHandle buffer 0 (fromIntegral'' fileSize)
fhCloseWrite toFsHandle
return r
) newRoot
-- |FIX: For now only works with file size < bufferSize. overwrites
-- existing file.
copyToHost :: FSRoot
-> FilePath -- ^From (fs)
-> FilePath -- ^To (host)
-> IO ((FileHandle, INInt), FSRoot)
copyToHost fsroot0 fromPath toPath = do
((fromFsHandle, bytesToCopy), fsroot1)
<- runFSReadIO (do a <- openFileAtPathRead fromPath
b <- fhSize a
return (a,b)
) fsroot0 (\e -> throwError e)
-- FIX: do this one block at a time? may run out of memory on big files.
putStrLn $ "copyTo bin mem: " ++ (show bytesToCopy)
-- Put up with fromIntegral'' here because this is just testing code.
buffer <- openBinMem (fromIntegral'' (bytesToCopy::INLong))
readVal@((_, numRead), _fsroot2) <-
runFSReadIO (fhRead fromFsHandle buffer 0 (fromIntegral'' bytesToCopy))
fsroot1 (\e -> throwError e)
resetBin buffer
hTo <- openFd toPath ReadWrite (Just (foldl1 unionFileModes [ ownerReadMode
, ownerWriteMode
, groupReadMode]))
defaultFileFlags
devBufferWriteHost hTo 0 buffer (inIntToInt numRead)
closeFd hTo
return readVal
-- ------------------------------------------------------------
-- * Testing
-- ------------------------------------------------------------
-- Problem: This function doesn't properly use the mvar for
-- threading. maybe I should make it impossible to get the mvar out
-- without blocking...
copyAndCheckFile :: String -> FilePath -> FilePath -> Test
copyAndCheckFile message path targetFS =
TestLabel message $ TestCase $ do
let targetNative = "tests/fromFS"
mRemoveFile targetNative
withFSWrite dirFS (unsafeLiftIOWriteWithRoot (\fsroot -> do
catchError (do
(_, fsroot1) <- copyFromHost fsroot path targetFS
unmountFS fsroot1
) (unmountAndThrow fsroot))) -- clean unmount
withFSWrite dirFS (unsafeLiftIOWriteWithRoot (\fsroot -> do
catchError (do
(_, fsroot1) <- copyToHost fsroot targetFS targetNative
assertCmd ("diff " ++ path ++ " " ++ targetNative)
("fs file copy failed: " ++ message)
(_, fsroot2) <- runFSWriteIO (unlink targetFS) fsroot1
unmountFS fsroot2
) (unmountAndThrow fsroot))) -- clean unmount
unmountAndThrow :: FSRoot -> IOError -> IO ()
unmountAndThrow fsroot e = unmountFS fsroot >> throwError e
dirFS :: FilePath
dirFS = "halfs-client1"
unitTests :: Bool -> UnitTests
unitTests runFast = hunitToUnitTest (hunitTests runFast)
assertEqualRW :: (Eq a, Show a, FSRW m) => String -> a -> a -> m ()
assertEqualRW s a b = unsafeLiftIORW $ assertEqual s a b
assertBoolRW :: (FSRW m) => String -> Bool -> m ()
assertBoolRW s b = unsafeLiftIORW $ assertBool s b
-- ------------------------------------------------------------
-- * Multi-Threaded tests
-- ------------------------------------------------------------
-- Helpers:
addResult :: MVar [Maybe String] -> (Maybe String) -> IO ()
addResult mv b =
modifyMVar_ mv (\l -> return (b:l))
assertMString Nothing = assertBool "" True
assertMString (Just s) = assertBool s False
waitAll :: [MVar a] -> IO ()
waitAll l = mapM_ takeMVar l
myForkIO :: IO () -> IO (MVar ())
myForkIO io = do
mvar <- newEmptyMVar
forkIO (io `finally` putMVar mvar ())
return mvar
delayRandom :: Int -- ^Max number of seconds to delay
-> IO ()
delayRandom s = do
randNum <- getStdRandom (\g -> randomR (0, secondToMicroSecond s) g)
threadDelay randNum
secondToMicroSecond n = n * 1000000
-- TESTS
nSheeps :: Int -> FilePath -> FSWrite [Maybe String]
nSheeps n dirName = do
mkdir dirName
makeFiles n dirName "sheep"
return [Nothing]
runTest :: MVar [Maybe String] -- ^Results
-> StateHandle
-> FSWrite [Maybe String]
-> IO (MVar ())
runTest mv h f =
myForkIO $ do delayRandom 10
-- results <- CE.catch (evalFSWriteIOMV f h)
-- (\e -> addResult mv (Just (show e))
-- >> CE.throwIO e)
results <- evalFSWriteIOMV f h
mapM_ (addResult mv) results
-- |Doesn't block on outer blocker.
runTest' :: MVar [Maybe String] -- ^Results
-> MVar FSRoot
-> FSWrite [Maybe String]
-> IO (MVar ())
runTest' mv h f =
myForkIO $ do delayRandom 10
results <- evalFSWriteIOMV f (StateHandle h Nothing)
mapM_ (addResult mv) results
-- Controller:
multiThreadTests :: Bool -> Test
multiThreadTests runFast =
TestLabel ("multi-thread tests") $ TestCase $ do
blocker <- newMVar ()
-- should fork off a sync thread:
stateHandle@(StateHandle smallMv _)
<- mountFSMV Nothing dirFS (Just blocker) False 500
resultsMV <- newMVar []
let rt = runTest resultsMV stateHandle
let rt' = runTest' resultsMV smallMv
let (sheepThreads, extraThreads, syncThreads)
= if runFast
then (50, 50, 1)
else (300, 100, 5)
-- many threads, grab the nSheep mvar many times and make only 4 dirs each.
waitOn2 <- mapM rt [nSheeps 2 ("/sheeps" ++ (show n)) | n <- [1..sheepThreads] ]
-- 50 fsck threads
waitOn3 <- mapM rt (replicate extraThreads (fsckWrite >> return [Nothing]))
waitOnSync <- mapM rt' (replicate syncThreads (syncPeriodicallyWrite blocker False
>> return [Nothing]))
waitOn1 <- mapM rt [ fsckWrite >> return [Nothing]
, nSheeps extraThreads "/sheeps" -- grabs mvar only once.
, fsckWrite >> return [Nothing]
]
waitAll $ concat [waitOn1, waitOn2, waitOn3]
evalFSWriteIOMV unmountWriteFS stateHandle
-- the sync thread won't die 'till we unmount.
waitAll waitOnSync
results <- takeMVar resultsMV
mapM assertMString results
putStrLn "unmounting"
return ()
-- ------------------------------------------------------------
-- * Block-level atomicity tests
-- ------------------------------------------------------------
toggle :: Word8 -> Word8
toggle 0 = 1
toggle 1 = 0
toggle 3 = 4
toggle 4 = 3
toggle n = (n-1)
writer :: RawDevice -> Word8 -> Int -> IO ()
writer lowDev n timeLeft
| timeLeft <= 0 = return ()
| otherwise = do
let word8s = replicate bytesPerBlock (n::Word8)
block <- getMemoryBlock
mapM (put block) word8s
resetBin block
devBufferWrite lowDev 1 block
writer lowDev (toggle n) (timeLeft - 1)
reader lowDev timeLeft
| timeLeft <= 0 = return ()
| otherwise = do
block <- getMemoryBlock
devBufferRead lowDev 1 block
resetBin block
((h::Word8):words) <- sequence $ replicate bytesPerBlock (get block)
let allEq = and [h == w | w <- words]
assertBool ("Failure in test for block-level atomicity at timeLeft = " ++
show timeLeft ++ ": " ++
show (h:words))
allEq
reader lowDev (timeLeft - 1)
testBuffs :: Test
testBuffs = TestLabel ("block-level atomicity tests") $ TestCase $ do
lowDev <- newDevice Nothing "halfs-client1" 2
-- highDev <- makeRawDevice "halfs-4EYES"
-- make block uniform before any read operations
writer lowDev 1 1
let numIterations = 100
waitWriters <- mapM myForkIO [ writer lowDev 1 numIterations
, writer lowDev 0 numIterations
, writer lowDev 3 numIterations
, writer lowDev 4 numIterations]
threadDelay (secondToMicroSecond 2)
waitReaders <- mapM myForkIO (replicate 5 (reader lowDev numIterations))
waitAll $ concat [waitWriters, waitReaders]
-- ------------------------------------------------------------
-- * Collection of tests
-- ------------------------------------------------------------
slowTests = [testBuffs]
hunitTests :: Bool -- ^run fast?
-> [Test]
hunitTests runFast = (if runFast then [] else slowTests) ++
[TestLabel "shell.hs: mkdir tmp" $ TestCase $ do
withNewFSWrite dirFS 5000 (mkdir "/tmp" >> unmountWriteFS)
,(multiThreadTests runFast)
,copyAndCheckFile "one-block file" "tests/from" "/tmp/toNative"
,copyAndCheckFile "small file" "tests/smallFile" "/tmp/smallFile"
,copyAndCheckFile "small file 2" "tests/smallFile" "/tmp/smallFile"
,copyAndCheckFile "multi-block file" "tests/multiBlockFile" "/tmp/multiBlockFile"
,copyAndCheckFile "level-2 file" "tests/70KFile" "/70KFile"
,TestLabel "move" $ TestCase $ do
withFSWrite dirFS (unsafeLiftIOWriteWithRoot (\fsroot -> do
(_, fsroot) <- copyFromHost fsroot "tests/multiBlockFile" "/tmp/multiBlockFile"
unmountFS fsroot))
withFSWrite dirFS (do
let ls = readToWrite . getDirectoryContents
ls "/tmp" >>= assertEqualRW "tmp dir has files we created"
[".", "multiBlockFile"]
rename "/tmp/multiBlockFile" "/tmp/multiBlockFile2"
ls "/tmp" >>= assertEqualRW "ls after a move"
[".", "multiBlockFile2"]
unmountWriteFS
)
]