{-# LANGUAGE PatternSignatures, Rank2Types #-} module Halfs.BasicIO (devBufferReadHost, devBufferWriteHost, devBufferWriteSafe, bytesPerBlock, getMemoryBlock, binSkip, getInodeRead, getInodeWrite, getDiskAddrOfBlockWrite, unitTests, readPartOfBlock, writePartOfBlock, getInodesBin, putPointersBin, syncInode, readInodeBootStrap, getBlockFromCacheOrDeviceWrite, readDirectoryBin, writeDirectoryBin, readInodeMapBin, readBlockMapBin, fsRootFreeInode, freeInodeData, allBlocksInInode, writeBlockMapBin, writeInodeMapBin, module Binary ) where import Data.Integral import Halfs.Utils hiding (inodesPerBlock) import qualified Halfs.Utils(inodesPerBlock) import Binary (copyBytes, BinHandle, Bin(BinPtr), BinArray, tellBin, seekBin, openBinIO, Bin(BinPtr), openBinMem, put_, sizeBinMem, resetBin, get) import Halfs.FSRW (unsafeLiftIORW) import Halfs.BinaryMonad (FSRW, resetBinRW, seekBinRW, tellBinRW, getRW) import Halfs.TestFramework (Test(..), UnitTests, assertCmd, assertEqual, hunitToUnitTest) import Halfs.FSRoot(FSRoot(..), InodeNum, InodeCache, addToInodeCache, getFromInodeCache, fsRootInode, fsRootUpdateInodeCache, fsRootRmFromDirectoryCache, InodeCacheAddStyle(InodeCacheKeep)) import Halfs.BufferBlockCache (getBlockFromCacheOrDevice,getBlockFromDevice) import Halfs.Inode (InodeBlock, Inode(..), InodeMetadata(..), inodeBumpSize, newInode, goodInodeMagic) import Halfs.BufferBlock (BufferBlock(..), writeToBufferBlock,Alloc(..), copyFromBufferBlock, copyToBufferBlock, mkBufferBlock, diskAddressListFromBufferBlock, PartBufferBlock, mkInodeBlock, putPartBufferBlock, getPartBufferBlock, zeroBufferBlock, startBufferBlockCursor ) import Halfs.TheBlockMap(TheBlockMap,mkBlockMap) import Halfs.Directory (DirectoryMap) import System.RawDevice(RawDevice, devBufferRead, devBufferWrite, makeRawDevice, finalizeDevice, BufferBlockHandle) import Halfs.FSState (FSRead, writeToBuffer, unsafeReadGet, unsafeLiftIOWrite, FSWrite, unsafeWriteToRead, unsafeModifyFSRead, readToWrite, readToWriteCont, modifyFSWrite, unsafeWriteGet) import qualified Halfs.FSState (modify) import Halfs.TheInodeMap(TheInodeMap(..), freeInode) import Halfs.TheBlockMap(TheBlockMap(..), freeBlock) import Halfs.Blocks (getDiskAddrOfBlockRead, getDiskAddrOfBlockRaw, markBlockDirtyWrite, getDiskAddrOfBlockWrite, getBlockFromCacheOrDeviceWrite) -- base import System.IO (openFile, hClose, hFileSize, IOMode(..)) import System.Posix.Types (Fd) import System.Posix.Files(unionFileModes, ownerReadMode, ownerWriteMode, groupReadMode) import System.Posix.IO (OpenMode(..), openFd, closeFd, defaultFileFlags) import System.Directory(removeFile, doesFileExist) import Control.Exception(assert) import Control.Monad(when, unless) import Data.Queue(Queue, emptyQueue, addToQueue, queueToList) import qualified Data.Map as Map import Data.Array (elems, assocs) inodesPerBlock :: INInt inodesPerBlock = intToINInt Halfs.Utils.inodesPerBlock binSkip :: (FSRW m) => BufferBlockHandle s -> Int -- bytes -> m () binSkip binHandle bytes = do (BinPtr bufLoc) <- tellBinRW binHandle seekBinRW binHandle (BinPtr (bufLoc + bytes)) return () -- ------------------------------------------------------------ -- * Block IO - reads right from disk rather than cache -- ------------------------------------------------------------ -- TODO: should this be here? -- |Reads a bunch of sequential bytes, presumably from the host OS. devBufferReadHost :: Fd -> DiskAddress -- ^location to start reading -> BinHandle -> Int -- ^Number of bytes to read -> IO () devBufferReadHost h diskAddr buffer numBytes = do fileHandle <- openBinIO h seekBin fileHandle (locationOfBlock diskAddr) copyBytes fileHandle buffer numBytes devBufferWriteSafe :: RawDevice -> DiskAddress -- ^Block number -> BufferBlockHandle s -- ^Buffer! -> FSWrite () devBufferWriteSafe r d b = unsafeLiftIOWrite $ devBufferWrite r d b -- TODO: should this be here? -- |Writes a bunch of sequential bytes, presumably to the host OS. devBufferWriteHost :: Fd -> DiskAddress -- ^location to write -> BinHandle -- ^Buffer! -> Int -- ^How many bytes? -> IO () devBufferWriteHost h diskAddr buffer numBytes = do fileHandle <- openBinIO h seekBin fileHandle (locationOfBlock diskAddr) copyBytes buffer fileHandle numBytes -- |read from cache readPartOfBlock :: DiskAddress -> INInt -- Block offset -> BinHandle -- Buffer -> INInt -- buffer offset -> INInt -- number of bytes to read -> FSRead INInt -- number of bytes read readPartOfBlock da blockOffset buffer bufferOffset numToRead = do FSRoot{device=dev, bbCache=cache} <- unsafeReadGet -- unsafeLiftIOWrite $ print "readPartOfBlock" getBlockFromCacheOrDevice cache dev da (\ bb -> do unsafeLiftIORW $ copyFromBufferBlock bb (inIntToInt blockOffset) buffer (inIntToInt bufferOffset) (inIntToInt numToRead) return numToRead) writePartOfBlock :: DiskAddress -> INInt -- Block offset -> BinHandle -- Buffer to read from -> INInt -- buffer offset -> INInt -- number of bytes to write -> FSWrite INInt -- number of bytes written writePartOfBlock da blockOffset buffer bufferOffset numToWrite = do FSRoot{device=dev, bbCache=cache} <- unsafeWriteGet -- FIX: Watch for concurrency issues here. -- unsafeLiftIOWrite $ print "writePartOfBlock" getBlockFromCacheOrDevice cache dev -- seek positions should be taking care of this. FIX: should we do so here too? (assert (blockOffset + numToWrite <= (intToINInt bytesPerBlock)) da) (\ bb -> do -- unsafeLiftIOWrite $ print $ "writePartOfBlock : " ++ show (bbDiskAddr bb) unsafeLiftIORW $ copyToBufferBlock buffer (inIntToInt bufferOffset) bb (inIntToInt blockOffset) (inIntToInt numToWrite) markBlockDirtyWrite bb return numToWrite) {- UNUSED: -- For convenience inBinPtr :: INInt -> Bin a inBinPtr i = BinPtr $ inIntToInt i -} -- ------------------------------------------------------------ -- * Inode Stuff -- ------------------------------------------------------------ -- |gets all the inodes in a BufferBlock. getInodesBin :: (FSRW m) => BufferBlock s -> m InodeBlock getInodesBin bb = do sequence [ do pbb <- unsafeLiftIORW $ mkInodeBlock bb i unsafeLiftIORW $ getPartBufferBlock pbb | i <- [0..(inIntToInt inodesPerBlock - 1) ] ] -- Write the DiskAddress pointers inside an Inode into a block, -- zeroing the rest of the block. putPointersBin :: (FSRW m) => BufferBlock s -> Inode -> m () putPointersBin bb (Inode _ ptrs) = do -- must zero this block so we know where the _real_ -- pointers end. unsafeLiftIORW $ zeroBufferBlock bb unsafeLiftIORW $ writeToBufferBlock bb startBufferBlockCursor [ Alloc e | e <- elems ptrs ] -- ------------------------------------------------------------ -- * Inode Stuff -- ------------------------------------------------------------ -- |Update this inode according to the given function. fsRootUpdateInode :: INInt -- Inode to look up -> (Inode -> Inode) -- function to apply -> FSWrite () -- Nothing if this inode isn't in cache fsRootUpdateInode inodeNum f = do inode <- getInodeWrite inodeNum Nothing modifyFSWrite (\fsroot -> return (fsRootUpdateInodeCache fsroot (f inode), ())) -- |In preparation for a read or a write, we might want to get a -- binhandle that's positioned at the beginning of the given inode. -- Reads from disk. May increase the size of the root inode if the -- requested inode is outside its current range. The size of the root -- inode is always along block boundries. FIX: Move to FSRead monad. unsafeMoveBinHandleForInodeNum :: INInt -- ^The inode number -> Bool -- ^True if it's safe to call "Write" functions -> (forall s . PartBufferBlock Inode s -> FSRead a) -> FSRead a unsafeMoveBinHandleForInodeNum inodeNum forWrite cont = do -- FIX: Below 'get' might indeed be thread unsafe. see comments below. rootInode <- (do fsroot <- unsafeReadGet return (fsRootInode fsroot)) if forWrite then unsafeWriteToRead (do -- FIX: concurrency: should possibly block on this interval (to update) getBlockFromInodeWrite rootInode inodeBlock (\ bb -> do -- getBlockFromInode has ensured that the root inode has the blocks -- it needs, but it hasn't actually bumped the size of this inode if -- necessary. Plus 1 since count from zero. let (minSizeForInode::INLong) = inIntToINLong $ (inodeBlock + 1) * (intToINInt bytesPerBlock) -- FIX: Better error in Nothing case. () <- fsRootUpdateInode rootInodeNum (\i -> inodeBumpSize i minSizeForInode) pbb <- unsafeLiftIORW $ mkInodeBlock bb (inIntToInt inodeIndexThisBlock) readToWrite (cont pbb))) else -- FIX: concurrency: should we block between 'get' and here? getBlockFromInodeRead rootInode inodeBlock (\ bb -> do -- getBlockFromInode has ensured that the root inode has the blocks -- it needs, but it hasn't actually bumped the size of this inode if -- necessary. Plus 1 since count from zero. -- AJG: because this is the read case, we should not need to get new space for the inode. -- let (minSizeForInode::INLong) -- = inIntToINLong $ (inodeBlock + 1) * (intToINInt bytesPerBlock) -- -- FIX: Better error in Nothing case. -- () <- fsRootUpdateInode rootInodeNum -- (\i -> inodeBumpSize i minSizeForInode) pbb <- unsafeLiftIORW $ mkInodeBlock bb (inIntToInt inodeIndexThisBlock) cont pbb) where inodeBlock = (inodeNum `div` inodesPerBlock) :: INInt inodeBaseThisBlock = (inodeBlock * inodesPerBlock) :: INInt inodeIndexThisBlock = inodeNum - inodeBaseThisBlock -- to be used for _writing_ inodes. Be sure to bump the size -- yourself when using this function. getBlockFromInodeWrite :: Inode -> BlockNumber -> (forall s . BufferBlock s -> FSWrite a) -> FSWrite a getBlockFromInodeWrite inode blockNumber ncont = do addr <- getDiskAddrOfBlockWrite inode blockNumber getBlockFromCacheOrDeviceWrite addr ncont -- to be used for _reading_ inodes getBlockFromInodeRead :: Inode -> BlockNumber -> (forall s . BufferBlock s -> FSRead a) -> FSRead a getBlockFromInodeRead inode blockNumber ncont = do FSRoot{device=dev, bbCache=cache} <- unsafeReadGet addr <- getDiskAddrOfBlockRead inode blockNumber getBlockFromCacheOrDevice cache dev addr ncont -- |Like getInode, syncs a single inode. syncInode ::Inode -> FSWrite () syncInode inode@Inode{metaData=InodeMetadata{inode_num=inodeNum}} = do readToWriteCont (unsafeMoveBinHandleForInodeNum inodeNum True) (\ inode_pbb -> -- FIX: we might have "bad" / uninitialized inodes that we've read -- from disk. How do we really know if we should sync them? This -- is an issue because we try to interpret some of the fields, like -- fileMode, into their more concrete types; maybe we just shouldn't -- do that :( when (goodInodeMagic inode) (unsafeLiftIORW $ putPartBufferBlock inode_pbb (assert (goodInodeMagic inode) inode)) ) getInodeWrite :: INInt -> Maybe FileType -> FSWrite Inode getInodeWrite num mType = readToWrite $ getInodeRead num mType -- |Gets an inode based on its inode number, performs some math to -- figure out what block it's in and stuff. FIX: Make it read a whole -- block of inodes in one go and adds them to the cache (via -- readInodeUpdateCache) getInodeRead :: INInt -- inode number to get -> Maybe FileType -- Nothing if Read from disk? -> FSRead Inode getInodeRead inodeNum mType = do -- FIX: concurrency: Check for threading issues w/ cache. FSRoot{inodeCache=cache} <- unsafeReadGet when (inodeNum < 0) (error $ "illegal inode value: " ++ (show inodeNum)) case getFromInodeCache cache inodeNum of Nothing -> case mType of Nothing -> do unsafeMoveBinHandleForInodeNum inodeNum False (\ inode_pbb -> do -- FIX: might want to turn off this optimization if it's not actually faster. {- let wholeBlockOptimization = True newElt' <- if wholeBlockOptimization then do (i, c') <- readInodeUpdateCache cache inodeNum bb -- FIX: concurrency: thread issues here w/ cache. unsafeModifyFSRead (\r -> (r{inodeCache=c'}, ())) return i else .. getPart + unsafeModify below .. -- AJG: turned off this optimization for now -- The moveBinHandle should not be used before reading -- the whole block, because it computes a location/offset. -- (conceptual issue, not a correctness issue) -} newElt' <- unsafeLiftIORW $ getPartBufferBlock inode_pbb -- update the cache: unsafeModifyFSRead (\fsroot@FSRoot{inodeCache=the_cache} -- FIX: Do something w/ snd? -> (fsroot{inodeCache=fst $ addToInodeCache InodeCacheKeep (the_cache, inodeNum) newElt'} , ())) unless ((inode_num $ metaData newElt') == inodeNum && (goodInodeMagic newElt')) (error $ "illegal inode: " ++ (show newElt')) assert ((inode_num $ metaData newElt') == inodeNum && (goodInodeMagic newElt')) (return newElt')) Just t -> return (newInode inodeNum t) Just n -> return n -- |Possibly modifies the inode cache. "Raw" function for reading an -- inode in. Uses rawDevice instead of fsroot. -- TODO: Add the BBC as an argumenthere. readInodeBootStrap :: InodeCache -> RawDevice -> Inode -- ^root inode -> InodeNum -> IO (Inode, InodeCache) readInodeBootStrap cache inDevice rootInode inodeNum = case getFromInodeCache cache inodeNum of Just n -> return (n, cache) Nothing -> do let (blockNum::BlockNumber) = inodeNum `div` inodesPerBlock addrM <- getDiskAddrOfBlockRaw rootInode blockNum inDevice let addr = fromJustErr ("FIX: uninitialized read. blockNum:inodeNum " ++ (show blockNum) ++ ":" ++ (show inodeNum)) addrM getBlockFromDevice inDevice addr (\ bb -> do readInodeUpdateCache cache inodeNum bb) -- |Reads all of the inodes from this binhandle, returns the new inode -- and cache. readInodeUpdateCache :: (FSRW m) => InodeCache -> InodeNum -> BufferBlock s -> m (Inode, InodeCache) readInodeUpdateCache cache inodeNum bb = do let blockNum = inodeNum `div` inodesPerBlock inodeBlock <- getInodesBin bb -- dylan says we'll never have more than 4 billion inodes. FIX: -- Where does that number come from? seems like 16 * 1024 to me. let (firstInodeNum::INInt) = blockNum * inodesPerBlock -- we throw away the 'max inode number' since these inodes may or -- may not be valid. That number has to get set in allocateInode. let (newCache, _) = foldl (addToInodeCache InodeCacheKeep) (cache, firstInodeNum) inodeBlock -- fromJustErr should be OK since we just added it above. let newInode' = fromJustErr "can't find inode which was just added to cache" (getFromInodeCache newCache inodeNum) return (newInode', newCache) -- |We ignore the dirty bit here and write it to this handle no matter -- what; the parent should check the dirty bit! writeInodeMapBin :: BinHandle -> TheInodeMap -> FSWrite () writeInodeMapBin buffer (TheInodeMap freeN _ numInodes) = do resetBinRW buffer writeToBuffer buffer (length freeN) mapM_ (writeToBuffer buffer) freeN writeToBuffer buffer numInodes -- ------------------------------------------------------------ -- * BlockMap -- ------------------------------------------------------------ -- |Reads the free block list from this handle. Size of the output -- queue is = input size. inputFreeBlockList :: (FSRW m) => INInt -- ^Num blocks -> BinHandle -> Queue DiskAddress -> m (Queue DiskAddress) inputFreeBlockList n buffer q | n <= 0 = return q | otherwise = do da <- getRW buffer inputFreeBlockList (n-1) buffer $! (addToQueue q da) -- |Write the block map into the beginning of this buffer. writeBlockMapBin :: BinHandle -> TheBlockMap -> FSWrite () writeBlockMapBin buffer bm = do resetBinRW buffer let listQ = queueToList (freeBlocks bm) writeToBuffer buffer (length listQ) writeToBuffer buffer (bmTotalSize bm) mapM_ (writeToBuffer buffer) listQ -- ------------------------------------------------------------ -- * Directory stuff -- ------------------------------------------------------------ writeDirectoryBin :: BinHandle -> DirectoryMap -> FSWrite () writeDirectoryBin buffer theMap = do resetBinRW buffer let l = Map.toList theMap writeToBuffer buffer (length l) mapM_ (writeToBuffer buffer) l readDirectoryBin :: (FSRW m) => BinHandle -> m DirectoryMap readDirectoryBin buffer = do resetBinRW buffer size <- getRW buffer l <- sequence $ replicate size (do f <- getRW buffer return f) return $ Map.fromList l ------------------------------------------------------------ readInodeMapBin :: (FSRW m) => BinHandle -> m TheInodeMap readInodeMapBin buffer = do resetBinRW buffer numInodes <- getRW buffer inodeNums <- inputInodeMap numInodes buffer maxInodeNum <- getRW buffer return $ TheInodeMap inodeNums False maxInodeNum -- |Reads the block map from the beginning of this buffer. Fix: Clean or dirty! readBlockMapBin :: (FSRW m) => BinHandle -> m TheBlockMap readBlockMapBin buffer = do resetBinRW buffer numFree <- getRW buffer size <- getRW buffer freeBlks <- inputFreeBlockList numFree buffer emptyQueue return (mkBlockMap freeBlks False size) inputInodeMap :: (FSRW m) => INInt -- Num inodes -> BinHandle -> m [INInt] inputInodeMap num buffer | num <= 0 = return [] | otherwise = do inodeNum <- getRW buffer tails <- inputInodeMap (num - 1) buffer return $ inodeNum : tails -- |Traverses these non-zero pointers removing them from block map freeInodeData :: Inode -> FSWrite () freeInodeData inode = do allBlocks <- allBlocksInInode' inode mapM_ freeDA allBlocks where freeDA :: DiskAddress -> FSWrite () freeDA bp = Halfs.FSState.modify (\fsroot'@FSRoot{blockMap=bm} -> fsroot'{blockMap=freeBlock bm bp}) allBlocksInInode :: INInt -- Inode number -> FSWrite [DiskAddress] allBlocksInInode inodeNum = do inode <- getInodeWrite inodeNum Nothing allBlocksInInode' inode -- |Get all the blocks from this inode, including indirect blocks and -- their children. allBlocksInInode' :: Inode -> FSWrite [DiskAddress] allBlocksInInode' inode@Inode{metaData=InodeMetadata{inode_num=inodeNum}} = do let lev = level $ metaData inode -- filter out zeros except 0th block in root. -- FIX: what happens when root becomes level 2? ptrs = [block | (index, block) <- assocs $ blockPtrs inode , (block /= 0) || (block == 0 && inodeNum == 0 && index == 0) ] mapM (allBlocksAt inodeNum lev) ptrs >>= return . concat where allBlocksAt :: InodeNum -> INInt -> DiskAddress -> FSWrite [DiskAddress] allBlocksAt _ 1 bp = return [bp] allBlocksAt inodeNum1 n indirBP = do allBlocks <- getBlockFromCacheOrDeviceWrite indirBP (\ bb -> unsafeLiftIORW $ diskAddressListFromBufferBlock bb) let allBlockElems = [block | (index, block) <- zip [(0::Int)..] allBlocks , (block /= 0) || (block == 0 && inodeNum1 == 0 && index == 0) ] rest <- mapM (allBlocksAt inodeNum1 (n - 1)) allBlockElems return $ indirBP:(concat rest) ------------------------------------------------------------ -- |Free the given inode from this fsroot. free the block pointers, -- remove from inode map. TODO: XX mvar to sync. fsRootFreeInode :: Inode -> FSWrite () fsRootFreeInode inode = do let num = inode_num $ metaData inode Halfs.FSState.modify (\fsroot@FSRoot{inodeMap=theMap} -> fsroot{inodeMap=freeInode theMap num}) -- remove it from the directory cache, in case it's a directory: Halfs.FSState.modify (fsRootRmFromDirectoryCache num) freeInodeData inode -- ------------------------------------------------------------ -- * Testing -- ------------------------------------------------------------ -- Not used, inclued as example binaryCopyFile :: FilePath -> FilePath -> IO () binaryCopyFile f1 f2 = do size <- (do h <- openFile f1 ReadMode s <- hFileSize h hClose h return s) h <- openFd f1 ReadOnly Nothing defaultFileFlags bh <- openBinIO h h' <- openFd f2 WriteOnly (Just (foldl1 unionFileModes [ ownerReadMode , ownerWriteMode , groupReadMode])) defaultFileFlags bh' <- openBinIO h' copyBytes bh bh' (fromIntegral'' size) closeFd h closeFd h' -- How to pretend we're using C... only works with a file size < -- buffer size -- TODO: should this be here? binaryCopyFile' :: FilePath -> FilePath -> IO () binaryCopyFile' from to = do numBytes <- (do h <- openFile from ReadMode s <- hFileSize h hClose h return s) buffer <- openBinMem (fromIntegral numBytes) hFrom <- openFd from ReadWrite Nothing defaultFileFlags devBufferReadHost hFrom 0 buffer (fromIntegral'' numBytes) resetBin buffer -- above call resets the buffer, so this is safe: hTo <- openFd to ReadWrite (Just (foldl1 unionFileModes [ ownerReadMode , ownerWriteMode , groupReadMode])) defaultFileFlags devBufferWriteHost hTo 0 buffer (fromIntegral'' numBytes) closeFd hFrom closeFd hTo -- ------------------------------------------------------------ -- * inode testing -- ------------------------------------------------------------ inodeHunitTests :: [Test] inodeHunitTests = [ TestLabel "root inode tests" $ TestCase $ do -- newFS "halfs-client1" 2000 Nothing dev <- makeRawDevice Nothing "halfs-client1" buffer <- getMemoryBlock bb <- mkBufferBlock buffer dev 0 devBufferRead dev 0 buffer (rootInode':_) <- getInodesBin bb assertEqual "root inode magic number incorrect" (magic1 $ metaData rootInode') rootInodeMagicNum -- Only level 1 is implemented assertEqual "root inode level unimplemented" (level $ metaData rootInode') 1 finalizeDevice dev -- It's no longer the case that the disk address of the block map is -- 0, since when it gets re- written, it may get a new address. -- let diskAddr = getPointerAt rootInode' 0 -- assertEqual "0th disk addr of root inode 0" diskAddr 0 -- let diskAddr' = getPointerAt rootInode' 1 -- assertEqual "0th disk addr of root inode 1" diskAddr' 1 ] -- tests: unitTests :: UnitTests unitTests = hunitToUnitTest hunitTests hunitTests :: [Test] hunitTests = [TestLabel "binary copy file" $ TestCase $ do doesFileExist "tests/to" >>= \e -> when e (removeFile "tests/to") binaryCopyFile "tests/from" "tests/to" assertCmd "diff tests/from tests/to" "binary file copy failed" doesFileExist "tests/to" >>= \e -> when e (removeFile "tests/to") binaryCopyFile' "tests/from" "tests/to" assertCmd "diff tests/from tests/to" "binary file copy' failed" doesFileExist "tests/bigTo" >>= \e -> when e (removeFile "tests/bigTo") binaryCopyFile "tests/multiBlockFile" "tests/bigTo" assertCmd "diff tests/multiBlockFile tests/bigTo" "binary file copy failed" doesFileExist "tests/bigTo" >>= \e -> when e (removeFile "tests/bigTo") binaryCopyFile' "tests/multiBlockFile" "tests/bigTo" assertCmd "diff tests/multiBlockFile tests/bigTo" "binary file copy failed" ] ++ inodeHunitTests