Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / HTools / QC.hs @ aed2325f

History | View | Annotate | Download (65 kB)

1
{-# LANGUAGE TemplateHaskell #-}
2
{-# OPTIONS_GHC -fno-warn-orphans -fno-warn-unused-imports #-}
3

    
4
-- FIXME: should remove the no-warn-unused-imports option, once we get
5
-- around to testing function from all modules; until then, we keep
6
-- the (unused) imports here to generate correct coverage (0 for
7
-- modules we don't use)
8

    
9
{-| Unittests for ganeti-htools.
10

    
11
-}
12

    
13
{-
14

    
15
Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
16

    
17
This program is free software; you can redistribute it and/or modify
18
it under the terms of the GNU General Public License as published by
19
the Free Software Foundation; either version 2 of the License, or
20
(at your option) any later version.
21

    
22
This program is distributed in the hope that it will be useful, but
23
WITHOUT ANY WARRANTY; without even the implied warranty of
24
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
25
General Public License for more details.
26

    
27
You should have received a copy of the GNU General Public License
28
along with this program; if not, write to the Free Software
29
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
30
02110-1301, USA.
31

    
32
-}
33

    
34
module Ganeti.HTools.QC
35
  ( testUtils
36
  , testPeerMap
37
  , testContainer
38
  , testInstance
39
  , testNode
40
  , testText
41
  , testSimu
42
  , testJobs
43
  , testCluster
44
  , testLoader
45
  , testTypes
46
  , testCLI
47
  , testJSON
48
  ) where
49

    
50
import qualified Test.HUnit as HUnit
51
import Test.QuickCheck
52
import Test.QuickCheck.Monadic (assert, monadicIO, run, stop)
53
import Text.Printf (printf)
54
import Data.List (intercalate, nub, isPrefixOf, sort, (\\))
55
import Data.Maybe
56
import qualified Data.Set as Set
57
import Control.Monad
58
import Control.Applicative
59
import qualified System.Console.GetOpt as GetOpt
60
import qualified Text.JSON as J
61
import qualified Data.Map as Map
62
import qualified Data.IntMap as IntMap
63
import Control.Concurrent (forkIO)
64
import Control.Exception (bracket, catchJust)
65
import System.Directory (getTemporaryDirectory, removeFile)
66
import System.Environment (getEnv)
67
import System.Exit (ExitCode(..))
68
import System.IO (hClose, openTempFile)
69
import System.IO.Error (isEOFErrorType, ioeGetErrorType, isDoesNotExistError)
70
import System.Process (readProcessWithExitCode)
71

    
72
import qualified Ganeti.Confd as Confd
73
import qualified Ganeti.Confd.Server as Confd.Server
74
import qualified Ganeti.Confd.Utils as Confd.Utils
75
import qualified Ganeti.Config as Config
76
import qualified Ganeti.Daemon as Daemon
77
import qualified Ganeti.Hash as Hash
78
import qualified Ganeti.BasicTypes as BasicTypes
79
import qualified Ganeti.Jobs as Jobs
80
import qualified Ganeti.Logging as Logging
81
import qualified Ganeti.Luxi as Luxi
82
import qualified Ganeti.Objects as Objects
83
import qualified Ganeti.OpCodes as OpCodes
84
import qualified Ganeti.Query.Language as Qlang
85
import qualified Ganeti.Runtime as Runtime
86
import qualified Ganeti.HTools.CLI as CLI
87
import qualified Ganeti.HTools.Cluster as Cluster
88
import qualified Ganeti.HTools.Container as Container
89
import qualified Ganeti.HTools.ExtLoader
90
import qualified Ganeti.HTools.Group as Group
91
import qualified Ganeti.HTools.IAlloc as IAlloc
92
import qualified Ganeti.HTools.Instance as Instance
93
import qualified Ganeti.HTools.JSON as JSON
94
import qualified Ganeti.HTools.Loader as Loader
95
import qualified Ganeti.HTools.Luxi as HTools.Luxi
96
import qualified Ganeti.HTools.Node as Node
97
import qualified Ganeti.HTools.PeerMap as PeerMap
98
import qualified Ganeti.HTools.Rapi
99
import qualified Ganeti.HTools.Simu as Simu
100
import qualified Ganeti.HTools.Text as Text
101
import qualified Ganeti.HTools.Types as Types
102
import qualified Ganeti.HTools.Utils as Utils
103
import qualified Ganeti.HTools.Version
104
import qualified Ganeti.Constants as C
105

    
106
import qualified Ganeti.HTools.Program as Program
107
import qualified Ganeti.HTools.Program.Hail
108
import qualified Ganeti.HTools.Program.Hbal
109
import qualified Ganeti.HTools.Program.Hscan
110
import qualified Ganeti.HTools.Program.Hspace
111

    
112
import Test.Ganeti.TestHelper (testSuite)
113
import Test.Ganeti.TestCommon
114

    
115
-- | All disk templates (used later)
116
allDiskTemplates :: [Types.DiskTemplate]
117
allDiskTemplates = [minBound..maxBound]
118

    
119
-- | Null iPolicy, and by null we mean very liberal.
120
nullIPolicy :: Types.IPolicy
121
nullIPolicy = Types.IPolicy
122
  { Types.iPolicyMinSpec = Types.ISpec { Types.iSpecMemorySize = 0
123
                                       , Types.iSpecCpuCount   = 0
124
                                       , Types.iSpecDiskSize   = 0
125
                                       , Types.iSpecDiskCount  = 0
126
                                       , Types.iSpecNicCount   = 0
127
                                       , Types.iSpecSpindleUse = 0
128
                                       }
129
  , Types.iPolicyMaxSpec = Types.ISpec { Types.iSpecMemorySize = maxBound
130
                                       , Types.iSpecCpuCount   = maxBound
131
                                       , Types.iSpecDiskSize   = maxBound
132
                                       , Types.iSpecDiskCount  = C.maxDisks
133
                                       , Types.iSpecNicCount   = C.maxNics
134
                                       , Types.iSpecSpindleUse = maxBound
135
                                       }
136
  , Types.iPolicyStdSpec = Types.ISpec { Types.iSpecMemorySize = Types.unitMem
137
                                       , Types.iSpecCpuCount   = Types.unitCpu
138
                                       , Types.iSpecDiskSize   = Types.unitDsk
139
                                       , Types.iSpecDiskCount  = 1
140
                                       , Types.iSpecNicCount   = 1
141
                                       , Types.iSpecSpindleUse = 1
142
                                       }
143
  , Types.iPolicyDiskTemplates = [minBound..maxBound]
144
  , Types.iPolicyVcpuRatio = maxVcpuRatio -- somewhat random value, high
145
                                          -- enough to not impact us
146
  , Types.iPolicySpindleRatio = maxSpindleRatio
147
  }
148

    
149

    
150
defGroup :: Group.Group
151
defGroup = flip Group.setIdx 0 $
152
             Group.create "default" Types.defaultGroupID Types.AllocPreferred
153
                  nullIPolicy
154

    
155
defGroupList :: Group.List
156
defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
157

    
158
defGroupAssoc :: Map.Map String Types.Gdx
159
defGroupAssoc = Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
160

    
161
-- * Helper functions
162

    
163
-- | Simple checker for whether OpResult is fail or pass.
164
isFailure :: Types.OpResult a -> Bool
165
isFailure (Types.OpFail _) = True
166
isFailure _ = False
167

    
168
-- | Update an instance to be smaller than a node.
169
setInstanceSmallerThanNode :: Node.Node
170
                           -> Instance.Instance -> Instance.Instance
171
setInstanceSmallerThanNode node inst =
172
  inst { Instance.mem = Node.availMem node `div` 2
173
       , Instance.dsk = Node.availDisk node `div` 2
174
       , Instance.vcpus = Node.availCpu node `div` 2
175
       }
176

    
177
-- | Create an instance given its spec.
178
createInstance :: Int -> Int -> Int -> Instance.Instance
179
createInstance mem dsk vcpus =
180
  Instance.create "inst-unnamed" mem dsk vcpus Types.Running [] True (-1) (-1)
181
    Types.DTDrbd8 1
182

    
183
-- | Create a small cluster by repeating a node spec.
184
makeSmallCluster :: Node.Node -> Int -> Node.List
185
makeSmallCluster node count =
186
  let origname = Node.name node
187
      origalias = Node.alias node
188
      nodes = map (\idx -> node { Node.name = origname ++ "-" ++ show idx
189
                                , Node.alias = origalias ++ "-" ++ show idx })
190
              [1..count]
191
      fn = flip Node.buildPeers Container.empty
192
      namelst = map (\n -> (Node.name n, fn n)) nodes
193
      (_, nlst) = Loader.assignIndices namelst
194
  in nlst
195

    
196
-- | Make a small cluster, both nodes and instances.
197
makeSmallEmptyCluster :: Node.Node -> Int -> Instance.Instance
198
                      -> (Node.List, Instance.List, Instance.Instance)
199
makeSmallEmptyCluster node count inst =
200
  (makeSmallCluster node count, Container.empty,
201
   setInstanceSmallerThanNode node inst)
202

    
203
-- | Checks if a node is "big" enough.
204
isNodeBig :: Int -> Node.Node -> Bool
205
isNodeBig size node = Node.availDisk node > size * Types.unitDsk
206
                      && Node.availMem node > size * Types.unitMem
207
                      && Node.availCpu node > size * Types.unitCpu
208

    
209
canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
210
canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
211

    
212
-- | Assigns a new fresh instance to a cluster; this is not
213
-- allocation, so no resource checks are done.
214
assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
215
                  Types.Idx -> Types.Idx ->
216
                  (Node.List, Instance.List)
217
assignInstance nl il inst pdx sdx =
218
  let pnode = Container.find pdx nl
219
      snode = Container.find sdx nl
220
      maxiidx = if Container.null il
221
                  then 0
222
                  else fst (Container.findMax il) + 1
223
      inst' = inst { Instance.idx = maxiidx,
224
                     Instance.pNode = pdx, Instance.sNode = sdx }
225
      pnode' = Node.setPri pnode inst'
226
      snode' = Node.setSec snode inst'
227
      nl' = Container.addTwo pdx pnode' sdx snode' nl
228
      il' = Container.add maxiidx inst' il
229
  in (nl', il')
230

    
231
-- | Generates a list of a given size with non-duplicate elements.
232
genUniquesList :: (Eq a, Arbitrary a) => Int -> Gen [a]
233
genUniquesList cnt =
234
  foldM (\lst _ -> do
235
           newelem <- arbitrary `suchThat` (`notElem` lst)
236
           return (newelem:lst)) [] [1..cnt]
237

    
238
-- | Checks if an instance is mirrored.
239
isMirrored :: Instance.Instance -> Bool
240
isMirrored = (/= Types.MirrorNone) . Instance.mirrorType
241

    
242
-- | Returns the possible change node types for a disk template.
243
evacModeOptions :: Types.MirrorType -> [Types.EvacMode]
244
evacModeOptions Types.MirrorNone     = []
245
evacModeOptions Types.MirrorInternal = [minBound..maxBound] -- DRBD can do all
246
evacModeOptions Types.MirrorExternal = [Types.ChangePrimary, Types.ChangeAll]
247

    
248
instance Arbitrary Types.InstanceStatus where
249
    arbitrary = elements [minBound..maxBound]
250

    
251
-- | Generates a random instance with maximum disk/mem/cpu values.
252
genInstanceSmallerThan :: Int -> Int -> Int -> Gen Instance.Instance
253
genInstanceSmallerThan lim_mem lim_dsk lim_cpu = do
254
  name <- getFQDN
255
  mem <- choose (0, lim_mem)
256
  dsk <- choose (0, lim_dsk)
257
  run_st <- arbitrary
258
  pn <- arbitrary
259
  sn <- arbitrary
260
  vcpus <- choose (0, lim_cpu)
261
  dt <- arbitrary
262
  return $ Instance.create name mem dsk vcpus run_st [] True pn sn dt 1
263

    
264
-- | Generates an instance smaller than a node.
265
genInstanceSmallerThanNode :: Node.Node -> Gen Instance.Instance
266
genInstanceSmallerThanNode node =
267
  genInstanceSmallerThan (Node.availMem node `div` 2)
268
                         (Node.availDisk node `div` 2)
269
                         (Node.availCpu node `div` 2)
270

    
271
-- let's generate a random instance
272
instance Arbitrary Instance.Instance where
273
  arbitrary = genInstanceSmallerThan maxMem maxDsk maxCpu
274

    
275
-- | Generas an arbitrary node based on sizing information.
276
genNode :: Maybe Int -- ^ Minimum node size in terms of units
277
        -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
278
                     -- just by the max... constants)
279
        -> Gen Node.Node
280
genNode min_multiplier max_multiplier = do
281
  let (base_mem, base_dsk, base_cpu) =
282
        case min_multiplier of
283
          Just mm -> (mm * Types.unitMem,
284
                      mm * Types.unitDsk,
285
                      mm * Types.unitCpu)
286
          Nothing -> (0, 0, 0)
287
      (top_mem, top_dsk, top_cpu)  =
288
        case max_multiplier of
289
          Just mm -> (mm * Types.unitMem,
290
                      mm * Types.unitDsk,
291
                      mm * Types.unitCpu)
292
          Nothing -> (maxMem, maxDsk, maxCpu)
293
  name  <- getFQDN
294
  mem_t <- choose (base_mem, top_mem)
295
  mem_f <- choose (base_mem, mem_t)
296
  mem_n <- choose (0, mem_t - mem_f)
297
  dsk_t <- choose (base_dsk, top_dsk)
298
  dsk_f <- choose (base_dsk, dsk_t)
299
  cpu_t <- choose (base_cpu, top_cpu)
300
  offl  <- arbitrary
301
  let n = Node.create name (fromIntegral mem_t) mem_n mem_f
302
          (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 1 0
303
      n' = Node.setPolicy nullIPolicy n
304
  return $ Node.buildPeers n' Container.empty
305

    
306
-- | Helper function to generate a sane node.
307
genOnlineNode :: Gen Node.Node
308
genOnlineNode = do
309
  arbitrary `suchThat` (\n -> not (Node.offline n) &&
310
                              not (Node.failN1 n) &&
311
                              Node.availDisk n > 0 &&
312
                              Node.availMem n > 0 &&
313
                              Node.availCpu n > 0)
314

    
315
-- and a random node
316
instance Arbitrary Node.Node where
317
  arbitrary = genNode Nothing Nothing
318

    
319
instance Arbitrary Jobs.OpStatus where
320
  arbitrary = elements [minBound..maxBound]
321

    
322
instance Arbitrary Jobs.JobStatus where
323
  arbitrary = elements [minBound..maxBound]
324

    
325
newtype SmallRatio = SmallRatio Double deriving Show
326
instance Arbitrary SmallRatio where
327
  arbitrary = do
328
    v <- choose (0, 1)
329
    return $ SmallRatio v
330

    
331
instance Arbitrary Types.AllocPolicy where
332
  arbitrary = elements [minBound..maxBound]
333

    
334
instance Arbitrary Types.DiskTemplate where
335
  arbitrary = elements [minBound..maxBound]
336

    
337
instance Arbitrary Types.FailMode where
338
  arbitrary = elements [minBound..maxBound]
339

    
340
instance Arbitrary Types.EvacMode where
341
  arbitrary = elements [minBound..maxBound]
342

    
343
instance Arbitrary a => Arbitrary (Types.OpResult a) where
344
  arbitrary = arbitrary >>= \c ->
345
              if c
346
                then Types.OpGood <$> arbitrary
347
                else Types.OpFail <$> arbitrary
348

    
349
instance Arbitrary Types.ISpec where
350
  arbitrary = do
351
    mem_s <- arbitrary::Gen (NonNegative Int)
352
    dsk_c <- arbitrary::Gen (NonNegative Int)
353
    dsk_s <- arbitrary::Gen (NonNegative Int)
354
    cpu_c <- arbitrary::Gen (NonNegative Int)
355
    nic_c <- arbitrary::Gen (NonNegative Int)
356
    su    <- arbitrary::Gen (NonNegative Int)
357
    return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
358
                       , Types.iSpecCpuCount   = fromIntegral cpu_c
359
                       , Types.iSpecDiskSize   = fromIntegral dsk_s
360
                       , Types.iSpecDiskCount  = fromIntegral dsk_c
361
                       , Types.iSpecNicCount   = fromIntegral nic_c
362
                       , Types.iSpecSpindleUse = fromIntegral su
363
                       }
364

    
365
-- | Generates an ispec bigger than the given one.
366
genBiggerISpec :: Types.ISpec -> Gen Types.ISpec
367
genBiggerISpec imin = do
368
  mem_s <- choose (Types.iSpecMemorySize imin, maxBound)
369
  dsk_c <- choose (Types.iSpecDiskCount imin, maxBound)
370
  dsk_s <- choose (Types.iSpecDiskSize imin, maxBound)
371
  cpu_c <- choose (Types.iSpecCpuCount imin, maxBound)
372
  nic_c <- choose (Types.iSpecNicCount imin, maxBound)
373
  su    <- choose (Types.iSpecSpindleUse imin, maxBound)
374
  return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
375
                     , Types.iSpecCpuCount   = fromIntegral cpu_c
376
                     , Types.iSpecDiskSize   = fromIntegral dsk_s
377
                     , Types.iSpecDiskCount  = fromIntegral dsk_c
378
                     , Types.iSpecNicCount   = fromIntegral nic_c
379
                     , Types.iSpecSpindleUse = fromIntegral su
380
                     }
381

    
382
instance Arbitrary Types.IPolicy where
383
  arbitrary = do
384
    imin <- arbitrary
385
    istd <- genBiggerISpec imin
386
    imax <- genBiggerISpec istd
387
    num_tmpl <- choose (0, length allDiskTemplates)
388
    dts  <- genUniquesList num_tmpl
389
    vcpu_ratio <- choose (1.0, maxVcpuRatio)
390
    spindle_ratio <- choose (1.0, maxSpindleRatio)
391
    return Types.IPolicy { Types.iPolicyMinSpec = imin
392
                         , Types.iPolicyStdSpec = istd
393
                         , Types.iPolicyMaxSpec = imax
394
                         , Types.iPolicyDiskTemplates = dts
395
                         , Types.iPolicyVcpuRatio = vcpu_ratio
396
                         , Types.iPolicySpindleRatio = spindle_ratio
397
                         }
398

    
399
-- * Actual tests
400

    
401
-- ** Utils tests
402

    
403
-- | Helper to generate a small string that doesn't contain commas.
404
genNonCommaString :: Gen [Char]
405
genNonCommaString = do
406
  size <- choose (0, 20) -- arbitrary max size
407
  vectorOf size (arbitrary `suchThat` ((/=) ','))
408

    
409
-- | If the list is not just an empty element, and if the elements do
410
-- not contain commas, then join+split should be idempotent.
411
prop_Utils_commaJoinSplit :: Property
412
prop_Utils_commaJoinSplit =
413
  forAll (choose (0, 20)) $ \llen ->
414
  forAll (vectorOf llen genNonCommaString `suchThat` ((/=) [""])) $ \lst ->
415
  Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
416

    
417
-- | Split and join should always be idempotent.
418
prop_Utils_commaSplitJoin :: [Char] -> Property
419
prop_Utils_commaSplitJoin s =
420
  Utils.commaJoin (Utils.sepSplit ',' s) ==? s
421

    
422
-- | fromObjWithDefault, we test using the Maybe monad and an integer
423
-- value.
424
prop_Utils_fromObjWithDefault :: Integer -> String -> Bool
425
prop_Utils_fromObjWithDefault def_value random_key =
426
  -- a missing key will be returned with the default
427
  JSON.fromObjWithDefault [] random_key def_value == Just def_value &&
428
  -- a found key will be returned as is, not with default
429
  JSON.fromObjWithDefault [(random_key, J.showJSON def_value)]
430
       random_key (def_value+1) == Just def_value
431

    
432
-- | Test that functional if' behaves like the syntactic sugar if.
433
prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
434
prop_Utils_if'if cnd a b =
435
  Utils.if' cnd a b ==? if cnd then a else b
436

    
437
-- | Test basic select functionality
438
prop_Utils_select :: Int      -- ^ Default result
439
                  -> [Int]    -- ^ List of False values
440
                  -> [Int]    -- ^ List of True values
441
                  -> Gen Prop -- ^ Test result
442
prop_Utils_select def lst1 lst2 =
443
  Utils.select def (flist ++ tlist) ==? expectedresult
444
    where expectedresult = Utils.if' (null lst2) def (head lst2)
445
          flist = zip (repeat False) lst1
446
          tlist = zip (repeat True)  lst2
447

    
448
-- | Test basic select functionality with undefined default
449
prop_Utils_select_undefd :: [Int]            -- ^ List of False values
450
                         -> NonEmptyList Int -- ^ List of True values
451
                         -> Gen Prop         -- ^ Test result
452
prop_Utils_select_undefd lst1 (NonEmpty lst2) =
453
  Utils.select undefined (flist ++ tlist) ==? head lst2
454
    where flist = zip (repeat False) lst1
455
          tlist = zip (repeat True)  lst2
456

    
457
-- | Test basic select functionality with undefined list values
458
prop_Utils_select_undefv :: [Int]            -- ^ List of False values
459
                         -> NonEmptyList Int -- ^ List of True values
460
                         -> Gen Prop         -- ^ Test result
461
prop_Utils_select_undefv lst1 (NonEmpty lst2) =
462
  Utils.select undefined cndlist ==? head lst2
463
    where flist = zip (repeat False) lst1
464
          tlist = zip (repeat True)  lst2
465
          cndlist = flist ++ tlist ++ [undefined]
466

    
467
prop_Utils_parseUnit :: NonNegative Int -> Property
468
prop_Utils_parseUnit (NonNegative n) =
469
  Utils.parseUnit (show n) ==? Types.Ok n .&&.
470
  Utils.parseUnit (show n ++ "m") ==? Types.Ok n .&&.
471
  Utils.parseUnit (show n ++ "M") ==? Types.Ok (truncate n_mb::Int) .&&.
472
  Utils.parseUnit (show n ++ "g") ==? Types.Ok (n*1024) .&&.
473
  Utils.parseUnit (show n ++ "G") ==? Types.Ok (truncate n_gb::Int) .&&.
474
  Utils.parseUnit (show n ++ "t") ==? Types.Ok (n*1048576) .&&.
475
  Utils.parseUnit (show n ++ "T") ==? Types.Ok (truncate n_tb::Int) .&&.
476
  printTestCase "Internal error/overflow?"
477
    (n_mb >=0 && n_gb >= 0 && n_tb >= 0) .&&.
478
  property (Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int))
479
  where n_mb = (fromIntegral n::Rational) * 1000 * 1000 / 1024 / 1024
480
        n_gb = n_mb * 1000
481
        n_tb = n_gb * 1000
482

    
483
-- | Test list for the Utils module.
484
testSuite "Utils"
485
            [ 'prop_Utils_commaJoinSplit
486
            , 'prop_Utils_commaSplitJoin
487
            , 'prop_Utils_fromObjWithDefault
488
            , 'prop_Utils_if'if
489
            , 'prop_Utils_select
490
            , 'prop_Utils_select_undefd
491
            , 'prop_Utils_select_undefv
492
            , 'prop_Utils_parseUnit
493
            ]
494

    
495
-- ** PeerMap tests
496

    
497
-- | Make sure add is idempotent.
498
prop_PeerMap_addIdempotent :: PeerMap.PeerMap
499
                           -> PeerMap.Key -> PeerMap.Elem -> Property
500
prop_PeerMap_addIdempotent pmap key em =
501
  fn puniq ==? fn (fn puniq)
502
    where fn = PeerMap.add key em
503
          puniq = PeerMap.accumArray const pmap
504

    
505
-- | Make sure remove is idempotent.
506
prop_PeerMap_removeIdempotent :: PeerMap.PeerMap -> PeerMap.Key -> Property
507
prop_PeerMap_removeIdempotent pmap key =
508
  fn puniq ==? fn (fn puniq)
509
    where fn = PeerMap.remove key
510
          puniq = PeerMap.accumArray const pmap
511

    
512
-- | Make sure a missing item returns 0.
513
prop_PeerMap_findMissing :: PeerMap.PeerMap -> PeerMap.Key -> Property
514
prop_PeerMap_findMissing pmap key =
515
  PeerMap.find key (PeerMap.remove key puniq) ==? 0
516
    where puniq = PeerMap.accumArray const pmap
517

    
518
-- | Make sure an added item is found.
519
prop_PeerMap_addFind :: PeerMap.PeerMap
520
                     -> PeerMap.Key -> PeerMap.Elem -> Property
521
prop_PeerMap_addFind pmap key em =
522
  PeerMap.find key (PeerMap.add key em puniq) ==? em
523
    where puniq = PeerMap.accumArray const pmap
524

    
525
-- | Manual check that maxElem returns the maximum indeed, or 0 for null.
526
prop_PeerMap_maxElem :: PeerMap.PeerMap -> Property
527
prop_PeerMap_maxElem pmap =
528
  PeerMap.maxElem puniq ==? if null puniq then 0
529
                              else (maximum . snd . unzip) puniq
530
    where puniq = PeerMap.accumArray const pmap
531

    
532
-- | List of tests for the PeerMap module.
533
testSuite "PeerMap"
534
            [ 'prop_PeerMap_addIdempotent
535
            , 'prop_PeerMap_removeIdempotent
536
            , 'prop_PeerMap_maxElem
537
            , 'prop_PeerMap_addFind
538
            , 'prop_PeerMap_findMissing
539
            ]
540

    
541
-- ** Container tests
542

    
543
-- we silence the following due to hlint bug fixed in later versions
544
{-# ANN prop_Container_addTwo "HLint: ignore Avoid lambda" #-}
545
prop_Container_addTwo :: [Container.Key] -> Int -> Int -> Bool
546
prop_Container_addTwo cdata i1 i2 =
547
  fn i1 i2 cont == fn i2 i1 cont &&
548
  fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
549
    where cont = foldl (\c x -> Container.add x x c) Container.empty cdata
550
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
551

    
552
prop_Container_nameOf :: Node.Node -> Property
553
prop_Container_nameOf node =
554
  let nl = makeSmallCluster node 1
555
      fnode = head (Container.elems nl)
556
  in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
557

    
558
-- | We test that in a cluster, given a random node, we can find it by
559
-- its name and alias, as long as all names and aliases are unique,
560
-- and that we fail to find a non-existing name.
561
prop_Container_findByName :: Property
562
prop_Container_findByName =
563
  forAll (genNode (Just 1) Nothing) $ \node ->
564
  forAll (choose (1, 20)) $ \ cnt ->
565
  forAll (choose (0, cnt - 1)) $ \ fidx ->
566
  forAll (genUniquesList (cnt * 2)) $ \ allnames ->
567
  forAll (arbitrary `suchThat` (`notElem` allnames)) $ \ othername ->
568
  let names = zip (take cnt allnames) (drop cnt allnames)
569
      nl = makeSmallCluster node cnt
570
      nodes = Container.elems nl
571
      nodes' = map (\((name, alias), nn) -> (Node.idx nn,
572
                                             nn { Node.name = name,
573
                                                  Node.alias = alias }))
574
               $ zip names nodes
575
      nl' = Container.fromList nodes'
576
      target = snd (nodes' !! fidx)
577
  in Container.findByName nl' (Node.name target) ==? Just target .&&.
578
     Container.findByName nl' (Node.alias target) ==? Just target .&&.
579
     printTestCase "Found non-existing name"
580
       (isNothing (Container.findByName nl' othername))
581

    
582
testSuite "Container"
583
            [ 'prop_Container_addTwo
584
            , 'prop_Container_nameOf
585
            , 'prop_Container_findByName
586
            ]
587

    
588
-- ** Instance tests
589

    
590
-- Simple instance tests, we only have setter/getters
591

    
592
prop_Instance_creat :: Instance.Instance -> Property
593
prop_Instance_creat inst =
594
  Instance.name inst ==? Instance.alias inst
595

    
596
prop_Instance_setIdx :: Instance.Instance -> Types.Idx -> Property
597
prop_Instance_setIdx inst idx =
598
  Instance.idx (Instance.setIdx inst idx) ==? idx
599

    
600
prop_Instance_setName :: Instance.Instance -> String -> Bool
601
prop_Instance_setName inst name =
602
  Instance.name newinst == name &&
603
  Instance.alias newinst == name
604
    where newinst = Instance.setName inst name
605

    
606
prop_Instance_setAlias :: Instance.Instance -> String -> Bool
607
prop_Instance_setAlias inst name =
608
  Instance.name newinst == Instance.name inst &&
609
  Instance.alias newinst == name
610
    where newinst = Instance.setAlias inst name
611

    
612
prop_Instance_setPri :: Instance.Instance -> Types.Ndx -> Property
613
prop_Instance_setPri inst pdx =
614
  Instance.pNode (Instance.setPri inst pdx) ==? pdx
615

    
616
prop_Instance_setSec :: Instance.Instance -> Types.Ndx -> Property
617
prop_Instance_setSec inst sdx =
618
  Instance.sNode (Instance.setSec inst sdx) ==? sdx
619

    
620
prop_Instance_setBoth :: Instance.Instance -> Types.Ndx -> Types.Ndx -> Bool
621
prop_Instance_setBoth inst pdx sdx =
622
  Instance.pNode si == pdx && Instance.sNode si == sdx
623
    where si = Instance.setBoth inst pdx sdx
624

    
625
prop_Instance_shrinkMG :: Instance.Instance -> Property
626
prop_Instance_shrinkMG inst =
627
  Instance.mem inst >= 2 * Types.unitMem ==>
628
    case Instance.shrinkByType inst Types.FailMem of
629
      Types.Ok inst' -> Instance.mem inst' == Instance.mem inst - Types.unitMem
630
      _ -> False
631

    
632
prop_Instance_shrinkMF :: Instance.Instance -> Property
633
prop_Instance_shrinkMF inst =
634
  forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
635
    let inst' = inst { Instance.mem = mem}
636
    in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
637

    
638
prop_Instance_shrinkCG :: Instance.Instance -> Property
639
prop_Instance_shrinkCG inst =
640
  Instance.vcpus inst >= 2 * Types.unitCpu ==>
641
    case Instance.shrinkByType inst Types.FailCPU of
642
      Types.Ok inst' ->
643
        Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
644
      _ -> False
645

    
646
prop_Instance_shrinkCF :: Instance.Instance -> Property
647
prop_Instance_shrinkCF inst =
648
  forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
649
    let inst' = inst { Instance.vcpus = vcpus }
650
    in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
651

    
652
prop_Instance_shrinkDG :: Instance.Instance -> Property
653
prop_Instance_shrinkDG inst =
654
  Instance.dsk inst >= 2 * Types.unitDsk ==>
655
    case Instance.shrinkByType inst Types.FailDisk of
656
      Types.Ok inst' ->
657
        Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
658
      _ -> False
659

    
660
prop_Instance_shrinkDF :: Instance.Instance -> Property
661
prop_Instance_shrinkDF inst =
662
  forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
663
    let inst' = inst { Instance.dsk = dsk }
664
    in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
665

    
666
prop_Instance_setMovable :: Instance.Instance -> Bool -> Property
667
prop_Instance_setMovable inst m =
668
  Instance.movable inst' ==? m
669
    where inst' = Instance.setMovable inst m
670

    
671
testSuite "Instance"
672
            [ 'prop_Instance_creat
673
            , 'prop_Instance_setIdx
674
            , 'prop_Instance_setName
675
            , 'prop_Instance_setAlias
676
            , 'prop_Instance_setPri
677
            , 'prop_Instance_setSec
678
            , 'prop_Instance_setBoth
679
            , 'prop_Instance_shrinkMG
680
            , 'prop_Instance_shrinkMF
681
            , 'prop_Instance_shrinkCG
682
            , 'prop_Instance_shrinkCF
683
            , 'prop_Instance_shrinkDG
684
            , 'prop_Instance_shrinkDF
685
            , 'prop_Instance_setMovable
686
            ]
687

    
688
-- ** Backends
689

    
690
-- *** Text backend tests
691

    
692
-- Instance text loader tests
693

    
694
prop_Text_Load_Instance :: String -> Int -> Int -> Int -> Types.InstanceStatus
695
                        -> NonEmptyList Char -> [Char]
696
                        -> NonNegative Int -> NonNegative Int -> Bool
697
                        -> Types.DiskTemplate -> Int -> Property
698
prop_Text_Load_Instance name mem dsk vcpus status
699
                        (NonEmpty pnode) snode
700
                        (NonNegative pdx) (NonNegative sdx) autobal dt su =
701
  pnode /= snode && pdx /= sdx ==>
702
  let vcpus_s = show vcpus
703
      dsk_s = show dsk
704
      mem_s = show mem
705
      su_s = show su
706
      status_s = Types.instanceStatusToRaw status
707
      ndx = if null snode
708
              then [(pnode, pdx)]
709
              else [(pnode, pdx), (snode, sdx)]
710
      nl = Map.fromList ndx
711
      tags = ""
712
      sbal = if autobal then "Y" else "N"
713
      sdt = Types.diskTemplateToRaw dt
714
      inst = Text.loadInst nl
715
             [name, mem_s, dsk_s, vcpus_s, status_s,
716
              sbal, pnode, snode, sdt, tags, su_s]
717
      fail1 = Text.loadInst nl
718
              [name, mem_s, dsk_s, vcpus_s, status_s,
719
               sbal, pnode, pnode, tags]
720
  in case inst of
721
       Types.Bad msg -> failTest $ "Failed to load instance: " ++ msg
722
       Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
723
                                        \ loading the instance" $
724
               Instance.name i == name &&
725
               Instance.vcpus i == vcpus &&
726
               Instance.mem i == mem &&
727
               Instance.pNode i == pdx &&
728
               Instance.sNode i == (if null snode
729
                                      then Node.noSecondary
730
                                      else sdx) &&
731
               Instance.autoBalance i == autobal &&
732
               Instance.spindleUse i == su &&
733
               Types.isBad fail1
734

    
735
prop_Text_Load_InstanceFail :: [(String, Int)] -> [String] -> Property
736
prop_Text_Load_InstanceFail ktn fields =
737
  length fields /= 10 && length fields /= 11 ==>
738
    case Text.loadInst nl fields of
739
      Types.Ok _ -> failTest "Managed to load instance from invalid data"
740
      Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
741
                       "Invalid/incomplete instance data: '" `isPrefixOf` msg
742
    where nl = Map.fromList ktn
743

    
744
prop_Text_Load_Node :: String -> Int -> Int -> Int -> Int -> Int
745
                    -> Int -> Bool -> Bool
746
prop_Text_Load_Node name tm nm fm td fd tc fo =
747
  let conv v = if v < 0
748
                 then "?"
749
                 else show v
750
      tm_s = conv tm
751
      nm_s = conv nm
752
      fm_s = conv fm
753
      td_s = conv td
754
      fd_s = conv fd
755
      tc_s = conv tc
756
      fo_s = if fo
757
               then "Y"
758
               else "N"
759
      any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
760
      gid = Group.uuid defGroup
761
  in case Text.loadNode defGroupAssoc
762
       [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
763
       Nothing -> False
764
       Just (name', node) ->
765
         if fo || any_broken
766
           then Node.offline node
767
           else Node.name node == name' && name' == name &&
768
                Node.alias node == name &&
769
                Node.tMem node == fromIntegral tm &&
770
                Node.nMem node == nm &&
771
                Node.fMem node == fm &&
772
                Node.tDsk node == fromIntegral td &&
773
                Node.fDsk node == fd &&
774
                Node.tCpu node == fromIntegral tc
775

    
776
prop_Text_Load_NodeFail :: [String] -> Property
777
prop_Text_Load_NodeFail fields =
778
  length fields /= 8 ==> isNothing $ Text.loadNode Map.empty fields
779

    
780
prop_Text_NodeLSIdempotent :: Property
781
prop_Text_NodeLSIdempotent =
782
  forAll (genNode (Just 1) Nothing) $ \node ->
783
  -- override failN1 to what loadNode returns by default
784
  let n = Node.setPolicy Types.defIPolicy $
785
          node { Node.failN1 = True, Node.offline = False }
786
  in
787
    (Text.loadNode defGroupAssoc.
788
         Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==?
789
    Just (Node.name n, n)
790

    
791
prop_Text_ISpecIdempotent :: Types.ISpec -> Property
792
prop_Text_ISpecIdempotent ispec =
793
  case Text.loadISpec "dummy" . Utils.sepSplit ',' .
794
       Text.serializeISpec $ ispec of
795
    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
796
    Types.Ok ispec' -> ispec ==? ispec'
797

    
798
prop_Text_IPolicyIdempotent :: Types.IPolicy -> Property
799
prop_Text_IPolicyIdempotent ipol =
800
  case Text.loadIPolicy . Utils.sepSplit '|' $
801
       Text.serializeIPolicy owner ipol of
802
    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
803
    Types.Ok res -> (owner, ipol) ==? res
804
  where owner = "dummy"
805

    
806
-- | This property, while being in the text tests, does more than just
807
-- test end-to-end the serialisation and loading back workflow; it
808
-- also tests the Loader.mergeData and the actuall
809
-- Cluster.iterateAlloc (for well-behaving w.r.t. instance
810
-- allocations, not for the business logic). As such, it's a quite
811
-- complex and slow test, and that's the reason we restrict it to
812
-- small cluster sizes.
813
prop_Text_CreateSerialise :: Property
814
prop_Text_CreateSerialise =
815
  forAll genTags $ \ctags ->
816
  forAll (choose (1, 20)) $ \maxiter ->
817
  forAll (choose (2, 10)) $ \count ->
818
  forAll genOnlineNode $ \node ->
819
  forAll (genInstanceSmallerThanNode node) $ \inst ->
820
  let nl = makeSmallCluster node count
821
      reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
822
  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>= \allocn ->
823
     Cluster.iterateAlloc nl Container.empty (Just maxiter) inst allocn [] []
824
     of
825
       Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
826
       Types.Ok (_, _, _, [], _) -> printTestCase
827
                                    "Failed to allocate: no allocations" False
828
       Types.Ok (_, nl', il', _, _) ->
829
         let cdata = Loader.ClusterData defGroupList nl' il' ctags
830
                     Types.defIPolicy
831
             saved = Text.serializeCluster cdata
832
         in case Text.parseData saved >>= Loader.mergeData [] [] [] [] of
833
              Types.Bad msg -> failTest $ "Failed to load/merge: " ++ msg
834
              Types.Ok (Loader.ClusterData gl2 nl2 il2 ctags2 cpol2) ->
835
                ctags ==? ctags2 .&&.
836
                Types.defIPolicy ==? cpol2 .&&.
837
                il' ==? il2 .&&.
838
                defGroupList ==? gl2 .&&.
839
                nl' ==? nl2
840

    
841
testSuite "Text"
842
            [ 'prop_Text_Load_Instance
843
            , 'prop_Text_Load_InstanceFail
844
            , 'prop_Text_Load_Node
845
            , 'prop_Text_Load_NodeFail
846
            , 'prop_Text_NodeLSIdempotent
847
            , 'prop_Text_ISpecIdempotent
848
            , 'prop_Text_IPolicyIdempotent
849
            , 'prop_Text_CreateSerialise
850
            ]
851

    
852
-- *** Simu backend
853

    
854
-- | Generates a tuple of specs for simulation.
855
genSimuSpec :: Gen (String, Int, Int, Int, Int)
856
genSimuSpec = do
857
  pol <- elements [C.allocPolicyPreferred,
858
                   C.allocPolicyLastResort, C.allocPolicyUnallocable,
859
                  "p", "a", "u"]
860
 -- should be reasonable (nodes/group), bigger values only complicate
861
 -- the display of failed tests, and we don't care (in this particular
862
 -- test) about big node groups
863
  nodes <- choose (0, 20)
864
  dsk <- choose (0, maxDsk)
865
  mem <- choose (0, maxMem)
866
  cpu <- choose (0, maxCpu)
867
  return (pol, nodes, dsk, mem, cpu)
868

    
869
-- | Checks that given a set of corrects specs, we can load them
870
-- successfully, and that at high-level the values look right.
871
prop_Simu_Load :: Property
872
prop_Simu_Load =
873
  forAll (choose (0, 10)) $ \ngroups ->
874
  forAll (replicateM ngroups genSimuSpec) $ \specs ->
875
  let strspecs = map (\(p, n, d, m, c) -> printf "%s,%d,%d,%d,%d"
876
                                          p n d m c::String) specs
877
      totnodes = sum $ map (\(_, n, _, _, _) -> n) specs
878
      mdc_in = concatMap (\(_, n, d, m, c) ->
879
                            replicate n (fromIntegral m, fromIntegral d,
880
                                         fromIntegral c,
881
                                         fromIntegral m, fromIntegral d))
882
               specs :: [(Double, Double, Double, Int, Int)]
883
  in case Simu.parseData strspecs of
884
       Types.Bad msg -> failTest $ "Failed to load specs: " ++ msg
885
       Types.Ok (Loader.ClusterData gl nl il tags ipol) ->
886
         let nodes = map snd $ IntMap.toAscList nl
887
             nidx = map Node.idx nodes
888
             mdc_out = map (\n -> (Node.tMem n, Node.tDsk n, Node.tCpu n,
889
                                   Node.fMem n, Node.fDsk n)) nodes
890
         in
891
         Container.size gl ==? ngroups .&&.
892
         Container.size nl ==? totnodes .&&.
893
         Container.size il ==? 0 .&&.
894
         length tags ==? 0 .&&.
895
         ipol ==? Types.defIPolicy .&&.
896
         nidx ==? [1..totnodes] .&&.
897
         mdc_in ==? mdc_out .&&.
898
         map Group.iPolicy (Container.elems gl) ==?
899
             replicate ngroups Types.defIPolicy
900

    
901
testSuite "Simu"
902
            [ 'prop_Simu_Load
903
            ]
904

    
905
-- ** Node tests
906

    
907
prop_Node_setAlias :: Node.Node -> String -> Bool
908
prop_Node_setAlias node name =
909
  Node.name newnode == Node.name node &&
910
  Node.alias newnode == name
911
    where newnode = Node.setAlias node name
912

    
913
prop_Node_setOffline :: Node.Node -> Bool -> Property
914
prop_Node_setOffline node status =
915
  Node.offline newnode ==? status
916
    where newnode = Node.setOffline node status
917

    
918
prop_Node_setXmem :: Node.Node -> Int -> Property
919
prop_Node_setXmem node xm =
920
  Node.xMem newnode ==? xm
921
    where newnode = Node.setXmem node xm
922

    
923
prop_Node_setMcpu :: Node.Node -> Double -> Property
924
prop_Node_setMcpu node mc =
925
  Types.iPolicyVcpuRatio (Node.iPolicy newnode) ==? mc
926
    where newnode = Node.setMcpu node mc
927

    
928
-- | Check that an instance add with too high memory or disk will be
929
-- rejected.
930
prop_Node_addPriFM :: Node.Node -> Instance.Instance -> Property
931
prop_Node_addPriFM node inst =
932
  Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
933
  not (Instance.isOffline inst) ==>
934
  case Node.addPri node inst'' of
935
    Types.OpFail Types.FailMem -> True
936
    _ -> False
937
  where inst' = setInstanceSmallerThanNode node inst
938
        inst'' = inst' { Instance.mem = Instance.mem inst }
939

    
940
-- | Check that adding a primary instance with too much disk fails
941
-- with type FailDisk.
942
prop_Node_addPriFD :: Node.Node -> Instance.Instance -> Property
943
prop_Node_addPriFD node inst =
944
  forAll (elements Instance.localStorageTemplates) $ \dt ->
945
  Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
946
  let inst' = setInstanceSmallerThanNode node inst
947
      inst'' = inst' { Instance.dsk = Instance.dsk inst
948
                     , Instance.diskTemplate = dt }
949
  in case Node.addPri node inst'' of
950
       Types.OpFail Types.FailDisk -> True
951
       _ -> False
952

    
953
-- | Check that adding a primary instance with too many VCPUs fails
954
-- with type FailCPU.
955
prop_Node_addPriFC :: Property
956
prop_Node_addPriFC =
957
  forAll (choose (1, maxCpu)) $ \extra ->
958
  forAll genOnlineNode $ \node ->
959
  forAll (arbitrary `suchThat` Instance.notOffline) $ \inst ->
960
  let inst' = setInstanceSmallerThanNode node inst
961
      inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
962
  in case Node.addPri node inst'' of
963
       Types.OpFail Types.FailCPU -> property True
964
       v -> failTest $ "Expected OpFail FailCPU, but got " ++ show v
965

    
966
-- | Check that an instance add with too high memory or disk will be
967
-- rejected.
968
prop_Node_addSec :: Node.Node -> Instance.Instance -> Int -> Property
969
prop_Node_addSec node inst pdx =
970
  ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
971
    not (Instance.isOffline inst)) ||
972
   Instance.dsk inst >= Node.fDsk node) &&
973
  not (Node.failN1 node) ==>
974
      isFailure (Node.addSec node inst pdx)
975

    
976
-- | Check that an offline instance with reasonable disk size but
977
-- extra mem/cpu can always be added.
978
prop_Node_addOfflinePri :: NonNegative Int -> NonNegative Int -> Property
979
prop_Node_addOfflinePri (NonNegative extra_mem) (NonNegative extra_cpu) =
980
  forAll genOnlineNode $ \node ->
981
  forAll (genInstanceSmallerThanNode node) $ \inst ->
982
  let inst' = inst { Instance.runSt = Types.AdminOffline
983
                   , Instance.mem = Node.availMem node + extra_mem
984
                   , Instance.vcpus = Node.availCpu node + extra_cpu }
985
  in case Node.addPri node inst' of
986
       Types.OpGood _ -> property True
987
       v -> failTest $ "Expected OpGood, but got: " ++ show v
988

    
989
-- | Check that an offline instance with reasonable disk size but
990
-- extra mem/cpu can always be added.
991
prop_Node_addOfflineSec :: NonNegative Int -> NonNegative Int
992
                        -> Types.Ndx -> Property
993
prop_Node_addOfflineSec (NonNegative extra_mem) (NonNegative extra_cpu) pdx =
994
  forAll genOnlineNode $ \node ->
995
  forAll (genInstanceSmallerThanNode node) $ \inst ->
996
  let inst' = inst { Instance.runSt = Types.AdminOffline
997
                   , Instance.mem = Node.availMem node + extra_mem
998
                   , Instance.vcpus = Node.availCpu node + extra_cpu
999
                   , Instance.diskTemplate = Types.DTDrbd8 }
1000
  in case Node.addSec node inst' pdx of
1001
       Types.OpGood _ -> property True
1002
       v -> failTest $ "Expected OpGood/OpGood, but got: " ++ show v
1003

    
1004
-- | Checks for memory reservation changes.
1005
prop_Node_rMem :: Instance.Instance -> Property
1006
prop_Node_rMem inst =
1007
  not (Instance.isOffline inst) ==>
1008
  forAll (genOnlineNode `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
1009
  -- ab = auto_balance, nb = non-auto_balance
1010
  -- we use -1 as the primary node of the instance
1011
  let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True
1012
                   , Instance.diskTemplate = Types.DTDrbd8 }
1013
      inst_ab = setInstanceSmallerThanNode node inst'
1014
      inst_nb = inst_ab { Instance.autoBalance = False }
1015
      -- now we have the two instances, identical except the
1016
      -- autoBalance attribute
1017
      orig_rmem = Node.rMem node
1018
      inst_idx = Instance.idx inst_ab
1019
      node_add_ab = Node.addSec node inst_ab (-1)
1020
      node_add_nb = Node.addSec node inst_nb (-1)
1021
      node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
1022
      node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
1023
  in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
1024
       (Types.OpGood a_ab, Types.OpGood a_nb,
1025
        Types.OpGood d_ab, Types.OpGood d_nb) ->
1026
         printTestCase "Consistency checks failed" $
1027
           Node.rMem a_ab >  orig_rmem &&
1028
           Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
1029
           Node.rMem a_nb == orig_rmem &&
1030
           Node.rMem d_ab == orig_rmem &&
1031
           Node.rMem d_nb == orig_rmem &&
1032
           -- this is not related to rMem, but as good a place to
1033
           -- test as any
1034
           inst_idx `elem` Node.sList a_ab &&
1035
           inst_idx `notElem` Node.sList d_ab
1036
       x -> failTest $ "Failed to add/remove instances: " ++ show x
1037

    
1038
-- | Check mdsk setting.
1039
prop_Node_setMdsk :: Node.Node -> SmallRatio -> Bool
1040
prop_Node_setMdsk node mx =
1041
  Node.loDsk node' >= 0 &&
1042
  fromIntegral (Node.loDsk node') <= Node.tDsk node &&
1043
  Node.availDisk node' >= 0 &&
1044
  Node.availDisk node' <= Node.fDsk node' &&
1045
  fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
1046
  Node.mDsk node' == mx'
1047
    where node' = Node.setMdsk node mx'
1048
          SmallRatio mx' = mx
1049

    
1050
-- Check tag maps
1051
prop_Node_tagMaps_idempotent :: Property
1052
prop_Node_tagMaps_idempotent =
1053
  forAll genTags $ \tags ->
1054
  Node.delTags (Node.addTags m tags) tags ==? m
1055
    where m = Map.empty
1056

    
1057
prop_Node_tagMaps_reject :: Property
1058
prop_Node_tagMaps_reject =
1059
  forAll (genTags `suchThat` (not . null)) $ \tags ->
1060
  let m = Node.addTags Map.empty tags
1061
  in all (\t -> Node.rejectAddTags m [t]) tags
1062

    
1063
prop_Node_showField :: Node.Node -> Property
1064
prop_Node_showField node =
1065
  forAll (elements Node.defaultFields) $ \ field ->
1066
  fst (Node.showHeader field) /= Types.unknownField &&
1067
  Node.showField node field /= Types.unknownField
1068

    
1069
prop_Node_computeGroups :: [Node.Node] -> Bool
1070
prop_Node_computeGroups nodes =
1071
  let ng = Node.computeGroups nodes
1072
      onlyuuid = map fst ng
1073
  in length nodes == sum (map (length . snd) ng) &&
1074
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
1075
     length (nub onlyuuid) == length onlyuuid &&
1076
     (null nodes || not (null ng))
1077

    
1078
-- Check idempotence of add/remove operations
1079
prop_Node_addPri_idempotent :: Property
1080
prop_Node_addPri_idempotent =
1081
  forAll genOnlineNode $ \node ->
1082
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1083
  case Node.addPri node inst of
1084
    Types.OpGood node' -> Node.removePri node' inst ==? node
1085
    _ -> failTest "Can't add instance"
1086

    
1087
prop_Node_addSec_idempotent :: Property
1088
prop_Node_addSec_idempotent =
1089
  forAll genOnlineNode $ \node ->
1090
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1091
  let pdx = Node.idx node + 1
1092
      inst' = Instance.setPri inst pdx
1093
      inst'' = inst' { Instance.diskTemplate = Types.DTDrbd8 }
1094
  in case Node.addSec node inst'' pdx of
1095
       Types.OpGood node' -> Node.removeSec node' inst'' ==? node
1096
       _ -> failTest "Can't add instance"
1097

    
1098
testSuite "Node"
1099
            [ 'prop_Node_setAlias
1100
            , 'prop_Node_setOffline
1101
            , 'prop_Node_setMcpu
1102
            , 'prop_Node_setXmem
1103
            , 'prop_Node_addPriFM
1104
            , 'prop_Node_addPriFD
1105
            , 'prop_Node_addPriFC
1106
            , 'prop_Node_addSec
1107
            , 'prop_Node_addOfflinePri
1108
            , 'prop_Node_addOfflineSec
1109
            , 'prop_Node_rMem
1110
            , 'prop_Node_setMdsk
1111
            , 'prop_Node_tagMaps_idempotent
1112
            , 'prop_Node_tagMaps_reject
1113
            , 'prop_Node_showField
1114
            , 'prop_Node_computeGroups
1115
            , 'prop_Node_addPri_idempotent
1116
            , 'prop_Node_addSec_idempotent
1117
            ]
1118

    
1119
-- ** Cluster tests
1120

    
1121
-- | Check that the cluster score is close to zero for a homogeneous
1122
-- cluster.
1123
prop_Cluster_Score_Zero :: Node.Node -> Property
1124
prop_Cluster_Score_Zero node =
1125
  forAll (choose (1, 1024)) $ \count ->
1126
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
1127
     (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
1128
  let fn = Node.buildPeers node Container.empty
1129
      nlst = replicate count fn
1130
      score = Cluster.compCVNodes nlst
1131
  -- we can't say == 0 here as the floating point errors accumulate;
1132
  -- this should be much lower than the default score in CLI.hs
1133
  in score <= 1e-12
1134

    
1135
-- | Check that cluster stats are sane.
1136
prop_Cluster_CStats_sane :: Property
1137
prop_Cluster_CStats_sane =
1138
  forAll (choose (1, 1024)) $ \count ->
1139
  forAll genOnlineNode $ \node ->
1140
  let fn = Node.buildPeers node Container.empty
1141
      nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
1142
      nl = Container.fromList nlst
1143
      cstats = Cluster.totalResources nl
1144
  in Cluster.csAdsk cstats >= 0 &&
1145
     Cluster.csAdsk cstats <= Cluster.csFdsk cstats
1146

    
1147
-- | Check that one instance is allocated correctly, without
1148
-- rebalances needed.
1149
prop_Cluster_Alloc_sane :: Instance.Instance -> Property
1150
prop_Cluster_Alloc_sane inst =
1151
  forAll (choose (5, 20)) $ \count ->
1152
  forAll genOnlineNode $ \node ->
1153
  let (nl, il, inst') = makeSmallEmptyCluster node count inst
1154
      reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
1155
  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1156
     Cluster.tryAlloc nl il inst' of
1157
       Types.Bad _ -> False
1158
       Types.Ok as ->
1159
         case Cluster.asSolution as of
1160
           Nothing -> False
1161
           Just (xnl, xi, _, cv) ->
1162
             let il' = Container.add (Instance.idx xi) xi il
1163
                 tbl = Cluster.Table xnl il' cv []
1164
             in not (canBalance tbl True True False)
1165

    
1166
-- | Checks that on a 2-5 node cluster, we can allocate a random
1167
-- instance spec via tiered allocation (whatever the original instance
1168
-- spec), on either one or two nodes. Furthermore, we test that
1169
-- computed allocation statistics are correct.
1170
prop_Cluster_CanTieredAlloc :: Instance.Instance -> Property
1171
prop_Cluster_CanTieredAlloc inst =
1172
  forAll (choose (2, 5)) $ \count ->
1173
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1174
  let nl = makeSmallCluster node count
1175
      il = Container.empty
1176
      rqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
1177
      allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
1178
  in case allocnodes >>= \allocnodes' ->
1179
    Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
1180
       Types.Bad msg -> failTest $ "Failed to tiered alloc: " ++ msg
1181
       Types.Ok (_, nl', il', ixes, cstats) ->
1182
         let (ai_alloc, ai_pool, ai_unav) =
1183
               Cluster.computeAllocationDelta
1184
                (Cluster.totalResources nl)
1185
                (Cluster.totalResources nl')
1186
             all_nodes = Container.elems nl
1187
         in property (not (null ixes)) .&&.
1188
            IntMap.size il' ==? length ixes .&&.
1189
            length ixes ==? length cstats .&&.
1190
            sum (map Types.allocInfoVCpus [ai_alloc, ai_pool, ai_unav]) ==?
1191
              sum (map Node.hiCpu all_nodes) .&&.
1192
            sum (map Types.allocInfoNCpus [ai_alloc, ai_pool, ai_unav]) ==?
1193
              sum (map Node.tCpu all_nodes) .&&.
1194
            sum (map Types.allocInfoMem [ai_alloc, ai_pool, ai_unav]) ==?
1195
              truncate (sum (map Node.tMem all_nodes)) .&&.
1196
            sum (map Types.allocInfoDisk [ai_alloc, ai_pool, ai_unav]) ==?
1197
              truncate (sum (map Node.tDsk all_nodes))
1198

    
1199
-- | Helper function to create a cluster with the given range of nodes
1200
-- and allocate an instance on it.
1201
genClusterAlloc :: Int -> Node.Node -> Instance.Instance
1202
                -> Types.Result (Node.List, Instance.List, Instance.Instance)
1203
genClusterAlloc count node inst =
1204
  let nl = makeSmallCluster node count
1205
      reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
1206
  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1207
     Cluster.tryAlloc nl Container.empty inst of
1208
       Types.Bad _ -> Types.Bad "Can't allocate"
1209
       Types.Ok as ->
1210
         case Cluster.asSolution as of
1211
           Nothing -> Types.Bad "Empty solution?"
1212
           Just (xnl, xi, _, _) ->
1213
             let xil = Container.add (Instance.idx xi) xi Container.empty
1214
             in Types.Ok (xnl, xil, xi)
1215

    
1216
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
1217
-- we can also relocate it.
1218
prop_Cluster_AllocRelocate :: Property
1219
prop_Cluster_AllocRelocate =
1220
  forAll (choose (4, 8)) $ \count ->
1221
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1222
  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
1223
  case genClusterAlloc count node inst of
1224
    Types.Bad msg -> failTest msg
1225
    Types.Ok (nl, il, inst') ->
1226
      case IAlloc.processRelocate defGroupList nl il
1227
             (Instance.idx inst) 1
1228
             [(if Instance.diskTemplate inst' == Types.DTDrbd8
1229
                 then Instance.sNode
1230
                 else Instance.pNode) inst'] of
1231
        Types.Ok _ -> property True
1232
        Types.Bad msg -> failTest $ "Failed to relocate: " ++ msg
1233

    
1234
-- | Helper property checker for the result of a nodeEvac or
1235
-- changeGroup operation.
1236
check_EvacMode :: Group.Group -> Instance.Instance
1237
               -> Types.Result (Node.List, Instance.List, Cluster.EvacSolution)
1238
               -> Property
1239
check_EvacMode grp inst result =
1240
  case result of
1241
    Types.Bad msg -> failTest $ "Couldn't evacuate/change group:" ++ msg
1242
    Types.Ok (_, _, es) ->
1243
      let moved = Cluster.esMoved es
1244
          failed = Cluster.esFailed es
1245
          opcodes = not . null $ Cluster.esOpCodes es
1246
      in failmsg ("'failed' not empty: " ++ show failed) (null failed) .&&.
1247
         failmsg "'opcodes' is null" opcodes .&&.
1248
         case moved of
1249
           [(idx', gdx, _)] -> failmsg "invalid instance moved" (idx == idx')
1250
                               .&&.
1251
                               failmsg "wrong target group"
1252
                                         (gdx == Group.idx grp)
1253
           v -> failmsg  ("invalid solution: " ++ show v) False
1254
  where failmsg :: String -> Bool -> Property
1255
        failmsg = \msg -> printTestCase ("Failed to evacuate: " ++ msg)
1256
        idx = Instance.idx inst
1257

    
1258
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
1259
-- we can also node-evacuate it.
1260
prop_Cluster_AllocEvacuate :: Property
1261
prop_Cluster_AllocEvacuate =
1262
  forAll (choose (4, 8)) $ \count ->
1263
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1264
  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
1265
  case genClusterAlloc count node inst of
1266
    Types.Bad msg -> failTest msg
1267
    Types.Ok (nl, il, inst') ->
1268
      conjoin . map (\mode -> check_EvacMode defGroup inst' $
1269
                              Cluster.tryNodeEvac defGroupList nl il mode
1270
                                [Instance.idx inst']) .
1271
                              evacModeOptions .
1272
                              Instance.mirrorType $ inst'
1273

    
1274
-- | Checks that on a 4-8 node cluster with two node groups, once we
1275
-- allocate an instance on the first node group, we can also change
1276
-- its group.
1277
prop_Cluster_AllocChangeGroup :: Property
1278
prop_Cluster_AllocChangeGroup =
1279
  forAll (choose (4, 8)) $ \count ->
1280
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1281
  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
1282
  case genClusterAlloc count node inst of
1283
    Types.Bad msg -> failTest msg
1284
    Types.Ok (nl, il, inst') ->
1285
      -- we need to add a second node group and nodes to the cluster
1286
      let nl2 = Container.elems $ makeSmallCluster node count
1287
          grp2 = Group.setIdx defGroup (Group.idx defGroup + 1)
1288
          maxndx = maximum . map Node.idx $ nl2
1289
          nl3 = map (\n -> n { Node.group = Group.idx grp2
1290
                             , Node.idx = Node.idx n + maxndx }) nl2
1291
          nl4 = Container.fromList . map (\n -> (Node.idx n, n)) $ nl3
1292
          gl' = Container.add (Group.idx grp2) grp2 defGroupList
1293
          nl' = IntMap.union nl nl4
1294
      in check_EvacMode grp2 inst' $
1295
         Cluster.tryChangeGroup gl' nl' il [] [Instance.idx inst']
1296

    
1297
-- | Check that allocating multiple instances on a cluster, then
1298
-- adding an empty node, results in a valid rebalance.
1299
prop_Cluster_AllocBalance :: Property
1300
prop_Cluster_AllocBalance =
1301
  forAll (genNode (Just 5) (Just 128)) $ \node ->
1302
  forAll (choose (3, 5)) $ \count ->
1303
  not (Node.offline node) && not (Node.failN1 node) ==>
1304
  let nl = makeSmallCluster node count
1305
      (hnode, nl') = IntMap.deleteFindMax nl
1306
      il = Container.empty
1307
      allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
1308
      i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
1309
  in case allocnodes >>= \allocnodes' ->
1310
    Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
1311
       Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
1312
       Types.Ok (_, _, _, [], _) -> failTest "Failed to allocate: no instances"
1313
       Types.Ok (_, xnl, il', _, _) ->
1314
         let ynl = Container.add (Node.idx hnode) hnode xnl
1315
             cv = Cluster.compCV ynl
1316
             tbl = Cluster.Table ynl il' cv []
1317
         in printTestCase "Failed to rebalance" $
1318
            canBalance tbl True True False
1319

    
1320
-- | Checks consistency.
1321
prop_Cluster_CheckConsistency :: Node.Node -> Instance.Instance -> Bool
1322
prop_Cluster_CheckConsistency node inst =
1323
  let nl = makeSmallCluster node 3
1324
      [node1, node2, node3] = Container.elems nl
1325
      node3' = node3 { Node.group = 1 }
1326
      nl' = Container.add (Node.idx node3') node3' nl
1327
      inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
1328
      inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
1329
      inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
1330
      ccheck = Cluster.findSplitInstances nl' . Container.fromList
1331
  in null (ccheck [(0, inst1)]) &&
1332
     null (ccheck [(0, inst2)]) &&
1333
     (not . null $ ccheck [(0, inst3)])
1334

    
1335
-- | For now, we only test that we don't lose instances during the split.
1336
prop_Cluster_SplitCluster :: Node.Node -> Instance.Instance -> Property
1337
prop_Cluster_SplitCluster node inst =
1338
  forAll (choose (0, 100)) $ \icnt ->
1339
  let nl = makeSmallCluster node 2
1340
      (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
1341
                   (nl, Container.empty) [1..icnt]
1342
      gni = Cluster.splitCluster nl' il'
1343
  in sum (map (Container.size . snd . snd) gni) == icnt &&
1344
     all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
1345
                                 (Container.elems nl'')) gni
1346

    
1347
-- | Helper function to check if we can allocate an instance on a
1348
-- given node list.
1349
canAllocOn :: Node.List -> Int -> Instance.Instance -> Bool
1350
canAllocOn nl reqnodes inst =
1351
  case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1352
       Cluster.tryAlloc nl (Container.empty) inst of
1353
       Types.Bad _ -> False
1354
       Types.Ok as ->
1355
         case Cluster.asSolution as of
1356
           Nothing -> False
1357
           Just _ -> True
1358

    
1359
-- | Checks that allocation obeys minimum and maximum instance
1360
-- policies. The unittest generates a random node, duplicates it /count/
1361
-- times, and generates a random instance that can be allocated on
1362
-- this mini-cluster; it then checks that after applying a policy that
1363
-- the instance doesn't fits, the allocation fails.
1364
prop_Cluster_AllocPolicy :: Node.Node -> Property
1365
prop_Cluster_AllocPolicy node =
1366
  -- rqn is the required nodes (1 or 2)
1367
  forAll (choose (1, 2)) $ \rqn ->
1368
  forAll (choose (5, 20)) $ \count ->
1369
  forAll (arbitrary `suchThat` (canAllocOn (makeSmallCluster node count) rqn))
1370
         $ \inst ->
1371
  forAll (arbitrary `suchThat` (isFailure .
1372
                                Instance.instMatchesPolicy inst)) $ \ipol ->
1373
  let node' = Node.setPolicy ipol node
1374
      nl = makeSmallCluster node' count
1375
  in not $ canAllocOn nl rqn inst
1376

    
1377
testSuite "Cluster"
1378
            [ 'prop_Cluster_Score_Zero
1379
            , 'prop_Cluster_CStats_sane
1380
            , 'prop_Cluster_Alloc_sane
1381
            , 'prop_Cluster_CanTieredAlloc
1382
            , 'prop_Cluster_AllocRelocate
1383
            , 'prop_Cluster_AllocEvacuate
1384
            , 'prop_Cluster_AllocChangeGroup
1385
            , 'prop_Cluster_AllocBalance
1386
            , 'prop_Cluster_CheckConsistency
1387
            , 'prop_Cluster_SplitCluster
1388
            , 'prop_Cluster_AllocPolicy
1389
            ]
1390

    
1391
-- ** Jobs tests
1392

    
1393
-- | Check that (queued) job\/opcode status serialization is idempotent.
1394
prop_Jobs_OpStatus_serialization :: Jobs.OpStatus -> Property
1395
prop_Jobs_OpStatus_serialization os =
1396
  case J.readJSON (J.showJSON os) of
1397
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1398
    J.Ok os' -> os ==? os'
1399

    
1400
prop_Jobs_JobStatus_serialization :: Jobs.JobStatus -> Property
1401
prop_Jobs_JobStatus_serialization js =
1402
  case J.readJSON (J.showJSON js) of
1403
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1404
    J.Ok js' -> js ==? js'
1405

    
1406
testSuite "Jobs"
1407
            [ 'prop_Jobs_OpStatus_serialization
1408
            , 'prop_Jobs_JobStatus_serialization
1409
            ]
1410

    
1411
-- ** Loader tests
1412

    
1413
prop_Loader_lookupNode :: [(String, Int)] -> String -> String -> Property
1414
prop_Loader_lookupNode ktn inst node =
1415
  Loader.lookupNode nl inst node ==? Map.lookup node nl
1416
    where nl = Map.fromList ktn
1417

    
1418
prop_Loader_lookupInstance :: [(String, Int)] -> String -> Property
1419
prop_Loader_lookupInstance kti inst =
1420
  Loader.lookupInstance il inst ==? Map.lookup inst il
1421
    where il = Map.fromList kti
1422

    
1423
prop_Loader_assignIndices :: Property
1424
prop_Loader_assignIndices =
1425
  -- generate nodes with unique names
1426
  forAll (arbitrary `suchThat`
1427
          (\nodes ->
1428
             let names = map Node.name nodes
1429
             in length names == length (nub names))) $ \nodes ->
1430
  let (nassoc, kt) =
1431
        Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1432
  in Map.size nassoc == length nodes &&
1433
     Container.size kt == length nodes &&
1434
     if not (null nodes)
1435
       then maximum (IntMap.keys kt) == length nodes - 1
1436
       else True
1437

    
1438
-- | Checks that the number of primary instances recorded on the nodes
1439
-- is zero.
1440
prop_Loader_mergeData :: [Node.Node] -> Bool
1441
prop_Loader_mergeData ns =
1442
  let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1443
  in case Loader.mergeData [] [] [] []
1444
         (Loader.emptyCluster {Loader.cdNodes = na}) of
1445
    Types.Bad _ -> False
1446
    Types.Ok (Loader.ClusterData _ nl il _ _) ->
1447
      let nodes = Container.elems nl
1448
          instances = Container.elems il
1449
      in (sum . map (length . Node.pList)) nodes == 0 &&
1450
         null instances
1451

    
1452
-- | Check that compareNameComponent on equal strings works.
1453
prop_Loader_compareNameComponent_equal :: String -> Bool
1454
prop_Loader_compareNameComponent_equal s =
1455
  BasicTypes.compareNameComponent s s ==
1456
    BasicTypes.LookupResult BasicTypes.ExactMatch s
1457

    
1458
-- | Check that compareNameComponent on prefix strings works.
1459
prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1460
prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1461
  BasicTypes.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1462
    BasicTypes.LookupResult BasicTypes.PartialMatch s1
1463

    
1464
testSuite "Loader"
1465
            [ 'prop_Loader_lookupNode
1466
            , 'prop_Loader_lookupInstance
1467
            , 'prop_Loader_assignIndices
1468
            , 'prop_Loader_mergeData
1469
            , 'prop_Loader_compareNameComponent_equal
1470
            , 'prop_Loader_compareNameComponent_prefix
1471
            ]
1472

    
1473
-- ** Types tests
1474

    
1475
prop_Types_AllocPolicy_serialisation :: Types.AllocPolicy -> Property
1476
prop_Types_AllocPolicy_serialisation apol =
1477
  case J.readJSON (J.showJSON apol) of
1478
    J.Ok p -> p ==? apol
1479
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1480

    
1481
prop_Types_DiskTemplate_serialisation :: Types.DiskTemplate -> Property
1482
prop_Types_DiskTemplate_serialisation dt =
1483
  case J.readJSON (J.showJSON dt) of
1484
    J.Ok p -> p ==? dt
1485
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1486

    
1487
prop_Types_ISpec_serialisation :: Types.ISpec -> Property
1488
prop_Types_ISpec_serialisation ispec =
1489
  case J.readJSON (J.showJSON ispec) of
1490
    J.Ok p -> p ==? ispec
1491
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1492

    
1493
prop_Types_IPolicy_serialisation :: Types.IPolicy -> Property
1494
prop_Types_IPolicy_serialisation ipol =
1495
  case J.readJSON (J.showJSON ipol) of
1496
    J.Ok p -> p ==? ipol
1497
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1498

    
1499
prop_Types_EvacMode_serialisation :: Types.EvacMode -> Property
1500
prop_Types_EvacMode_serialisation em =
1501
  case J.readJSON (J.showJSON em) of
1502
    J.Ok p -> p ==? em
1503
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1504

    
1505
prop_Types_opToResult :: Types.OpResult Int -> Bool
1506
prop_Types_opToResult op =
1507
  case op of
1508
    Types.OpFail _ -> Types.isBad r
1509
    Types.OpGood v -> case r of
1510
                        Types.Bad _ -> False
1511
                        Types.Ok v' -> v == v'
1512
  where r = Types.opToResult op
1513

    
1514
prop_Types_eitherToResult :: Either String Int -> Bool
1515
prop_Types_eitherToResult ei =
1516
  case ei of
1517
    Left _ -> Types.isBad r
1518
    Right v -> case r of
1519
                 Types.Bad _ -> False
1520
                 Types.Ok v' -> v == v'
1521
    where r = Types.eitherToResult ei
1522

    
1523
testSuite "Types"
1524
            [ 'prop_Types_AllocPolicy_serialisation
1525
            , 'prop_Types_DiskTemplate_serialisation
1526
            , 'prop_Types_ISpec_serialisation
1527
            , 'prop_Types_IPolicy_serialisation
1528
            , 'prop_Types_EvacMode_serialisation
1529
            , 'prop_Types_opToResult
1530
            , 'prop_Types_eitherToResult
1531
            ]
1532

    
1533
-- ** CLI tests
1534

    
1535
-- | Test correct parsing.
1536
prop_CLI_parseISpec :: String -> Int -> Int -> Int -> Property
1537
prop_CLI_parseISpec descr dsk mem cpu =
1538
  let str = printf "%d,%d,%d" dsk mem cpu::String
1539
  in CLI.parseISpecString descr str ==? Types.Ok (Types.RSpec cpu mem dsk)
1540

    
1541
-- | Test parsing failure due to wrong section count.
1542
prop_CLI_parseISpecFail :: String -> Property
1543
prop_CLI_parseISpecFail descr =
1544
  forAll (choose (0,100) `suchThat` ((/=) 3)) $ \nelems ->
1545
  forAll (replicateM nelems arbitrary) $ \values ->
1546
  let str = intercalate "," $ map show (values::[Int])
1547
  in case CLI.parseISpecString descr str of
1548
       Types.Ok v -> failTest $ "Expected failure, got " ++ show v
1549
       _ -> property True
1550

    
1551
-- | Test parseYesNo.
1552
prop_CLI_parseYesNo :: Bool -> Bool -> [Char] -> Property
1553
prop_CLI_parseYesNo def testval val =
1554
  forAll (elements [val, "yes", "no"]) $ \actual_val ->
1555
  if testval
1556
    then CLI.parseYesNo def Nothing ==? Types.Ok def
1557
    else let result = CLI.parseYesNo def (Just actual_val)
1558
         in if actual_val `elem` ["yes", "no"]
1559
              then result ==? Types.Ok (actual_val == "yes")
1560
              else property $ Types.isBad result
1561

    
1562
-- | Helper to check for correct parsing of string arg.
1563
checkStringArg :: [Char]
1564
               -> (GetOpt.OptDescr (CLI.Options -> Types.Result CLI.Options),
1565
                   CLI.Options -> Maybe [Char])
1566
               -> Property
1567
checkStringArg val (opt, fn) =
1568
  let GetOpt.Option _ longs _ _ = opt
1569
  in case longs of
1570
       [] -> failTest "no long options?"
1571
       cmdarg:_ ->
1572
         case CLI.parseOptsInner ["--" ++ cmdarg ++ "=" ++ val] "prog" [opt] of
1573
           Left e -> failTest $ "Failed to parse option: " ++ show e
1574
           Right (options, _) -> fn options ==? Just val
1575

    
1576
-- | Test a few string arguments.
1577
prop_CLI_StringArg :: [Char] -> Property
1578
prop_CLI_StringArg argument =
1579
  let args = [ (CLI.oDataFile,      CLI.optDataFile)
1580
             , (CLI.oDynuFile,      CLI.optDynuFile)
1581
             , (CLI.oSaveCluster,   CLI.optSaveCluster)
1582
             , (CLI.oReplay,        CLI.optReplay)
1583
             , (CLI.oPrintCommands, CLI.optShowCmds)
1584
             , (CLI.oLuxiSocket,    CLI.optLuxi)
1585
             ]
1586
  in conjoin $ map (checkStringArg argument) args
1587

    
1588
-- | Helper to test that a given option is accepted OK with quick exit.
1589
checkEarlyExit :: String -> [CLI.OptType] -> String -> Property
1590
checkEarlyExit name options param =
1591
  case CLI.parseOptsInner [param] name options of
1592
    Left (code, _) -> if code == 0
1593
                          then property True
1594
                          else failTest $ "Program " ++ name ++
1595
                                 " returns invalid code " ++ show code ++
1596
                                 " for option " ++ param
1597
    _ -> failTest $ "Program " ++ name ++ " doesn't consider option " ++
1598
         param ++ " as early exit one"
1599

    
1600
-- | Test that all binaries support some common options. There is
1601
-- nothing actually random about this test...
1602
prop_CLI_stdopts :: Property
1603
prop_CLI_stdopts =
1604
  let params = ["-h", "--help", "-V", "--version"]
1605
      opts = map (\(name, (_, o)) -> (name, o)) Program.personalities
1606
      -- apply checkEarlyExit across the cartesian product of params and opts
1607
  in conjoin [checkEarlyExit n o p | p <- params, (n, o) <- opts]
1608

    
1609
testSuite "CLI"
1610
          [ 'prop_CLI_parseISpec
1611
          , 'prop_CLI_parseISpecFail
1612
          , 'prop_CLI_parseYesNo
1613
          , 'prop_CLI_StringArg
1614
          , 'prop_CLI_stdopts
1615
          ]
1616

    
1617
-- * JSON tests
1618

    
1619
prop_JSON_toArray :: [Int] -> Property
1620
prop_JSON_toArray intarr =
1621
  let arr = map J.showJSON intarr in
1622
  case JSON.toArray (J.JSArray arr) of
1623
    Types.Ok arr' -> arr ==? arr'
1624
    Types.Bad err -> failTest $ "Failed to parse array: " ++ err
1625

    
1626
prop_JSON_toArrayFail :: Int -> String -> Bool -> Property
1627
prop_JSON_toArrayFail i s b =
1628
  -- poor man's instance Arbitrary JSValue
1629
  forAll (elements [J.showJSON i, J.showJSON s, J.showJSON b]) $ \item ->
1630
  case JSON.toArray item of
1631
    Types.Bad _ -> property True
1632
    Types.Ok result -> failTest $ "Unexpected parse, got " ++ show result
1633

    
1634
testSuite "JSON"
1635
          [ 'prop_JSON_toArray
1636
          , 'prop_JSON_toArrayFail
1637
          ]