1 {-# LANGUAGE TemplateHaskell #-}
3 {-| Unittests for the job queue functionality.
9 Copyright (C) 2012 Google Inc.
11 This program is free software; you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation; either version 2 of the License, or
14 (at your option) any later version.
16 This program is distributed in the hope that it will be useful, but
17 WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with this program; if not, write to the Free Software
23 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
28 module Test.Ganeti.JQueue (testJQueue) where
30 import Control.Applicative
31 import Control.Monad (when)
32 import Data.Char (isAscii)
33 import Data.List (nub, sort)
34 import System.Directory
35 import System.FilePath
37 import System.Posix.Files
39 import Test.QuickCheck as QuickCheck
40 import Test.QuickCheck.Monadic
43 import Test.Ganeti.TestCommon
44 import Test.Ganeti.TestHelper
45 import Test.Ganeti.Types ()
46 import Test.Ganeti.OpCodes
48 import Ganeti.BasicTypes
49 import qualified Ganeti.Constants as C
53 import Ganeti.Types as Types
55 {-# ANN module "HLint: ignore Use camelCase" #-}
59 -- | noTimestamp in Just form.
60 justNoTs :: Maybe Timestamp
61 justNoTs = Just noTimestamp
63 -- | Generates a simple queued opcode.
64 genQueuedOpCode :: Gen QueuedOpCode
66 QueuedOpCode <$> pure (ValidOpCode $ wrapOpCode OpClusterQuery) <*>
67 arbitrary <*> pure JSNull <*> pure [] <*>
68 choose (C.opPrioLowest, C.opPrioHighest) <*>
69 pure justNoTs <*> pure justNoTs <*> pure justNoTs
71 -- | Generates an static, empty job.
72 emptyJob :: (Monad m) => m QueuedJob
75 return $ QueuedJob jid0 [] justNoTs justNoTs justNoTs
77 -- | Generates a job ID.
80 p <- arbitrary::Gen (Types.NonNegative Int)
81 makeJobId $ fromNonNegative p
85 -- | Tests default priority value.
86 case_JobPriorityDef :: Assertion
87 case_JobPriorityDef = do
89 assertEqual "for default priority" C.opPrioDefault $ calcJobPriority ej
91 -- | Test arbitrary priorities.
92 prop_JobPriority :: Property
94 forAll (listOf1 (genQueuedOpCode `suchThat`
95 (not . opStatusFinalized . qoStatus))) $ \ops -> do
97 let job = QueuedJob jid0 ops justNoTs justNoTs justNoTs
98 calcJobPriority job ==? minimum (map qoPriority ops)
100 -- | Tests default job status.
101 case_JobStatusDef :: Assertion
102 case_JobStatusDef = do
104 assertEqual "for job status" JOB_STATUS_SUCCESS $ calcJobStatus ej
106 -- | Test some job status properties.
107 prop_JobStatus :: Property
109 forAll genJobId $ \jid ->
110 forAll genQueuedOpCode $ \op ->
111 let job1 = QueuedJob jid [op] justNoTs justNoTs justNoTs
112 st1 = calcJobStatus job1
113 op_succ = op { qoStatus = OP_STATUS_SUCCESS }
114 op_err = op { qoStatus = OP_STATUS_ERROR }
115 op_cnl = op { qoStatus = OP_STATUS_CANCELING }
116 op_cnd = op { qoStatus = OP_STATUS_CANCELED }
117 -- computes status for a job with an added opcode before
118 st_pre_op pop = calcJobStatus (job1 { qjOps = pop:qjOps job1 })
119 -- computes status for a job with an added opcode after
120 st_post_op pop = calcJobStatus (job1 { qjOps = qjOps job1 ++ [pop] })
122 [ printTestCase "pre-success doesn't change status"
123 (st_pre_op op_succ ==? st1)
124 , printTestCase "post-success doesn't change status"
125 (st_post_op op_succ ==? st1)
126 , printTestCase "pre-error is error"
127 (st_pre_op op_err ==? JOB_STATUS_ERROR)
128 , printTestCase "pre-canceling is canceling"
129 (st_pre_op op_cnl ==? JOB_STATUS_CANCELING)
130 , printTestCase "pre-canceled is canceled"
131 (st_pre_op op_cnd ==? JOB_STATUS_CANCELED)
134 -- | Tests job status equivalence with Python. Very similar to OpCodes test.
135 case_JobStatusPri_py_equiv :: Assertion
136 case_JobStatusPri_py_equiv = do
137 let num_jobs = 2000::Int
138 sample_jobs <- sample' (vectorOf num_jobs $ do
139 num_ops <- choose (1, 5)
140 ops <- vectorOf num_ops genQueuedOpCode
142 return $ QueuedJob jid ops justNoTs justNoTs
144 let jobs = head sample_jobs
145 serialized = encode jobs
146 -- check for non-ASCII fields, usually due to 'arbitrary :: String'
147 mapM_ (\job -> when (any (not . isAscii) (encode job)) .
148 assertFailure $ "Job has non-ASCII fields: " ++ show job
151 runPython "from ganeti import jqueue\n\
152 \from ganeti import serializer\n\
154 \job_data = serializer.Load(sys.stdin.read())\n\
155 \decoded = [jqueue._QueuedJob.Restore(None, o, False, False)\n\
156 \ for o in job_data]\n\
157 \encoded = [(job.CalcStatus(), job.CalcPriority())\n\
158 \ for job in decoded]\n\
159 \print serializer.Dump(encoded)" serialized
160 >>= checkPythonResult
161 let deserialised = decode py_stdout::Text.JSON.Result [(String, Int)]
162 decoded <- case deserialised of
163 Text.JSON.Ok jobs' -> return jobs'
165 assertFailure ("Unable to decode jobs: " ++ msg)
166 -- this already raised an expection, but we need it
168 >> fail "Unable to decode jobs"
169 assertEqual "Mismatch in number of returned jobs"
170 (length decoded) (length jobs)
171 mapM_ (\(py_sp, job) ->
172 let hs_sp = (jobStatusToRaw $ calcJobStatus job,
174 in assertEqual ("Different result after encoding/decoding for " ++
175 show job) py_sp hs_sp
178 -- | Tests listing of Job ids.
179 prop_ListJobIDs :: Property
180 prop_ListJobIDs = monadicIO $ do
181 jobs <- pick $ resize 10 (listOf1 genJobId `suchThat` (\l -> l == nub l))
183 run . withSystemTempDirectory "jqueue-test." $ \tempdir -> do
184 empty_dir <- getJobIDs [tempdir]
185 mapM_ (\jid -> writeFile (tempdir </> jobFileName jid) "") jobs
186 full_dir <- getJobIDs [tempdir]
187 invalid_dir <- getJobIDs [tempdir </> "no-such-dir"]
188 return (empty_dir, sortJobIDs full_dir, invalid_dir)
189 stop $ conjoin [ printTestCase "empty directory" $ e ==? []
190 , printTestCase "directory with valid names" $
191 f ==? sortJobIDs jobs
192 , printTestCase "invalid directory" $ g ==? []
195 -- | Tests loading jobs from disk.
196 prop_LoadJobs :: Property
197 prop_LoadJobs = monadicIO $ do
198 ops <- pick $ resize 5 (listOf1 genQueuedOpCode)
200 let job = QueuedJob jid ops justNoTs justNoTs justNoTs
202 -- check that jobs in the right directories are parsed correctly
203 (missing, current, archived, missing_current, broken) <-
204 run . withSystemTempDirectory "jqueue-test." $ \tempdir -> do
205 let load a = loadJobFromDisk tempdir a jid
206 live_path = liveJobFile tempdir jid
207 arch_path = archivedJobFile tempdir jid
208 createDirectory $ tempdir </> jobQueueArchiveSubDir
209 createDirectory $ dropFileName arch_path
212 writeFile live_path job_s
214 current <- load False
216 writeFile arch_path job_s
217 -- this should exist (archived)
218 archived <- load True
219 -- this should be missing
220 missing_current <- load False
222 writeFile live_path "invalid job"
224 return (missing, current, archived, missing_current, broken)
225 stop $ conjoin [ missing ==? noSuchJob
226 , current ==? Ganeti.BasicTypes.Ok (job, False)
227 , archived ==? Ganeti.BasicTypes.Ok (job, True)
228 , missing_current ==? noSuchJob
229 , printTestCase "broken job" (isBad broken)
232 -- | Tests computing job directories. Creates random directories,
233 -- files and stale symlinks in a directory, and checks that we return
234 -- \"the right thing\".
235 prop_DetermineDirs :: Property
236 prop_DetermineDirs = monadicIO $ do
237 count <- pick $ choose (2, 10)
238 nums <- pick $ genUniquesList count
239 (arbitrary::Gen (QuickCheck.Positive Int))
240 let (valid, invalid) = splitAt (count `div` 2) $
241 map (\(QuickCheck.Positive i) -> show i) nums
242 (tempdir, non_arch, with_arch, invalid_root) <-
243 run . withSystemTempDirectory "jqueue-test." $ \tempdir -> do
244 let arch_dir = tempdir </> jobQueueArchiveSubDir
245 createDirectory arch_dir
246 mapM_ (createDirectory . (arch_dir </>)) valid
247 mapM_ (\p -> writeFile (arch_dir </> p) "") invalid
248 mapM_ (\p -> createSymbolicLink "/dev/null/no/such/file"
249 (arch_dir </> p <.> "missing")) invalid
250 non_arch <- determineJobDirectories tempdir False
251 with_arch <- determineJobDirectories tempdir True
252 invalid_root <- determineJobDirectories (tempdir </> "no-such-subdir") True
253 return (tempdir, non_arch, with_arch, invalid_root)
254 let arch_dir = tempdir </> jobQueueArchiveSubDir
255 stop $ conjoin [ non_arch ==? [tempdir]
256 , sort with_arch ==? sort (tempdir:map (arch_dir </>) valid)
257 , invalid_root ==? [tempdir </> "no-such-subdir"]
260 -- | Tests the JSON serialisation for 'InputOpCode'.
261 prop_InputOpCode :: MetaOpCode -> Int -> Property
262 prop_InputOpCode meta i =
263 conjoin [ readJSON (showJSON valid) ==? Text.JSON.Ok valid
264 , readJSON (showJSON invalid) ==? Text.JSON.Ok invalid
266 where valid = ValidOpCode meta
267 invalid = InvalidOpCode (showJSON i)
269 -- | Tests 'extractOpSummary'.
270 prop_extractOpSummary :: MetaOpCode -> Int -> Property
271 prop_extractOpSummary meta i =
272 conjoin [ printTestCase "valid opcode" $
273 extractOpSummary (ValidOpCode meta) ==? summary
274 , printTestCase "invalid opcode, correct object" $
275 extractOpSummary (InvalidOpCode jsobj) ==? summary
276 , printTestCase "invalid opcode, empty object" $
277 extractOpSummary (InvalidOpCode emptyo) ==? invalid
278 , printTestCase "invalid opcode, object with invalid OP_ID" $
279 extractOpSummary (InvalidOpCode invobj) ==? invalid
280 , printTestCase "invalid opcode, not jsobject" $
281 extractOpSummary (InvalidOpCode jsinval) ==? invalid
283 where summary = opSummary (metaOpCode meta)
284 jsobj = showJSON $ toJSObject [("OP_ID",
285 showJSON ("OP_" ++ summary))]
286 emptyo = showJSON $ toJSObject ([]::[(String, JSValue)])
287 invobj = showJSON $ toJSObject [("OP_ID", showJSON False)]
289 invalid = "INVALID_OP"
292 [ 'case_JobPriorityDef
296 , 'case_JobStatusPri_py_equiv
299 , 'prop_DetermineDirs
301 , 'prop_extractOpSummary