Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (43.7 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 :: Int -> Node.Node -> Bool
171
isNodeBig size node = 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
-- | Helper function to generate a sane node.
269
genOnlineNode :: Gen Node.Node
270
genOnlineNode = do
271
  arbitrary `suchThat` (\n -> not (Node.offline n) &&
272
                              not (Node.failN1 n) &&
273
                              Node.availDisk n > 0 &&
274
                              Node.availMem n > 0 &&
275
                              Node.availCpu n > 0)
276

    
277
-- and a random node
278
instance Arbitrary Node.Node where
279
  arbitrary = genNode Nothing Nothing
280

    
281
-- replace disks
282
instance Arbitrary OpCodes.ReplaceDisksMode where
283
  arbitrary = elements [minBound..maxBound]
284

    
285
instance Arbitrary OpCodes.OpCode where
286
  arbitrary = do
287
    op_id <- elements [ "OP_TEST_DELAY"
288
                      , "OP_INSTANCE_REPLACE_DISKS"
289
                      , "OP_INSTANCE_FAILOVER"
290
                      , "OP_INSTANCE_MIGRATE"
291
                      ]
292
    case op_id of
293
      "OP_TEST_DELAY" ->
294
        liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
295
      "OP_INSTANCE_REPLACE_DISKS" ->
296
        liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
297
          arbitrary arbitrary arbitrary
298
      "OP_INSTANCE_FAILOVER" ->
299
        liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
300
          arbitrary
301
      "OP_INSTANCE_MIGRATE" ->
302
        liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
303
          arbitrary arbitrary arbitrary
304
      _ -> fail "Wrong opcode"
305

    
306
instance Arbitrary Jobs.OpStatus where
307
  arbitrary = elements [minBound..maxBound]
308

    
309
instance Arbitrary Jobs.JobStatus where
310
  arbitrary = elements [minBound..maxBound]
311

    
312
newtype SmallRatio = SmallRatio Double deriving Show
313
instance Arbitrary SmallRatio where
314
  arbitrary = do
315
    v <- choose (0, 1)
316
    return $ SmallRatio v
317

    
318
instance Arbitrary Types.AllocPolicy where
319
  arbitrary = elements [minBound..maxBound]
320

    
321
instance Arbitrary Types.DiskTemplate where
322
  arbitrary = elements [minBound..maxBound]
323

    
324
instance Arbitrary Types.FailMode where
325
  arbitrary = elements [minBound..maxBound]
326

    
327
instance Arbitrary a => Arbitrary (Types.OpResult a) where
328
  arbitrary = arbitrary >>= \c ->
329
              if c
330
                then liftM Types.OpGood arbitrary
331
                else liftM Types.OpFail arbitrary
332

    
333
instance Arbitrary Types.ISpec where
334
  arbitrary = do
335
    mem <- arbitrary::Gen (NonNegative Int)
336
    dsk_c <- arbitrary::Gen (NonNegative Int)
337
    dsk_s <- arbitrary::Gen (NonNegative Int)
338
    cpu <- arbitrary::Gen (NonNegative Int)
339
    nic <- arbitrary::Gen (NonNegative Int)
340
    return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem
341
                       , Types.iSpecCpuCount   = fromIntegral cpu
342
                       , Types.iSpecDiskSize   = fromIntegral dsk_s
343
                       , Types.iSpecDiskCount  = fromIntegral dsk_c
344
                       , Types.iSpecNicCount   = fromIntegral nic
345
                       }
346

    
347
-- | Helper function to check whether a spec is LTE than another
348
iSpecSmaller :: Types.ISpec -> Types.ISpec -> Bool
349
iSpecSmaller imin imax =
350
  Types.iSpecMemorySize imin <= Types.iSpecMemorySize imax &&
351
  Types.iSpecCpuCount imin   <= Types.iSpecCpuCount imax &&
352
  Types.iSpecDiskSize imin   <= Types.iSpecDiskSize imax &&
353
  Types.iSpecDiskCount imin  <= Types.iSpecDiskCount imax &&
354
  Types.iSpecNicCount imin   <= Types.iSpecNicCount imax
355

    
356
instance Arbitrary Types.IPolicy where
357
  arbitrary = do
358
    imin <- arbitrary
359
    istd <- arbitrary `suchThat` (iSpecSmaller imin)
360
    imax <- arbitrary `suchThat` (iSpecSmaller istd)
361
    dts  <- arbitrary
362
    return Types.IPolicy { Types.iPolicyMinSpec = imin
363
                         , Types.iPolicyStdSpec = istd
364
                         , Types.iPolicyMaxSpec = imax
365
                         , Types.iPolicyDiskTemplates = dts
366
                         }
367

    
368
-- * Actual tests
369

    
370
-- ** Utils tests
371

    
372
-- | If the list is not just an empty element, and if the elements do
373
-- not contain commas, then join+split should be idempotent.
374
prop_Utils_commaJoinSplit =
375
  forAll (arbitrary `suchThat`
376
          (\l -> l /= [""] && all (notElem ',') l )) $ \lst ->
377
  Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
378

    
379
-- | Split and join should always be idempotent.
380
prop_Utils_commaSplitJoin s =
381
  Utils.commaJoin (Utils.sepSplit ',' s) ==? s
382

    
383
-- | fromObjWithDefault, we test using the Maybe monad and an integer
384
-- value.
385
prop_Utils_fromObjWithDefault def_value random_key =
386
  -- a missing key will be returned with the default
387
  JSON.fromObjWithDefault [] random_key def_value == Just def_value &&
388
  -- a found key will be returned as is, not with default
389
  JSON.fromObjWithDefault [(random_key, J.showJSON def_value)]
390
       random_key (def_value+1) == Just def_value
391
    where _types = def_value :: Integer
392

    
393
-- | Test that functional if' behaves like the syntactic sugar if.
394
prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
395
prop_Utils_if'if cnd a b =
396
  Utils.if' cnd a b ==? if cnd then a else b
397

    
398
-- | Test basic select functionality
399
prop_Utils_select :: Int      -- ^ Default result
400
                  -> [Int]    -- ^ List of False values
401
                  -> [Int]    -- ^ List of True values
402
                  -> Gen Prop -- ^ Test result
403
prop_Utils_select def lst1 lst2 =
404
  Utils.select def (flist ++ tlist) ==? expectedresult
405
    where expectedresult = Utils.if' (null lst2) def (head lst2)
406
          flist = zip (repeat False) lst1
407
          tlist = zip (repeat True)  lst2
408

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

    
418
-- | Test basic select functionality with undefined list values
419
prop_Utils_select_undefv :: [Int]            -- ^ List of False values
420
                         -> NonEmptyList Int -- ^ List of True values
421
                         -> Gen Prop         -- ^ Test result
422
prop_Utils_select_undefv lst1 (NonEmpty lst2) =
423
  Utils.select undefined cndlist ==? head lst2
424
    where flist = zip (repeat False) lst1
425
          tlist = zip (repeat True)  lst2
426
          cndlist = flist ++ tlist ++ [undefined]
427

    
428
prop_Utils_parseUnit (NonNegative n) =
429
  Utils.parseUnit (show n) == Types.Ok n &&
430
  Utils.parseUnit (show n ++ "m") == Types.Ok n &&
431
  (case Utils.parseUnit (show n ++ "M") of
432
     Types.Ok m -> if n > 0
433
                     then m < n  -- for positive values, X MB is < than X MiB
434
                     else m == 0 -- but for 0, 0 MB == 0 MiB
435
     Types.Bad _ -> False) &&
436
  Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
437
  Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
438
  Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
439
    where _types = n::Int
440

    
441
-- | Test list for the Utils module.
442
testSuite "Utils"
443
            [ 'prop_Utils_commaJoinSplit
444
            , 'prop_Utils_commaSplitJoin
445
            , 'prop_Utils_fromObjWithDefault
446
            , 'prop_Utils_if'if
447
            , 'prop_Utils_select
448
            , 'prop_Utils_select_undefd
449
            , 'prop_Utils_select_undefv
450
            , 'prop_Utils_parseUnit
451
            ]
452

    
453
-- ** PeerMap tests
454

    
455
-- | Make sure add is idempotent.
456
prop_PeerMap_addIdempotent pmap key em =
457
  fn puniq ==? fn (fn puniq)
458
    where _types = (pmap::PeerMap.PeerMap,
459
                    key::PeerMap.Key, em::PeerMap.Elem)
460
          fn = PeerMap.add key em
461
          puniq = PeerMap.accumArray const pmap
462

    
463
-- | Make sure remove is idempotent.
464
prop_PeerMap_removeIdempotent pmap key =
465
  fn puniq ==? fn (fn puniq)
466
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
467
          fn = PeerMap.remove key
468
          puniq = PeerMap.accumArray const pmap
469

    
470
-- | Make sure a missing item returns 0.
471
prop_PeerMap_findMissing pmap key =
472
  PeerMap.find key (PeerMap.remove key puniq) ==? 0
473
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
474
          puniq = PeerMap.accumArray const pmap
475

    
476
-- | Make sure an added item is found.
477
prop_PeerMap_addFind pmap key em =
478
  PeerMap.find key (PeerMap.add key em puniq) ==? em
479
    where _types = (pmap::PeerMap.PeerMap,
480
                    key::PeerMap.Key, em::PeerMap.Elem)
481
          puniq = PeerMap.accumArray const pmap
482

    
483
-- | Manual check that maxElem returns the maximum indeed, or 0 for null.
484
prop_PeerMap_maxElem pmap =
485
  PeerMap.maxElem puniq ==? if null puniq then 0
486
                              else (maximum . snd . unzip) puniq
487
    where _types = pmap::PeerMap.PeerMap
488
          puniq = PeerMap.accumArray const pmap
489

    
490
-- | List of tests for the PeerMap module.
491
testSuite "PeerMap"
492
            [ 'prop_PeerMap_addIdempotent
493
            , 'prop_PeerMap_removeIdempotent
494
            , 'prop_PeerMap_maxElem
495
            , 'prop_PeerMap_addFind
496
            , 'prop_PeerMap_findMissing
497
            ]
498

    
499
-- ** Container tests
500

    
501
-- we silence the following due to hlint bug fixed in later versions
502
{-# ANN prop_Container_addTwo "HLint: ignore Avoid lambda" #-}
503
prop_Container_addTwo cdata i1 i2 =
504
  fn i1 i2 cont == fn i2 i1 cont &&
505
  fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
506
    where _types = (cdata::[Int],
507
                    i1::Int, i2::Int)
508
          cont = foldl (\c x -> Container.add x x c) Container.empty cdata
509
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
510

    
511
prop_Container_nameOf node =
512
  let nl = makeSmallCluster node 1
513
      fnode = head (Container.elems nl)
514
  in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
515

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

    
538
testSuite "Container"
539
            [ 'prop_Container_addTwo
540
            , 'prop_Container_nameOf
541
            , 'prop_Container_findByName
542
            ]
543

    
544
-- ** Instance tests
545

    
546
-- Simple instance tests, we only have setter/getters
547

    
548
prop_Instance_creat inst =
549
  Instance.name inst ==? Instance.alias inst
550

    
551
prop_Instance_setIdx inst idx =
552
  Instance.idx (Instance.setIdx inst idx) ==? idx
553
    where _types = (inst::Instance.Instance, idx::Types.Idx)
554

    
555
prop_Instance_setName inst name =
556
  Instance.name newinst == name &&
557
  Instance.alias newinst == name
558
    where _types = (inst::Instance.Instance, name::String)
559
          newinst = Instance.setName inst name
560

    
561
prop_Instance_setAlias inst name =
562
  Instance.name newinst == Instance.name inst &&
563
  Instance.alias newinst == name
564
    where _types = (inst::Instance.Instance, name::String)
565
          newinst = Instance.setAlias inst name
566

    
567
prop_Instance_setPri inst pdx =
568
  Instance.pNode (Instance.setPri inst pdx) ==? pdx
569
    where _types = (inst::Instance.Instance, pdx::Types.Ndx)
570

    
571
prop_Instance_setSec inst sdx =
572
  Instance.sNode (Instance.setSec inst sdx) ==? sdx
573
    where _types = (inst::Instance.Instance, sdx::Types.Ndx)
574

    
575
prop_Instance_setBoth inst pdx sdx =
576
  Instance.pNode si == pdx && Instance.sNode si == sdx
577
    where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
578
          si = Instance.setBoth inst pdx sdx
579

    
580
prop_Instance_shrinkMG inst =
581
  Instance.mem inst >= 2 * Types.unitMem ==>
582
    case Instance.shrinkByType inst Types.FailMem of
583
      Types.Ok inst' -> Instance.mem inst' == Instance.mem inst - Types.unitMem
584
      _ -> False
585

    
586
prop_Instance_shrinkMF inst =
587
  forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
588
    let inst' = inst { Instance.mem = mem}
589
    in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
590

    
591
prop_Instance_shrinkCG inst =
592
  Instance.vcpus inst >= 2 * Types.unitCpu ==>
593
    case Instance.shrinkByType inst Types.FailCPU of
594
      Types.Ok inst' ->
595
        Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
596
      _ -> False
597

    
598
prop_Instance_shrinkCF inst =
599
  forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
600
    let inst' = inst { Instance.vcpus = vcpus }
601
    in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
602

    
603
prop_Instance_shrinkDG inst =
604
  Instance.dsk inst >= 2 * Types.unitDsk ==>
605
    case Instance.shrinkByType inst Types.FailDisk of
606
      Types.Ok inst' ->
607
        Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
608
      _ -> False
609

    
610
prop_Instance_shrinkDF inst =
611
  forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
612
    let inst' = inst { Instance.dsk = dsk }
613
    in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
614

    
615
prop_Instance_setMovable inst m =
616
  Instance.movable inst' ==? m
617
    where inst' = Instance.setMovable inst m
618

    
619
testSuite "Instance"
620
            [ 'prop_Instance_creat
621
            , 'prop_Instance_setIdx
622
            , 'prop_Instance_setName
623
            , 'prop_Instance_setAlias
624
            , 'prop_Instance_setPri
625
            , 'prop_Instance_setSec
626
            , 'prop_Instance_setBoth
627
            , 'prop_Instance_shrinkMG
628
            , 'prop_Instance_shrinkMF
629
            , 'prop_Instance_shrinkCG
630
            , 'prop_Instance_shrinkCF
631
            , 'prop_Instance_shrinkDG
632
            , 'prop_Instance_shrinkDF
633
            , 'prop_Instance_setMovable
634
            ]
635

    
636
-- ** Text backend tests
637

    
638
-- Instance text loader tests
639

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

    
680
prop_Text_Load_InstanceFail ktn fields =
681
  length fields /= 10 ==>
682
    case Text.loadInst nl fields of
683
      Types.Ok _ -> printTestCase "Managed to load instance from invalid\
684
                                  \ data" False
685
      Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
686
                       "Invalid/incomplete instance data: '" `isPrefixOf` msg
687
    where nl = Data.Map.fromList ktn
688

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

    
719
prop_Text_Load_NodeFail fields =
720
  length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
721

    
722
prop_Text_NodeLSIdempotent node =
723
  (Text.loadNode defGroupAssoc.
724
       Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
725
  Just (Node.name n, n)
726
    -- override failN1 to what loadNode returns by default
727
    where n = node { Node.failN1 = True, Node.offline = False
728
                   , Node.iPolicy = Types.defIPolicy }
729

    
730
testSuite "Text"
731
            [ 'prop_Text_Load_Instance
732
            , 'prop_Text_Load_InstanceFail
733
            , 'prop_Text_Load_Node
734
            , 'prop_Text_Load_NodeFail
735
            , 'prop_Text_NodeLSIdempotent
736
            ]
737

    
738
-- ** Node tests
739

    
740
prop_Node_setAlias node name =
741
  Node.name newnode == Node.name node &&
742
  Node.alias newnode == name
743
    where _types = (node::Node.Node, name::String)
744
          newnode = Node.setAlias node name
745

    
746
prop_Node_setOffline node status =
747
  Node.offline newnode ==? status
748
    where newnode = Node.setOffline node status
749

    
750
prop_Node_setXmem node xm =
751
  Node.xMem newnode ==? xm
752
    where newnode = Node.setXmem node xm
753

    
754
prop_Node_setMcpu node mc =
755
  Node.mCpu newnode ==? mc
756
    where newnode = Node.setMcpu node mc
757

    
758
-- | Check that an instance add with too high memory or disk will be
759
-- rejected.
760
prop_Node_addPriFM node inst =
761
  Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
762
  not (Instance.instanceOffline inst) ==>
763
  case Node.addPri node inst'' of
764
    Types.OpFail Types.FailMem -> True
765
    _ -> False
766
  where _types = (node::Node.Node, inst::Instance.Instance)
767
        inst' = setInstanceSmallerThanNode node inst
768
        inst'' = inst' { Instance.mem = Instance.mem inst }
769

    
770
prop_Node_addPriFD node inst =
771
  Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
772
    case Node.addPri node inst'' of
773
      Types.OpFail Types.FailDisk -> True
774
      _ -> False
775
    where _types = (node::Node.Node, inst::Instance.Instance)
776
          inst' = setInstanceSmallerThanNode node inst
777
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
778

    
779
prop_Node_addPriFC node inst (Positive extra) =
780
  not (Node.failN1 node) && not (Instance.instanceOffline inst) ==>
781
      case Node.addPri node inst'' of
782
        Types.OpFail Types.FailCPU -> True
783
        _ -> False
784
    where _types = (node::Node.Node, inst::Instance.Instance)
785
          inst' = setInstanceSmallerThanNode node inst
786
          inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
787

    
788
-- | Check that an instance add with too high memory or disk will be
789
-- rejected.
790
prop_Node_addSec node inst pdx =
791
  ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
792
    not (Instance.instanceOffline inst)) ||
793
   Instance.dsk inst >= Node.fDsk node) &&
794
  not (Node.failN1 node) ==>
795
      isFailure (Node.addSec node inst pdx)
796
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
797

    
798
-- | Check that an offline instance with reasonable disk size can always
799
-- be added.
800
prop_Node_addPriOffline =
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.addPri node inst of
806
    Types.OpGood _ -> True
807
    _ -> False
808

    
809
prop_Node_addSecOffline pdx =
810
  forAll (arbitrary `suchThat` ((> 0) . Node.fMem)) $ \node ->
811
  forAll (arbitrary `suchThat`
812
          (\ x ->  (Instance.dsk x  < Node.fDsk node) &&
813
                   Instance.instanceOffline x)) $ \inst ->
814
  case Node.addSec node inst pdx of
815
    Types.OpGood _ -> True
816
    _ -> False
817

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

    
850
-- | Check mdsk setting.
851
prop_Node_setMdsk node mx =
852
  Node.loDsk node' >= 0 &&
853
  fromIntegral (Node.loDsk node') <= Node.tDsk node &&
854
  Node.availDisk node' >= 0 &&
855
  Node.availDisk node' <= Node.fDsk node' &&
856
  fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
857
  Node.mDsk node' == mx'
858
    where _types = (node::Node.Node, mx::SmallRatio)
859
          node' = Node.setMdsk node mx'
860
          SmallRatio mx' = mx
861

    
862
-- Check tag maps
863
prop_Node_tagMaps_idempotent tags =
864
  Node.delTags (Node.addTags m tags) tags ==? m
865
    where m = Data.Map.empty
866

    
867
prop_Node_tagMaps_reject tags =
868
  not (null tags) ==>
869
  all (\t -> Node.rejectAddTags m [t]) tags
870
    where m = Node.addTags Data.Map.empty tags
871

    
872
prop_Node_showField node =
873
  forAll (elements Node.defaultFields) $ \ field ->
874
  fst (Node.showHeader field) /= Types.unknownField &&
875
  Node.showField node field /= Types.unknownField
876

    
877
prop_Node_computeGroups nodes =
878
  let ng = Node.computeGroups nodes
879
      onlyuuid = map fst ng
880
  in length nodes == sum (map (length . snd) ng) &&
881
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
882
     length (nub onlyuuid) == length onlyuuid &&
883
     (null nodes || not (null ng))
884

    
885
testSuite "Node"
886
            [ 'prop_Node_setAlias
887
            , 'prop_Node_setOffline
888
            , 'prop_Node_setMcpu
889
            , 'prop_Node_setXmem
890
            , 'prop_Node_addPriFM
891
            , 'prop_Node_addPriFD
892
            , 'prop_Node_addPriFC
893
            , 'prop_Node_addSec
894
            , 'prop_Node_addPriOffline
895
            , 'prop_Node_addSecOffline
896
            , 'prop_Node_rMem
897
            , 'prop_Node_setMdsk
898
            , 'prop_Node_tagMaps_idempotent
899
            , 'prop_Node_tagMaps_reject
900
            , 'prop_Node_showField
901
            , 'prop_Node_computeGroups
902
            ]
903

    
904
-- ** Cluster tests
905

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

    
919
-- | Check that cluster stats are sane.
920
prop_CStats_sane =
921
  forAll (choose (1, 1024)) $ \count ->
922
  forAll genOnlineNode $ \node ->
923
  let fn = Node.buildPeers node Container.empty
924
      nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
925
      nl = Container.fromList nlst
926
      cstats = Cluster.totalResources nl
927
  in Cluster.csAdsk cstats >= 0 &&
928
     Cluster.csAdsk cstats <= Cluster.csFdsk cstats
929

    
930
-- | Check that one instance is allocated correctly, without
931
-- rebalances needed.
932
prop_ClusterAlloc_sane inst =
933
  forAll (choose (5, 20)) $ \count ->
934
  forAll genOnlineNode $ \node ->
935
  let (nl, il, inst') = makeSmallEmptyCluster node count inst
936
  in case Cluster.genAllocNodes defGroupList nl 2 True >>=
937
     Cluster.tryAlloc nl il inst' of
938
       Types.Bad _ -> False
939
       Types.Ok as ->
940
         case Cluster.asSolution as of
941
           Nothing -> False
942
           Just (xnl, xi, _, cv) ->
943
             let il' = Container.add (Instance.idx xi) xi il
944
                 tbl = Cluster.Table xnl il' cv []
945
             in not (canBalance tbl True True False)
946

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

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

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

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

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

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

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

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

    
1072
-- ** OpCodes tests
1073

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

    
1081
testSuite "OpCodes"
1082
            [ 'prop_OpCodes_serialization ]
1083

    
1084
-- ** Jobs tests
1085

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

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

    
1099
testSuite "Jobs"
1100
            [ 'prop_OpStatus_serialization
1101
            , 'prop_JobStatus_serialization
1102
            ]
1103

    
1104
-- ** Loader tests
1105

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

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

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

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

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

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

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

    
1157
-- ** Types tests
1158

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

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

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

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

    
1192
testSuite "Types"
1193
            [ 'prop_Types_AllocPolicy_serialisation
1194
            , 'prop_Types_DiskTemplate_serialisation
1195
            , 'prop_Types_opToResult
1196
            , 'prop_Types_eitherToResult
1197
            ]