Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / HTools / QC.hs @ 00b70680

History | View | Annotate | Download (43.6 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
  , testOpCodes
36
  , testJobs
37
  , testCluster
38
  , testLoader
39
  , testTypes
40
  ) where
41

    
42
import Test.QuickCheck
43
import Data.List (findIndex, intercalate, nub, isPrefixOf)
44
import Data.Maybe
45
import Control.Monad
46
import qualified Text.JSON as J
47
import qualified Data.Map
48
import qualified Data.IntMap as IntMap
49
import qualified Ganeti.OpCodes as OpCodes
50
import qualified Ganeti.Jobs as Jobs
51
import qualified Ganeti.Luxi
52
import qualified Ganeti.HTools.CLI as CLI
53
import qualified Ganeti.HTools.Cluster as Cluster
54
import qualified Ganeti.HTools.Container as Container
55
import qualified Ganeti.HTools.ExtLoader
56
import qualified Ganeti.HTools.IAlloc as IAlloc
57
import qualified Ganeti.HTools.Instance as Instance
58
import qualified Ganeti.HTools.JSON as JSON
59
import qualified Ganeti.HTools.Loader as Loader
60
import qualified Ganeti.HTools.Luxi
61
import qualified Ganeti.HTools.Node as Node
62
import qualified Ganeti.HTools.Group as Group
63
import qualified Ganeti.HTools.PeerMap as PeerMap
64
import qualified Ganeti.HTools.Rapi
65
import qualified Ganeti.HTools.Simu
66
import qualified Ganeti.HTools.Text as Text
67
import qualified Ganeti.HTools.Types as Types
68
import qualified Ganeti.HTools.Utils as Utils
69
import qualified Ganeti.HTools.Version
70
import qualified Ganeti.Constants as C
71

    
72
import qualified Ganeti.HTools.Program.Hail
73
import qualified Ganeti.HTools.Program.Hbal
74
import qualified Ganeti.HTools.Program.Hscan
75
import qualified Ganeti.HTools.Program.Hspace
76

    
77
import Ganeti.HTools.QCHelper (testSuite)
78

    
79
-- * Constants
80

    
81
-- | Maximum memory (1TiB, somewhat random value).
82
maxMem :: Int
83
maxMem = 1024 * 1024
84

    
85
-- | Maximum disk (8TiB, somewhat random value).
86
maxDsk :: Int
87
maxDsk = 1024 * 1024 * 8
88

    
89
-- | Max CPUs (1024, somewhat random value).
90
maxCpu :: Int
91
maxCpu = 1024
92

    
93
-- | Null iPolicy, and by null we mean very liberal.
94
nullIPolicy = Types.IPolicy
95
  { Types.iPolicyMinSpec = Types.ISpec { Types.iSpecMemorySize = 0
96
                                       , Types.iSpecCpuCount   = 0
97
                                       , Types.iSpecDiskSize   = 0
98
                                       , Types.iSpecDiskCount  = 0
99
                                       , Types.iSpecNicCount   = 0
100
                                       }
101
  , Types.iPolicyMaxSpec = Types.ISpec { Types.iSpecMemorySize = maxBound
102
                                       , Types.iSpecCpuCount   = maxBound
103
                                       , Types.iSpecDiskSize   = maxBound
104
                                       , Types.iSpecDiskCount  = C.maxDisks
105
                                       , Types.iSpecNicCount   = C.maxNics
106
                                       }
107
  , Types.iPolicyStdSpec = Types.ISpec { Types.iSpecMemorySize = Types.unitMem
108
                                       , Types.iSpecCpuCount   = Types.unitCpu
109
                                       , Types.iSpecDiskSize   = Types.unitDsk
110
                                       , Types.iSpecDiskCount  = 1
111
                                       , Types.iSpecNicCount   = 1
112
                                       }
113
  , Types.iPolicyDiskTemplates = [Types.DTDrbd8, Types.DTPlain]
114
  }
115

    
116

    
117
defGroup :: Group.Group
118
defGroup = flip Group.setIdx 0 $
119
             Group.create "default" Types.defaultGroupID Types.AllocPreferred
120
                  nullIPolicy
121

    
122
defGroupList :: Group.List
123
defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
124

    
125
defGroupAssoc :: Data.Map.Map String Types.Gdx
126
defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
127

    
128
-- * Helper functions
129

    
130
-- | Simple checker for whether OpResult is fail or pass.
131
isFailure :: Types.OpResult a -> Bool
132
isFailure (Types.OpFail _) = True
133
isFailure _ = False
134

    
135
-- | Checks for equality with proper annotation.
136
(==?) :: (Show a, Eq a) => a -> a -> Property
137
(==?) x y = printTestCase
138
            ("Expected equality, but '" ++
139
             show x ++ "' /= '" ++ show y ++ "'") (x == y)
140
infix 3 ==?
141

    
142
-- | Update an instance to be smaller than a node.
143
setInstanceSmallerThanNode node inst =
144
  inst { Instance.mem = Node.availMem node `div` 2
145
       , Instance.dsk = Node.availDisk node `div` 2
146
       , Instance.vcpus = Node.availCpu node `div` 2
147
       }
148

    
149
-- | Create an instance given its spec.
150
createInstance mem dsk vcpus =
151
  Instance.create "inst-unnamed" mem dsk vcpus Types.Running [] True (-1) (-1)
152
    Types.DTDrbd8
153

    
154
-- | Create a small cluster by repeating a node spec.
155
makeSmallCluster :: Node.Node -> Int -> Node.List
156
makeSmallCluster node count =
157
  let fn = Node.buildPeers node Container.empty
158
      namelst = map (\n -> (Node.name n, n)) (replicate count fn)
159
      (_, nlst) = Loader.assignIndices namelst
160
  in nlst
161

    
162
-- | Make a small cluster, both nodes and instances.
163
makeSmallEmptyCluster :: Node.Node -> Int -> Instance.Instance
164
                      -> (Node.List, Instance.List, Instance.Instance)
165
makeSmallEmptyCluster node count inst =
166
  (makeSmallCluster node count, Container.empty,
167
   setInstanceSmallerThanNode node inst)
168

    
169
-- | Checks if a node is "big" enough.
170
isNodeBig :: Node.Node -> Int -> Bool
171
isNodeBig node size = Node.availDisk node > size * Types.unitDsk
172
                      && Node.availMem node > size * Types.unitMem
173
                      && Node.availCpu node > size * Types.unitCpu
174

    
175
canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
176
canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
177

    
178
-- | Assigns a new fresh instance to a cluster; this is not
179
-- allocation, so no resource checks are done.
180
assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
181
                  Types.Idx -> Types.Idx ->
182
                  (Node.List, Instance.List)
183
assignInstance nl il inst pdx sdx =
184
  let pnode = Container.find pdx nl
185
      snode = Container.find sdx nl
186
      maxiidx = if Container.null il
187
                  then 0
188
                  else fst (Container.findMax il) + 1
189
      inst' = inst { Instance.idx = maxiidx,
190
                     Instance.pNode = pdx, Instance.sNode = sdx }
191
      pnode' = Node.setPri pnode inst'
192
      snode' = Node.setSec snode inst'
193
      nl' = Container.addTwo pdx pnode' sdx snode' nl
194
      il' = Container.add maxiidx inst' il
195
  in (nl', il')
196

    
197
-- * Arbitrary instances
198

    
199
-- | Defines a DNS name.
200
newtype DNSChar = DNSChar { dnsGetChar::Char }
201

    
202
instance Arbitrary DNSChar where
203
  arbitrary = do
204
    x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
205
    return (DNSChar x)
206

    
207
getName :: Gen String
208
getName = do
209
  n <- choose (1, 64)
210
  dn <- vector n::Gen [DNSChar]
211
  return (map dnsGetChar dn)
212

    
213
getFQDN :: Gen String
214
getFQDN = do
215
  felem <- getName
216
  ncomps <- choose (1, 4)
217
  frest <- vector ncomps::Gen [[DNSChar]]
218
  let frest' = map (map dnsGetChar) frest
219
  return (felem ++ "." ++ intercalate "." frest')
220

    
221
instance Arbitrary Types.InstanceStatus where
222
    arbitrary = elements [minBound..maxBound]
223

    
224
-- let's generate a random instance
225
instance Arbitrary Instance.Instance where
226
  arbitrary = do
227
    name <- getFQDN
228
    mem <- choose (0, maxMem)
229
    dsk <- choose (0, maxDsk)
230
    run_st <- arbitrary
231
    pn <- arbitrary
232
    sn <- arbitrary
233
    vcpus <- choose (0, maxCpu)
234
    return $ Instance.create name mem dsk vcpus run_st [] True pn sn
235
              Types.DTDrbd8
236

    
237
-- | Generas an arbitrary node based on sizing information.
238
genNode :: Maybe Int -- ^ Minimum node size in terms of units
239
        -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
240
                     -- just by the max... constants)
241
        -> Gen Node.Node
242
genNode min_multiplier max_multiplier = do
243
  let (base_mem, base_dsk, base_cpu) =
244
        case min_multiplier of
245
          Just mm -> (mm * Types.unitMem,
246
                      mm * Types.unitDsk,
247
                      mm * Types.unitCpu)
248
          Nothing -> (0, 0, 0)
249
      (top_mem, top_dsk, top_cpu)  =
250
        case max_multiplier of
251
          Just mm -> (mm * Types.unitMem,
252
                      mm * Types.unitDsk,
253
                      mm * Types.unitCpu)
254
          Nothing -> (maxMem, maxDsk, maxCpu)
255
  name  <- getFQDN
256
  mem_t <- choose (base_mem, top_mem)
257
  mem_f <- choose (base_mem, mem_t)
258
  mem_n <- choose (0, mem_t - mem_f)
259
  dsk_t <- choose (base_dsk, top_dsk)
260
  dsk_f <- choose (base_dsk, dsk_t)
261
  cpu_t <- choose (base_cpu, top_cpu)
262
  offl  <- arbitrary
263
  let n = Node.create name (fromIntegral mem_t) mem_n mem_f
264
          (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
265
      n' = Node.setPolicy nullIPolicy n
266
  return $ Node.buildPeers n' Container.empty
267

    
268
-- and a random node
269
instance Arbitrary Node.Node where
270
  arbitrary = genNode Nothing Nothing
271

    
272
-- replace disks
273
instance Arbitrary OpCodes.ReplaceDisksMode where
274
  arbitrary = elements [minBound..maxBound]
275

    
276
instance Arbitrary OpCodes.OpCode where
277
  arbitrary = do
278
    op_id <- elements [ "OP_TEST_DELAY"
279
                      , "OP_INSTANCE_REPLACE_DISKS"
280
                      , "OP_INSTANCE_FAILOVER"
281
                      , "OP_INSTANCE_MIGRATE"
282
                      ]
283
    case op_id of
284
      "OP_TEST_DELAY" ->
285
        liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
286
      "OP_INSTANCE_REPLACE_DISKS" ->
287
        liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
288
          arbitrary arbitrary arbitrary
289
      "OP_INSTANCE_FAILOVER" ->
290
        liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
291
          arbitrary
292
      "OP_INSTANCE_MIGRATE" ->
293
        liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
294
          arbitrary arbitrary arbitrary
295
      _ -> fail "Wrong opcode"
296

    
297
instance Arbitrary Jobs.OpStatus where
298
  arbitrary = elements [minBound..maxBound]
299

    
300
instance Arbitrary Jobs.JobStatus where
301
  arbitrary = elements [minBound..maxBound]
302

    
303
newtype SmallRatio = SmallRatio Double deriving Show
304
instance Arbitrary SmallRatio where
305
  arbitrary = do
306
    v <- choose (0, 1)
307
    return $ SmallRatio v
308

    
309
instance Arbitrary Types.AllocPolicy where
310
  arbitrary = elements [minBound..maxBound]
311

    
312
instance Arbitrary Types.DiskTemplate where
313
  arbitrary = elements [minBound..maxBound]
314

    
315
instance Arbitrary Types.FailMode where
316
  arbitrary = elements [minBound..maxBound]
317

    
318
instance Arbitrary a => Arbitrary (Types.OpResult a) where
319
  arbitrary = arbitrary >>= \c ->
320
              if c
321
                then liftM Types.OpGood arbitrary
322
                else liftM Types.OpFail arbitrary
323

    
324
instance Arbitrary Types.ISpec where
325
  arbitrary = do
326
    mem <- arbitrary::Gen (NonNegative Int)
327
    dsk_c <- arbitrary::Gen (NonNegative Int)
328
    dsk_s <- arbitrary::Gen (NonNegative Int)
329
    cpu <- arbitrary::Gen (NonNegative Int)
330
    nic <- arbitrary::Gen (NonNegative Int)
331
    return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem
332
                       , Types.iSpecCpuCount   = fromIntegral cpu
333
                       , Types.iSpecDiskSize   = fromIntegral dsk_s
334
                       , Types.iSpecDiskCount  = fromIntegral dsk_c
335
                       , Types.iSpecNicCount   = fromIntegral nic
336
                       }
337

    
338
-- | Helper function to check whether a spec is LTE than another
339
iSpecSmaller :: Types.ISpec -> Types.ISpec -> Bool
340
iSpecSmaller imin imax =
341
  Types.iSpecMemorySize imin <= Types.iSpecMemorySize imax &&
342
  Types.iSpecCpuCount imin   <= Types.iSpecCpuCount imax &&
343
  Types.iSpecDiskSize imin   <= Types.iSpecDiskSize imax &&
344
  Types.iSpecDiskCount imin  <= Types.iSpecDiskCount imax &&
345
  Types.iSpecNicCount imin   <= Types.iSpecNicCount imax
346

    
347
instance Arbitrary Types.IPolicy where
348
  arbitrary = do
349
    imin <- arbitrary
350
    istd <- arbitrary `suchThat` (iSpecSmaller imin)
351
    imax <- arbitrary `suchThat` (iSpecSmaller istd)
352
    dts  <- arbitrary
353
    return Types.IPolicy { Types.iPolicyMinSpec = imin
354
                         , Types.iPolicyStdSpec = istd
355
                         , Types.iPolicyMaxSpec = imax
356
                         , Types.iPolicyDiskTemplates = dts
357
                         }
358

    
359
-- * Actual tests
360

    
361
-- ** Utils tests
362

    
363
-- | If the list is not just an empty element, and if the elements do
364
-- not contain commas, then join+split should be idempotent.
365
prop_Utils_commaJoinSplit =
366
  forAll (arbitrary `suchThat`
367
          (\l -> l /= [""] && all (notElem ',') l )) $ \lst ->
368
  Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
369

    
370
-- | Split and join should always be idempotent.
371
prop_Utils_commaSplitJoin s =
372
  Utils.commaJoin (Utils.sepSplit ',' s) ==? s
373

    
374
-- | fromObjWithDefault, we test using the Maybe monad and an integer
375
-- value.
376
prop_Utils_fromObjWithDefault def_value random_key =
377
  -- a missing key will be returned with the default
378
  JSON.fromObjWithDefault [] random_key def_value == Just def_value &&
379
  -- a found key will be returned as is, not with default
380
  JSON.fromObjWithDefault [(random_key, J.showJSON def_value)]
381
       random_key (def_value+1) == Just def_value
382
    where _types = def_value :: Integer
383

    
384
-- | Test that functional if' behaves like the syntactic sugar if.
385
prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
386
prop_Utils_if'if cnd a b =
387
  Utils.if' cnd a b ==? if cnd then a else b
388

    
389
-- | Test basic select functionality
390
prop_Utils_select :: Int      -- ^ Default result
391
                  -> [Int]    -- ^ List of False values
392
                  -> [Int]    -- ^ List of True values
393
                  -> Gen Prop -- ^ Test result
394
prop_Utils_select def lst1 lst2 =
395
  Utils.select def (flist ++ tlist) ==? expectedresult
396
    where expectedresult = Utils.if' (null lst2) def (head lst2)
397
          flist = zip (repeat False) lst1
398
          tlist = zip (repeat True)  lst2
399

    
400
-- | Test basic select functionality with undefined default
401
prop_Utils_select_undefd :: [Int]            -- ^ List of False values
402
                         -> NonEmptyList Int -- ^ List of True values
403
                         -> Gen Prop         -- ^ Test result
404
prop_Utils_select_undefd lst1 (NonEmpty lst2) =
405
  Utils.select undefined (flist ++ tlist) ==? head lst2
406
    where flist = zip (repeat False) lst1
407
          tlist = zip (repeat True)  lst2
408

    
409
-- | Test basic select functionality with undefined list values
410
prop_Utils_select_undefv :: [Int]            -- ^ List of False values
411
                         -> NonEmptyList Int -- ^ List of True values
412
                         -> Gen Prop         -- ^ Test result
413
prop_Utils_select_undefv lst1 (NonEmpty lst2) =
414
  Utils.select undefined cndlist ==? head lst2
415
    where flist = zip (repeat False) lst1
416
          tlist = zip (repeat True)  lst2
417
          cndlist = flist ++ tlist ++ [undefined]
418

    
419
prop_Utils_parseUnit (NonNegative n) =
420
  Utils.parseUnit (show n) == Types.Ok n &&
421
  Utils.parseUnit (show n ++ "m") == Types.Ok n &&
422
  (case Utils.parseUnit (show n ++ "M") of
423
     Types.Ok m -> if n > 0
424
                     then m < n  -- for positive values, X MB is < than X MiB
425
                     else m == 0 -- but for 0, 0 MB == 0 MiB
426
     Types.Bad _ -> False) &&
427
  Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
428
  Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
429
  Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
430
    where _types = n::Int
431

    
432
-- | Test list for the Utils module.
433
testSuite "Utils"
434
            [ 'prop_Utils_commaJoinSplit
435
            , 'prop_Utils_commaSplitJoin
436
            , 'prop_Utils_fromObjWithDefault
437
            , 'prop_Utils_if'if
438
            , 'prop_Utils_select
439
            , 'prop_Utils_select_undefd
440
            , 'prop_Utils_select_undefv
441
            , 'prop_Utils_parseUnit
442
            ]
443

    
444
-- ** PeerMap tests
445

    
446
-- | Make sure add is idempotent.
447
prop_PeerMap_addIdempotent pmap key em =
448
  fn puniq ==? fn (fn puniq)
449
    where _types = (pmap::PeerMap.PeerMap,
450
                    key::PeerMap.Key, em::PeerMap.Elem)
451
          fn = PeerMap.add key em
452
          puniq = PeerMap.accumArray const pmap
453

    
454
-- | Make sure remove is idempotent.
455
prop_PeerMap_removeIdempotent pmap key =
456
  fn puniq ==? fn (fn puniq)
457
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
458
          fn = PeerMap.remove key
459
          puniq = PeerMap.accumArray const pmap
460

    
461
-- | Make sure a missing item returns 0.
462
prop_PeerMap_findMissing pmap key =
463
  PeerMap.find key (PeerMap.remove key puniq) ==? 0
464
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
465
          puniq = PeerMap.accumArray const pmap
466

    
467
-- | Make sure an added item is found.
468
prop_PeerMap_addFind pmap key em =
469
  PeerMap.find key (PeerMap.add key em puniq) ==? em
470
    where _types = (pmap::PeerMap.PeerMap,
471
                    key::PeerMap.Key, em::PeerMap.Elem)
472
          puniq = PeerMap.accumArray const pmap
473

    
474
-- | Manual check that maxElem returns the maximum indeed, or 0 for null.
475
prop_PeerMap_maxElem pmap =
476
  PeerMap.maxElem puniq ==? if null puniq then 0
477
                              else (maximum . snd . unzip) puniq
478
    where _types = pmap::PeerMap.PeerMap
479
          puniq = PeerMap.accumArray const pmap
480

    
481
-- | List of tests for the PeerMap module.
482
testSuite "PeerMap"
483
            [ 'prop_PeerMap_addIdempotent
484
            , 'prop_PeerMap_removeIdempotent
485
            , 'prop_PeerMap_maxElem
486
            , 'prop_PeerMap_addFind
487
            , 'prop_PeerMap_findMissing
488
            ]
489

    
490
-- ** Container tests
491

    
492
-- we silence the following due to hlint bug fixed in later versions
493
{-# ANN prop_Container_addTwo "HLint: ignore Avoid lambda" #-}
494
prop_Container_addTwo cdata i1 i2 =
495
  fn i1 i2 cont == fn i2 i1 cont &&
496
  fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
497
    where _types = (cdata::[Int],
498
                    i1::Int, i2::Int)
499
          cont = foldl (\c x -> Container.add x x c) Container.empty cdata
500
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
501

    
502
prop_Container_nameOf node =
503
  let nl = makeSmallCluster node 1
504
      fnode = head (Container.elems nl)
505
  in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
506

    
507
-- | We test that in a cluster, given a random node, we can find it by
508
-- its name and alias, as long as all names and aliases are unique,
509
-- and that we fail to find a non-existing name.
510
prop_Container_findByName node othername =
511
  forAll (choose (1, 20)) $ \ cnt ->
512
  forAll (choose (0, cnt - 1)) $ \ fidx ->
513
  forAll (vector cnt) $ \ names ->
514
  (length . nub) (map fst names ++ map snd names) ==
515
  length names * 2 &&
516
  othername `notElem` (map fst names ++ map snd names) ==>
517
  let nl = makeSmallCluster node cnt
518
      nodes = Container.elems nl
519
      nodes' = map (\((name, alias), nn) -> (Node.idx nn,
520
                                             nn { Node.name = name,
521
                                                  Node.alias = alias }))
522
               $ zip names nodes
523
      nl' = Container.fromList nodes'
524
      target = snd (nodes' !! fidx)
525
  in Container.findByName nl' (Node.name target) == Just target &&
526
     Container.findByName nl' (Node.alias target) == Just target &&
527
     isNothing (Container.findByName nl' othername)
528

    
529
testSuite "Container"
530
            [ 'prop_Container_addTwo
531
            , 'prop_Container_nameOf
532
            , 'prop_Container_findByName
533
            ]
534

    
535
-- ** Instance tests
536

    
537
-- Simple instance tests, we only have setter/getters
538

    
539
prop_Instance_creat inst =
540
  Instance.name inst ==? Instance.alias inst
541

    
542
prop_Instance_setIdx inst idx =
543
  Instance.idx (Instance.setIdx inst idx) ==? idx
544
    where _types = (inst::Instance.Instance, idx::Types.Idx)
545

    
546
prop_Instance_setName inst name =
547
  Instance.name newinst == name &&
548
  Instance.alias newinst == name
549
    where _types = (inst::Instance.Instance, name::String)
550
          newinst = Instance.setName inst name
551

    
552
prop_Instance_setAlias inst name =
553
  Instance.name newinst == Instance.name inst &&
554
  Instance.alias newinst == name
555
    where _types = (inst::Instance.Instance, name::String)
556
          newinst = Instance.setAlias inst name
557

    
558
prop_Instance_setPri inst pdx =
559
  Instance.pNode (Instance.setPri inst pdx) ==? pdx
560
    where _types = (inst::Instance.Instance, pdx::Types.Ndx)
561

    
562
prop_Instance_setSec inst sdx =
563
  Instance.sNode (Instance.setSec inst sdx) ==? sdx
564
    where _types = (inst::Instance.Instance, sdx::Types.Ndx)
565

    
566
prop_Instance_setBoth inst pdx sdx =
567
  Instance.pNode si == pdx && Instance.sNode si == sdx
568
    where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
569
          si = Instance.setBoth inst pdx sdx
570

    
571
prop_Instance_shrinkMG inst =
572
  Instance.mem inst >= 2 * Types.unitMem ==>
573
    case Instance.shrinkByType inst Types.FailMem of
574
      Types.Ok inst' -> Instance.mem inst' == Instance.mem inst - Types.unitMem
575
      _ -> False
576

    
577
prop_Instance_shrinkMF inst =
578
  forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
579
    let inst' = inst { Instance.mem = mem}
580
    in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
581

    
582
prop_Instance_shrinkCG inst =
583
  Instance.vcpus inst >= 2 * Types.unitCpu ==>
584
    case Instance.shrinkByType inst Types.FailCPU of
585
      Types.Ok inst' ->
586
        Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
587
      _ -> False
588

    
589
prop_Instance_shrinkCF inst =
590
  forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
591
    let inst' = inst { Instance.vcpus = vcpus }
592
    in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
593

    
594
prop_Instance_shrinkDG inst =
595
  Instance.dsk inst >= 2 * Types.unitDsk ==>
596
    case Instance.shrinkByType inst Types.FailDisk of
597
      Types.Ok inst' ->
598
        Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
599
      _ -> False
600

    
601
prop_Instance_shrinkDF inst =
602
  forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
603
    let inst' = inst { Instance.dsk = dsk }
604
    in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
605

    
606
prop_Instance_setMovable inst m =
607
  Instance.movable inst' ==? m
608
    where inst' = Instance.setMovable inst m
609

    
610
testSuite "Instance"
611
            [ 'prop_Instance_creat
612
            , 'prop_Instance_setIdx
613
            , 'prop_Instance_setName
614
            , 'prop_Instance_setAlias
615
            , 'prop_Instance_setPri
616
            , 'prop_Instance_setSec
617
            , 'prop_Instance_setBoth
618
            , 'prop_Instance_shrinkMG
619
            , 'prop_Instance_shrinkMF
620
            , 'prop_Instance_shrinkCG
621
            , 'prop_Instance_shrinkCF
622
            , 'prop_Instance_shrinkDG
623
            , 'prop_Instance_shrinkDF
624
            , 'prop_Instance_setMovable
625
            ]
626

    
627
-- ** Text backend tests
628

    
629
-- Instance text loader tests
630

    
631
prop_Text_Load_Instance name mem dsk vcpus status
632
                        (NonEmpty pnode) snode
633
                        (NonNegative pdx) (NonNegative sdx) autobal dt =
634
  pnode /= snode && pdx /= sdx ==>
635
  let vcpus_s = show vcpus
636
      dsk_s = show dsk
637
      mem_s = show mem
638
      status_s = Types.instanceStatusToRaw status
639
      ndx = if null snode
640
              then [(pnode, pdx)]
641
              else [(pnode, pdx), (snode, sdx)]
642
      nl = Data.Map.fromList ndx
643
      tags = ""
644
      sbal = if autobal then "Y" else "N"
645
      sdt = Types.diskTemplateToRaw dt
646
      inst = Text.loadInst nl
647
             [name, mem_s, dsk_s, vcpus_s, status_s,
648
              sbal, pnode, snode, sdt, tags]
649
      fail1 = Text.loadInst nl
650
              [name, mem_s, dsk_s, vcpus_s, status_s,
651
               sbal, pnode, pnode, tags]
652
      _types = ( name::String, mem::Int, dsk::Int
653
               , vcpus::Int, status::Types.InstanceStatus
654
               , snode::String
655
               , autobal::Bool)
656
  in case inst of
657
       Types.Bad msg -> printTestCase ("Failed to load instance: " ++ msg)
658
                        False
659
       Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
660
                                        \ loading the instance" $
661
               Instance.name i == name &&
662
               Instance.vcpus i == vcpus &&
663
               Instance.mem i == mem &&
664
               Instance.pNode i == pdx &&
665
               Instance.sNode i == (if null snode
666
                                      then Node.noSecondary
667
                                      else sdx) &&
668
               Instance.autoBalance i == autobal &&
669
               Types.isBad fail1
670

    
671
prop_Text_Load_InstanceFail ktn fields =
672
  length fields /= 10 ==>
673
    case Text.loadInst nl fields of
674
      Types.Ok _ -> printTestCase "Managed to load instance from invalid\
675
                                  \ data" False
676
      Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
677
                       "Invalid/incomplete instance data: '" `isPrefixOf` msg
678
    where nl = Data.Map.fromList ktn
679

    
680
prop_Text_Load_Node name tm nm fm td fd tc fo =
681
  let conv v = if v < 0
682
                 then "?"
683
                 else show v
684
      tm_s = conv tm
685
      nm_s = conv nm
686
      fm_s = conv fm
687
      td_s = conv td
688
      fd_s = conv fd
689
      tc_s = conv tc
690
      fo_s = if fo
691
               then "Y"
692
               else "N"
693
      any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
694
      gid = Group.uuid defGroup
695
  in case Text.loadNode defGroupAssoc
696
       [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
697
       Nothing -> False
698
       Just (name', node) ->
699
         if fo || any_broken
700
           then Node.offline node
701
           else Node.name node == name' && name' == name &&
702
                Node.alias node == name &&
703
                Node.tMem node == fromIntegral tm &&
704
                Node.nMem node == nm &&
705
                Node.fMem node == fm &&
706
                Node.tDsk node == fromIntegral td &&
707
                Node.fDsk node == fd &&
708
                Node.tCpu node == fromIntegral tc
709

    
710
prop_Text_Load_NodeFail fields =
711
  length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
712

    
713
prop_Text_NodeLSIdempotent node =
714
  (Text.loadNode defGroupAssoc.
715
       Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
716
  Just (Node.name n, n)
717
    -- override failN1 to what loadNode returns by default
718
    where n = node { Node.failN1 = True, Node.offline = False
719
                   , Node.iPolicy = Types.defIPolicy }
720

    
721
testSuite "Text"
722
            [ 'prop_Text_Load_Instance
723
            , 'prop_Text_Load_InstanceFail
724
            , 'prop_Text_Load_Node
725
            , 'prop_Text_Load_NodeFail
726
            , 'prop_Text_NodeLSIdempotent
727
            ]
728

    
729
-- ** Node tests
730

    
731
prop_Node_setAlias node name =
732
  Node.name newnode == Node.name node &&
733
  Node.alias newnode == name
734
    where _types = (node::Node.Node, name::String)
735
          newnode = Node.setAlias node name
736

    
737
prop_Node_setOffline node status =
738
  Node.offline newnode ==? status
739
    where newnode = Node.setOffline node status
740

    
741
prop_Node_setXmem node xm =
742
  Node.xMem newnode ==? xm
743
    where newnode = Node.setXmem node xm
744

    
745
prop_Node_setMcpu node mc =
746
  Node.mCpu newnode ==? mc
747
    where newnode = Node.setMcpu node mc
748

    
749
-- | Check that an instance add with too high memory or disk will be
750
-- rejected.
751
prop_Node_addPriFM node inst =
752
  Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
753
  not (Instance.instanceOffline inst) ==>
754
  case Node.addPri node inst'' of
755
    Types.OpFail Types.FailMem -> True
756
    _ -> False
757
  where _types = (node::Node.Node, inst::Instance.Instance)
758
        inst' = setInstanceSmallerThanNode node inst
759
        inst'' = inst' { Instance.mem = Instance.mem inst }
760

    
761
prop_Node_addPriFD node inst =
762
  Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
763
    case Node.addPri node inst'' of
764
      Types.OpFail Types.FailDisk -> True
765
      _ -> False
766
    where _types = (node::Node.Node, inst::Instance.Instance)
767
          inst' = setInstanceSmallerThanNode node inst
768
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
769

    
770
prop_Node_addPriFC node inst (Positive extra) =
771
  not (Node.failN1 node) && not (Instance.instanceOffline inst) ==>
772
      case Node.addPri node inst'' of
773
        Types.OpFail Types.FailCPU -> True
774
        _ -> False
775
    where _types = (node::Node.Node, inst::Instance.Instance)
776
          inst' = setInstanceSmallerThanNode node inst
777
          inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
778

    
779
-- | Check that an instance add with too high memory or disk will be
780
-- rejected.
781
prop_Node_addSec node inst pdx =
782
  ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
783
    not (Instance.instanceOffline inst)) ||
784
   Instance.dsk inst >= Node.fDsk node) &&
785
  not (Node.failN1 node) ==>
786
      isFailure (Node.addSec node inst pdx)
787
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
788

    
789
-- | Check that an offline instance with reasonable disk size can always
790
-- be added.
791
prop_Node_addPriOffline =
792
  forAll (arbitrary `suchThat` ((> 0) . Node.fMem)) $ \node ->
793
  forAll (arbitrary `suchThat`
794
          (\ x ->  (Instance.dsk x  < Node.fDsk node) &&
795
                   Instance.instanceOffline x)) $ \inst ->
796
  case Node.addPri node inst of
797
    Types.OpGood _ -> True
798
    _ -> False
799

    
800
prop_Node_addSecOffline pdx =
801
  forAll (arbitrary `suchThat` ((> 0) . Node.fMem)) $ \node ->
802
  forAll (arbitrary `suchThat`
803
          (\ x ->  (Instance.dsk x  < Node.fDsk node) &&
804
                   Instance.instanceOffline x)) $ \inst ->
805
  case Node.addSec node inst pdx of
806
    Types.OpGood _ -> True
807
    _ -> False
808

    
809
-- | Checks for memory reservation changes.
810
prop_Node_rMem inst =
811
  not (Instance.instanceOffline inst) ==>
812
  forAll (arbitrary `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
813
  -- ab = auto_balance, nb = non-auto_balance
814
  -- we use -1 as the primary node of the instance
815
  let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
816
      inst_ab = setInstanceSmallerThanNode node inst'
817
      inst_nb = inst_ab { Instance.autoBalance = False }
818
      -- now we have the two instances, identical except the
819
      -- autoBalance attribute
820
      orig_rmem = Node.rMem node
821
      inst_idx = Instance.idx inst_ab
822
      node_add_ab = Node.addSec node inst_ab (-1)
823
      node_add_nb = Node.addSec node inst_nb (-1)
824
      node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
825
      node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
826
  in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
827
       (Types.OpGood a_ab, Types.OpGood a_nb,
828
        Types.OpGood d_ab, Types.OpGood d_nb) ->
829
         printTestCase "Consistency checks failed" $
830
           Node.rMem a_ab >  orig_rmem &&
831
           Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
832
           Node.rMem a_nb == orig_rmem &&
833
           Node.rMem d_ab == orig_rmem &&
834
           Node.rMem d_nb == orig_rmem &&
835
           -- this is not related to rMem, but as good a place to
836
           -- test as any
837
           inst_idx `elem` Node.sList a_ab &&
838
           inst_idx `notElem` Node.sList d_ab
839
       x -> printTestCase ("Failed to add/remove instances: " ++ show x) False
840

    
841
-- | Check mdsk setting.
842
prop_Node_setMdsk node mx =
843
  Node.loDsk node' >= 0 &&
844
  fromIntegral (Node.loDsk node') <= Node.tDsk node &&
845
  Node.availDisk node' >= 0 &&
846
  Node.availDisk node' <= Node.fDsk node' &&
847
  fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
848
  Node.mDsk node' == mx'
849
    where _types = (node::Node.Node, mx::SmallRatio)
850
          node' = Node.setMdsk node mx'
851
          SmallRatio mx' = mx
852

    
853
-- Check tag maps
854
prop_Node_tagMaps_idempotent tags =
855
  Node.delTags (Node.addTags m tags) tags ==? m
856
    where m = Data.Map.empty
857

    
858
prop_Node_tagMaps_reject tags =
859
  not (null tags) ==>
860
  all (\t -> Node.rejectAddTags m [t]) tags
861
    where m = Node.addTags Data.Map.empty tags
862

    
863
prop_Node_showField node =
864
  forAll (elements Node.defaultFields) $ \ field ->
865
  fst (Node.showHeader field) /= Types.unknownField &&
866
  Node.showField node field /= Types.unknownField
867

    
868
prop_Node_computeGroups nodes =
869
  let ng = Node.computeGroups nodes
870
      onlyuuid = map fst ng
871
  in length nodes == sum (map (length . snd) ng) &&
872
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
873
     length (nub onlyuuid) == length onlyuuid &&
874
     (null nodes || not (null ng))
875

    
876
testSuite "Node"
877
            [ 'prop_Node_setAlias
878
            , 'prop_Node_setOffline
879
            , 'prop_Node_setMcpu
880
            , 'prop_Node_setXmem
881
            , 'prop_Node_addPriFM
882
            , 'prop_Node_addPriFD
883
            , 'prop_Node_addPriFC
884
            , 'prop_Node_addSec
885
            , 'prop_Node_addPriOffline
886
            , 'prop_Node_addSecOffline
887
            , 'prop_Node_rMem
888
            , 'prop_Node_setMdsk
889
            , 'prop_Node_tagMaps_idempotent
890
            , 'prop_Node_tagMaps_reject
891
            , 'prop_Node_showField
892
            , 'prop_Node_computeGroups
893
            ]
894

    
895
-- ** Cluster tests
896

    
897
-- | Check that the cluster score is close to zero for a homogeneous
898
-- cluster.
899
prop_Score_Zero node =
900
  forAll (choose (1, 1024)) $ \count ->
901
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
902
     (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
903
  let fn = Node.buildPeers node Container.empty
904
      nlst = replicate count fn
905
      score = Cluster.compCVNodes nlst
906
  -- we can't say == 0 here as the floating point errors accumulate;
907
  -- this should be much lower than the default score in CLI.hs
908
  in score <= 1e-12
909

    
910
-- | Check that cluster stats are sane.
911
prop_CStats_sane node =
912
  forAll (choose (1, 1024)) $ \count ->
913
    (not (Node.offline node) && not (Node.failN1 node) &&
914
     (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
915
  let fn = Node.buildPeers node Container.empty
916
      nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
917
      nl = Container.fromList nlst
918
      cstats = Cluster.totalResources nl
919
  in Cluster.csAdsk cstats >= 0 &&
920
     Cluster.csAdsk cstats <= Cluster.csFdsk cstats
921

    
922
-- | Check that one instance is allocated correctly, without
923
-- rebalances needed.
924
prop_ClusterAlloc_sane node inst =
925
  forAll (choose (5, 20)) $ \count ->
926
  not (Node.offline node)
927
        && not (Node.failN1 node)
928
        && Node.availDisk node > 0
929
        && Node.availMem node > 0
930
        ==>
931
  let (nl, il, inst') = makeSmallEmptyCluster node count inst
932
  in case Cluster.genAllocNodes defGroupList nl 2 True >>=
933
     Cluster.tryAlloc nl il inst' of
934
       Types.Bad _ -> False
935
       Types.Ok as ->
936
         case Cluster.asSolution as of
937
           Nothing -> False
938
           Just (xnl, xi, _, cv) ->
939
             let il' = Container.add (Instance.idx xi) xi il
940
                 tbl = Cluster.Table xnl il' cv []
941
             in not (canBalance tbl True True False)
942

    
943
-- | Checks that on a 2-5 node cluster, we can allocate a random
944
-- instance spec via tiered allocation (whatever the original instance
945
-- spec), on either one or two nodes.
946
prop_ClusterCanTieredAlloc node inst =
947
  forAll (choose (2, 5)) $ \count ->
948
  forAll (choose (1, 2)) $ \rqnodes ->
949
  not (Node.offline node)
950
        && not (Node.failN1 node)
951
        && isNodeBig node 4
952
        ==>
953
  let nl = makeSmallCluster node count
954
      il = Container.empty
955
      allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
956
  in case allocnodes >>= \allocnodes' ->
957
    Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
958
       Types.Bad _ -> False
959
       Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
960
                                             IntMap.size il' == length ixes &&
961
                                             length ixes == length cstats
962

    
963
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
964
-- we can also evacuate it.
965
prop_ClusterAllocEvac node inst =
966
  forAll (choose (4, 8)) $ \count ->
967
  not (Node.offline node)
968
        && not (Node.failN1 node)
969
        && isNodeBig node 4
970
        ==>
971
  let (nl, il, inst') = makeSmallEmptyCluster node count inst
972
  in case Cluster.genAllocNodes defGroupList nl 2 True >>=
973
     Cluster.tryAlloc nl il inst' of
974
       Types.Bad _ -> False
975
       Types.Ok as ->
976
         case Cluster.asSolution as of
977
           Nothing -> False
978
           Just (xnl, xi, _, _) ->
979
             let sdx = Instance.sNode xi
980
                 il' = Container.add (Instance.idx xi) xi il
981
             in case IAlloc.processRelocate defGroupList xnl il'
982
                  (Instance.idx xi) 1 [sdx] of
983
                  Types.Ok _ -> True
984
                  _ -> False
985

    
986
-- | Check that allocating multiple instances on a cluster, then
987
-- adding an empty node, results in a valid rebalance.
988
prop_ClusterAllocBalance =
989
  forAll (genNode (Just 5) (Just 128)) $ \node ->
990
  forAll (choose (3, 5)) $ \count ->
991
  not (Node.offline node) && not (Node.failN1 node) ==>
992
  let nl = makeSmallCluster node count
993
      (hnode, nl') = IntMap.deleteFindMax nl
994
      il = Container.empty
995
      allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
996
      i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
997
  in case allocnodes >>= \allocnodes' ->
998
    Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
999
       Types.Bad _ -> printTestCase "Failed to allocate" False
1000
       Types.Ok (_, _, _, [], _) -> printTestCase "Failed to allocate" False
1001
       Types.Ok (_, xnl, il', _, _) ->
1002
         let ynl = Container.add (Node.idx hnode) hnode xnl
1003
             cv = Cluster.compCV ynl
1004
             tbl = Cluster.Table ynl il' cv []
1005
         in printTestCase "Failed to rebalance" $
1006
            canBalance tbl True True False
1007

    
1008
-- | Checks consistency.
1009
prop_ClusterCheckConsistency node inst =
1010
  let nl = makeSmallCluster node 3
1011
      [node1, node2, node3] = Container.elems nl
1012
      node3' = node3 { Node.group = 1 }
1013
      nl' = Container.add (Node.idx node3') node3' nl
1014
      inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
1015
      inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
1016
      inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
1017
      ccheck = Cluster.findSplitInstances nl' . Container.fromList
1018
  in null (ccheck [(0, inst1)]) &&
1019
     null (ccheck [(0, inst2)]) &&
1020
     (not . null $ ccheck [(0, inst3)])
1021

    
1022
-- | For now, we only test that we don't lose instances during the split.
1023
prop_ClusterSplitCluster node inst =
1024
  forAll (choose (0, 100)) $ \icnt ->
1025
  let nl = makeSmallCluster node 2
1026
      (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
1027
                   (nl, Container.empty) [1..icnt]
1028
      gni = Cluster.splitCluster nl' il'
1029
  in sum (map (Container.size . snd . snd) gni) == icnt &&
1030
     all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
1031
                                 (Container.elems nl'')) gni
1032

    
1033
-- | Helper function to check if we can allocate an instance on a
1034
-- given node list.
1035
canAllocOn :: Node.List -> Int -> Instance.Instance -> Bool
1036
canAllocOn nl reqnodes inst =
1037
  case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1038
       Cluster.tryAlloc nl (Container.empty) inst of
1039
       Types.Bad _ -> False
1040
       Types.Ok as ->
1041
         case Cluster.asSolution as of
1042
           Nothing -> False
1043
           Just _ -> True
1044

    
1045
-- | Checks that allocation obeys minimum and maximum instance
1046
-- policies. The unittest generates a random node, duplicates it count
1047
-- times, and generates a random instance that can be allocated on
1048
-- this mini-cluster; it then checks that after applying a policy that
1049
-- the instance doesn't fits, the allocation fails.
1050
prop_ClusterAllocPolicy node =
1051
  -- rqn is the required nodes (1 or 2)
1052
  forAll (choose (1, 2)) $ \rqn ->
1053
  forAll (choose (5, 20)) $ \count ->
1054
  forAll (arbitrary `suchThat` (canAllocOn (makeSmallCluster node count) rqn))
1055
         $ \inst ->
1056
  forAll (arbitrary `suchThat` (isFailure .
1057
                                Instance.instMatchesPolicy inst)) $ \ipol ->
1058
  let node' = Node.setPolicy ipol node
1059
      nl = makeSmallCluster node' count
1060
  in not $ canAllocOn nl rqn inst
1061

    
1062
testSuite "Cluster"
1063
            [ 'prop_Score_Zero
1064
            , 'prop_CStats_sane
1065
            , 'prop_ClusterAlloc_sane
1066
            , 'prop_ClusterCanTieredAlloc
1067
            , 'prop_ClusterAllocEvac
1068
            , 'prop_ClusterAllocBalance
1069
            , 'prop_ClusterCheckConsistency
1070
            , 'prop_ClusterSplitCluster
1071
            , 'prop_ClusterAllocPolicy
1072
            ]
1073

    
1074
-- ** OpCodes tests
1075

    
1076
-- | Check that opcode serialization is idempotent.
1077
prop_OpCodes_serialization op =
1078
  case J.readJSON (J.showJSON op) of
1079
    J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
1080
    J.Ok op' -> op ==? op'
1081
  where _types = op::OpCodes.OpCode
1082

    
1083
testSuite "OpCodes"
1084
            [ 'prop_OpCodes_serialization ]
1085

    
1086
-- ** Jobs tests
1087

    
1088
-- | Check that (queued) job\/opcode status serialization is idempotent.
1089
prop_OpStatus_serialization os =
1090
  case J.readJSON (J.showJSON os) of
1091
    J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
1092
    J.Ok os' -> os ==? os'
1093
  where _types = os::Jobs.OpStatus
1094

    
1095
prop_JobStatus_serialization js =
1096
  case J.readJSON (J.showJSON js) of
1097
    J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
1098
    J.Ok js' -> js ==? js'
1099
  where _types = js::Jobs.JobStatus
1100

    
1101
testSuite "Jobs"
1102
            [ 'prop_OpStatus_serialization
1103
            , 'prop_JobStatus_serialization
1104
            ]
1105

    
1106
-- ** Loader tests
1107

    
1108
prop_Loader_lookupNode ktn inst node =
1109
  Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
1110
    where nl = Data.Map.fromList ktn
1111

    
1112
prop_Loader_lookupInstance kti inst =
1113
  Loader.lookupInstance il inst ==? Data.Map.lookup inst il
1114
    where il = Data.Map.fromList kti
1115

    
1116
prop_Loader_assignIndices nodes =
1117
  Data.Map.size nassoc == length nodes &&
1118
  Container.size kt == length nodes &&
1119
  (if not (null nodes)
1120
   then maximum (IntMap.keys kt) == length nodes - 1
1121
   else True)
1122
    where (nassoc, kt) =
1123
            Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1124

    
1125
-- | Checks that the number of primary instances recorded on the nodes
1126
-- is zero.
1127
prop_Loader_mergeData ns =
1128
  let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1129
  in case Loader.mergeData [] [] [] []
1130
         (Loader.emptyCluster {Loader.cdNodes = na}) of
1131
    Types.Bad _ -> False
1132
    Types.Ok (Loader.ClusterData _ nl il _ _) ->
1133
      let nodes = Container.elems nl
1134
          instances = Container.elems il
1135
      in (sum . map (length . Node.pList)) nodes == 0 &&
1136
         null instances
1137

    
1138
-- | Check that compareNameComponent on equal strings works.
1139
prop_Loader_compareNameComponent_equal :: String -> Bool
1140
prop_Loader_compareNameComponent_equal s =
1141
  Loader.compareNameComponent s s ==
1142
    Loader.LookupResult Loader.ExactMatch s
1143

    
1144
-- | Check that compareNameComponent on prefix strings works.
1145
prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1146
prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1147
  Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1148
    Loader.LookupResult Loader.PartialMatch s1
1149

    
1150
testSuite "Loader"
1151
            [ 'prop_Loader_lookupNode
1152
            , 'prop_Loader_lookupInstance
1153
            , 'prop_Loader_assignIndices
1154
            , 'prop_Loader_mergeData
1155
            , 'prop_Loader_compareNameComponent_equal
1156
            , 'prop_Loader_compareNameComponent_prefix
1157
            ]
1158

    
1159
-- ** Types tests
1160

    
1161
prop_Types_AllocPolicy_serialisation apol =
1162
  case J.readJSON (J.showJSON apol) of
1163
    J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1164
              p == apol
1165
    J.Error s -> printTestCase ("failed to deserialise: " ++ s) False
1166
      where _types = apol::Types.AllocPolicy
1167

    
1168
prop_Types_DiskTemplate_serialisation dt =
1169
  case J.readJSON (J.showJSON dt) of
1170
    J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1171
              p == dt
1172
    J.Error s -> printTestCase ("failed to deserialise: " ++ s)
1173
                 False
1174
      where _types = dt::Types.DiskTemplate
1175

    
1176
prop_Types_opToResult op =
1177
  case op of
1178
    Types.OpFail _ -> Types.isBad r
1179
    Types.OpGood v -> case r of
1180
                        Types.Bad _ -> False
1181
                        Types.Ok v' -> v == v'
1182
  where r = Types.opToResult op
1183
        _types = op::Types.OpResult Int
1184

    
1185
prop_Types_eitherToResult ei =
1186
  case ei of
1187
    Left _ -> Types.isBad r
1188
    Right v -> case r of
1189
                 Types.Bad _ -> False
1190
                 Types.Ok v' -> v == v'
1191
    where r = Types.eitherToResult ei
1192
          _types = ei::Either String Int
1193

    
1194
testSuite "Types"
1195
            [ 'prop_Types_AllocPolicy_serialisation
1196
            , 'prop_Types_DiskTemplate_serialisation
1197
            , 'prop_Types_opToResult
1198
            , 'prop_Types_eitherToResult
1199
            ]