Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / HTools / QC.hs @ 89298c04

History | View | Annotate | Download (56.5 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.Hail
79
import qualified Ganeti.HTools.Program.Hbal
80
import qualified Ganeti.HTools.Program.Hscan
81
import qualified Ganeti.HTools.Program.Hspace
82

    
83
import Ganeti.HTools.QCHelper (testSuite)
84

    
85
-- * Constants
86

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

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

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

    
99
-- | All disk templates (used later)
100
allDiskTemplates :: [Types.DiskTemplate]
101
allDiskTemplates = [minBound..maxBound]
102

    
103
-- | Null iPolicy, and by null we mean very liberal.
104
nullIPolicy = Types.IPolicy
105
  { Types.iPolicyMinSpec = Types.ISpec { Types.iSpecMemorySize = 0
106
                                       , Types.iSpecCpuCount   = 0
107
                                       , Types.iSpecDiskSize   = 0
108
                                       , Types.iSpecDiskCount  = 0
109
                                       , Types.iSpecNicCount   = 0
110
                                       }
111
  , Types.iPolicyMaxSpec = Types.ISpec { Types.iSpecMemorySize = maxBound
112
                                       , Types.iSpecCpuCount   = maxBound
113
                                       , Types.iSpecDiskSize   = maxBound
114
                                       , Types.iSpecDiskCount  = C.maxDisks
115
                                       , Types.iSpecNicCount   = C.maxNics
116
                                       }
117
  , Types.iPolicyStdSpec = Types.ISpec { Types.iSpecMemorySize = Types.unitMem
118
                                       , Types.iSpecCpuCount   = Types.unitCpu
119
                                       , Types.iSpecDiskSize   = Types.unitDsk
120
                                       , Types.iSpecDiskCount  = 1
121
                                       , Types.iSpecNicCount   = 1
122
                                       }
123
  , Types.iPolicyDiskTemplates = [Types.DTDrbd8, Types.DTPlain]
124
  }
125

    
126

    
127
defGroup :: Group.Group
128
defGroup = flip Group.setIdx 0 $
129
             Group.create "default" Types.defaultGroupID Types.AllocPreferred
130
                  nullIPolicy
131

    
132
defGroupList :: Group.List
133
defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
134

    
135
defGroupAssoc :: Data.Map.Map String Types.Gdx
136
defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
137

    
138
-- * Helper functions
139

    
140
-- | Simple checker for whether OpResult is fail or pass.
141
isFailure :: Types.OpResult a -> Bool
142
isFailure (Types.OpFail _) = True
143
isFailure _ = False
144

    
145
-- | Checks for equality with proper annotation.
146
(==?) :: (Show a, Eq a) => a -> a -> Property
147
(==?) x y = printTestCase
148
            ("Expected equality, but '" ++
149
             show x ++ "' /= '" ++ show y ++ "'") (x == y)
150
infix 3 ==?
151

    
152
-- | Show a message and fail the test.
153
failTest :: String -> Property
154
failTest msg = printTestCase msg False
155

    
156
-- | Update an instance to be smaller than a node.
157
setInstanceSmallerThanNode node inst =
158
  inst { Instance.mem = Node.availMem node `div` 2
159
       , Instance.dsk = Node.availDisk node `div` 2
160
       , Instance.vcpus = Node.availCpu node `div` 2
161
       }
162

    
163
-- | Create an instance given its spec.
164
createInstance mem dsk vcpus =
165
  Instance.create "inst-unnamed" mem dsk vcpus Types.Running [] True (-1) (-1)
166
    Types.DTDrbd8
167

    
168
-- | Create a small cluster by repeating a node spec.
169
makeSmallCluster :: Node.Node -> Int -> Node.List
170
makeSmallCluster node count =
171
  let origname = Node.name node
172
      origalias = Node.alias node
173
      nodes = map (\idx -> node { Node.name = origname ++ "-" ++ show idx
174
                                , Node.alias = origalias ++ "-" ++ show idx })
175
              [1..count]
176
      fn = flip Node.buildPeers Container.empty
177
      namelst = map (\n -> (Node.name n, fn n)) nodes
178
      (_, nlst) = Loader.assignIndices namelst
179
  in nlst
180

    
181
-- | Make a small cluster, both nodes and instances.
182
makeSmallEmptyCluster :: Node.Node -> Int -> Instance.Instance
183
                      -> (Node.List, Instance.List, Instance.Instance)
184
makeSmallEmptyCluster node count inst =
185
  (makeSmallCluster node count, Container.empty,
186
   setInstanceSmallerThanNode node inst)
187

    
188
-- | Checks if a node is "big" enough.
189
isNodeBig :: Int -> Node.Node -> Bool
190
isNodeBig size node = Node.availDisk node > size * Types.unitDsk
191
                      && Node.availMem node > size * Types.unitMem
192
                      && Node.availCpu node > size * Types.unitCpu
193

    
194
canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
195
canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
196

    
197
-- | Assigns a new fresh instance to a cluster; this is not
198
-- allocation, so no resource checks are done.
199
assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
200
                  Types.Idx -> Types.Idx ->
201
                  (Node.List, Instance.List)
202
assignInstance nl il inst pdx sdx =
203
  let pnode = Container.find pdx nl
204
      snode = Container.find sdx nl
205
      maxiidx = if Container.null il
206
                  then 0
207
                  else fst (Container.findMax il) + 1
208
      inst' = inst { Instance.idx = maxiidx,
209
                     Instance.pNode = pdx, Instance.sNode = sdx }
210
      pnode' = Node.setPri pnode inst'
211
      snode' = Node.setSec snode inst'
212
      nl' = Container.addTwo pdx pnode' sdx snode' nl
213
      il' = Container.add maxiidx inst' il
214
  in (nl', il')
215

    
216
-- | Generates a list of a given size with non-duplicate elements.
217
genUniquesList :: (Eq a, Arbitrary a) => Int -> Gen [a]
218
genUniquesList cnt =
219
  foldM (\lst _ -> do
220
           newelem <- arbitrary `suchThat` (`notElem` lst)
221
           return (newelem:lst)) [] [1..cnt]
222

    
223
-- * Arbitrary instances
224

    
225
-- | Defines a DNS name.
226
newtype DNSChar = DNSChar { dnsGetChar::Char }
227

    
228
instance Arbitrary DNSChar where
229
  arbitrary = do
230
    x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
231
    return (DNSChar x)
232

    
233
-- | Generates a single name component.
234
getName :: Gen String
235
getName = do
236
  n <- choose (1, 64)
237
  dn <- vector n::Gen [DNSChar]
238
  return (map dnsGetChar dn)
239

    
240
-- | Generates an entire FQDN.
241
getFQDN :: Gen String
242
getFQDN = do
243
  ncomps <- choose (1, 4)
244
  names <- mapM (const getName) [1..ncomps::Int]
245
  return $ intercalate "." names
246

    
247
-- | Defines a tag type.
248
newtype TagChar = TagChar { tagGetChar :: Char }
249

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

    
255
instance Arbitrary TagChar where
256
  arbitrary = do
257
    c <- elements tagChar
258
    return (TagChar c)
259

    
260
-- | Generates a tag
261
genTag :: Gen [TagChar]
262
genTag = do
263
  -- the correct value would be C.maxTagLen, but that's way too
264
  -- verbose in unittests, and at the moment I don't see any possible
265
  -- bugs with longer tags and the way we use tags in htools
266
  n <- choose (1, 10)
267
  vector n
268

    
269
-- | Generates a list of tags (correctly upper bounded).
270
genTags :: Gen [String]
271
genTags = do
272
  -- the correct value would be C.maxTagsPerObj, but per the comment
273
  -- in genTag, we don't use tags enough in htools to warrant testing
274
  -- such big values
275
  n <- choose (0, 10::Int)
276
  tags <- mapM (const genTag) [1..n]
277
  return $ map (map tagGetChar) tags
278

    
279
instance Arbitrary Types.InstanceStatus where
280
    arbitrary = elements [minBound..maxBound]
281

    
282
-- | Generates a random instance with maximum disk/mem/cpu values.
283
genInstanceSmallerThan :: Int -> Int -> Int -> Gen Instance.Instance
284
genInstanceSmallerThan lim_mem lim_dsk lim_cpu = do
285
  name <- getFQDN
286
  mem <- choose (0, lim_mem)
287
  dsk <- choose (0, lim_dsk)
288
  run_st <- arbitrary
289
  pn <- arbitrary
290
  sn <- arbitrary
291
  vcpus <- choose (0, lim_cpu)
292
  return $ Instance.create name mem dsk vcpus run_st [] True pn sn
293
         Types.DTDrbd8
294

    
295
-- | Generates an instance smaller than a node.
296
genInstanceSmallerThanNode :: Node.Node -> Gen Instance.Instance
297
genInstanceSmallerThanNode node =
298
  genInstanceSmallerThan (Node.availMem node `div` 2)
299
                         (Node.availDisk node `div` 2)
300
                         (Node.availCpu node `div` 2)
301

    
302
-- let's generate a random instance
303
instance Arbitrary Instance.Instance where
304
  arbitrary = genInstanceSmallerThan maxMem maxDsk maxCpu
305

    
306
-- | Generas an arbitrary node based on sizing information.
307
genNode :: Maybe Int -- ^ Minimum node size in terms of units
308
        -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
309
                     -- just by the max... constants)
310
        -> Gen Node.Node
311
genNode min_multiplier max_multiplier = do
312
  let (base_mem, base_dsk, base_cpu) =
313
        case min_multiplier of
314
          Just mm -> (mm * Types.unitMem,
315
                      mm * Types.unitDsk,
316
                      mm * Types.unitCpu)
317
          Nothing -> (0, 0, 0)
318
      (top_mem, top_dsk, top_cpu)  =
319
        case max_multiplier of
320
          Just mm -> (mm * Types.unitMem,
321
                      mm * Types.unitDsk,
322
                      mm * Types.unitCpu)
323
          Nothing -> (maxMem, maxDsk, maxCpu)
324
  name  <- getFQDN
325
  mem_t <- choose (base_mem, top_mem)
326
  mem_f <- choose (base_mem, mem_t)
327
  mem_n <- choose (0, mem_t - mem_f)
328
  dsk_t <- choose (base_dsk, top_dsk)
329
  dsk_f <- choose (base_dsk, dsk_t)
330
  cpu_t <- choose (base_cpu, top_cpu)
331
  offl  <- arbitrary
332
  let n = Node.create name (fromIntegral mem_t) mem_n mem_f
333
          (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
334
      n' = Node.setPolicy nullIPolicy n
335
  return $ Node.buildPeers n' Container.empty
336

    
337
-- | Helper function to generate a sane node.
338
genOnlineNode :: Gen Node.Node
339
genOnlineNode = do
340
  arbitrary `suchThat` (\n -> not (Node.offline n) &&
341
                              not (Node.failN1 n) &&
342
                              Node.availDisk n > 0 &&
343
                              Node.availMem n > 0 &&
344
                              Node.availCpu n > 0)
345

    
346
-- and a random node
347
instance Arbitrary Node.Node where
348
  arbitrary = genNode Nothing Nothing
349

    
350
-- replace disks
351
instance Arbitrary OpCodes.ReplaceDisksMode where
352
  arbitrary = elements [minBound..maxBound]
353

    
354
instance Arbitrary OpCodes.OpCode where
355
  arbitrary = do
356
    op_id <- elements [ "OP_TEST_DELAY"
357
                      , "OP_INSTANCE_REPLACE_DISKS"
358
                      , "OP_INSTANCE_FAILOVER"
359
                      , "OP_INSTANCE_MIGRATE"
360
                      ]
361
    case op_id of
362
      "OP_TEST_DELAY" ->
363
        liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
364
      "OP_INSTANCE_REPLACE_DISKS" ->
365
        liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
366
          arbitrary arbitrary arbitrary
367
      "OP_INSTANCE_FAILOVER" ->
368
        liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
369
          arbitrary
370
      "OP_INSTANCE_MIGRATE" ->
371
        liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
372
          arbitrary arbitrary arbitrary
373
      _ -> fail "Wrong opcode"
374

    
375
instance Arbitrary Jobs.OpStatus where
376
  arbitrary = elements [minBound..maxBound]
377

    
378
instance Arbitrary Jobs.JobStatus where
379
  arbitrary = elements [minBound..maxBound]
380

    
381
newtype SmallRatio = SmallRatio Double deriving Show
382
instance Arbitrary SmallRatio where
383
  arbitrary = do
384
    v <- choose (0, 1)
385
    return $ SmallRatio v
386

    
387
instance Arbitrary Types.AllocPolicy where
388
  arbitrary = elements [minBound..maxBound]
389

    
390
instance Arbitrary Types.DiskTemplate where
391
  arbitrary = elements [minBound..maxBound]
392

    
393
instance Arbitrary Types.FailMode where
394
  arbitrary = elements [minBound..maxBound]
395

    
396
instance Arbitrary Types.EvacMode where
397
  arbitrary = elements [minBound..maxBound]
398

    
399
instance Arbitrary a => Arbitrary (Types.OpResult a) where
400
  arbitrary = arbitrary >>= \c ->
401
              if c
402
                then liftM Types.OpGood arbitrary
403
                else liftM Types.OpFail arbitrary
404

    
405
instance Arbitrary Types.ISpec where
406
  arbitrary = do
407
    mem_s <- arbitrary::Gen (NonNegative Int)
408
    dsk_c <- arbitrary::Gen (NonNegative Int)
409
    dsk_s <- arbitrary::Gen (NonNegative Int)
410
    cpu_c <- arbitrary::Gen (NonNegative Int)
411
    nic_c <- arbitrary::Gen (NonNegative Int)
412
    return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
413
                       , Types.iSpecCpuCount   = fromIntegral cpu_c
414
                       , Types.iSpecDiskSize   = fromIntegral dsk_s
415
                       , Types.iSpecDiskCount  = fromIntegral dsk_c
416
                       , Types.iSpecNicCount   = fromIntegral nic_c
417
                       }
418

    
419
-- | Generates an ispec bigger than the given one.
420
genBiggerISpec :: Types.ISpec -> Gen Types.ISpec
421
genBiggerISpec imin = do
422
  mem_s <- choose (Types.iSpecMemorySize imin, maxBound)
423
  dsk_c <- choose (Types.iSpecDiskCount imin, maxBound)
424
  dsk_s <- choose (Types.iSpecDiskSize imin, maxBound)
425
  cpu_c <- choose (Types.iSpecCpuCount imin, maxBound)
426
  nic_c <- choose (Types.iSpecNicCount imin, maxBound)
427
  return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
428
                     , Types.iSpecCpuCount   = fromIntegral cpu_c
429
                     , Types.iSpecDiskSize   = fromIntegral dsk_s
430
                     , Types.iSpecDiskCount  = fromIntegral dsk_c
431
                     , Types.iSpecNicCount   = fromIntegral nic_c
432
                     }
433

    
434
instance Arbitrary Types.IPolicy where
435
  arbitrary = do
436
    imin <- arbitrary
437
    istd <- genBiggerISpec imin
438
    imax <- genBiggerISpec istd
439
    num_tmpl <- choose (0, length allDiskTemplates)
440
    dts  <- genUniquesList num_tmpl
441
    return Types.IPolicy { Types.iPolicyMinSpec = imin
442
                         , Types.iPolicyStdSpec = istd
443
                         , Types.iPolicyMaxSpec = imax
444
                         , Types.iPolicyDiskTemplates = dts
445
                         }
446

    
447
-- * Actual tests
448

    
449
-- ** Utils tests
450

    
451
-- | If the list is not just an empty element, and if the elements do
452
-- not contain commas, then join+split should be idempotent.
453
prop_Utils_commaJoinSplit =
454
  forAll (arbitrary `suchThat`
455
          (\l -> l /= [""] && all (notElem ',') l )) $ \lst ->
456
  Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
457

    
458
-- | Split and join should always be idempotent.
459
prop_Utils_commaSplitJoin s =
460
  Utils.commaJoin (Utils.sepSplit ',' s) ==? s
461

    
462
-- | fromObjWithDefault, we test using the Maybe monad and an integer
463
-- value.
464
prop_Utils_fromObjWithDefault def_value random_key =
465
  -- a missing key will be returned with the default
466
  JSON.fromObjWithDefault [] random_key def_value == Just def_value &&
467
  -- a found key will be returned as is, not with default
468
  JSON.fromObjWithDefault [(random_key, J.showJSON def_value)]
469
       random_key (def_value+1) == Just def_value
470
    where _types = def_value :: Integer
471

    
472
-- | Test that functional if' behaves like the syntactic sugar if.
473
prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
474
prop_Utils_if'if cnd a b =
475
  Utils.if' cnd a b ==? if cnd then a else b
476

    
477
-- | Test basic select functionality
478
prop_Utils_select :: Int      -- ^ Default result
479
                  -> [Int]    -- ^ List of False values
480
                  -> [Int]    -- ^ List of True values
481
                  -> Gen Prop -- ^ Test result
482
prop_Utils_select def lst1 lst2 =
483
  Utils.select def (flist ++ tlist) ==? expectedresult
484
    where expectedresult = Utils.if' (null lst2) def (head lst2)
485
          flist = zip (repeat False) lst1
486
          tlist = zip (repeat True)  lst2
487

    
488
-- | Test basic select functionality with undefined default
489
prop_Utils_select_undefd :: [Int]            -- ^ List of False values
490
                         -> NonEmptyList Int -- ^ List of True values
491
                         -> Gen Prop         -- ^ Test result
492
prop_Utils_select_undefd lst1 (NonEmpty lst2) =
493
  Utils.select undefined (flist ++ tlist) ==? head lst2
494
    where flist = zip (repeat False) lst1
495
          tlist = zip (repeat True)  lst2
496

    
497
-- | Test basic select functionality with undefined list values
498
prop_Utils_select_undefv :: [Int]            -- ^ List of False values
499
                         -> NonEmptyList Int -- ^ List of True values
500
                         -> Gen Prop         -- ^ Test result
501
prop_Utils_select_undefv lst1 (NonEmpty lst2) =
502
  Utils.select undefined cndlist ==? head lst2
503
    where flist = zip (repeat False) lst1
504
          tlist = zip (repeat True)  lst2
505
          cndlist = flist ++ tlist ++ [undefined]
506

    
507
prop_Utils_parseUnit (NonNegative n) =
508
  Utils.parseUnit (show n) == Types.Ok n &&
509
  Utils.parseUnit (show n ++ "m") == Types.Ok n &&
510
  (case Utils.parseUnit (show n ++ "M") of
511
     Types.Ok m -> if n > 0
512
                     then m < n  -- for positive values, X MB is < than X MiB
513
                     else m == 0 -- but for 0, 0 MB == 0 MiB
514
     Types.Bad _ -> False) &&
515
  Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
516
  Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
517
  Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
518
    where _types = n::Int
519

    
520
-- | Test list for the Utils module.
521
testSuite "Utils"
522
            [ 'prop_Utils_commaJoinSplit
523
            , 'prop_Utils_commaSplitJoin
524
            , 'prop_Utils_fromObjWithDefault
525
            , 'prop_Utils_if'if
526
            , 'prop_Utils_select
527
            , 'prop_Utils_select_undefd
528
            , 'prop_Utils_select_undefv
529
            , 'prop_Utils_parseUnit
530
            ]
531

    
532
-- ** PeerMap tests
533

    
534
-- | Make sure add is idempotent.
535
prop_PeerMap_addIdempotent pmap key em =
536
  fn puniq ==? fn (fn puniq)
537
    where _types = (pmap::PeerMap.PeerMap,
538
                    key::PeerMap.Key, em::PeerMap.Elem)
539
          fn = PeerMap.add key em
540
          puniq = PeerMap.accumArray const pmap
541

    
542
-- | Make sure remove is idempotent.
543
prop_PeerMap_removeIdempotent pmap key =
544
  fn puniq ==? fn (fn puniq)
545
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
546
          fn = PeerMap.remove key
547
          puniq = PeerMap.accumArray const pmap
548

    
549
-- | Make sure a missing item returns 0.
550
prop_PeerMap_findMissing pmap key =
551
  PeerMap.find key (PeerMap.remove key puniq) ==? 0
552
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
553
          puniq = PeerMap.accumArray const pmap
554

    
555
-- | Make sure an added item is found.
556
prop_PeerMap_addFind pmap key em =
557
  PeerMap.find key (PeerMap.add key em puniq) ==? em
558
    where _types = (pmap::PeerMap.PeerMap,
559
                    key::PeerMap.Key, em::PeerMap.Elem)
560
          puniq = PeerMap.accumArray const pmap
561

    
562
-- | Manual check that maxElem returns the maximum indeed, or 0 for null.
563
prop_PeerMap_maxElem pmap =
564
  PeerMap.maxElem puniq ==? if null puniq then 0
565
                              else (maximum . snd . unzip) puniq
566
    where _types = pmap::PeerMap.PeerMap
567
          puniq = PeerMap.accumArray const pmap
568

    
569
-- | List of tests for the PeerMap module.
570
testSuite "PeerMap"
571
            [ 'prop_PeerMap_addIdempotent
572
            , 'prop_PeerMap_removeIdempotent
573
            , 'prop_PeerMap_maxElem
574
            , 'prop_PeerMap_addFind
575
            , 'prop_PeerMap_findMissing
576
            ]
577

    
578
-- ** Container tests
579

    
580
-- we silence the following due to hlint bug fixed in later versions
581
{-# ANN prop_Container_addTwo "HLint: ignore Avoid lambda" #-}
582
prop_Container_addTwo cdata i1 i2 =
583
  fn i1 i2 cont == fn i2 i1 cont &&
584
  fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
585
    where _types = (cdata::[Int],
586
                    i1::Int, i2::Int)
587
          cont = foldl (\c x -> Container.add x x c) Container.empty cdata
588
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
589

    
590
prop_Container_nameOf node =
591
  let nl = makeSmallCluster node 1
592
      fnode = head (Container.elems nl)
593
  in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
594

    
595
-- | We test that in a cluster, given a random node, we can find it by
596
-- its name and alias, as long as all names and aliases are unique,
597
-- and that we fail to find a non-existing name.
598
prop_Container_findByName node =
599
  forAll (choose (1, 20)) $ \ cnt ->
600
  forAll (choose (0, cnt - 1)) $ \ fidx ->
601
  forAll (genUniquesList (cnt * 2)) $ \ allnames ->
602
  forAll (arbitrary `suchThat` (`notElem` allnames)) $ \ othername ->
603
  let names = zip (take cnt allnames) (drop cnt allnames)
604
      nl = makeSmallCluster node cnt
605
      nodes = Container.elems nl
606
      nodes' = map (\((name, alias), nn) -> (Node.idx nn,
607
                                             nn { Node.name = name,
608
                                                  Node.alias = alias }))
609
               $ zip names nodes
610
      nl' = Container.fromList nodes'
611
      target = snd (nodes' !! fidx)
612
  in Container.findByName nl' (Node.name target) == Just target &&
613
     Container.findByName nl' (Node.alias target) == Just target &&
614
     isNothing (Container.findByName nl' othername)
615

    
616
testSuite "Container"
617
            [ 'prop_Container_addTwo
618
            , 'prop_Container_nameOf
619
            , 'prop_Container_findByName
620
            ]
621

    
622
-- ** Instance tests
623

    
624
-- Simple instance tests, we only have setter/getters
625

    
626
prop_Instance_creat inst =
627
  Instance.name inst ==? Instance.alias inst
628

    
629
prop_Instance_setIdx inst idx =
630
  Instance.idx (Instance.setIdx inst idx) ==? idx
631
    where _types = (inst::Instance.Instance, idx::Types.Idx)
632

    
633
prop_Instance_setName inst name =
634
  Instance.name newinst == name &&
635
  Instance.alias newinst == name
636
    where _types = (inst::Instance.Instance, name::String)
637
          newinst = Instance.setName inst name
638

    
639
prop_Instance_setAlias inst name =
640
  Instance.name newinst == Instance.name inst &&
641
  Instance.alias newinst == name
642
    where _types = (inst::Instance.Instance, name::String)
643
          newinst = Instance.setAlias inst name
644

    
645
prop_Instance_setPri inst pdx =
646
  Instance.pNode (Instance.setPri inst pdx) ==? pdx
647
    where _types = (inst::Instance.Instance, pdx::Types.Ndx)
648

    
649
prop_Instance_setSec inst sdx =
650
  Instance.sNode (Instance.setSec inst sdx) ==? sdx
651
    where _types = (inst::Instance.Instance, sdx::Types.Ndx)
652

    
653
prop_Instance_setBoth inst pdx sdx =
654
  Instance.pNode si == pdx && Instance.sNode si == sdx
655
    where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
656
          si = Instance.setBoth inst pdx sdx
657

    
658
prop_Instance_shrinkMG inst =
659
  Instance.mem inst >= 2 * Types.unitMem ==>
660
    case Instance.shrinkByType inst Types.FailMem of
661
      Types.Ok inst' -> Instance.mem inst' == Instance.mem inst - Types.unitMem
662
      _ -> False
663

    
664
prop_Instance_shrinkMF inst =
665
  forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
666
    let inst' = inst { Instance.mem = mem}
667
    in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
668

    
669
prop_Instance_shrinkCG inst =
670
  Instance.vcpus inst >= 2 * Types.unitCpu ==>
671
    case Instance.shrinkByType inst Types.FailCPU of
672
      Types.Ok inst' ->
673
        Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
674
      _ -> False
675

    
676
prop_Instance_shrinkCF inst =
677
  forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
678
    let inst' = inst { Instance.vcpus = vcpus }
679
    in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
680

    
681
prop_Instance_shrinkDG inst =
682
  Instance.dsk inst >= 2 * Types.unitDsk ==>
683
    case Instance.shrinkByType inst Types.FailDisk of
684
      Types.Ok inst' ->
685
        Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
686
      _ -> False
687

    
688
prop_Instance_shrinkDF inst =
689
  forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
690
    let inst' = inst { Instance.dsk = dsk }
691
    in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
692

    
693
prop_Instance_setMovable inst m =
694
  Instance.movable inst' ==? m
695
    where inst' = Instance.setMovable inst m
696

    
697
testSuite "Instance"
698
            [ 'prop_Instance_creat
699
            , 'prop_Instance_setIdx
700
            , 'prop_Instance_setName
701
            , 'prop_Instance_setAlias
702
            , 'prop_Instance_setPri
703
            , 'prop_Instance_setSec
704
            , 'prop_Instance_setBoth
705
            , 'prop_Instance_shrinkMG
706
            , 'prop_Instance_shrinkMF
707
            , 'prop_Instance_shrinkCG
708
            , 'prop_Instance_shrinkCF
709
            , 'prop_Instance_shrinkDG
710
            , 'prop_Instance_shrinkDF
711
            , 'prop_Instance_setMovable
712
            ]
713

    
714
-- ** Backends
715

    
716
-- *** Text backend tests
717

    
718
-- Instance text loader tests
719

    
720
prop_Text_Load_Instance name mem dsk vcpus status
721
                        (NonEmpty pnode) snode
722
                        (NonNegative pdx) (NonNegative sdx) autobal dt =
723
  pnode /= snode && pdx /= sdx ==>
724
  let vcpus_s = show vcpus
725
      dsk_s = show dsk
726
      mem_s = show mem
727
      status_s = Types.instanceStatusToRaw status
728
      ndx = if null snode
729
              then [(pnode, pdx)]
730
              else [(pnode, pdx), (snode, sdx)]
731
      nl = Data.Map.fromList ndx
732
      tags = ""
733
      sbal = if autobal then "Y" else "N"
734
      sdt = Types.diskTemplateToRaw dt
735
      inst = Text.loadInst nl
736
             [name, mem_s, dsk_s, vcpus_s, status_s,
737
              sbal, pnode, snode, sdt, tags]
738
      fail1 = Text.loadInst nl
739
              [name, mem_s, dsk_s, vcpus_s, status_s,
740
               sbal, pnode, pnode, tags]
741
      _types = ( name::String, mem::Int, dsk::Int
742
               , vcpus::Int, status::Types.InstanceStatus
743
               , snode::String
744
               , autobal::Bool)
745
  in case inst of
746
       Types.Bad msg -> failTest $ "Failed to load instance: " ++ msg
747
       Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
748
                                        \ loading the instance" $
749
               Instance.name i == name &&
750
               Instance.vcpus i == vcpus &&
751
               Instance.mem i == mem &&
752
               Instance.pNode i == pdx &&
753
               Instance.sNode i == (if null snode
754
                                      then Node.noSecondary
755
                                      else sdx) &&
756
               Instance.autoBalance i == autobal &&
757
               Types.isBad fail1
758

    
759
prop_Text_Load_InstanceFail ktn fields =
760
  length fields /= 10 ==>
761
    case Text.loadInst nl fields of
762
      Types.Ok _ -> failTest "Managed to load instance from invalid data"
763
      Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
764
                       "Invalid/incomplete instance data: '" `isPrefixOf` msg
765
    where nl = Data.Map.fromList ktn
766

    
767
prop_Text_Load_Node name tm nm fm td fd tc fo =
768
  let conv v = if v < 0
769
                 then "?"
770
                 else show v
771
      tm_s = conv tm
772
      nm_s = conv nm
773
      fm_s = conv fm
774
      td_s = conv td
775
      fd_s = conv fd
776
      tc_s = conv tc
777
      fo_s = if fo
778
               then "Y"
779
               else "N"
780
      any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
781
      gid = Group.uuid defGroup
782
  in case Text.loadNode defGroupAssoc
783
       [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
784
       Nothing -> False
785
       Just (name', node) ->
786
         if fo || any_broken
787
           then Node.offline node
788
           else Node.name node == name' && name' == name &&
789
                Node.alias node == name &&
790
                Node.tMem node == fromIntegral tm &&
791
                Node.nMem node == nm &&
792
                Node.fMem node == fm &&
793
                Node.tDsk node == fromIntegral td &&
794
                Node.fDsk node == fd &&
795
                Node.tCpu node == fromIntegral tc
796

    
797
prop_Text_Load_NodeFail fields =
798
  length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
799

    
800
prop_Text_NodeLSIdempotent node =
801
  (Text.loadNode defGroupAssoc.
802
       Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
803
  Just (Node.name n, n)
804
    -- override failN1 to what loadNode returns by default
805
    where n = node { Node.failN1 = True, Node.offline = False
806
                   , Node.iPolicy = Types.defIPolicy }
807

    
808
prop_Text_ISpecIdempotent ispec =
809
  case Text.loadISpec "dummy" . Utils.sepSplit ',' .
810
       Text.serializeISpec $ ispec of
811
    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
812
    Types.Ok ispec' -> ispec ==? ispec'
813

    
814
prop_Text_IPolicyIdempotent ipol =
815
  case Text.loadIPolicy . Utils.sepSplit '|' $
816
       Text.serializeIPolicy owner ipol of
817
    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
818
    Types.Ok res -> (owner, ipol) ==? res
819
  where owner = "dummy"
820

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

    
856
testSuite "Text"
857
            [ 'prop_Text_Load_Instance
858
            , 'prop_Text_Load_InstanceFail
859
            , 'prop_Text_Load_Node
860
            , 'prop_Text_Load_NodeFail
861
            , 'prop_Text_NodeLSIdempotent
862
            , 'prop_Text_ISpecIdempotent
863
            , 'prop_Text_IPolicyIdempotent
864
            , 'prop_Text_CreateSerialise
865
            ]
866

    
867
-- *** Simu backend
868

    
869
-- | Generates a tuple of specs for simulation.
870
genSimuSpec :: Gen (String, Int, Int, Int, Int)
871
genSimuSpec = do
872
  pol <- elements [C.allocPolicyPreferred,
873
                   C.allocPolicyLastResort, C.allocPolicyUnallocable,
874
                  "p", "a", "u"]
875
 -- should be reasonable (nodes/group), bigger values only complicate
876
 -- the display of failed tests, and we don't care (in this particular
877
 -- test) about big node groups
878
  nodes <- choose (0, 20)
879
  dsk <- choose (0, maxDsk)
880
  mem <- choose (0, maxMem)
881
  cpu <- choose (0, maxCpu)
882
  return (pol, nodes, dsk, mem, cpu)
883

    
884
-- | Checks that given a set of corrects specs, we can load them
885
-- successfully, and that at high-level the values look right.
886
prop_SimuLoad =
887
  forAll (choose (0, 10)) $ \ngroups ->
888
  forAll (replicateM ngroups genSimuSpec) $ \specs ->
889
  let strspecs = map (\(p, n, d, m, c) -> printf "%s,%d,%d,%d,%d"
890
                                          p n d m c::String) specs
891
      totnodes = sum $ map (\(_, n, _, _, _) -> n) specs
892
      mdc_in = concatMap (\(_, n, d, m, c) ->
893
                            replicate n (fromIntegral m, fromIntegral d,
894
                                         fromIntegral c,
895
                                         fromIntegral m, fromIntegral d)) specs
896
  in case Simu.parseData strspecs of
897
       Types.Bad msg -> failTest $ "Failed to load specs: " ++ msg
898
       Types.Ok (Loader.ClusterData gl nl il tags ipol) ->
899
         let nodes = map snd $ IntMap.toAscList nl
900
             nidx = map Node.idx nodes
901
             mdc_out = map (\n -> (Node.tMem n, Node.tDsk n, Node.tCpu n,
902
                                   Node.fMem n, Node.fDsk n)) nodes
903
         in
904
         Container.size gl ==? ngroups .&&.
905
         Container.size nl ==? totnodes .&&.
906
         Container.size il ==? 0 .&&.
907
         length tags ==? 0 .&&.
908
         ipol ==? Types.defIPolicy .&&.
909
         nidx ==? [1..totnodes] .&&.
910
         mdc_in ==? mdc_out .&&.
911
         map Group.iPolicy (Container.elems gl) ==?
912
             replicate ngroups Types.defIPolicy
913

    
914
testSuite "Simu"
915
            [ 'prop_SimuLoad
916
            ]
917

    
918
-- ** Node tests
919

    
920
prop_Node_setAlias node name =
921
  Node.name newnode == Node.name node &&
922
  Node.alias newnode == name
923
    where _types = (node::Node.Node, name::String)
924
          newnode = Node.setAlias node name
925

    
926
prop_Node_setOffline node status =
927
  Node.offline newnode ==? status
928
    where newnode = Node.setOffline node status
929

    
930
prop_Node_setXmem node xm =
931
  Node.xMem newnode ==? xm
932
    where newnode = Node.setXmem node xm
933

    
934
prop_Node_setMcpu node mc =
935
  Node.mCpu newnode ==? mc
936
    where newnode = Node.setMcpu node mc
937

    
938
-- | Check that an instance add with too high memory or disk will be
939
-- rejected.
940
prop_Node_addPriFM node inst =
941
  Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
942
  not (Instance.instanceOffline inst) ==>
943
  case Node.addPri node inst'' of
944
    Types.OpFail Types.FailMem -> True
945
    _ -> False
946
  where _types = (node::Node.Node, inst::Instance.Instance)
947
        inst' = setInstanceSmallerThanNode node inst
948
        inst'' = inst' { Instance.mem = Instance.mem inst }
949

    
950
prop_Node_addPriFD node inst =
951
  Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
952
    case Node.addPri node inst'' of
953
      Types.OpFail Types.FailDisk -> True
954
      _ -> False
955
    where _types = (node::Node.Node, inst::Instance.Instance)
956
          inst' = setInstanceSmallerThanNode node inst
957
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
958

    
959
prop_Node_addPriFC (Positive extra) =
960
  forAll genOnlineNode $ \node ->
961
  forAll (arbitrary `suchThat` Instance.instanceNotOffline) $ \inst ->
962
  let inst' = setInstanceSmallerThanNode node inst
963
      inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
964
  in case Node.addPri node inst'' of
965
       Types.OpFail Types.FailCPU -> property True
966
       v -> failTest $ "Expected OpFail FailCPU, but got " ++ show v
967

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

    
978
-- | Check that an offline instance with reasonable disk size can always
979
-- be added.
980
prop_Node_addPriOffline =
981
  forAll (arbitrary `suchThat` ((> 0) . Node.fMem)) $ \node ->
982
  forAll (arbitrary `suchThat`
983
          (\ x ->  (Instance.dsk x  < Node.fDsk node) &&
984
                   Instance.instanceOffline x)) $ \inst ->
985
  case Node.addPri node inst of
986
    Types.OpGood _ -> True
987
    _ -> False
988

    
989
prop_Node_addSecOffline pdx =
990
  forAll genOnlineNode $ \node ->
991
  forAll (arbitrary `suchThat`
992
          (\ inst -> Instance.dsk inst  < Node.availDisk node)) $ \inst ->
993
  case Node.addSec node (inst { Instance.runSt = Types.AdminOffline }) pdx of
994
    Types.OpGood _ -> True
995
    _ -> False
996

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

    
1029
-- | Check mdsk setting.
1030
prop_Node_setMdsk node mx =
1031
  Node.loDsk node' >= 0 &&
1032
  fromIntegral (Node.loDsk node') <= Node.tDsk node &&
1033
  Node.availDisk node' >= 0 &&
1034
  Node.availDisk node' <= Node.fDsk node' &&
1035
  fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
1036
  Node.mDsk node' == mx'
1037
    where _types = (node::Node.Node, mx::SmallRatio)
1038
          node' = Node.setMdsk node mx'
1039
          SmallRatio mx' = mx
1040

    
1041
-- Check tag maps
1042
prop_Node_tagMaps_idempotent tags =
1043
  Node.delTags (Node.addTags m tags) tags ==? m
1044
    where m = Data.Map.empty
1045

    
1046
prop_Node_tagMaps_reject tags =
1047
  not (null tags) ==>
1048
  all (\t -> Node.rejectAddTags m [t]) tags
1049
    where m = Node.addTags Data.Map.empty tags
1050

    
1051
prop_Node_showField node =
1052
  forAll (elements Node.defaultFields) $ \ field ->
1053
  fst (Node.showHeader field) /= Types.unknownField &&
1054
  Node.showField node field /= Types.unknownField
1055

    
1056
prop_Node_computeGroups nodes =
1057
  let ng = Node.computeGroups nodes
1058
      onlyuuid = map fst ng
1059
  in length nodes == sum (map (length . snd) ng) &&
1060
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
1061
     length (nub onlyuuid) == length onlyuuid &&
1062
     (null nodes || not (null ng))
1063

    
1064
testSuite "Node"
1065
            [ 'prop_Node_setAlias
1066
            , 'prop_Node_setOffline
1067
            , 'prop_Node_setMcpu
1068
            , 'prop_Node_setXmem
1069
            , 'prop_Node_addPriFM
1070
            , 'prop_Node_addPriFD
1071
            , 'prop_Node_addPriFC
1072
            , 'prop_Node_addSec
1073
            , 'prop_Node_addPriOffline
1074
            , 'prop_Node_addSecOffline
1075
            , 'prop_Node_rMem
1076
            , 'prop_Node_setMdsk
1077
            , 'prop_Node_tagMaps_idempotent
1078
            , 'prop_Node_tagMaps_reject
1079
            , 'prop_Node_showField
1080
            , 'prop_Node_computeGroups
1081
            ]
1082

    
1083
-- ** Cluster tests
1084

    
1085
-- | Check that the cluster score is close to zero for a homogeneous
1086
-- cluster.
1087
prop_Score_Zero node =
1088
  forAll (choose (1, 1024)) $ \count ->
1089
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
1090
     (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
1091
  let fn = Node.buildPeers node Container.empty
1092
      nlst = replicate count fn
1093
      score = Cluster.compCVNodes nlst
1094
  -- we can't say == 0 here as the floating point errors accumulate;
1095
  -- this should be much lower than the default score in CLI.hs
1096
  in score <= 1e-12
1097

    
1098
-- | Check that cluster stats are sane.
1099
prop_CStats_sane =
1100
  forAll (choose (1, 1024)) $ \count ->
1101
  forAll genOnlineNode $ \node ->
1102
  let fn = Node.buildPeers node Container.empty
1103
      nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
1104
      nl = Container.fromList nlst
1105
      cstats = Cluster.totalResources nl
1106
  in Cluster.csAdsk cstats >= 0 &&
1107
     Cluster.csAdsk cstats <= Cluster.csFdsk cstats
1108

    
1109
-- | Check that one instance is allocated correctly, without
1110
-- rebalances needed.
1111
prop_ClusterAlloc_sane inst =
1112
  forAll (choose (5, 20)) $ \count ->
1113
  forAll genOnlineNode $ \node ->
1114
  let (nl, il, inst') = makeSmallEmptyCluster node count inst
1115
  in case Cluster.genAllocNodes defGroupList nl 2 True >>=
1116
     Cluster.tryAlloc nl il inst' of
1117
       Types.Bad _ -> False
1118
       Types.Ok as ->
1119
         case Cluster.asSolution as of
1120
           Nothing -> False
1121
           Just (xnl, xi, _, cv) ->
1122
             let il' = Container.add (Instance.idx xi) xi il
1123
                 tbl = Cluster.Table xnl il' cv []
1124
             in not (canBalance tbl True True False)
1125

    
1126
-- | Checks that on a 2-5 node cluster, we can allocate a random
1127
-- instance spec via tiered allocation (whatever the original instance
1128
-- spec), on either one or two nodes.
1129
prop_ClusterCanTieredAlloc inst =
1130
  forAll (choose (2, 5)) $ \count ->
1131
  forAll (choose (1, 2)) $ \rqnodes ->
1132
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1133
  let nl = makeSmallCluster node count
1134
      il = Container.empty
1135
      allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
1136
  in case allocnodes >>= \allocnodes' ->
1137
    Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
1138
       Types.Bad _ -> False
1139
       Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
1140
                                             IntMap.size il' == length ixes &&
1141
                                             length ixes == length cstats
1142

    
1143
-- | Helper function to create a cluster with the given range of nodes
1144
-- and allocate an instance on it.
1145
genClusterAlloc count node inst =
1146
  let nl = makeSmallCluster node count
1147
  in case Cluster.genAllocNodes defGroupList nl 2 True >>=
1148
     Cluster.tryAlloc nl Container.empty inst of
1149
       Types.Bad _ -> Types.Bad "Can't allocate"
1150
       Types.Ok as ->
1151
         case Cluster.asSolution as of
1152
           Nothing -> Types.Bad "Empty solution?"
1153
           Just (xnl, xi, _, _) ->
1154
             let xil = Container.add (Instance.idx xi) xi Container.empty
1155
             in Types.Ok (xnl, xil, xi)
1156

    
1157
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
1158
-- we can also relocate it.
1159
prop_ClusterAllocRelocate =
1160
  forAll (choose (4, 8)) $ \count ->
1161
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1162
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1163
  case genClusterAlloc count node inst of
1164
    Types.Bad msg -> failTest msg
1165
    Types.Ok (nl, il, inst') ->
1166
      case IAlloc.processRelocate defGroupList nl il
1167
             (Instance.idx inst) 1 [Instance.sNode inst'] of
1168
        Types.Ok _ -> printTestCase "??" True  -- huh, how to make
1169
                                               -- this nicer...
1170
        Types.Bad msg -> failTest $ "Failed to relocate: " ++ msg
1171

    
1172
-- | Helper property checker for the result of a nodeEvac or
1173
-- changeGroup operation.
1174
check_EvacMode grp inst result =
1175
  case result of
1176
    Types.Bad msg -> failTest $ "Couldn't evacuate/change group:" ++ msg
1177
    Types.Ok (_, _, es) ->
1178
      let moved = Cluster.esMoved es
1179
          failed = Cluster.esFailed es
1180
          opcodes = not . null $ Cluster.esOpCodes es
1181
      in failmsg ("'failed' not empty: " ++ show failed) (null failed) .&&.
1182
         failmsg "'opcodes' is null" opcodes .&&.
1183
         case moved of
1184
           [(idx', gdx, _)] -> failmsg "invalid instance moved" (idx == idx')
1185
                               .&&.
1186
                               failmsg "wrong target group"
1187
                                         (gdx == Group.idx grp)
1188
           v -> failmsg  ("invalid solution: " ++ show v) False
1189
  where failmsg = \msg -> printTestCase ("Failed to evacuate: " ++ msg)
1190
        idx = Instance.idx inst
1191

    
1192
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
1193
-- we can also node-evacuate it.
1194
prop_ClusterAllocEvacuate =
1195
  forAll (choose (4, 8)) $ \count ->
1196
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1197
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1198
  case genClusterAlloc count node inst of
1199
    Types.Bad msg -> failTest msg
1200
    Types.Ok (nl, il, inst') ->
1201
      conjoin $ map (\mode -> check_EvacMode defGroup inst' $
1202
                              Cluster.tryNodeEvac defGroupList nl il mode
1203
                                [Instance.idx inst']) [minBound..maxBound]
1204

    
1205
-- | Checks that on a 4-8 node cluster with two node groups, once we
1206
-- allocate an instance on the first node group, we can also change
1207
-- its group.
1208
prop_ClusterAllocChangeGroup =
1209
  forAll (choose (4, 8)) $ \count ->
1210
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1211
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1212
  case genClusterAlloc count node inst of
1213
    Types.Bad msg -> failTest msg
1214
    Types.Ok (nl, il, inst') ->
1215
      -- we need to add a second node group and nodes to the cluster
1216
      let nl2 = Container.elems $ makeSmallCluster node count
1217
          grp2 = Group.setIdx defGroup (Group.idx defGroup + 1)
1218
          maxndx = maximum . map Node.idx $ nl2
1219
          nl3 = map (\n -> n { Node.group = Group.idx grp2
1220
                             , Node.idx = Node.idx n + maxndx }) nl2
1221
          nl4 = Container.fromList . map (\n -> (Node.idx n, n)) $ nl3
1222
          gl' = Container.add (Group.idx grp2) grp2 defGroupList
1223
          nl' = IntMap.union nl nl4
1224
      in check_EvacMode grp2 inst' $
1225
         Cluster.tryChangeGroup gl' nl' il [] [Instance.idx inst']
1226

    
1227
-- | Check that allocating multiple instances on a cluster, then
1228
-- adding an empty node, results in a valid rebalance.
1229
prop_ClusterAllocBalance =
1230
  forAll (genNode (Just 5) (Just 128)) $ \node ->
1231
  forAll (choose (3, 5)) $ \count ->
1232
  not (Node.offline node) && not (Node.failN1 node) ==>
1233
  let nl = makeSmallCluster node count
1234
      (hnode, nl') = IntMap.deleteFindMax nl
1235
      il = Container.empty
1236
      allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
1237
      i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
1238
  in case allocnodes >>= \allocnodes' ->
1239
    Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
1240
       Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
1241
       Types.Ok (_, _, _, [], _) -> failTest "Failed to allocate: no instances"
1242
       Types.Ok (_, xnl, il', _, _) ->
1243
         let ynl = Container.add (Node.idx hnode) hnode xnl
1244
             cv = Cluster.compCV ynl
1245
             tbl = Cluster.Table ynl il' cv []
1246
         in printTestCase "Failed to rebalance" $
1247
            canBalance tbl True True False
1248

    
1249
-- | Checks consistency.
1250
prop_ClusterCheckConsistency node inst =
1251
  let nl = makeSmallCluster node 3
1252
      [node1, node2, node3] = Container.elems nl
1253
      node3' = node3 { Node.group = 1 }
1254
      nl' = Container.add (Node.idx node3') node3' nl
1255
      inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
1256
      inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
1257
      inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
1258
      ccheck = Cluster.findSplitInstances nl' . Container.fromList
1259
  in null (ccheck [(0, inst1)]) &&
1260
     null (ccheck [(0, inst2)]) &&
1261
     (not . null $ ccheck [(0, inst3)])
1262

    
1263
-- | For now, we only test that we don't lose instances during the split.
1264
prop_ClusterSplitCluster node inst =
1265
  forAll (choose (0, 100)) $ \icnt ->
1266
  let nl = makeSmallCluster node 2
1267
      (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
1268
                   (nl, Container.empty) [1..icnt]
1269
      gni = Cluster.splitCluster nl' il'
1270
  in sum (map (Container.size . snd . snd) gni) == icnt &&
1271
     all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
1272
                                 (Container.elems nl'')) gni
1273

    
1274
-- | Helper function to check if we can allocate an instance on a
1275
-- given node list.
1276
canAllocOn :: Node.List -> Int -> Instance.Instance -> Bool
1277
canAllocOn nl reqnodes inst =
1278
  case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1279
       Cluster.tryAlloc nl (Container.empty) inst of
1280
       Types.Bad _ -> False
1281
       Types.Ok as ->
1282
         case Cluster.asSolution as of
1283
           Nothing -> False
1284
           Just _ -> True
1285

    
1286
-- | Checks that allocation obeys minimum and maximum instance
1287
-- policies. The unittest generates a random node, duplicates it count
1288
-- times, and generates a random instance that can be allocated on
1289
-- this mini-cluster; it then checks that after applying a policy that
1290
-- the instance doesn't fits, the allocation fails.
1291
prop_ClusterAllocPolicy node =
1292
  -- rqn is the required nodes (1 or 2)
1293
  forAll (choose (1, 2)) $ \rqn ->
1294
  forAll (choose (5, 20)) $ \count ->
1295
  forAll (arbitrary `suchThat` (canAllocOn (makeSmallCluster node count) rqn))
1296
         $ \inst ->
1297
  forAll (arbitrary `suchThat` (isFailure .
1298
                                Instance.instMatchesPolicy inst)) $ \ipol ->
1299
  let node' = Node.setPolicy ipol node
1300
      nl = makeSmallCluster node' count
1301
  in not $ canAllocOn nl rqn inst
1302

    
1303
testSuite "Cluster"
1304
            [ 'prop_Score_Zero
1305
            , 'prop_CStats_sane
1306
            , 'prop_ClusterAlloc_sane
1307
            , 'prop_ClusterCanTieredAlloc
1308
            , 'prop_ClusterAllocRelocate
1309
            , 'prop_ClusterAllocEvacuate
1310
            , 'prop_ClusterAllocChangeGroup
1311
            , 'prop_ClusterAllocBalance
1312
            , 'prop_ClusterCheckConsistency
1313
            , 'prop_ClusterSplitCluster
1314
            , 'prop_ClusterAllocPolicy
1315
            ]
1316

    
1317
-- ** OpCodes tests
1318

    
1319
-- | Check that opcode serialization is idempotent.
1320
prop_OpCodes_serialization op =
1321
  case J.readJSON (J.showJSON op) of
1322
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1323
    J.Ok op' -> op ==? op'
1324
  where _types = op::OpCodes.OpCode
1325

    
1326
testSuite "OpCodes"
1327
            [ 'prop_OpCodes_serialization ]
1328

    
1329
-- ** Jobs tests
1330

    
1331
-- | Check that (queued) job\/opcode status serialization is idempotent.
1332
prop_OpStatus_serialization os =
1333
  case J.readJSON (J.showJSON os) of
1334
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1335
    J.Ok os' -> os ==? os'
1336
  where _types = os::Jobs.OpStatus
1337

    
1338
prop_JobStatus_serialization js =
1339
  case J.readJSON (J.showJSON js) of
1340
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1341
    J.Ok js' -> js ==? js'
1342
  where _types = js::Jobs.JobStatus
1343

    
1344
testSuite "Jobs"
1345
            [ 'prop_OpStatus_serialization
1346
            , 'prop_JobStatus_serialization
1347
            ]
1348

    
1349
-- ** Loader tests
1350

    
1351
prop_Loader_lookupNode ktn inst node =
1352
  Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
1353
    where nl = Data.Map.fromList ktn
1354

    
1355
prop_Loader_lookupInstance kti inst =
1356
  Loader.lookupInstance il inst ==? Data.Map.lookup inst il
1357
    where il = Data.Map.fromList kti
1358

    
1359
prop_Loader_assignIndices =
1360
  -- generate nodes with unique names
1361
  forAll (arbitrary `suchThat`
1362
          (\nodes ->
1363
             let names = map Node.name nodes
1364
             in length names == length (nub names))) $ \nodes ->
1365
  let (nassoc, kt) =
1366
        Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1367
  in Data.Map.size nassoc == length nodes &&
1368
     Container.size kt == length nodes &&
1369
     if not (null nodes)
1370
       then maximum (IntMap.keys kt) == length nodes - 1
1371
       else True
1372

    
1373
-- | Checks that the number of primary instances recorded on the nodes
1374
-- is zero.
1375
prop_Loader_mergeData ns =
1376
  let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1377
  in case Loader.mergeData [] [] [] []
1378
         (Loader.emptyCluster {Loader.cdNodes = na}) of
1379
    Types.Bad _ -> False
1380
    Types.Ok (Loader.ClusterData _ nl il _ _) ->
1381
      let nodes = Container.elems nl
1382
          instances = Container.elems il
1383
      in (sum . map (length . Node.pList)) nodes == 0 &&
1384
         null instances
1385

    
1386
-- | Check that compareNameComponent on equal strings works.
1387
prop_Loader_compareNameComponent_equal :: String -> Bool
1388
prop_Loader_compareNameComponent_equal s =
1389
  Loader.compareNameComponent s s ==
1390
    Loader.LookupResult Loader.ExactMatch s
1391

    
1392
-- | Check that compareNameComponent on prefix strings works.
1393
prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1394
prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1395
  Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1396
    Loader.LookupResult Loader.PartialMatch s1
1397

    
1398
testSuite "Loader"
1399
            [ 'prop_Loader_lookupNode
1400
            , 'prop_Loader_lookupInstance
1401
            , 'prop_Loader_assignIndices
1402
            , 'prop_Loader_mergeData
1403
            , 'prop_Loader_compareNameComponent_equal
1404
            , 'prop_Loader_compareNameComponent_prefix
1405
            ]
1406

    
1407
-- ** Types tests
1408

    
1409
prop_Types_AllocPolicy_serialisation apol =
1410
  case J.readJSON (J.showJSON apol) of
1411
    J.Ok p -> p ==? apol
1412
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1413
      where _types = apol::Types.AllocPolicy
1414

    
1415
prop_Types_DiskTemplate_serialisation dt =
1416
  case J.readJSON (J.showJSON dt) of
1417
    J.Ok p -> p ==? dt
1418
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1419
      where _types = dt::Types.DiskTemplate
1420

    
1421
prop_Types_ISpec_serialisation ispec =
1422
  case J.readJSON (J.showJSON ispec) of
1423
    J.Ok p -> p ==? ispec
1424
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1425
      where _types = ispec::Types.ISpec
1426

    
1427
prop_Types_IPolicy_serialisation ipol =
1428
  case J.readJSON (J.showJSON ipol) of
1429
    J.Ok p -> p ==? ipol
1430
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1431
      where _types = ipol::Types.IPolicy
1432

    
1433
prop_Types_EvacMode_serialisation em =
1434
  case J.readJSON (J.showJSON em) of
1435
    J.Ok p -> p ==? em
1436
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1437
      where _types = em::Types.EvacMode
1438

    
1439
prop_Types_opToResult op =
1440
  case op of
1441
    Types.OpFail _ -> Types.isBad r
1442
    Types.OpGood v -> case r of
1443
                        Types.Bad _ -> False
1444
                        Types.Ok v' -> v == v'
1445
  where r = Types.opToResult op
1446
        _types = op::Types.OpResult Int
1447

    
1448
prop_Types_eitherToResult ei =
1449
  case ei of
1450
    Left _ -> Types.isBad r
1451
    Right v -> case r of
1452
                 Types.Bad _ -> False
1453
                 Types.Ok v' -> v == v'
1454
    where r = Types.eitherToResult ei
1455
          _types = ei::Either String Int
1456

    
1457
testSuite "Types"
1458
            [ 'prop_Types_AllocPolicy_serialisation
1459
            , 'prop_Types_DiskTemplate_serialisation
1460
            , 'prop_Types_ISpec_serialisation
1461
            , 'prop_Types_IPolicy_serialisation
1462
            , 'prop_Types_EvacMode_serialisation
1463
            , 'prop_Types_opToResult
1464
            , 'prop_Types_eitherToResult
1465
            ]
1466

    
1467
-- ** CLI tests
1468

    
1469
-- | Test correct parsing.
1470
prop_CLI_parseISpec descr dsk mem cpu =
1471
  let str = printf "%d,%d,%d" dsk mem cpu
1472
  in CLI.parseISpecString descr str ==? Types.Ok (Types.RSpec cpu mem dsk)
1473

    
1474
-- | Test parsing failure due to wrong section count.
1475
prop_CLI_parseISpecFail descr =
1476
  forAll (choose (0,100) `suchThat` ((/=) 3)) $ \nelems ->
1477
  forAll (replicateM nelems arbitrary) $ \values ->
1478
  let str = intercalate "," $ map show (values::[Int])
1479
  in case CLI.parseISpecString descr str of
1480
       Types.Ok v -> failTest $ "Expected failure, got " ++ show v
1481
       _ -> property True
1482

    
1483
-- | Test parseYesNo.
1484
prop_CLI_parseYesNo def testval val =
1485
  forAll (elements [val, "yes", "no"]) $ \actual_val ->
1486
  if testval
1487
    then CLI.parseYesNo def Nothing ==? Types.Ok def
1488
    else let result = CLI.parseYesNo def (Just actual_val)
1489
         in if actual_val `elem` ["yes", "no"]
1490
              then result ==? Types.Ok (actual_val == "yes")
1491
              else property $ Types.isBad result
1492

    
1493
-- | Helper to check for correct parsing of string arg.
1494
checkStringArg val (opt, fn) =
1495
  let GetOpt.Option _ longs _ _ = opt
1496
  in case longs of
1497
       [] -> failTest "no long options?"
1498
       cmdarg:_ ->
1499
         case CLI.parseOptsInner ["--" ++ cmdarg ++ "=" ++ val] "prog" [opt] of
1500
           Left e -> failTest $ "Failed to parse option: " ++ show e
1501
           Right (options, _) -> fn options ==? Just val
1502

    
1503
-- | Test a few string arguments.
1504
prop_CLI_StringArg argument =
1505
  let args = [ (CLI.oDataFile,      CLI.optDataFile)
1506
             , (CLI.oDynuFile,      CLI.optDynuFile)
1507
             , (CLI.oSaveCluster,   CLI.optSaveCluster)
1508
             , (CLI.oReplay,        CLI.optReplay)
1509
             , (CLI.oPrintCommands, CLI.optShowCmds)
1510
             , (CLI.oLuxiSocket,    CLI.optLuxi)
1511
             ]
1512
  in conjoin $ map (checkStringArg argument) args
1513

    
1514
testSuite "CLI"
1515
          [ 'prop_CLI_parseISpec
1516
          , 'prop_CLI_parseISpecFail
1517
          , 'prop_CLI_parseYesNo
1518
          , 'prop_CLI_StringArg
1519
          ]