Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (62.3 kB)

1
{-# LANGUAGE TemplateHaskell #-}
2

    
3
{-| Unittests for ganeti-htools.
4

    
5
-}
6

    
7
{-
8

    
9
Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
10

    
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.
15

    
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.
20

    
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
24
02110-1301, USA.
25

    
26
-}
27

    
28
module Ganeti.HTools.QC
29
  ( testUtils
30
  , testPeerMap
31
  , testContainer
32
  , testInstance
33
  , testNode
34
  , testText
35
  , testSimu
36
  , testOpCodes
37
  , testJobs
38
  , testCluster
39
  , testLoader
40
  , testTypes
41
  , testCLI
42
  ) where
43

    
44
import Test.QuickCheck
45
import Text.Printf (printf)
46
import Data.List (findIndex, intercalate, nub, isPrefixOf)
47
import qualified Data.Set as Set
48
import Data.Maybe
49
import Control.Monad
50
import qualified System.Console.GetOpt as GetOpt
51
import qualified Text.JSON as J
52
import qualified Data.Map
53
import qualified Data.IntMap as IntMap
54

    
55
import qualified Ganeti.OpCodes as OpCodes
56
import qualified Ganeti.Jobs as Jobs
57
import qualified Ganeti.Luxi
58
import qualified Ganeti.HTools.CLI as CLI
59
import qualified Ganeti.HTools.Cluster as Cluster
60
import qualified Ganeti.HTools.Container as Container
61
import qualified Ganeti.HTools.ExtLoader
62
import qualified Ganeti.HTools.IAlloc as IAlloc
63
import qualified Ganeti.HTools.Instance as Instance
64
import qualified Ganeti.HTools.JSON as JSON
65
import qualified Ganeti.HTools.Loader as Loader
66
import qualified Ganeti.HTools.Luxi
67
import qualified Ganeti.HTools.Node as Node
68
import qualified Ganeti.HTools.Group as Group
69
import qualified Ganeti.HTools.PeerMap as PeerMap
70
import qualified Ganeti.HTools.Rapi
71
import qualified Ganeti.HTools.Simu as Simu
72
import qualified Ganeti.HTools.Text as Text
73
import qualified Ganeti.HTools.Types as Types
74
import qualified Ganeti.HTools.Utils as Utils
75
import qualified Ganeti.HTools.Version
76
import qualified Ganeti.Constants as C
77

    
78
import qualified Ganeti.HTools.Program as Program
79
import qualified Ganeti.HTools.Program.Hail
80
import qualified Ganeti.HTools.Program.Hbal
81
import qualified Ganeti.HTools.Program.Hscan
82
import qualified Ganeti.HTools.Program.Hspace
83

    
84
import Ganeti.HTools.QCHelper (testSuite)
85

    
86
-- * Constants
87

    
88
-- | Maximum memory (1TiB, somewhat random value).
89
maxMem :: Int
90
maxMem = 1024 * 1024
91

    
92
-- | Maximum disk (8TiB, somewhat random value).
93
maxDsk :: Int
94
maxDsk = 1024 * 1024 * 8
95

    
96
-- | Max CPUs (1024, somewhat random value).
97
maxCpu :: Int
98
maxCpu = 1024
99

    
100
-- | Max vcpu ratio (random value).
101
maxVcpuRatio :: Double
102
maxVcpuRatio = 1024.0
103

    
104
-- | Max spindle ratio (random value).
105
maxSpindleRatio :: Double
106
maxSpindleRatio = 1024.0
107

    
108
-- | All disk templates (used later)
109
allDiskTemplates :: [Types.DiskTemplate]
110
allDiskTemplates = [minBound..maxBound]
111

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

    
141

    
142
defGroup :: Group.Group
143
defGroup = flip Group.setIdx 0 $
144
             Group.create "default" Types.defaultGroupID Types.AllocPreferred
145
                  nullIPolicy
146

    
147
defGroupList :: Group.List
148
defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
149

    
150
defGroupAssoc :: Data.Map.Map String Types.Gdx
151
defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
152

    
153
-- * Helper functions
154

    
155
-- | Simple checker for whether OpResult is fail or pass.
156
isFailure :: Types.OpResult a -> Bool
157
isFailure (Types.OpFail _) = True
158
isFailure _ = False
159

    
160
-- | Checks for equality with proper annotation.
161
(==?) :: (Show a, Eq a) => a -> a -> Property
162
(==?) x y = printTestCase
163
            ("Expected equality, but '" ++
164
             show x ++ "' /= '" ++ show y ++ "'") (x == y)
165
infix 3 ==?
166

    
167
-- | Show a message and fail the test.
168
failTest :: String -> Property
169
failTest msg = printTestCase msg False
170

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

    
178
-- | Create an instance given its spec.
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
-- * Arbitrary instances
249

    
250
-- | Defines a DNS name.
251
newtype DNSChar = DNSChar { dnsGetChar::Char }
252

    
253
instance Arbitrary DNSChar where
254
  arbitrary = do
255
    x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
256
    return (DNSChar x)
257

    
258
-- | Generates a single name component.
259
getName :: Gen String
260
getName = do
261
  n <- choose (1, 64)
262
  dn <- vector n::Gen [DNSChar]
263
  return (map dnsGetChar dn)
264

    
265
-- | Generates an entire FQDN.
266
getFQDN :: Gen String
267
getFQDN = do
268
  ncomps <- choose (1, 4)
269
  names <- mapM (const getName) [1..ncomps::Int]
270
  return $ intercalate "." names
271

    
272
-- | Defines a tag type.
273
newtype TagChar = TagChar { tagGetChar :: Char }
274

    
275
-- | All valid tag chars. This doesn't need to match _exactly_
276
-- Ganeti's own tag regex, just enough for it to be close.
277
tagChar :: [Char]
278
tagChar = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ ".+*/:@-"
279

    
280
instance Arbitrary TagChar where
281
  arbitrary = do
282
    c <- elements tagChar
283
    return (TagChar c)
284

    
285
-- | Generates a tag
286
genTag :: Gen [TagChar]
287
genTag = do
288
  -- the correct value would be C.maxTagLen, but that's way too
289
  -- verbose in unittests, and at the moment I don't see any possible
290
  -- bugs with longer tags and the way we use tags in htools
291
  n <- choose (1, 10)
292
  vector n
293

    
294
-- | Generates a list of tags (correctly upper bounded).
295
genTags :: Gen [String]
296
genTags = do
297
  -- the correct value would be C.maxTagsPerObj, but per the comment
298
  -- in genTag, we don't use tags enough in htools to warrant testing
299
  -- such big values
300
  n <- choose (0, 10::Int)
301
  tags <- mapM (const genTag) [1..n]
302
  return $ map (map tagGetChar) tags
303

    
304
instance Arbitrary Types.InstanceStatus where
305
    arbitrary = elements [minBound..maxBound]
306

    
307
-- | Generates a random instance with maximum disk/mem/cpu values.
308
genInstanceSmallerThan :: Int -> Int -> Int -> Gen Instance.Instance
309
genInstanceSmallerThan lim_mem lim_dsk lim_cpu = do
310
  name <- getFQDN
311
  mem <- choose (0, lim_mem)
312
  dsk <- choose (0, lim_dsk)
313
  run_st <- arbitrary
314
  pn <- arbitrary
315
  sn <- arbitrary
316
  vcpus <- choose (0, lim_cpu)
317
  dt <- arbitrary
318
  return $ Instance.create name mem dsk vcpus run_st [] True pn sn dt 1
319

    
320
-- | Generates an instance smaller than a node.
321
genInstanceSmallerThanNode :: Node.Node -> Gen Instance.Instance
322
genInstanceSmallerThanNode node =
323
  genInstanceSmallerThan (Node.availMem node `div` 2)
324
                         (Node.availDisk node `div` 2)
325
                         (Node.availCpu node `div` 2)
326

    
327
-- let's generate a random instance
328
instance Arbitrary Instance.Instance where
329
  arbitrary = genInstanceSmallerThan maxMem maxDsk maxCpu
330

    
331
-- | Generas an arbitrary node based on sizing information.
332
genNode :: Maybe Int -- ^ Minimum node size in terms of units
333
        -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
334
                     -- just by the max... constants)
335
        -> Gen Node.Node
336
genNode min_multiplier max_multiplier = do
337
  let (base_mem, base_dsk, base_cpu) =
338
        case min_multiplier of
339
          Just mm -> (mm * Types.unitMem,
340
                      mm * Types.unitDsk,
341
                      mm * Types.unitCpu)
342
          Nothing -> (0, 0, 0)
343
      (top_mem, top_dsk, top_cpu)  =
344
        case max_multiplier of
345
          Just mm -> (mm * Types.unitMem,
346
                      mm * Types.unitDsk,
347
                      mm * Types.unitCpu)
348
          Nothing -> (maxMem, maxDsk, maxCpu)
349
  name  <- getFQDN
350
  mem_t <- choose (base_mem, top_mem)
351
  mem_f <- choose (base_mem, mem_t)
352
  mem_n <- choose (0, mem_t - mem_f)
353
  dsk_t <- choose (base_dsk, top_dsk)
354
  dsk_f <- choose (base_dsk, dsk_t)
355
  cpu_t <- choose (base_cpu, top_cpu)
356
  offl  <- arbitrary
357
  let n = Node.create name (fromIntegral mem_t) mem_n mem_f
358
          (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 1 0
359
      n' = Node.setPolicy nullIPolicy n
360
  return $ Node.buildPeers n' Container.empty
361

    
362
-- | Helper function to generate a sane node.
363
genOnlineNode :: Gen Node.Node
364
genOnlineNode = do
365
  arbitrary `suchThat` (\n -> not (Node.offline n) &&
366
                              not (Node.failN1 n) &&
367
                              Node.availDisk n > 0 &&
368
                              Node.availMem n > 0 &&
369
                              Node.availCpu n > 0)
370

    
371
-- and a random node
372
instance Arbitrary Node.Node where
373
  arbitrary = genNode Nothing Nothing
374

    
375
-- replace disks
376
instance Arbitrary OpCodes.ReplaceDisksMode where
377
  arbitrary = elements [minBound..maxBound]
378

    
379
instance Arbitrary OpCodes.OpCode where
380
  arbitrary = do
381
    op_id <- elements [ "OP_TEST_DELAY"
382
                      , "OP_INSTANCE_REPLACE_DISKS"
383
                      , "OP_INSTANCE_FAILOVER"
384
                      , "OP_INSTANCE_MIGRATE"
385
                      ]
386
    case op_id of
387
      "OP_TEST_DELAY" ->
388
        liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
389
      "OP_INSTANCE_REPLACE_DISKS" ->
390
        liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
391
          arbitrary arbitrary arbitrary
392
      "OP_INSTANCE_FAILOVER" ->
393
        liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
394
          arbitrary
395
      "OP_INSTANCE_MIGRATE" ->
396
        liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
397
          arbitrary arbitrary arbitrary
398
      _ -> fail "Wrong opcode"
399

    
400
instance Arbitrary Jobs.OpStatus where
401
  arbitrary = elements [minBound..maxBound]
402

    
403
instance Arbitrary Jobs.JobStatus where
404
  arbitrary = elements [minBound..maxBound]
405

    
406
newtype SmallRatio = SmallRatio Double deriving Show
407
instance Arbitrary SmallRatio where
408
  arbitrary = do
409
    v <- choose (0, 1)
410
    return $ SmallRatio v
411

    
412
instance Arbitrary Types.AllocPolicy where
413
  arbitrary = elements [minBound..maxBound]
414

    
415
instance Arbitrary Types.DiskTemplate where
416
  arbitrary = elements [minBound..maxBound]
417

    
418
instance Arbitrary Types.FailMode where
419
  arbitrary = elements [minBound..maxBound]
420

    
421
instance Arbitrary Types.EvacMode where
422
  arbitrary = elements [minBound..maxBound]
423

    
424
instance Arbitrary a => Arbitrary (Types.OpResult a) where
425
  arbitrary = arbitrary >>= \c ->
426
              if c
427
                then liftM Types.OpGood arbitrary
428
                else liftM Types.OpFail arbitrary
429

    
430
instance Arbitrary Types.ISpec where
431
  arbitrary = do
432
    mem_s <- arbitrary::Gen (NonNegative Int)
433
    dsk_c <- arbitrary::Gen (NonNegative Int)
434
    dsk_s <- arbitrary::Gen (NonNegative Int)
435
    cpu_c <- arbitrary::Gen (NonNegative Int)
436
    nic_c <- arbitrary::Gen (NonNegative Int)
437
    su    <- arbitrary::Gen (NonNegative Int)
438
    return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
439
                       , Types.iSpecCpuCount   = fromIntegral cpu_c
440
                       , Types.iSpecDiskSize   = fromIntegral dsk_s
441
                       , Types.iSpecDiskCount  = fromIntegral dsk_c
442
                       , Types.iSpecNicCount   = fromIntegral nic_c
443
                       , Types.iSpecSpindleUse = fromIntegral su
444
                       }
445

    
446
-- | Generates an ispec bigger than the given one.
447
genBiggerISpec :: Types.ISpec -> Gen Types.ISpec
448
genBiggerISpec imin = do
449
  mem_s <- choose (Types.iSpecMemorySize imin, maxBound)
450
  dsk_c <- choose (Types.iSpecDiskCount imin, maxBound)
451
  dsk_s <- choose (Types.iSpecDiskSize imin, maxBound)
452
  cpu_c <- choose (Types.iSpecCpuCount imin, maxBound)
453
  nic_c <- choose (Types.iSpecNicCount imin, maxBound)
454
  su    <- choose (Types.iSpecSpindleUse imin, maxBound)
455
  return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
456
                     , Types.iSpecCpuCount   = fromIntegral cpu_c
457
                     , Types.iSpecDiskSize   = fromIntegral dsk_s
458
                     , Types.iSpecDiskCount  = fromIntegral dsk_c
459
                     , Types.iSpecNicCount   = fromIntegral nic_c
460
                     , Types.iSpecSpindleUse = fromIntegral su
461
                     }
462

    
463
instance Arbitrary Types.IPolicy where
464
  arbitrary = do
465
    imin <- arbitrary
466
    istd <- genBiggerISpec imin
467
    imax <- genBiggerISpec istd
468
    num_tmpl <- choose (0, length allDiskTemplates)
469
    dts  <- genUniquesList num_tmpl
470
    vcpu_ratio <- choose (1.0, maxVcpuRatio)
471
    spindle_ratio <- choose (1.0, maxSpindleRatio)
472
    return Types.IPolicy { Types.iPolicyMinSpec = imin
473
                         , Types.iPolicyStdSpec = istd
474
                         , Types.iPolicyMaxSpec = imax
475
                         , Types.iPolicyDiskTemplates = dts
476
                         , Types.iPolicyVcpuRatio = vcpu_ratio
477
                         , Types.iPolicySpindleRatio = spindle_ratio
478
                         }
479

    
480
-- * Actual tests
481

    
482
-- ** Utils tests
483

    
484
-- | Helper to generate a small string that doesn't contain commas.
485
genNonCommaString = do
486
  size <- choose (0, 20) -- arbitrary max size
487
  vectorOf size (arbitrary `suchThat` ((/=) ','))
488

    
489
-- | If the list is not just an empty element, and if the elements do
490
-- not contain commas, then join+split should be idempotent.
491
prop_Utils_commaJoinSplit =
492
  forAll (choose (0, 20)) $ \llen ->
493
  forAll (vectorOf llen genNonCommaString `suchThat` ((/=) [""])) $ \lst ->
494
  Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
495

    
496
-- | Split and join should always be idempotent.
497
prop_Utils_commaSplitJoin s =
498
  Utils.commaJoin (Utils.sepSplit ',' s) ==? s
499

    
500
-- | fromObjWithDefault, we test using the Maybe monad and an integer
501
-- value.
502
prop_Utils_fromObjWithDefault def_value random_key =
503
  -- a missing key will be returned with the default
504
  JSON.fromObjWithDefault [] random_key def_value == Just def_value &&
505
  -- a found key will be returned as is, not with default
506
  JSON.fromObjWithDefault [(random_key, J.showJSON def_value)]
507
       random_key (def_value+1) == Just def_value
508
    where _types = def_value :: Integer
509

    
510
-- | Test that functional if' behaves like the syntactic sugar if.
511
prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
512
prop_Utils_if'if cnd a b =
513
  Utils.if' cnd a b ==? if cnd then a else b
514

    
515
-- | Test basic select functionality
516
prop_Utils_select :: Int      -- ^ Default result
517
                  -> [Int]    -- ^ List of False values
518
                  -> [Int]    -- ^ List of True values
519
                  -> Gen Prop -- ^ Test result
520
prop_Utils_select def lst1 lst2 =
521
  Utils.select def (flist ++ tlist) ==? expectedresult
522
    where expectedresult = Utils.if' (null lst2) def (head lst2)
523
          flist = zip (repeat False) lst1
524
          tlist = zip (repeat True)  lst2
525

    
526
-- | Test basic select functionality with undefined default
527
prop_Utils_select_undefd :: [Int]            -- ^ List of False values
528
                         -> NonEmptyList Int -- ^ List of True values
529
                         -> Gen Prop         -- ^ Test result
530
prop_Utils_select_undefd lst1 (NonEmpty lst2) =
531
  Utils.select undefined (flist ++ tlist) ==? head lst2
532
    where flist = zip (repeat False) lst1
533
          tlist = zip (repeat True)  lst2
534

    
535
-- | Test basic select functionality with undefined list values
536
prop_Utils_select_undefv :: [Int]            -- ^ List of False values
537
                         -> NonEmptyList Int -- ^ List of True values
538
                         -> Gen Prop         -- ^ Test result
539
prop_Utils_select_undefv lst1 (NonEmpty lst2) =
540
  Utils.select undefined cndlist ==? head lst2
541
    where flist = zip (repeat False) lst1
542
          tlist = zip (repeat True)  lst2
543
          cndlist = flist ++ tlist ++ [undefined]
544

    
545
prop_Utils_parseUnit (NonNegative n) =
546
  Utils.parseUnit (show n) ==? Types.Ok n .&&.
547
  Utils.parseUnit (show n ++ "m") ==? Types.Ok n .&&.
548
  Utils.parseUnit (show n ++ "M") ==? Types.Ok (truncate n_mb::Int) .&&.
549
  Utils.parseUnit (show n ++ "g") ==? Types.Ok (n*1024) .&&.
550
  Utils.parseUnit (show n ++ "G") ==? Types.Ok (truncate n_gb::Int) .&&.
551
  Utils.parseUnit (show n ++ "t") ==? Types.Ok (n*1048576) .&&.
552
  Utils.parseUnit (show n ++ "T") ==? Types.Ok (truncate n_tb::Int) .&&.
553
  printTestCase "Internal error/overflow?"
554
    (n_mb >=0 && n_gb >= 0 && n_tb >= 0) .&&.
555
  property (Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int))
556
  where _types = (n::Int)
557
        n_mb = (fromIntegral n::Rational) * 1000 * 1000 / 1024 / 1024
558
        n_gb = n_mb * 1000
559
        n_tb = n_gb * 1000
560

    
561
-- | Test list for the Utils module.
562
testSuite "Utils"
563
            [ 'prop_Utils_commaJoinSplit
564
            , 'prop_Utils_commaSplitJoin
565
            , 'prop_Utils_fromObjWithDefault
566
            , 'prop_Utils_if'if
567
            , 'prop_Utils_select
568
            , 'prop_Utils_select_undefd
569
            , 'prop_Utils_select_undefv
570
            , 'prop_Utils_parseUnit
571
            ]
572

    
573
-- ** PeerMap tests
574

    
575
-- | Make sure add is idempotent.
576
prop_PeerMap_addIdempotent pmap key em =
577
  fn puniq ==? fn (fn puniq)
578
    where _types = (pmap::PeerMap.PeerMap,
579
                    key::PeerMap.Key, em::PeerMap.Elem)
580
          fn = PeerMap.add key em
581
          puniq = PeerMap.accumArray const pmap
582

    
583
-- | Make sure remove is idempotent.
584
prop_PeerMap_removeIdempotent pmap key =
585
  fn puniq ==? fn (fn puniq)
586
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
587
          fn = PeerMap.remove key
588
          puniq = PeerMap.accumArray const pmap
589

    
590
-- | Make sure a missing item returns 0.
591
prop_PeerMap_findMissing pmap key =
592
  PeerMap.find key (PeerMap.remove key puniq) ==? 0
593
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
594
          puniq = PeerMap.accumArray const pmap
595

    
596
-- | Make sure an added item is found.
597
prop_PeerMap_addFind pmap key em =
598
  PeerMap.find key (PeerMap.add key em puniq) ==? em
599
    where _types = (pmap::PeerMap.PeerMap,
600
                    key::PeerMap.Key, em::PeerMap.Elem)
601
          puniq = PeerMap.accumArray const pmap
602

    
603
-- | Manual check that maxElem returns the maximum indeed, or 0 for null.
604
prop_PeerMap_maxElem pmap =
605
  PeerMap.maxElem puniq ==? if null puniq then 0
606
                              else (maximum . snd . unzip) puniq
607
    where _types = pmap::PeerMap.PeerMap
608
          puniq = PeerMap.accumArray const pmap
609

    
610
-- | List of tests for the PeerMap module.
611
testSuite "PeerMap"
612
            [ 'prop_PeerMap_addIdempotent
613
            , 'prop_PeerMap_removeIdempotent
614
            , 'prop_PeerMap_maxElem
615
            , 'prop_PeerMap_addFind
616
            , 'prop_PeerMap_findMissing
617
            ]
618

    
619
-- ** Container tests
620

    
621
-- we silence the following due to hlint bug fixed in later versions
622
{-# ANN prop_Container_addTwo "HLint: ignore Avoid lambda" #-}
623
prop_Container_addTwo cdata i1 i2 =
624
  fn i1 i2 cont == fn i2 i1 cont &&
625
  fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
626
    where _types = (cdata::[Int],
627
                    i1::Int, i2::Int)
628
          cont = foldl (\c x -> Container.add x x c) Container.empty cdata
629
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
630

    
631
prop_Container_nameOf node =
632
  let nl = makeSmallCluster node 1
633
      fnode = head (Container.elems nl)
634
  in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
635

    
636
-- | We test that in a cluster, given a random node, we can find it by
637
-- its name and alias, as long as all names and aliases are unique,
638
-- and that we fail to find a non-existing name.
639
prop_Container_findByName node =
640
  forAll (choose (1, 20)) $ \ cnt ->
641
  forAll (choose (0, cnt - 1)) $ \ fidx ->
642
  forAll (genUniquesList (cnt * 2)) $ \ allnames ->
643
  forAll (arbitrary `suchThat` (`notElem` allnames)) $ \ othername ->
644
  let names = zip (take cnt allnames) (drop cnt allnames)
645
      nl = makeSmallCluster node cnt
646
      nodes = Container.elems nl
647
      nodes' = map (\((name, alias), nn) -> (Node.idx nn,
648
                                             nn { Node.name = name,
649
                                                  Node.alias = alias }))
650
               $ zip names nodes
651
      nl' = Container.fromList nodes'
652
      target = snd (nodes' !! fidx)
653
  in Container.findByName nl' (Node.name target) == Just target &&
654
     Container.findByName nl' (Node.alias target) == Just target &&
655
     isNothing (Container.findByName nl' othername)
656

    
657
testSuite "Container"
658
            [ 'prop_Container_addTwo
659
            , 'prop_Container_nameOf
660
            , 'prop_Container_findByName
661
            ]
662

    
663
-- ** Instance tests
664

    
665
-- Simple instance tests, we only have setter/getters
666

    
667
prop_Instance_creat inst =
668
  Instance.name inst ==? Instance.alias inst
669

    
670
prop_Instance_setIdx inst idx =
671
  Instance.idx (Instance.setIdx inst idx) ==? idx
672
    where _types = (inst::Instance.Instance, idx::Types.Idx)
673

    
674
prop_Instance_setName inst name =
675
  Instance.name newinst == name &&
676
  Instance.alias newinst == name
677
    where _types = (inst::Instance.Instance, name::String)
678
          newinst = Instance.setName inst name
679

    
680
prop_Instance_setAlias inst name =
681
  Instance.name newinst == Instance.name inst &&
682
  Instance.alias newinst == name
683
    where _types = (inst::Instance.Instance, name::String)
684
          newinst = Instance.setAlias inst name
685

    
686
prop_Instance_setPri inst pdx =
687
  Instance.pNode (Instance.setPri inst pdx) ==? pdx
688
    where _types = (inst::Instance.Instance, pdx::Types.Ndx)
689

    
690
prop_Instance_setSec inst sdx =
691
  Instance.sNode (Instance.setSec inst sdx) ==? sdx
692
    where _types = (inst::Instance.Instance, sdx::Types.Ndx)
693

    
694
prop_Instance_setBoth inst pdx sdx =
695
  Instance.pNode si == pdx && Instance.sNode si == sdx
696
    where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
697
          si = Instance.setBoth inst pdx sdx
698

    
699
prop_Instance_shrinkMG inst =
700
  Instance.mem inst >= 2 * Types.unitMem ==>
701
    case Instance.shrinkByType inst Types.FailMem of
702
      Types.Ok inst' -> Instance.mem inst' == Instance.mem inst - Types.unitMem
703
      _ -> False
704

    
705
prop_Instance_shrinkMF inst =
706
  forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
707
    let inst' = inst { Instance.mem = mem}
708
    in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
709

    
710
prop_Instance_shrinkCG inst =
711
  Instance.vcpus inst >= 2 * Types.unitCpu ==>
712
    case Instance.shrinkByType inst Types.FailCPU of
713
      Types.Ok inst' ->
714
        Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
715
      _ -> False
716

    
717
prop_Instance_shrinkCF inst =
718
  forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
719
    let inst' = inst { Instance.vcpus = vcpus }
720
    in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
721

    
722
prop_Instance_shrinkDG inst =
723
  Instance.dsk inst >= 2 * Types.unitDsk ==>
724
    case Instance.shrinkByType inst Types.FailDisk of
725
      Types.Ok inst' ->
726
        Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
727
      _ -> False
728

    
729
prop_Instance_shrinkDF inst =
730
  forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
731
    let inst' = inst { Instance.dsk = dsk }
732
    in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
733

    
734
prop_Instance_setMovable inst m =
735
  Instance.movable inst' ==? m
736
    where inst' = Instance.setMovable inst m
737

    
738
testSuite "Instance"
739
            [ 'prop_Instance_creat
740
            , 'prop_Instance_setIdx
741
            , 'prop_Instance_setName
742
            , 'prop_Instance_setAlias
743
            , 'prop_Instance_setPri
744
            , 'prop_Instance_setSec
745
            , 'prop_Instance_setBoth
746
            , 'prop_Instance_shrinkMG
747
            , 'prop_Instance_shrinkMF
748
            , 'prop_Instance_shrinkCG
749
            , 'prop_Instance_shrinkCF
750
            , 'prop_Instance_shrinkDG
751
            , 'prop_Instance_shrinkDF
752
            , 'prop_Instance_setMovable
753
            ]
754

    
755
-- ** Backends
756

    
757
-- *** Text backend tests
758

    
759
-- Instance text loader tests
760

    
761
prop_Text_Load_Instance name mem dsk vcpus status
762
                        (NonEmpty pnode) snode
763
                        (NonNegative pdx) (NonNegative sdx) autobal dt su =
764
  pnode /= snode && pdx /= sdx ==>
765
  let vcpus_s = show vcpus
766
      dsk_s = show dsk
767
      mem_s = show mem
768
      su_s = show su
769
      status_s = Types.instanceStatusToRaw status
770
      ndx = if null snode
771
              then [(pnode, pdx)]
772
              else [(pnode, pdx), (snode, sdx)]
773
      nl = Data.Map.fromList ndx
774
      tags = ""
775
      sbal = if autobal then "Y" else "N"
776
      sdt = Types.diskTemplateToRaw dt
777
      inst = Text.loadInst nl
778
             [name, mem_s, dsk_s, vcpus_s, status_s,
779
              sbal, pnode, snode, sdt, tags, su_s]
780
      fail1 = Text.loadInst nl
781
              [name, mem_s, dsk_s, vcpus_s, status_s,
782
               sbal, pnode, pnode, tags]
783
      _types = ( name::String, mem::Int, dsk::Int
784
               , vcpus::Int, status::Types.InstanceStatus
785
               , snode::String
786
               , autobal::Bool)
787
  in case inst of
788
       Types.Bad msg -> failTest $ "Failed to load instance: " ++ msg
789
       Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
790
                                        \ loading the instance" $
791
               Instance.name i == name &&
792
               Instance.vcpus i == vcpus &&
793
               Instance.mem i == mem &&
794
               Instance.pNode i == pdx &&
795
               Instance.sNode i == (if null snode
796
                                      then Node.noSecondary
797
                                      else sdx) &&
798
               Instance.autoBalance i == autobal &&
799
               Instance.spindleUsage i == su &&
800
               Types.isBad fail1
801

    
802
prop_Text_Load_InstanceFail ktn fields =
803
  length fields /= 10 && length fields /= 11 ==>
804
    case Text.loadInst nl fields of
805
      Types.Ok _ -> failTest "Managed to load instance from invalid data"
806
      Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
807
                       "Invalid/incomplete instance data: '" `isPrefixOf` msg
808
    where nl = Data.Map.fromList ktn
809

    
810
prop_Text_Load_Node name tm nm fm td fd tc fo =
811
  let conv v = if v < 0
812
                 then "?"
813
                 else show v
814
      tm_s = conv tm
815
      nm_s = conv nm
816
      fm_s = conv fm
817
      td_s = conv td
818
      fd_s = conv fd
819
      tc_s = conv tc
820
      fo_s = if fo
821
               then "Y"
822
               else "N"
823
      any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
824
      gid = Group.uuid defGroup
825
  in case Text.loadNode defGroupAssoc
826
       [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
827
       Nothing -> False
828
       Just (name', node) ->
829
         if fo || any_broken
830
           then Node.offline node
831
           else Node.name node == name' && name' == name &&
832
                Node.alias node == name &&
833
                Node.tMem node == fromIntegral tm &&
834
                Node.nMem node == nm &&
835
                Node.fMem node == fm &&
836
                Node.tDsk node == fromIntegral td &&
837
                Node.fDsk node == fd &&
838
                Node.tCpu node == fromIntegral tc
839

    
840
prop_Text_Load_NodeFail fields =
841
  length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
842

    
843
prop_Text_NodeLSIdempotent node =
844
  (Text.loadNode defGroupAssoc.
845
       Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==?
846
  Just (Node.name n, n)
847
    -- override failN1 to what loadNode returns by default
848
    where n = Node.setPolicy Types.defIPolicy $
849
              node { Node.failN1 = True, Node.offline = False }
850

    
851
prop_Text_ISpecIdempotent ispec =
852
  case Text.loadISpec "dummy" . Utils.sepSplit ',' .
853
       Text.serializeISpec $ ispec of
854
    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
855
    Types.Ok ispec' -> ispec ==? ispec'
856

    
857
prop_Text_IPolicyIdempotent ipol =
858
  case Text.loadIPolicy . Utils.sepSplit '|' $
859
       Text.serializeIPolicy owner ipol of
860
    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
861
    Types.Ok res -> (owner, ipol) ==? res
862
  where owner = "dummy"
863

    
864
-- | This property, while being in the text tests, does more than just
865
-- test end-to-end the serialisation and loading back workflow; it
866
-- also tests the Loader.mergeData and the actuall
867
-- Cluster.iterateAlloc (for well-behaving w.r.t. instance
868
-- allocations, not for the business logic). As such, it's a quite
869
-- complex and slow test, and that's the reason we restrict it to
870
-- small cluster sizes.
871
prop_Text_CreateSerialise =
872
  forAll genTags $ \ctags ->
873
  forAll (choose (1, 20)) $ \maxiter ->
874
  forAll (choose (2, 10)) $ \count ->
875
  forAll genOnlineNode $ \node ->
876
  forAll (genInstanceSmallerThanNode node) $ \inst ->
877
  let nl = makeSmallCluster node count
878
      reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
879
  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>= \allocn ->
880
     Cluster.iterateAlloc nl Container.empty (Just maxiter) inst allocn [] []
881
     of
882
       Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
883
       Types.Ok (_, _, _, [], _) -> printTestCase
884
                                    "Failed to allocate: no allocations" False
885
       Types.Ok (_, nl', il', _, _) ->
886
         let cdata = Loader.ClusterData defGroupList nl' il' ctags
887
                     Types.defIPolicy
888
             saved = Text.serializeCluster cdata
889
         in case Text.parseData saved >>= Loader.mergeData [] [] [] [] of
890
              Types.Bad msg -> failTest $ "Failed to load/merge: " ++ msg
891
              Types.Ok (Loader.ClusterData gl2 nl2 il2 ctags2 cpol2) ->
892
                ctags ==? ctags2 .&&.
893
                Types.defIPolicy ==? cpol2 .&&.
894
                il' ==? il2 .&&.
895
                defGroupList ==? gl2 .&&.
896
                nl' ==? nl2
897

    
898
testSuite "Text"
899
            [ 'prop_Text_Load_Instance
900
            , 'prop_Text_Load_InstanceFail
901
            , 'prop_Text_Load_Node
902
            , 'prop_Text_Load_NodeFail
903
            , 'prop_Text_NodeLSIdempotent
904
            , 'prop_Text_ISpecIdempotent
905
            , 'prop_Text_IPolicyIdempotent
906
            , 'prop_Text_CreateSerialise
907
            ]
908

    
909
-- *** Simu backend
910

    
911
-- | Generates a tuple of specs for simulation.
912
genSimuSpec :: Gen (String, Int, Int, Int, Int)
913
genSimuSpec = do
914
  pol <- elements [C.allocPolicyPreferred,
915
                   C.allocPolicyLastResort, C.allocPolicyUnallocable,
916
                  "p", "a", "u"]
917
 -- should be reasonable (nodes/group), bigger values only complicate
918
 -- the display of failed tests, and we don't care (in this particular
919
 -- test) about big node groups
920
  nodes <- choose (0, 20)
921
  dsk <- choose (0, maxDsk)
922
  mem <- choose (0, maxMem)
923
  cpu <- choose (0, maxCpu)
924
  return (pol, nodes, dsk, mem, cpu)
925

    
926
-- | Checks that given a set of corrects specs, we can load them
927
-- successfully, and that at high-level the values look right.
928
prop_SimuLoad =
929
  forAll (choose (0, 10)) $ \ngroups ->
930
  forAll (replicateM ngroups genSimuSpec) $ \specs ->
931
  let strspecs = map (\(p, n, d, m, c) -> printf "%s,%d,%d,%d,%d"
932
                                          p n d m c::String) specs
933
      totnodes = sum $ map (\(_, n, _, _, _) -> n) specs
934
      mdc_in = concatMap (\(_, n, d, m, c) ->
935
                            replicate n (fromIntegral m, fromIntegral d,
936
                                         fromIntegral c,
937
                                         fromIntegral m, fromIntegral d)) specs
938
  in case Simu.parseData strspecs of
939
       Types.Bad msg -> failTest $ "Failed to load specs: " ++ msg
940
       Types.Ok (Loader.ClusterData gl nl il tags ipol) ->
941
         let nodes = map snd $ IntMap.toAscList nl
942
             nidx = map Node.idx nodes
943
             mdc_out = map (\n -> (Node.tMem n, Node.tDsk n, Node.tCpu n,
944
                                   Node.fMem n, Node.fDsk n)) nodes
945
         in
946
         Container.size gl ==? ngroups .&&.
947
         Container.size nl ==? totnodes .&&.
948
         Container.size il ==? 0 .&&.
949
         length tags ==? 0 .&&.
950
         ipol ==? Types.defIPolicy .&&.
951
         nidx ==? [1..totnodes] .&&.
952
         mdc_in ==? mdc_out .&&.
953
         map Group.iPolicy (Container.elems gl) ==?
954
             replicate ngroups Types.defIPolicy
955

    
956
testSuite "Simu"
957
            [ 'prop_SimuLoad
958
            ]
959

    
960
-- ** Node tests
961

    
962
prop_Node_setAlias node name =
963
  Node.name newnode == Node.name node &&
964
  Node.alias newnode == name
965
    where _types = (node::Node.Node, name::String)
966
          newnode = Node.setAlias node name
967

    
968
prop_Node_setOffline node status =
969
  Node.offline newnode ==? status
970
    where newnode = Node.setOffline node status
971

    
972
prop_Node_setXmem node xm =
973
  Node.xMem newnode ==? xm
974
    where newnode = Node.setXmem node xm
975

    
976
prop_Node_setMcpu node mc =
977
  Types.iPolicyVcpuRatio (Node.iPolicy newnode) ==? mc
978
    where newnode = Node.setMcpu node mc
979

    
980
-- | Check that an instance add with too high memory or disk will be
981
-- rejected.
982
prop_Node_addPriFM node inst =
983
  Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
984
  not (Instance.isOffline inst) ==>
985
  case Node.addPri node inst'' of
986
    Types.OpFail Types.FailMem -> True
987
    _ -> False
988
  where _types = (node::Node.Node, inst::Instance.Instance)
989
        inst' = setInstanceSmallerThanNode node inst
990
        inst'' = inst' { Instance.mem = Instance.mem inst }
991

    
992
-- | Check that adding a primary instance with too much disk fails
993
-- with type FailDisk.
994
prop_Node_addPriFD node inst =
995
  forAll (elements Instance.localStorageTemplates) $ \dt ->
996
  Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
997
  let inst' = setInstanceSmallerThanNode node inst
998
      inst'' = inst' { Instance.dsk = Instance.dsk inst
999
                     , Instance.diskTemplate = dt }
1000
  in case Node.addPri node inst'' of
1001
       Types.OpFail Types.FailDisk -> True
1002
       _ -> False
1003

    
1004
-- | Check that adding a primary instance with too many VCPUs fails
1005
-- with type FailCPU.
1006
prop_Node_addPriFC =
1007
  forAll (choose (1, maxCpu)) $ \extra ->
1008
  forAll genOnlineNode $ \node ->
1009
  forAll (arbitrary `suchThat` Instance.notOffline) $ \inst ->
1010
  let inst' = setInstanceSmallerThanNode node inst
1011
      inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
1012
  in case Node.addPri node inst'' of
1013
       Types.OpFail Types.FailCPU -> property True
1014
       v -> failTest $ "Expected OpFail FailCPU, but got " ++ show v
1015

    
1016
-- | Check that an instance add with too high memory or disk will be
1017
-- rejected.
1018
prop_Node_addSec node inst pdx =
1019
  ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
1020
    not (Instance.isOffline inst)) ||
1021
   Instance.dsk inst >= Node.fDsk node) &&
1022
  not (Node.failN1 node) ==>
1023
      isFailure (Node.addSec node inst pdx)
1024
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
1025

    
1026
-- | Check that an offline instance with reasonable disk size but
1027
-- extra mem/cpu can always be added.
1028
prop_Node_addOfflinePri (NonNegative extra_mem) (NonNegative extra_cpu) =
1029
  forAll genOnlineNode $ \node ->
1030
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1031
  let inst' = inst { Instance.runSt = Types.AdminOffline
1032
                   , Instance.mem = Node.availMem node + extra_mem
1033
                   , Instance.vcpus = Node.availCpu node + extra_cpu }
1034
  in case Node.addPri node inst' of
1035
       Types.OpGood _ -> property True
1036
       v -> failTest $ "Expected OpGood, but got: " ++ show v
1037

    
1038
-- | Check that an offline instance with reasonable disk size but
1039
-- extra mem/cpu can always be added.
1040
prop_Node_addOfflineSec (NonNegative extra_mem) (NonNegative extra_cpu) pdx =
1041
  forAll genOnlineNode $ \node ->
1042
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1043
  let inst' = inst { Instance.runSt = Types.AdminOffline
1044
                   , Instance.mem = Node.availMem node + extra_mem
1045
                   , Instance.vcpus = Node.availCpu node + extra_cpu
1046
                   , Instance.diskTemplate = Types.DTDrbd8 }
1047
  in case Node.addSec node inst' pdx of
1048
       Types.OpGood _ -> property True
1049
       v -> failTest $ "Expected OpGood/OpGood, but got: " ++ show v
1050

    
1051
-- | Checks for memory reservation changes.
1052
prop_Node_rMem inst =
1053
  not (Instance.isOffline inst) ==>
1054
  forAll (genOnlineNode `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
1055
  -- ab = auto_balance, nb = non-auto_balance
1056
  -- we use -1 as the primary node of the instance
1057
  let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True
1058
                   , Instance.diskTemplate = Types.DTDrbd8 }
1059
      inst_ab = setInstanceSmallerThanNode node inst'
1060
      inst_nb = inst_ab { Instance.autoBalance = False }
1061
      -- now we have the two instances, identical except the
1062
      -- autoBalance attribute
1063
      orig_rmem = Node.rMem node
1064
      inst_idx = Instance.idx inst_ab
1065
      node_add_ab = Node.addSec node inst_ab (-1)
1066
      node_add_nb = Node.addSec node inst_nb (-1)
1067
      node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
1068
      node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
1069
  in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
1070
       (Types.OpGood a_ab, Types.OpGood a_nb,
1071
        Types.OpGood d_ab, Types.OpGood d_nb) ->
1072
         printTestCase "Consistency checks failed" $
1073
           Node.rMem a_ab >  orig_rmem &&
1074
           Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
1075
           Node.rMem a_nb == orig_rmem &&
1076
           Node.rMem d_ab == orig_rmem &&
1077
           Node.rMem d_nb == orig_rmem &&
1078
           -- this is not related to rMem, but as good a place to
1079
           -- test as any
1080
           inst_idx `elem` Node.sList a_ab &&
1081
           inst_idx `notElem` Node.sList d_ab
1082
       x -> failTest $ "Failed to add/remove instances: " ++ show x
1083

    
1084
-- | Check mdsk setting.
1085
prop_Node_setMdsk node mx =
1086
  Node.loDsk node' >= 0 &&
1087
  fromIntegral (Node.loDsk node') <= Node.tDsk node &&
1088
  Node.availDisk node' >= 0 &&
1089
  Node.availDisk node' <= Node.fDsk node' &&
1090
  fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
1091
  Node.mDsk node' == mx'
1092
    where _types = (node::Node.Node, mx::SmallRatio)
1093
          node' = Node.setMdsk node mx'
1094
          SmallRatio mx' = mx
1095

    
1096
-- Check tag maps
1097
prop_Node_tagMaps_idempotent =
1098
  forAll genTags $ \tags ->
1099
  Node.delTags (Node.addTags m tags) tags ==? m
1100
    where m = Data.Map.empty
1101

    
1102
prop_Node_tagMaps_reject =
1103
  forAll (genTags `suchThat` (not . null)) $ \tags ->
1104
  let m = Node.addTags Data.Map.empty tags
1105
  in all (\t -> Node.rejectAddTags m [t]) tags
1106

    
1107
prop_Node_showField node =
1108
  forAll (elements Node.defaultFields) $ \ field ->
1109
  fst (Node.showHeader field) /= Types.unknownField &&
1110
  Node.showField node field /= Types.unknownField
1111

    
1112
prop_Node_computeGroups nodes =
1113
  let ng = Node.computeGroups nodes
1114
      onlyuuid = map fst ng
1115
  in length nodes == sum (map (length . snd) ng) &&
1116
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
1117
     length (nub onlyuuid) == length onlyuuid &&
1118
     (null nodes || not (null ng))
1119

    
1120
-- Check idempotence of add/remove operations
1121
prop_Node_addPri_idempotent =
1122
  forAll genOnlineNode $ \node ->
1123
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1124
  case Node.addPri node inst of
1125
    Types.OpGood node' -> Node.removePri node' inst ==? node
1126
    _ -> failTest "Can't add instance"
1127

    
1128
prop_Node_addSec_idempotent =
1129
  forAll genOnlineNode $ \node ->
1130
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1131
  let pdx = Node.idx node + 1
1132
      inst' = Instance.setPri inst pdx
1133
      inst'' = inst' { Instance.diskTemplate = Types.DTDrbd8 }
1134
  in case Node.addSec node inst'' pdx of
1135
       Types.OpGood node' -> Node.removeSec node' inst'' ==? node
1136
       _ -> failTest "Can't add instance"
1137

    
1138
testSuite "Node"
1139
            [ 'prop_Node_setAlias
1140
            , 'prop_Node_setOffline
1141
            , 'prop_Node_setMcpu
1142
            , 'prop_Node_setXmem
1143
            , 'prop_Node_addPriFM
1144
            , 'prop_Node_addPriFD
1145
            , 'prop_Node_addPriFC
1146
            , 'prop_Node_addSec
1147
            , 'prop_Node_addOfflinePri
1148
            , 'prop_Node_addOfflineSec
1149
            , 'prop_Node_rMem
1150
            , 'prop_Node_setMdsk
1151
            , 'prop_Node_tagMaps_idempotent
1152
            , 'prop_Node_tagMaps_reject
1153
            , 'prop_Node_showField
1154
            , 'prop_Node_computeGroups
1155
            , 'prop_Node_addPri_idempotent
1156
            , 'prop_Node_addSec_idempotent
1157
            ]
1158

    
1159
-- ** Cluster tests
1160

    
1161
-- | Check that the cluster score is close to zero for a homogeneous
1162
-- cluster.
1163
prop_Score_Zero node =
1164
  forAll (choose (1, 1024)) $ \count ->
1165
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
1166
     (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
1167
  let fn = Node.buildPeers node Container.empty
1168
      nlst = replicate count fn
1169
      score = Cluster.compCVNodes nlst
1170
  -- we can't say == 0 here as the floating point errors accumulate;
1171
  -- this should be much lower than the default score in CLI.hs
1172
  in score <= 1e-12
1173

    
1174
-- | Check that cluster stats are sane.
1175
prop_CStats_sane =
1176
  forAll (choose (1, 1024)) $ \count ->
1177
  forAll genOnlineNode $ \node ->
1178
  let fn = Node.buildPeers node Container.empty
1179
      nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
1180
      nl = Container.fromList nlst
1181
      cstats = Cluster.totalResources nl
1182
  in Cluster.csAdsk cstats >= 0 &&
1183
     Cluster.csAdsk cstats <= Cluster.csFdsk cstats
1184

    
1185
-- | Check that one instance is allocated correctly, without
1186
-- rebalances needed.
1187
prop_ClusterAlloc_sane inst =
1188
  forAll (choose (5, 20)) $ \count ->
1189
  forAll genOnlineNode $ \node ->
1190
  let (nl, il, inst') = makeSmallEmptyCluster node count inst
1191
      reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
1192
  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1193
     Cluster.tryAlloc nl il inst' of
1194
       Types.Bad _ -> False
1195
       Types.Ok as ->
1196
         case Cluster.asSolution as of
1197
           Nothing -> False
1198
           Just (xnl, xi, _, cv) ->
1199
             let il' = Container.add (Instance.idx xi) xi il
1200
                 tbl = Cluster.Table xnl il' cv []
1201
             in not (canBalance tbl True True False)
1202

    
1203
-- | Checks that on a 2-5 node cluster, we can allocate a random
1204
-- instance spec via tiered allocation (whatever the original instance
1205
-- spec), on either one or two nodes. Furthermore, we test that
1206
-- computed allocation statistics are correct.
1207
prop_ClusterCanTieredAlloc inst =
1208
  forAll (choose (2, 5)) $ \count ->
1209
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1210
  let nl = makeSmallCluster node count
1211
      il = Container.empty
1212
      rqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
1213
      allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
1214
  in case allocnodes >>= \allocnodes' ->
1215
    Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
1216
       Types.Bad msg -> failTest $ "Failed to tiered alloc: " ++ msg
1217
       Types.Ok (_, nl', il', ixes, cstats) ->
1218
         let (ai_alloc, ai_pool, ai_unav) =
1219
               Cluster.computeAllocationDelta
1220
                (Cluster.totalResources nl)
1221
                (Cluster.totalResources nl')
1222
             all_nodes = Container.elems nl
1223
         in property (not (null ixes)) .&&.
1224
            IntMap.size il' ==? length ixes .&&.
1225
            length ixes ==? length cstats .&&.
1226
            sum (map Types.allocInfoVCpus [ai_alloc, ai_pool, ai_unav]) ==?
1227
              sum (map Node.hiCpu all_nodes) .&&.
1228
            sum (map Types.allocInfoNCpus [ai_alloc, ai_pool, ai_unav]) ==?
1229
              sum (map Node.tCpu all_nodes) .&&.
1230
            sum (map Types.allocInfoMem [ai_alloc, ai_pool, ai_unav]) ==?
1231
              truncate (sum (map Node.tMem all_nodes)) .&&.
1232
            sum (map Types.allocInfoDisk [ai_alloc, ai_pool, ai_unav]) ==?
1233
              truncate (sum (map Node.tDsk all_nodes))
1234

    
1235
-- | Helper function to create a cluster with the given range of nodes
1236
-- and allocate an instance on it.
1237
genClusterAlloc count node inst =
1238
  let nl = makeSmallCluster node count
1239
      reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
1240
  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1241
     Cluster.tryAlloc nl Container.empty inst of
1242
       Types.Bad _ -> Types.Bad "Can't allocate"
1243
       Types.Ok as ->
1244
         case Cluster.asSolution as of
1245
           Nothing -> Types.Bad "Empty solution?"
1246
           Just (xnl, xi, _, _) ->
1247
             let xil = Container.add (Instance.idx xi) xi Container.empty
1248
             in Types.Ok (xnl, xil, xi)
1249

    
1250
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
1251
-- we can also relocate it.
1252
prop_ClusterAllocRelocate =
1253
  forAll (choose (4, 8)) $ \count ->
1254
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1255
  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
1256
  case genClusterAlloc count node inst of
1257
    Types.Bad msg -> failTest msg
1258
    Types.Ok (nl, il, inst') ->
1259
      case IAlloc.processRelocate defGroupList nl il
1260
             (Instance.idx inst) 1
1261
             [(if Instance.diskTemplate inst' == Types.DTDrbd8
1262
                 then Instance.sNode
1263
                 else Instance.pNode) inst'] of
1264
        Types.Ok _ -> property True
1265
        Types.Bad msg -> failTest $ "Failed to relocate: " ++ msg
1266

    
1267
-- | Helper property checker for the result of a nodeEvac or
1268
-- changeGroup operation.
1269
check_EvacMode grp inst result =
1270
  case result of
1271
    Types.Bad msg -> failTest $ "Couldn't evacuate/change group:" ++ msg
1272
    Types.Ok (_, _, es) ->
1273
      let moved = Cluster.esMoved es
1274
          failed = Cluster.esFailed es
1275
          opcodes = not . null $ Cluster.esOpCodes es
1276
      in failmsg ("'failed' not empty: " ++ show failed) (null failed) .&&.
1277
         failmsg "'opcodes' is null" opcodes .&&.
1278
         case moved of
1279
           [(idx', gdx, _)] -> failmsg "invalid instance moved" (idx == idx')
1280
                               .&&.
1281
                               failmsg "wrong target group"
1282
                                         (gdx == Group.idx grp)
1283
           v -> failmsg  ("invalid solution: " ++ show v) False
1284
  where failmsg = \msg -> printTestCase ("Failed to evacuate: " ++ msg)
1285
        idx = Instance.idx inst
1286

    
1287
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
1288
-- we can also node-evacuate it.
1289
prop_ClusterAllocEvacuate =
1290
  forAll (choose (4, 8)) $ \count ->
1291
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1292
  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
1293
  case genClusterAlloc count node inst of
1294
    Types.Bad msg -> failTest msg
1295
    Types.Ok (nl, il, inst') ->
1296
      conjoin $ map (\mode -> check_EvacMode defGroup inst' $
1297
                              Cluster.tryNodeEvac defGroupList nl il mode
1298
                                [Instance.idx inst']) .
1299
                              evacModeOptions .
1300
                              Instance.mirrorType $ inst'
1301

    
1302
-- | Checks that on a 4-8 node cluster with two node groups, once we
1303
-- allocate an instance on the first node group, we can also change
1304
-- its group.
1305
prop_ClusterAllocChangeGroup =
1306
  forAll (choose (4, 8)) $ \count ->
1307
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1308
  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
1309
  case genClusterAlloc count node inst of
1310
    Types.Bad msg -> failTest msg
1311
    Types.Ok (nl, il, inst') ->
1312
      -- we need to add a second node group and nodes to the cluster
1313
      let nl2 = Container.elems $ makeSmallCluster node count
1314
          grp2 = Group.setIdx defGroup (Group.idx defGroup + 1)
1315
          maxndx = maximum . map Node.idx $ nl2
1316
          nl3 = map (\n -> n { Node.group = Group.idx grp2
1317
                             , Node.idx = Node.idx n + maxndx }) nl2
1318
          nl4 = Container.fromList . map (\n -> (Node.idx n, n)) $ nl3
1319
          gl' = Container.add (Group.idx grp2) grp2 defGroupList
1320
          nl' = IntMap.union nl nl4
1321
      in check_EvacMode grp2 inst' $
1322
         Cluster.tryChangeGroup gl' nl' il [] [Instance.idx inst']
1323

    
1324
-- | Check that allocating multiple instances on a cluster, then
1325
-- adding an empty node, results in a valid rebalance.
1326
prop_ClusterAllocBalance =
1327
  forAll (genNode (Just 5) (Just 128)) $ \node ->
1328
  forAll (choose (3, 5)) $ \count ->
1329
  not (Node.offline node) && not (Node.failN1 node) ==>
1330
  let nl = makeSmallCluster node count
1331
      (hnode, nl') = IntMap.deleteFindMax nl
1332
      il = Container.empty
1333
      allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
1334
      i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
1335
  in case allocnodes >>= \allocnodes' ->
1336
    Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
1337
       Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
1338
       Types.Ok (_, _, _, [], _) -> failTest "Failed to allocate: no instances"
1339
       Types.Ok (_, xnl, il', _, _) ->
1340
         let ynl = Container.add (Node.idx hnode) hnode xnl
1341
             cv = Cluster.compCV ynl
1342
             tbl = Cluster.Table ynl il' cv []
1343
         in printTestCase "Failed to rebalance" $
1344
            canBalance tbl True True False
1345

    
1346
-- | Checks consistency.
1347
prop_ClusterCheckConsistency node inst =
1348
  let nl = makeSmallCluster node 3
1349
      [node1, node2, node3] = Container.elems nl
1350
      node3' = node3 { Node.group = 1 }
1351
      nl' = Container.add (Node.idx node3') node3' nl
1352
      inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
1353
      inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
1354
      inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
1355
      ccheck = Cluster.findSplitInstances nl' . Container.fromList
1356
  in null (ccheck [(0, inst1)]) &&
1357
     null (ccheck [(0, inst2)]) &&
1358
     (not . null $ ccheck [(0, inst3)])
1359

    
1360
-- | For now, we only test that we don't lose instances during the split.
1361
prop_ClusterSplitCluster node inst =
1362
  forAll (choose (0, 100)) $ \icnt ->
1363
  let nl = makeSmallCluster node 2
1364
      (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
1365
                   (nl, Container.empty) [1..icnt]
1366
      gni = Cluster.splitCluster nl' il'
1367
  in sum (map (Container.size . snd . snd) gni) == icnt &&
1368
     all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
1369
                                 (Container.elems nl'')) gni
1370

    
1371
-- | Helper function to check if we can allocate an instance on a
1372
-- given node list.
1373
canAllocOn :: Node.List -> Int -> Instance.Instance -> Bool
1374
canAllocOn nl reqnodes inst =
1375
  case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1376
       Cluster.tryAlloc nl (Container.empty) inst of
1377
       Types.Bad _ -> False
1378
       Types.Ok as ->
1379
         case Cluster.asSolution as of
1380
           Nothing -> False
1381
           Just _ -> True
1382

    
1383
-- | Checks that allocation obeys minimum and maximum instance
1384
-- policies. The unittest generates a random node, duplicates it count
1385
-- times, and generates a random instance that can be allocated on
1386
-- this mini-cluster; it then checks that after applying a policy that
1387
-- the instance doesn't fits, the allocation fails.
1388
prop_ClusterAllocPolicy node =
1389
  -- rqn is the required nodes (1 or 2)
1390
  forAll (choose (1, 2)) $ \rqn ->
1391
  forAll (choose (5, 20)) $ \count ->
1392
  forAll (arbitrary `suchThat` (canAllocOn (makeSmallCluster node count) rqn))
1393
         $ \inst ->
1394
  forAll (arbitrary `suchThat` (isFailure .
1395
                                Instance.instMatchesPolicy inst)) $ \ipol ->
1396
  let node' = Node.setPolicy ipol node
1397
      nl = makeSmallCluster node' count
1398
  in not $ canAllocOn nl rqn inst
1399

    
1400
testSuite "Cluster"
1401
            [ 'prop_Score_Zero
1402
            , 'prop_CStats_sane
1403
            , 'prop_ClusterAlloc_sane
1404
            , 'prop_ClusterCanTieredAlloc
1405
            , 'prop_ClusterAllocRelocate
1406
            , 'prop_ClusterAllocEvacuate
1407
            , 'prop_ClusterAllocChangeGroup
1408
            , 'prop_ClusterAllocBalance
1409
            , 'prop_ClusterCheckConsistency
1410
            , 'prop_ClusterSplitCluster
1411
            , 'prop_ClusterAllocPolicy
1412
            ]
1413

    
1414
-- ** OpCodes tests
1415

    
1416
-- | Check that opcode serialization is idempotent.
1417
prop_OpCodes_serialization op =
1418
  case J.readJSON (J.showJSON op) of
1419
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1420
    J.Ok op' -> op ==? op'
1421
  where _types = op::OpCodes.OpCode
1422

    
1423
testSuite "OpCodes"
1424
            [ 'prop_OpCodes_serialization ]
1425

    
1426
-- ** Jobs tests
1427

    
1428
-- | Check that (queued) job\/opcode status serialization is idempotent.
1429
prop_OpStatus_serialization os =
1430
  case J.readJSON (J.showJSON os) of
1431
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1432
    J.Ok os' -> os ==? os'
1433
  where _types = os::Jobs.OpStatus
1434

    
1435
prop_JobStatus_serialization js =
1436
  case J.readJSON (J.showJSON js) of
1437
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1438
    J.Ok js' -> js ==? js'
1439
  where _types = js::Jobs.JobStatus
1440

    
1441
testSuite "Jobs"
1442
            [ 'prop_OpStatus_serialization
1443
            , 'prop_JobStatus_serialization
1444
            ]
1445

    
1446
-- ** Loader tests
1447

    
1448
prop_Loader_lookupNode ktn inst node =
1449
  Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
1450
    where nl = Data.Map.fromList ktn
1451

    
1452
prop_Loader_lookupInstance kti inst =
1453
  Loader.lookupInstance il inst ==? Data.Map.lookup inst il
1454
    where il = Data.Map.fromList kti
1455

    
1456
prop_Loader_assignIndices =
1457
  -- generate nodes with unique names
1458
  forAll (arbitrary `suchThat`
1459
          (\nodes ->
1460
             let names = map Node.name nodes
1461
             in length names == length (nub names))) $ \nodes ->
1462
  let (nassoc, kt) =
1463
        Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1464
  in Data.Map.size nassoc == length nodes &&
1465
     Container.size kt == length nodes &&
1466
     if not (null nodes)
1467
       then maximum (IntMap.keys kt) == length nodes - 1
1468
       else True
1469

    
1470
-- | Checks that the number of primary instances recorded on the nodes
1471
-- is zero.
1472
prop_Loader_mergeData ns =
1473
  let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1474
  in case Loader.mergeData [] [] [] []
1475
         (Loader.emptyCluster {Loader.cdNodes = na}) of
1476
    Types.Bad _ -> False
1477
    Types.Ok (Loader.ClusterData _ nl il _ _) ->
1478
      let nodes = Container.elems nl
1479
          instances = Container.elems il
1480
      in (sum . map (length . Node.pList)) nodes == 0 &&
1481
         null instances
1482

    
1483
-- | Check that compareNameComponent on equal strings works.
1484
prop_Loader_compareNameComponent_equal :: String -> Bool
1485
prop_Loader_compareNameComponent_equal s =
1486
  Loader.compareNameComponent s s ==
1487
    Loader.LookupResult Loader.ExactMatch s
1488

    
1489
-- | Check that compareNameComponent on prefix strings works.
1490
prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1491
prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1492
  Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1493
    Loader.LookupResult Loader.PartialMatch s1
1494

    
1495
testSuite "Loader"
1496
            [ 'prop_Loader_lookupNode
1497
            , 'prop_Loader_lookupInstance
1498
            , 'prop_Loader_assignIndices
1499
            , 'prop_Loader_mergeData
1500
            , 'prop_Loader_compareNameComponent_equal
1501
            , 'prop_Loader_compareNameComponent_prefix
1502
            ]
1503

    
1504
-- ** Types tests
1505

    
1506
prop_Types_AllocPolicy_serialisation apol =
1507
  case J.readJSON (J.showJSON apol) of
1508
    J.Ok p -> p ==? apol
1509
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1510
      where _types = apol::Types.AllocPolicy
1511

    
1512
prop_Types_DiskTemplate_serialisation dt =
1513
  case J.readJSON (J.showJSON dt) of
1514
    J.Ok p -> p ==? dt
1515
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1516
      where _types = dt::Types.DiskTemplate
1517

    
1518
prop_Types_ISpec_serialisation ispec =
1519
  case J.readJSON (J.showJSON ispec) of
1520
    J.Ok p -> p ==? ispec
1521
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1522
      where _types = ispec::Types.ISpec
1523

    
1524
prop_Types_IPolicy_serialisation ipol =
1525
  case J.readJSON (J.showJSON ipol) of
1526
    J.Ok p -> p ==? ipol
1527
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1528
      where _types = ipol::Types.IPolicy
1529

    
1530
prop_Types_EvacMode_serialisation em =
1531
  case J.readJSON (J.showJSON em) of
1532
    J.Ok p -> p ==? em
1533
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1534
      where _types = em::Types.EvacMode
1535

    
1536
prop_Types_opToResult op =
1537
  case op of
1538
    Types.OpFail _ -> Types.isBad r
1539
    Types.OpGood v -> case r of
1540
                        Types.Bad _ -> False
1541
                        Types.Ok v' -> v == v'
1542
  where r = Types.opToResult op
1543
        _types = op::Types.OpResult Int
1544

    
1545
prop_Types_eitherToResult ei =
1546
  case ei of
1547
    Left _ -> Types.isBad r
1548
    Right v -> case r of
1549
                 Types.Bad _ -> False
1550
                 Types.Ok v' -> v == v'
1551
    where r = Types.eitherToResult ei
1552
          _types = ei::Either String Int
1553

    
1554
testSuite "Types"
1555
            [ 'prop_Types_AllocPolicy_serialisation
1556
            , 'prop_Types_DiskTemplate_serialisation
1557
            , 'prop_Types_ISpec_serialisation
1558
            , 'prop_Types_IPolicy_serialisation
1559
            , 'prop_Types_EvacMode_serialisation
1560
            , 'prop_Types_opToResult
1561
            , 'prop_Types_eitherToResult
1562
            ]
1563

    
1564
-- ** CLI tests
1565

    
1566
-- | Test correct parsing.
1567
prop_CLI_parseISpec descr dsk mem cpu =
1568
  let str = printf "%d,%d,%d" dsk mem cpu
1569
  in CLI.parseISpecString descr str ==? Types.Ok (Types.RSpec cpu mem dsk)
1570

    
1571
-- | Test parsing failure due to wrong section count.
1572
prop_CLI_parseISpecFail descr =
1573
  forAll (choose (0,100) `suchThat` ((/=) 3)) $ \nelems ->
1574
  forAll (replicateM nelems arbitrary) $ \values ->
1575
  let str = intercalate "," $ map show (values::[Int])
1576
  in case CLI.parseISpecString descr str of
1577
       Types.Ok v -> failTest $ "Expected failure, got " ++ show v
1578
       _ -> property True
1579

    
1580
-- | Test parseYesNo.
1581
prop_CLI_parseYesNo def testval val =
1582
  forAll (elements [val, "yes", "no"]) $ \actual_val ->
1583
  if testval
1584
    then CLI.parseYesNo def Nothing ==? Types.Ok def
1585
    else let result = CLI.parseYesNo def (Just actual_val)
1586
         in if actual_val `elem` ["yes", "no"]
1587
              then result ==? Types.Ok (actual_val == "yes")
1588
              else property $ Types.isBad result
1589

    
1590
-- | Helper to check for correct parsing of string arg.
1591
checkStringArg val (opt, fn) =
1592
  let GetOpt.Option _ longs _ _ = opt
1593
  in case longs of
1594
       [] -> failTest "no long options?"
1595
       cmdarg:_ ->
1596
         case CLI.parseOptsInner ["--" ++ cmdarg ++ "=" ++ val] "prog" [opt] of
1597
           Left e -> failTest $ "Failed to parse option: " ++ show e
1598
           Right (options, _) -> fn options ==? Just val
1599

    
1600
-- | Test a few string arguments.
1601
prop_CLI_StringArg argument =
1602
  let args = [ (CLI.oDataFile,      CLI.optDataFile)
1603
             , (CLI.oDynuFile,      CLI.optDynuFile)
1604
             , (CLI.oSaveCluster,   CLI.optSaveCluster)
1605
             , (CLI.oReplay,        CLI.optReplay)
1606
             , (CLI.oPrintCommands, CLI.optShowCmds)
1607
             , (CLI.oLuxiSocket,    CLI.optLuxi)
1608
             ]
1609
  in conjoin $ map (checkStringArg argument) args
1610

    
1611
-- | Helper to test that a given option is accepted OK with quick exit.
1612
checkEarlyExit name options param =
1613
  case CLI.parseOptsInner [param] name options of
1614
    Left (code, _) -> if code == 0
1615
                          then property True
1616
                          else failTest $ "Program " ++ name ++
1617
                                 " returns invalid code " ++ show code ++
1618
                                 " for option " ++ param
1619
    _ -> failTest $ "Program " ++ name ++ " doesn't consider option " ++
1620
         param ++ " as early exit one"
1621

    
1622
-- | Test that all binaries support some common options. There is
1623
-- nothing actually random about this test...
1624
prop_CLI_stdopts =
1625
  let params = ["-h", "--help", "-V", "--version"]
1626
      opts = map (\(name, (_, o)) -> (name, o)) Program.personalities
1627
      -- apply checkEarlyExit across the cartesian product of params and opts
1628
  in conjoin [checkEarlyExit n o p | p <- params, (n, o) <- opts]
1629

    
1630
testSuite "CLI"
1631
          [ 'prop_CLI_parseISpec
1632
          , 'prop_CLI_parseISpecFail
1633
          , 'prop_CLI_parseYesNo
1634
          , 'prop_CLI_StringArg
1635
          , 'prop_CLI_stdopts
1636
          ]