Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (57.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 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
-- | All disk templates (used later)
101
allDiskTemplates :: [Types.DiskTemplate]
102
allDiskTemplates = [minBound..maxBound]
103

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

    
127

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

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

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

    
139
-- * Helper functions
140

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

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

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

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

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

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

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

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

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

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

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

    
224
-- * Arbitrary instances
225

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
448
-- * Actual tests
449

    
450
-- ** Utils tests
451

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

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

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

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

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

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

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

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

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

    
533
-- ** PeerMap tests
534

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

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

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

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

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

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

    
579
-- ** Container tests
580

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

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

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

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

    
623
-- ** Instance tests
624

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
715
-- ** Backends
716

    
717
-- *** Text backend tests
718

    
719
-- Instance text loader tests
720

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

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

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

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

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

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

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

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

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

    
868
-- *** Simu backend
869

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

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

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

    
919
-- ** Node tests
920

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1084
-- ** Cluster tests
1085

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1318
-- ** OpCodes tests
1319

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

    
1327
testSuite "OpCodes"
1328
            [ 'prop_OpCodes_serialization ]
1329

    
1330
-- ** Jobs tests
1331

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

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

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

    
1350
-- ** Loader tests
1351

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

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

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

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

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

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

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

    
1408
-- ** Types tests
1409

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

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

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

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

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

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

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

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

    
1468
-- ** CLI tests
1469

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

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

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

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

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

    
1515
-- | Helper to test that a given option is accepted OK with quick exit.
1516
checkEarlyExit name options param =
1517
  case CLI.parseOptsInner [param] name options of
1518
    Left (code, _) -> if code == 0
1519
                          then property True
1520
                          else failTest $ "Program " ++ name ++
1521
                                 " returns invalid code " ++ show code ++
1522
                                 " for option " ++ param
1523
    _ -> failTest $ "Program " ++ name ++ " doesn't consider option " ++
1524
         param ++ " as early exit one"
1525

    
1526
-- | Test that all binaries support some common options. There is
1527
-- nothing actually random about this test...
1528
prop_CLI_stdopts =
1529
  let params = ["-h", "--help", "-V", "--version"]
1530
      opts = map (\(name, (_, o)) -> (name, o)) Program.personalities
1531
      -- apply checkEarlyExit across the cartesian product of params and opts
1532
  in conjoin [checkEarlyExit n o p | p <- params, (n, o) <- opts]
1533

    
1534
testSuite "CLI"
1535
          [ 'prop_CLI_parseISpec
1536
          , 'prop_CLI_parseISpecFail
1537
          , 'prop_CLI_parseYesNo
1538
          , 'prop_CLI_StringArg
1539
          , 'prop_CLI_stdopts
1540
          ]