module Test.Ganeti.JQueue (testJQueue) where
import Control.Applicative
import Control.Monad (when)
import Data.Char (isAscii)
import Data.List (nub, sort)
import System.Directory
import System.FilePath
import System.IO.Temp
import System.Posix.Files
import Test.HUnit
import Test.QuickCheck as QuickCheck
import Test.QuickCheck.Monadic
import Text.JSON
import Test.Ganeti.TestCommon
import Test.Ganeti.TestHelper
import Test.Ganeti.Types ()
import Test.Ganeti.OpCodes
import Ganeti.BasicTypes
import qualified Ganeti.Constants as C
import Ganeti.JQueue
import Ganeti.OpCodes
import Ganeti.Path
import Ganeti.Types as Types
justNoTs :: Maybe Timestamp
justNoTs = Just noTimestamp
genQueuedOpCode :: Gen QueuedOpCode
genQueuedOpCode =
QueuedOpCode <$> pure (ValidOpCode $ wrapOpCode OpClusterQuery) <*>
arbitrary <*> pure JSNull <*> pure [] <*>
choose (C.opPrioLowest, C.opPrioHighest) <*>
pure justNoTs <*> pure justNoTs <*> pure justNoTs
emptyJob :: (Monad m) => m QueuedJob
emptyJob = do
jid0 <- makeJobId 0
return $ QueuedJob jid0 [] justNoTs justNoTs justNoTs
genJobId :: Gen JobId
genJobId = do
p <- arbitrary::Gen (Types.NonNegative Int)
makeJobId $ fromNonNegative p
case_JobPriorityDef :: Assertion
case_JobPriorityDef = do
ej <- emptyJob
assertEqual "for default priority" C.opPrioDefault $ calcJobPriority ej
prop_JobPriority :: Property
prop_JobPriority =
forAll (listOf1 (genQueuedOpCode `suchThat`
(not . opStatusFinalized . qoStatus))) $ \ops -> do
jid0 <- makeJobId 0
let job = QueuedJob jid0 ops justNoTs justNoTs justNoTs
calcJobPriority job ==? minimum (map qoPriority ops)
case_JobStatusDef :: Assertion
case_JobStatusDef = do
ej <- emptyJob
assertEqual "for job status" JOB_STATUS_SUCCESS $ calcJobStatus ej
prop_JobStatus :: Property
prop_JobStatus =
forAll genJobId $ \jid ->
forAll genQueuedOpCode $ \op ->
let job1 = QueuedJob jid [op] justNoTs justNoTs justNoTs
st1 = calcJobStatus job1
op_succ = op { qoStatus = OP_STATUS_SUCCESS }
op_err = op { qoStatus = OP_STATUS_ERROR }
op_cnl = op { qoStatus = OP_STATUS_CANCELING }
op_cnd = op { qoStatus = OP_STATUS_CANCELED }
st_pre_op pop = calcJobStatus (job1 { qjOps = pop:qjOps job1 })
st_post_op pop = calcJobStatus (job1 { qjOps = qjOps job1 ++ [pop] })
in conjoin
[ printTestCase "pre-success doesn't change status"
(st_pre_op op_succ ==? st1)
, printTestCase "post-success doesn't change status"
(st_post_op op_succ ==? st1)
, printTestCase "pre-error is error"
(st_pre_op op_err ==? JOB_STATUS_ERROR)
, printTestCase "pre-canceling is canceling"
(st_pre_op op_cnl ==? JOB_STATUS_CANCELING)
, printTestCase "pre-canceled is canceled"
(st_pre_op op_cnd ==? JOB_STATUS_CANCELED)
]
case_JobStatusPri_py_equiv :: Assertion
case_JobStatusPri_py_equiv = do
let num_jobs = 2000::Int
jobs <- genSample (vectorOf num_jobs $ do
num_ops <- choose (1, 5)
ops <- vectorOf num_ops genQueuedOpCode
jid <- genJobId
return $ QueuedJob jid ops justNoTs justNoTs justNoTs)
let serialized = encode jobs
mapM_ (\job -> when (any (not . isAscii) (encode job)) .
assertFailure $ "Job has non-ASCII fields: " ++ show job
) jobs
py_stdout <-
runPython "from ganeti import jqueue\n\
\from ganeti import serializer\n\
\import sys\n\
\job_data = serializer.Load(sys.stdin.read())\n\
\decoded = [jqueue._QueuedJob.Restore(None, o, False, False)\n\
\ for o in job_data]\n\
\encoded = [(job.CalcStatus(), job.CalcPriority())\n\
\ for job in decoded]\n\
\print serializer.Dump(encoded)" serialized
>>= checkPythonResult
let deserialised = decode py_stdout::Text.JSON.Result [(String, Int)]
decoded <- case deserialised of
Text.JSON.Ok jobs' -> return jobs'
Error msg ->
assertFailure ("Unable to decode jobs: " ++ msg)
>> fail "Unable to decode jobs"
assertEqual "Mismatch in number of returned jobs"
(length decoded) (length jobs)
mapM_ (\(py_sp, job) ->
let hs_sp = (jobStatusToRaw $ calcJobStatus job,
calcJobPriority job)
in assertEqual ("Different result after encoding/decoding for " ++
show job) py_sp hs_sp
) $ zip decoded jobs
prop_ListJobIDs :: Property
prop_ListJobIDs = monadicIO $ do
let extractJobIDs jIDs = do
either_jobs <- jIDs
case either_jobs of
Right j -> return j
Left e -> fail $ show e
isLeft e =
case e of
Left _ -> True
_ -> False
jobs <- pick $ resize 10 (listOf1 genJobId `suchThat` (\l -> l == nub l))
(e, f, g) <-
run . withSystemTempDirectory "jqueue-test." $ \tempdir -> do
empty_dir <- extractJobIDs $ getJobIDs [tempdir]
mapM_ (\jid -> writeFile (tempdir </> jobFileName jid) "") jobs
full_dir <- extractJobIDs $ getJobIDs [tempdir]
invalid_dir <- getJobIDs [tempdir </> "no-such-dir"]
return (empty_dir, sortJobIDs full_dir, invalid_dir)
stop $ conjoin [ printTestCase "empty directory" $ e ==? []
, printTestCase "directory with valid names" $
f ==? sortJobIDs jobs
, printTestCase "invalid directory" $ isLeft g
]
prop_LoadJobs :: Property
prop_LoadJobs = monadicIO $ do
ops <- pick $ resize 5 (listOf1 genQueuedOpCode)
jid <- pick genJobId
let job = QueuedJob jid ops justNoTs justNoTs justNoTs
job_s = encode job
(missing, current, archived, missing_current, broken) <-
run . withSystemTempDirectory "jqueue-test." $ \tempdir -> do
let load a = loadJobFromDisk tempdir a jid
live_path = liveJobFile tempdir jid
arch_path = archivedJobFile tempdir jid
createDirectory $ tempdir </> jobQueueArchiveSubDir
createDirectory $ dropFileName arch_path
missing <- load True
writeFile live_path job_s
current <- load False
removeFile live_path
writeFile arch_path job_s
archived <- load True
missing_current <- load False
removeFile arch_path
writeFile live_path "invalid job"
broken <- load True
return (missing, current, archived, missing_current, broken)
stop $ conjoin [ missing ==? noSuchJob
, current ==? Ganeti.BasicTypes.Ok (job, False)
, archived ==? Ganeti.BasicTypes.Ok (job, True)
, missing_current ==? noSuchJob
, printTestCase "broken job" (isBad broken)
]
prop_DetermineDirs :: Property
prop_DetermineDirs = monadicIO $ do
count <- pick $ choose (2, 10)
nums <- pick $ genUniquesList count
(arbitrary::Gen (QuickCheck.Positive Int))
let (valid, invalid) = splitAt (count `div` 2) $
map (\(QuickCheck.Positive i) -> show i) nums
(tempdir, non_arch, with_arch, invalid_root) <-
run . withSystemTempDirectory "jqueue-test." $ \tempdir -> do
let arch_dir = tempdir </> jobQueueArchiveSubDir
createDirectory arch_dir
mapM_ (createDirectory . (arch_dir </>)) valid
mapM_ (\p -> writeFile (arch_dir </> p) "") invalid
mapM_ (\p -> createSymbolicLink "/dev/null/no/such/file"
(arch_dir </> p <.> "missing")) invalid
non_arch <- determineJobDirectories tempdir False
with_arch <- determineJobDirectories tempdir True
invalid_root <- determineJobDirectories (tempdir </> "no-such-subdir") True
return (tempdir, non_arch, with_arch, invalid_root)
let arch_dir = tempdir </> jobQueueArchiveSubDir
stop $ conjoin [ non_arch ==? [tempdir]
, sort with_arch ==? sort (tempdir:map (arch_dir </>) valid)
, invalid_root ==? [tempdir </> "no-such-subdir"]
]
prop_InputOpCode :: MetaOpCode -> Int -> Property
prop_InputOpCode meta i =
conjoin [ readJSON (showJSON valid) ==? Text.JSON.Ok valid
, readJSON (showJSON invalid) ==? Text.JSON.Ok invalid
]
where valid = ValidOpCode meta
invalid = InvalidOpCode (showJSON i)
prop_extractOpSummary :: MetaOpCode -> Int -> Property
prop_extractOpSummary meta i =
conjoin [ printTestCase "valid opcode" $
extractOpSummary (ValidOpCode meta) ==? summary
, printTestCase "invalid opcode, correct object" $
extractOpSummary (InvalidOpCode jsobj) ==? summary
, printTestCase "invalid opcode, empty object" $
extractOpSummary (InvalidOpCode emptyo) ==? invalid
, printTestCase "invalid opcode, object with invalid OP_ID" $
extractOpSummary (InvalidOpCode invobj) ==? invalid
, printTestCase "invalid opcode, not jsobject" $
extractOpSummary (InvalidOpCode jsinval) ==? invalid
]
where summary = opSummary (metaOpCode meta)
jsobj = showJSON $ toJSObject [("OP_ID",
showJSON ("OP_" ++ summary))]
emptyo = showJSON $ toJSObject ([]::[(String, JSValue)])
invobj = showJSON $ toJSObject [("OP_ID", showJSON False)]
jsinval = showJSON i
invalid = "INVALID_OP"
testSuite "JQueue"
[ 'case_JobPriorityDef
, 'prop_JobPriority
, 'case_JobStatusDef
, 'prop_JobStatus
, 'case_JobStatusPri_py_equiv
, 'prop_ListJobIDs
, 'prop_LoadJobs
, 'prop_DetermineDirs
, 'prop_InputOpCode
, 'prop_extractOpSummary
]