plist-buddy-0.1.0.0: tests/Main.hs
{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveGeneric, OverloadedStrings, ScopedTypeVariables #-}
import Database.PlistBuddy
import Data.Monoid
import Control.Monad (when)
import Data.Text(Text,pack,unpack)
import qualified Data.Text as T
import Data.Text.IO as TIO
import Data.Time
import qualified Data.ByteString as BS
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck hiding (replay)
import Test.QuickCheck.Exception
import Control.Exception (evaluate, bracket, catch)
import qualified System.IO as IO
import System.Timeout
import Control.Concurrent (threadDelay)
import System.Mem
import System.Directory (removeFile, doesFileExist)
import Data.List (sortBy, sort, nub, transpose,lookup)
import GHC.Generics
import Control.Monad.Reader
import System.Environment
import Data.Char (isDigit)
clearAudit :: IO ()
clearAudit = do
TIO.writeFile "test.audit" ""
clearDB :: IO ()
clearDB = do
TIO.writeFile "test.plist" "{}"
TIO.writeFile "test.audit" ""
rmDB :: IO ()
rmDB = do
doesFileExist "test.plist" >>= \ b -> when b (removeFile "test.plist")
openConnection :: Bool -> IO Plist
openConnection audit = do
d <- openPlist "test.plist"
send d $ clear (Dict [])
if audit
then auditOn "test.audit" d
else return d
closeConnection :: Plist -> IO ()
closeConnection d = send d $ exit
-- only for tests that write then read
withPlistConnection :: Bool -> (Plist -> IO ()) -> IO ()
withPlistConnection audit
= guardPlistBuddyException
. bracket (openConnection audit)
closeConnection
guardPlistBuddyException :: IO a -> IO a
guardPlistBuddyException m = m `catch` \ (PlistBuddyException msg) -> do
IO.putStrLn $ "\ndiscarded: " ++ show msg
discard
main :: IO ()
main = hspec $ do
beforeAll clearDB $ do
describe "initial plist" $ modifyMaxSuccess (\ x -> 100) $ do
it "check initial dict is an dictionary" $ withPlistConnection False $ \ d -> do
r0 <- send d $ get []
r0 `shouldBe` Dict []
it "check reset to array" $ withPlistConnection False $ \ d -> do
_ <- send d $ clear (Array [])
r0 <- send d $ get []
r0 `shouldBe` Array []
it "check reset to back to an dict" $ withPlistConnection False $ \ d -> do
_ <- send d $ clear (Array [])
_ <- send d $ clear (Dict [])
r0 <- send d $ get []
r0 `shouldBe` Dict []
it "check adding a value at top level" $
property $ \ (Label lbl) (OneValue v) audit -> withPlistConnection audit $ \ d -> do
debug $ ("add val top",lbl,v)
_ <- send d $ add [lbl] v
r0 <- send d $ get []
r0 `shouldBe` Dict [(lbl,v)]
it "check adding then setting a value at top level" $
property $ \ (Label lbl) (PrimValue v1) audit ->
forAll (arbitrarySameType v1) $ \ v2 ->
withPlistConnection audit $ \ d -> do
debug $ ("add then set top",lbl,v1,v2)
_ <- send d $ add [lbl] v1
_ <- send d $ set [lbl] v2
r0 <- send d $ get []
r0 `shouldBe` Dict [(lbl,v2)]
it "populate a DB" $
property $ \ (DictValue v) audit -> withPlistConnection audit $ \ d -> do
debug $ ("populate",v)
r0 <- send d $ do
populateDict v
get []
r0 `shouldBe` v
it "test deeper get" $
property $ \ (DictValue v) audit ->
forAll (arbitraryReadPath 0.8 v) $ \ (Path ps,v') -> do
withPlistConnection audit $ \ d -> do
send d $ populateDict v
r0 <- send d $ get ps
r0 `shouldBe` v'
it "test deepest get" $
property $ \ (DictValue v) audit ->
forAll (arbitraryReadPath 1.0 v) $ \ (Path ps,v') -> do
withPlistConnection audit $ \ d -> do
send d $ populateDict v
r0 <- send d $ get ps
r0 `shouldBe` v'
it "test deepest set then get" $
property $ \ (DictValue v) audit ->
forAll (arbitraryReadPath 1.0 v) $ \ (Path ps,v1) ->
not (null ps) && (case v1 of { Dict {} -> False; Array {} -> False ; _-> True}) ==>
forAll (arbitrarySameType v1) $ \ v2 ->
withPlistConnection audit $ \ d -> do
debug (v1,v2,ps)
send d $ populateDict v
send d $ set ps v2
r0 <- send d $ get ps
r0 `shouldBe` v2
it "test delete" $
property $ \ (DictValue v) audit ->
forAll (arbitraryReadPath 1.0 v) $ \ (Path ps,v1) ->
not (null ps) ==>
withPlistConnection audit $ \ d -> do
-- print (v1,ps)
(r1,parent) <- send d $ do
populateDict v
r1 <- get ps
parent <- get (init ps)
delete ps
return (r1,parent)
case parent of
Dict {} -> do
r2 <- send d $ ((Just <$> get ps) `catchPlistError` \ _ -> return Nothing)
(r1,r2) `shouldBe` (v1,Nothing)
Array xs -> do
xs' <- send d $ do
Array xs' <- get (init ps)
return xs'
(r1,length xs) `shouldBe` (v1,length xs' + 1)
it "check for bad path error handling" $
property $ \ (DictValue v) (Path p) audit -> p `notIn` v ==> withPlistConnection audit $ \ d -> do
debug $ ("bad path",v,p)
r <- (send d $ (do
populateDict v
get p
return False) `catchPlistError` \ e -> do
return True)
r `shouldBe` True
it "test get/set/delete sequences" $
property $ \ audit ->
forAll (modSized 8 return) $ \ n ->
forAll (Blind <$> arbitraryUpdates n (Dict [])) $ \ (Blind updates) ->
withPlistConnection audit $ \ d -> do
send d $ clear (Dict [])
let xs = []
xs <- sequence [ send d $ do
u -- the update
r <- get []
return (v,r)
| (u,v) <- updates
]
map fst xs `shouldBe` map snd xs
beforeAll clearDB $ do
describe "plist modification" $ do
it "test save of DB" $
property $ \ (DictValue v) -> guardPlistBuddyException $ do
d <- openPlist "test.plist"
send d $ clear (Dict []) -- clear dict
send d $ do
populateDict v
save
exit
d <- openPlist "test.plist"
r0 <- send d $ get []
send d $ exit
r0 `shouldBe` v
it "test save of DB, with changes in between" $
property $ \ (DictValue v) (DictValue v') -> guardPlistBuddyException $ do
d <- openPlist "test.plist"
send d $ clear (Dict []) -- clear dict
send d $ do
populateDict v
save
clear $ Dict []
populateDict v'
exit
d <- openPlist "test.plist"
r0 <- send d $ get []
send d $ exit
r0 `shouldBe` v
it "test save and revert of DB" $
property $ \ (DictValue v) (DictValue v') -> guardPlistBuddyException $ do
d <- openPlist "test.plist"
send d $ clear (Dict []) -- clear dict
r0 <- send d $ do
populateDict v
save
clear $ Dict []
populateDict v'
revert
r <- get []
exit
return r
r0 `shouldBe` v
it "test double exit" $
property $ \ (DictValue v) (DictValue v') -> guardPlistBuddyException $ do
d <- openPlist "test.plist"
send d $ clear (Dict []) -- clear dict
send d $ do
populateDict v
save
exit
d <- openPlist "test.plist"
r0 <- send d $ get []
send d $ exit
res <- (send d $ do { clear $ Dict [] ; populateDict v' ; return True})
`catch` \ (e :: PlistBuddyException) -> do { return False }
(r0,res) `shouldBe` (v,False)
beforeAll clearDB $ do
describe "plist audit test" $ do
it "test get/set/delete sequences, with basic audit trail usage" $
forAll (modSized 8 return) $ \ n1 ->
forAll (modSized 8 return) $ \ n2 ->
forAll (modSized 8 return) $ \ n3 ->
let s = Dict [] in
forAll (Blind <$> arbitraryUpdates n1 (s) ) $ \ (Blind updates1) ->
forAll (Blind <$> arbitraryUpdates n2 (lastOf s $ updates1)) $ \ (Blind updates2) ->
forAll (Blind <$> arbitraryUpdates n3 (lastOf s $ updates1 ++ updates2)) $ \ (Blind updates3) ->
do d <- openPlist "test.plist"
-- Populate dictionary randomly
send d $ clear (Dict []) -- clear dict
sequence_ [ send d u | (u,_) <- updates1 ]
send d $ save
send d $ exit
-- Reload, with (new) audit
clearAudit
d <- openPlist "test.plist"
d <- auditOn "test.audit" d
sequence_ [ send d u | (u,_) <- updates2 ]
send d $ save
-- And do more stuff, without saving, but with audit
sequence_ [ send d u | (u,_) <- updates3 ]
auditOff d -- turn off audit, before turning exiting the plist
send d $ exit
-- now need to restore
h <- hashcode "test.plist"
-- print ("hashcode",h)
auditTrails <- recover "test.audit"
-- print ("audit trails",auditTrails)
let trail = findTrail h auditTrails
d <- openPlist "test.plist"
send d $ sequence_ $ map replay trail
let target = lastOf s $ updates1 ++ updates2 ++ updates3
r0 <- send d $ get []
send d $ exit
r0 `shouldBe` target
beforeAll rmDB $ do
describe "create plists" $ do
it "open non-existent plist" $ do
property $ do
d <- openPlist "test.plist"
r0 <- send d $ get []
send d $ exit
r0 `shouldBe` Dict []
beforeAll clearDB $ do
describe "auto-save plists" $ modifyMaxSuccess (\ x -> 3) $ do
it "test bg save of DB, with changes in between" $
property $ noShrinking $ \ (DictValue v) (DictValue v') -> guardPlistBuddyException $ do
bg <- backgroundPlist 1 $ openPlist "test.plist"
bgSend bg $ clear (Dict []) -- clear dict
bgSend bg $ do
populateDict v
save
clear $ Dict []
populateDict v'
threadDelay $ 2 * 1000 * 1000
-- the save should have automatically happened
d <- openPlist "test.plist"
r0 <- send d $ get []
send d $ exit
r0 `shouldBe` v'
it "auto-restart" $
property $ noShrinking $ \ (DictValue v) (DictValue v') -> guardPlistBuddyException $ do
bg <- backgroundPlist 1 $ openPlist "test.plist"
bgSend bg $ clear (Dict []) -- clear dict
bgSend bg $ do
populateDict v
save
clear $ Dict []
populateDict v'
threadDelay $ 2 * 1000 * 1000
-- the save should have automatically happened
r0 <- bgSend bg $ get []
bgSend bg $ exit
r0 `shouldBe` v'
lastOf :: Value -> [(a,Value)] -> Value
lastOf v xs = last (v : map snd xs)
populateDict :: Value -> PlistBuddy ()
populateDict (Dict xs) =
do sequence_ [ populate (Path $ [i]) v | (i,v) <- xs ]
populateDict _ = error "expecting a Dict"
populate :: Path -> Value -> PlistBuddy ()
populate (Path ps) val =
case val of
Dict xs -> do
add ps (Dict [])
sequence_ [ populate (Path $ ps ++ [i]) v | (i,v) <- xs ]
Array vs -> do
add ps (Array [])
sequence_ [ populate (Path $ ps ++ [pack (show i)]) v | (i,v) <- [0..] `zip` vs ]
_ -> do
add ps val
[] `notIn` _ = False
(p:ps) `notIn` (Dict xs) = case lookup p xs of
Nothing -> True
Just v -> ps `notIn` v
(p:ps) `notIn` (Array vs)
| T.all isDigit p && not (T.null p)
= case drop (read (unpack p)) vs of
[] -> False
(v:_) -> ps `notIn` v
_ `notIn` _ = True
arbitraryUpdates :: Int -> Value -> Gen [(PlistBuddy (),Value)]
arbitraryUpdates 0 v = return []
arbitraryUpdates n v = do
(u,v') <- arbitraryUpdate v
rest <- arbitraryUpdates (n-1) v'
return $ (u,v') : rest
arbitraryUpdate :: Value -> Gen (PlistBuddy (),Value)
arbitraryUpdate v = frequency
[ (200, arbitraryAdd v)
, (200, arbitrarySet v)
, (100, arbitraryDelete v)
, (1, return (clear $ Dict [],Dict [])) -- Infrequently!
]
-- for now, does not go deep
-- TODO: make this go deeper (randomly)
arbitraryAdd :: Value -> Gen (PlistBuddy (),Value)
arbitraryAdd = addMe []
where
addMe p (Dict xs) = do
Label lbl <- arbitrary
OneValue val <- arbitrary
if (lbl `elem` map fst xs)
then addMe p (Dict xs) -- try again
else return (add (p ++ [lbl]) val,Dict $ xs ++ [(lbl,val)])
addMe p (Array vs) = do
let lbl = T.pack (show $ length vs) -- for now, append at end
OneValue val <- arbitrary
return (add (p ++ [lbl]) val,Array $ vs ++ [val])
addMe p other = return (return (), other)
arbitrarySet :: Value -> Gen (PlistBuddy (),Value)
arbitrarySet = setMe []
where
setMe p (Dict []) = return (return (), Dict [])
setMe p (Dict xs) = do
Label lbl <- Label <$> elements (map fst xs)
case lookup lbl xs of
Just v -> do
if valueType v `elem` ["array","dict"]
then return (return (), Dict xs)
else do
v' <- arbitrarySameType v
return ( set (p ++ [lbl]) v'
, Dict [ if l == lbl then (l,v') else (l,x) | (l,x) <- xs ]
)
Nothing -> error $ "should never happen" ++ show (lbl,map fst xs,xs)
-- for now
-- setMe p (Array vs) =
setMe p other = return (return (), other)
arbitraryDelete :: Value -> Gen (PlistBuddy (),Value)
arbitraryDelete = delMe []
where
delMe p (Dict []) = return (return (), Dict [])
delMe p (Dict xs) = do
Label lbl <- Label <$> elements (map fst xs)
return ( delete (p ++ [lbl])
, Dict [ (l,x) | (l,x) <- xs, l /= lbl ]
)
delMe p other = return (return (), other)
{-
addMe p (Array vs) = do
let lbl = T.pack (show $ length vs) -- for now, append at end
OneValue val <- arbitrary
vs <- sequence $
[ return [ addMe (p ++ [T.pack $ show $ i]) v | (v,i) <- xs `zip` [(0::Int)..] ]
return $ concat xs
addMe p (Dict xs) = do
Label lbl <- arbitrary
OneValue val <- arbitrary
xs <- sequence $
[ return [ (add (p ++ [lbl]) val, Dict (xs ++ [(lbl,val)]))]
| not (lbl `elem` map fst xs) ] ++
[ addMe (p ++ [l]) v | (l,v) <- xs ]
return $ concat xs
addMe p other = return []
-}
{-
arbitraryAdd :: Value -> Gen [(PlistBuddy (),Value)]
addInValue = addMe []
where
addMe p (Array vs) = do
let lbl = T.pack (show $ length vs) -- for now, append at end
OneValue val <- arbitrary
vs <- sequence $
[ return [ addMe (p ++ [T.pack $ show $ i]) v | (v,i) <- xs `zip` [(0::Int)..] ]
return $ concat xs
addMe p (Dict xs) = do
Label lbl <- arbitrary
OneValue val <- arbitrary
xs <- sequence $
[ return [ (add (p ++ [lbl]) val, Dict (xs ++ [(lbl,val)]))]
| not (lbl `elem` map fst xs) ] ++
[ addMe (p ++ [l]) v | (l,v) <- xs ]
return $ concat xs
addMe p other = return []
-}
{-
-- Set of commands that change the value somehow
deleteInValue :: Value -> Gen [PlistBuddy ()]
deleteInValue = del []
where
del (Dict xs)
del (Array vs) = [ n <- [0..(length n - 1)]
del p other = delete p
-}
{-
-- TO ADD
-- try get type error
r <- send d $ ((set ["I1"] (String "foo")>>return "no failed") `catchPlistError` \ msg -> return msg)
check "check for type error" r $ "set failed: \"Unrecognized Integer Format\""
_ <- send d $ exit
d <- openPlist "test.plist"
now <- getCurrentTime
send d $ add ["S5"] (Date now)
Date r0 <- send d $ get ["S5"]
check "check for date storage" (abs (diffUTCTime now r0) < 1) $ True
_ <- send d $ exit
d <- debugOn <$> openPlist "test.plist"
-}
check :: (Eq a, Show a) => Text -> a -> a -> IO ()
check msg t1 t2 = if t1 /= t2 then fail ("check failed: " ++ show (msg,t1,t2)) else TIO.putStrLn msg
arbitraryValue :: Int -> Gen Value
arbitraryValue n = frequency
[(7,(\ (PrimValue v) -> v) <$> arbitrary),
(2,mysized $ \ n' -> mkDict <$> sequence [ arbitraryDict (n-1) | _ <- [1..n]]),
(1,mysized $ \ n' -> Array <$> sequence [ arbitraryValue (n-1) | _ <- [1..n]])
]
where mysized k | n == 0 = k 0
| otherwise = modSized 8 k
-- removes dup labels
mkDict :: [(Text,Value)] -> Value
mkDict xs = Dict [ (lbl,v) | (lbl,v) <- nub (map fst xs) `zip` map snd xs ]
arbitraryDict :: Int -> Gen (Text,Value)
arbitraryDict n = do
Label nm <- arbitrary
v <- arbitraryValue n
return (nm,v)
arbitraryDate :: Gen UTCTime
arbitraryDate =
UTCTime <$> ((\ d -> addDays d (fromGregorian 1950 1 1)) -- 2003 10 25
<$> choose (0, 85 * 365) -- dates after 2038 have issues (wordsize?)
-- +2
)
<*> (fromInteger <$> choose (0,60 * 60 * 24 - 1))
arbitraryText :: Gen Text
arbitraryText = modSized 10 $ \ n -> pack <$> (vectorOf n $ elements ('\n':[' '..'~']))
-- 28
arbitraryData :: Gen BS.ByteString
arbitraryData = modSized 32 $ \ n -> BS.pack <$> (vectorOf n $ elements ([0..255]))
instance Eq Value where
(==) = eqValue
eqValue :: Value -> Value -> Bool
eqValue (String s1) (String s2) = s1 == s2
eqValue (Array a1) (Array a2) = a1 == a2
eqValue (Dict d1) (Dict d2) = sortBy f d1 == sortBy f d2 -- order should not matter
where f (a,_) (b,_) = a `compare` b
eqValue (Bool a1) (Bool a2) = a1 == a2
eqValue (Real a1) (Real a2) = abs (a1 - a2) <= abs ((a1 + a2) / 1e6)
eqValue (Integer a1) (Integer a2) = a1 == a2
eqValue (Date d1) (Date d2) = d1 == d2
eqValue (Data d1) (Data d2) = d1 == d2
eqValue _ _ = False
---------------------------------------
---------------------------------------
valueShrink :: Value -> [Value]
valueShrink (Dict []) = []
valueShrink (Dict [(lbl,x)]) = [x]
valueShrink (Dict xs) =
[ Dict (take i xs ++ drop (i + 1) xs)
| i <- [0..length xs - 1]
] ++
[ Dict (map fst xs `zip` vs)
| vs <- transpose $ fmap valueShrink (map snd xs)
]
valueShrink (Array []) = []
valueShrink (Array [x]) = [x]
valueShrink (Array vs) =
[ Array (take i vs ++ drop (i + 1) vs)
| i <- [0..length vs - 1]
] ++
[ Array vs
| vs <- transpose $ map valueShrink vs
]
--valueShrink (Date d) = [Date $ addUTCTime (60*60) d,Date $ addUTCTime (60) d,Date $ addUTCTime (1) d]
valueShrink other = []
---------------------------------------
arbitrarySameType :: Value -> Gen Value
arbitrarySameType v0 = do
(OneValue v) <- arbitrary
if valueType v0 == valueType v
then return v
else arbitrarySameType v0
newtype PrimValue = PrimValue Value -- any primitive
deriving (Show,Generic)
instance Arbitrary PrimValue where
arbitrary = PrimValue <$> oneof
[ Integer <$> arbitrary
, String <$> arbitraryText
, Bool <$> arbitrary
, Real <$> arbitrary
, Date <$> arbitraryDate -- for now
, Data <$> arbitraryData
]
shrink (PrimValue v) = [ PrimValue v' | v' <- valueShrink v, valueType v == valueType v']
newtype OneValue = OneValue Value -- primitive + empty dict or empty array
deriving (Show,Generic)
instance Arbitrary OneValue where
arbitrary = OneValue <$> arbitraryValue 0
shrink (OneValue v) = [ OneValue v' | v' <- valueShrink v, valueType v == valueType v']
newtype DeepValue = DeepValue Value -- any value, to any depth
deriving (Show,Generic)
instance Arbitrary DeepValue where
arbitrary = DeepValue <$> modSized 8 arbitraryValue
shrink (DeepValue v) = [ DeepValue v' | v' <- valueShrink v, valueType v == valueType v']
newtype DictValue = DictValue Value -- any value, to any depth
deriving (Show,Generic)
instance Arbitrary DictValue where
arbitrary = (DictValue . mkDict) <$> (modSized 8 $ \ n -> vectorOf n (modSized 8 arbitraryDict))
shrink (DictValue v) = [ DictValue v' | v' <- valueShrink v, valueType v == valueType v']
newtype Label = Label Text
deriving (Show,Generic)
instance Arbitrary Label where
arbitrary = modSized 32 $ \ n -> (Label . pack) <$> sequence
[ elements (['0'..'9'] ++ ['a'..'z'] ++ ['A'..'Z'])
| _ <- [0..n]
]
newtype Path = Path [Text] -- non-empty
deriving (Show,Generic)
instance Arbitrary Path where
arbitrary = Path <$> (modSized 8 $ \ n -> vectorOf (n+1) ((\ (Label t) -> t) <$> arbitrary))
modSized :: Int -> (Int -> Gen a) -> Gen a
modSized n k = choose (0,n-1) >>= k
newtype ReadPath = ReadPath [Text] -- can be empty, must be valid
deriving (Show,Generic)
instance Arbitrary ReadPath where
arbitrary = ReadPath <$> (modSized 8 $ \ n -> vectorOf n ((\ (Label t) -> t) <$> arbitrary))
arbitraryReadPath :: Double -> Value -> Gen (Path,Value)
arbitraryReadPath n v@(Dict xs) = do
stop <- choose (0,1)
if (stop > n) || null xs
then return (Path [],v)
else do nm <- elements (map fst xs)
case lookup nm xs of
Just v' -> do
(Path ps,vr) <- arbitraryReadPath n v'
return (Path (nm:ps),vr)
Nothing -> error "arbitraryReadPath internal error"
arbitraryReadPath n v@(Array vs) = do
stop <- choose (0,1)
if (stop > n) || null vs
then return (Path [],v)
else do i <- elements [0..(length vs - 1)]
(Path ps,vr) <- arbitraryReadPath n (vs !! i)
return (Path (pack (show i):ps),vr)
arbitraryReadPath _ v = return (Path [],v)
compareValue :: Path -> Value -> Value -> String
compareValue (Path ps) v1 v2 | valueType v1 /= valueType v2 = "different types : " ++ show (ps,v1,v2)
compareValue (Path ps) (Dict ds1) (Dict ds2)
| nm1 /= nm2 = "different names of fields in dict : " ++ show (ps,nm1,nm2)
| otherwise = concat [ case (lookup nm ds1,lookup nm ds2) of
(Just v1,Just v2) -> compareValue (Path (ps ++ [nm])) v1 v2
_ -> "internal error in dict compare " ++ show ps
| nm <- nm1 ]
where
nm1 = sort (nub (map fst ds1))
nm2 = sort (nub (map fst ds2))
compareValue (Path ps) (Array ds1) (Array ds2)
| length ds1 /= length ds2 = "different lengths of array : " ++ show (ps,length ds1,length ds2)
| otherwise = concat [ compareValue (Path (ps ++ [pack (show i)])) d1 d2 | (i,d1,d2) <- zip3 [0..] ds1 ds2 ]
compareValue (Path ps) v1 v2
| v1 /= v2 = "different values : " ++ show (ps,v1,v2)
| otherwise = ""
debug :: Show a => a -> IO ()
debug = const $ return ()
--debug = print
-- TODO: make all labels length 1, and test for testing