Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / HTools / QC.hs @ 7dd14211

History | View | Annotate | Download (40.2 kB)

1
{-# LANGUAGE TemplateHaskell #-}
2

    
3
{-| Unittests for ganeti-htools.
4

    
5
-}
6

    
7
{-
8

    
9
Copyright (C) 2009, 2010, 2011 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.Loader as Loader
59
import qualified Ganeti.HTools.Luxi
60
import qualified Ganeti.HTools.Node as Node
61
import qualified Ganeti.HTools.Group as Group
62
import qualified Ganeti.HTools.PeerMap as PeerMap
63
import qualified Ganeti.HTools.Rapi
64
import qualified Ganeti.HTools.Simu
65
import qualified Ganeti.HTools.Text as Text
66
import qualified Ganeti.HTools.Types as Types
67
import qualified Ganeti.HTools.Utils as Utils
68
import qualified Ganeti.HTools.Version
69
import qualified Ganeti.Constants as C
70

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

    
76
import Ganeti.HTools.QCHelper (testSuite)
77

    
78
-- * Constants
79

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

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

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

    
92
defGroup :: Group.Group
93
defGroup = flip Group.setIdx 0 $
94
               Group.create "default" Utils.defaultGroupID
95
                    Types.AllocPreferred
96

    
97
defGroupList :: Group.List
98
defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
99

    
100
defGroupAssoc :: Data.Map.Map String Types.Gdx
101
defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
102

    
103
-- * Helper functions
104

    
105
-- | Simple checker for whether OpResult is fail or pass.
106
isFailure :: Types.OpResult a -> Bool
107
isFailure (Types.OpFail _) = True
108
isFailure _ = False
109

    
110
-- | Checks for equality with proper annotation.
111
(==?) :: (Show a, Eq a) => a -> a -> Property
112
(==?) x y = printTestCase
113
            ("Expected equality, but '" ++
114
             show x ++ "' /= '" ++ show y ++ "'") (x == y)
115
infix 3 ==?
116

    
117
-- | Update an instance to be smaller than a node.
118
setInstanceSmallerThanNode node inst =
119
    inst { Instance.mem = Node.availMem node `div` 2
120
         , Instance.dsk = Node.availDisk node `div` 2
121
         , Instance.vcpus = Node.availCpu node `div` 2
122
         }
123

    
124
-- | Create an instance given its spec.
125
createInstance mem dsk vcpus =
126
    Instance.create "inst-unnamed" mem dsk vcpus Types.Running [] True (-1) (-1)
127
                    Types.DTDrbd8
128

    
129
-- | Create a small cluster by repeating a node spec.
130
makeSmallCluster :: Node.Node -> Int -> Node.List
131
makeSmallCluster node count =
132
    let fn = Node.buildPeers node Container.empty
133
        namelst = map (\n -> (Node.name n, n)) (replicate count fn)
134
        (_, nlst) = Loader.assignIndices namelst
135
    in nlst
136

    
137
-- | Checks if a node is "big" enough.
138
isNodeBig :: Node.Node -> Int -> Bool
139
isNodeBig node size = Node.availDisk node > size * Types.unitDsk
140
                      && Node.availMem node > size * Types.unitMem
141
                      && Node.availCpu node > size * Types.unitCpu
142

    
143
canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
144
canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
145

    
146
-- | Assigns a new fresh instance to a cluster; this is not
147
-- allocation, so no resource checks are done.
148
assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
149
                  Types.Idx -> Types.Idx ->
150
                  (Node.List, Instance.List)
151
assignInstance nl il inst pdx sdx =
152
  let pnode = Container.find pdx nl
153
      snode = Container.find sdx nl
154
      maxiidx = if Container.null il
155
                then 0
156
                else fst (Container.findMax il) + 1
157
      inst' = inst { Instance.idx = maxiidx,
158
                     Instance.pNode = pdx, Instance.sNode = sdx }
159
      pnode' = Node.setPri pnode inst'
160
      snode' = Node.setSec snode inst'
161
      nl' = Container.addTwo pdx pnode' sdx snode' nl
162
      il' = Container.add maxiidx inst' il
163
  in (nl', il')
164

    
165
-- * Arbitrary instances
166

    
167
-- | Defines a DNS name.
168
newtype DNSChar = DNSChar { dnsGetChar::Char }
169

    
170
instance Arbitrary DNSChar where
171
    arbitrary = do
172
      x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
173
      return (DNSChar x)
174

    
175
getName :: Gen String
176
getName = do
177
  n <- choose (1, 64)
178
  dn <- vector n::Gen [DNSChar]
179
  return (map dnsGetChar dn)
180

    
181

    
182
getFQDN :: Gen String
183
getFQDN = do
184
  felem <- getName
185
  ncomps <- choose (1, 4)
186
  frest <- vector ncomps::Gen [[DNSChar]]
187
  let frest' = map (map dnsGetChar) frest
188
  return (felem ++ "." ++ intercalate "." frest')
189

    
190
instance Arbitrary Types.InstanceStatus where
191
    arbitrary = elements [ Types.AdminDown
192
                         , Types.AdminOffline
193
                         , Types.ErrorDown
194
                         , Types.ErrorUp
195
                         , Types.NodeDown
196
                         , Types.NodeOffline
197
                         , Types.Running
198
                         , Types.WrongNode]
199

    
200
-- let's generate a random instance
201
instance Arbitrary Instance.Instance where
202
    arbitrary = do
203
      name <- getFQDN
204
      mem <- choose (0, maxMem)
205
      dsk <- choose (0, maxDsk)
206
      run_st <- arbitrary
207
      pn <- arbitrary
208
      sn <- arbitrary
209
      vcpus <- choose (0, maxCpu)
210
      return $ Instance.create name mem dsk vcpus run_st [] True pn sn
211
                               Types.DTDrbd8
212

    
213
-- | Generas an arbitrary node based on sizing information.
214
genNode :: Maybe Int -- ^ Minimum node size in terms of units
215
        -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
216
                     -- just by the max... constants)
217
        -> Gen Node.Node
218
genNode min_multiplier max_multiplier = do
219
  let (base_mem, base_dsk, base_cpu) =
220
          case min_multiplier of
221
            Just mm -> (mm * Types.unitMem,
222
                        mm * Types.unitDsk,
223
                        mm * Types.unitCpu)
224
            Nothing -> (0, 0, 0)
225
      (top_mem, top_dsk, top_cpu)  =
226
          case max_multiplier of
227
            Just mm -> (mm * Types.unitMem,
228
                        mm * Types.unitDsk,
229
                        mm * Types.unitCpu)
230
            Nothing -> (maxMem, maxDsk, maxCpu)
231
  name  <- getFQDN
232
  mem_t <- choose (base_mem, top_mem)
233
  mem_f <- choose (base_mem, mem_t)
234
  mem_n <- choose (0, mem_t - mem_f)
235
  dsk_t <- choose (base_dsk, top_dsk)
236
  dsk_f <- choose (base_dsk, dsk_t)
237
  cpu_t <- choose (base_cpu, top_cpu)
238
  offl  <- arbitrary
239
  let n = Node.create name (fromIntegral mem_t) mem_n mem_f
240
          (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
241
  return $ Node.buildPeers n Container.empty
242

    
243
-- and a random node
244
instance Arbitrary Node.Node where
245
    arbitrary = genNode Nothing Nothing
246

    
247
-- replace disks
248
instance Arbitrary OpCodes.ReplaceDisksMode where
249
  arbitrary = elements [ OpCodes.ReplaceOnPrimary
250
                       , OpCodes.ReplaceOnSecondary
251
                       , OpCodes.ReplaceNewSecondary
252
                       , OpCodes.ReplaceAuto
253
                       ]
254

    
255
instance Arbitrary OpCodes.OpCode where
256
  arbitrary = do
257
    op_id <- elements [ "OP_TEST_DELAY"
258
                      , "OP_INSTANCE_REPLACE_DISKS"
259
                      , "OP_INSTANCE_FAILOVER"
260
                      , "OP_INSTANCE_MIGRATE"
261
                      ]
262
    (case op_id of
263
        "OP_TEST_DELAY" ->
264
          liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
265
        "OP_INSTANCE_REPLACE_DISKS" ->
266
          liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
267
          arbitrary arbitrary arbitrary
268
        "OP_INSTANCE_FAILOVER" ->
269
          liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
270
                 arbitrary
271
        "OP_INSTANCE_MIGRATE" ->
272
          liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
273
                 arbitrary arbitrary
274
          arbitrary
275
        _ -> fail "Wrong opcode")
276

    
277
instance Arbitrary Jobs.OpStatus where
278
  arbitrary = elements [minBound..maxBound]
279

    
280
instance Arbitrary Jobs.JobStatus where
281
  arbitrary = elements [minBound..maxBound]
282

    
283
newtype SmallRatio = SmallRatio Double deriving Show
284
instance Arbitrary SmallRatio where
285
    arbitrary = do
286
      v <- choose (0, 1)
287
      return $ SmallRatio v
288

    
289
instance Arbitrary Types.AllocPolicy where
290
  arbitrary = elements [minBound..maxBound]
291

    
292
instance Arbitrary Types.DiskTemplate where
293
  arbitrary = elements [minBound..maxBound]
294

    
295
instance Arbitrary Types.FailMode where
296
    arbitrary = elements [minBound..maxBound]
297

    
298
instance Arbitrary a => Arbitrary (Types.OpResult a) where
299
    arbitrary = arbitrary >>= \c ->
300
                case c of
301
                  False -> liftM Types.OpFail arbitrary
302
                  True -> liftM Types.OpGood arbitrary
303

    
304
-- * Actual tests
305

    
306
-- ** Utils tests
307

    
308
-- | If the list is not just an empty element, and if the elements do
309
-- not contain commas, then join+split should be idempotent.
310
prop_Utils_commaJoinSplit =
311
    forAll (arbitrary `suchThat`
312
            (\l -> l /= [""] && all (not . elem ',') l )) $ \lst ->
313
    Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
314

    
315
-- | Split and join should always be idempotent.
316
prop_Utils_commaSplitJoin s =
317
    Utils.commaJoin (Utils.sepSplit ',' s) ==? s
318

    
319
-- | fromObjWithDefault, we test using the Maybe monad and an integer
320
-- value.
321
prop_Utils_fromObjWithDefault def_value random_key =
322
    -- a missing key will be returned with the default
323
    Utils.fromObjWithDefault [] random_key def_value == Just def_value &&
324
    -- a found key will be returned as is, not with default
325
    Utils.fromObjWithDefault [(random_key, J.showJSON def_value)]
326
         random_key (def_value+1) == Just def_value
327
        where _types = def_value :: Integer
328

    
329
-- | Test that functional if' behaves like the syntactic sugar if.
330
prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
331
prop_Utils_if'if cnd a b =
332
    Utils.if' cnd a b ==? if cnd then a else b
333

    
334
-- | Test basic select functionality
335
prop_Utils_select :: Int      -- ^ Default result
336
                  -> [Int]    -- ^ List of False values
337
                  -> [Int]    -- ^ List of True values
338
                  -> Gen Prop -- ^ Test result
339
prop_Utils_select def lst1 lst2 =
340
  Utils.select def cndlist ==? expectedresult
341
  where expectedresult = Utils.if' (null lst2) def (head lst2)
342
        flist = map (\e -> (False, e)) lst1
343
        tlist = map (\e -> (True, e)) lst2
344
        cndlist = flist ++ tlist
345

    
346
-- | Test basic select functionality with undefined default
347
prop_Utils_select_undefd :: [Int]            -- ^ List of False values
348
                         -> NonEmptyList Int -- ^ List of True values
349
                         -> Gen Prop         -- ^ Test result
350
prop_Utils_select_undefd lst1 (NonEmpty lst2) =
351
  Utils.select undefined cndlist ==? head lst2
352
  where flist = map (\e -> (False, e)) lst1
353
        tlist = map (\e -> (True, e)) lst2
354
        cndlist = flist ++ tlist
355

    
356
-- | Test basic select functionality with undefined list values
357
prop_Utils_select_undefv :: [Int]            -- ^ List of False values
358
                         -> NonEmptyList Int -- ^ List of True values
359
                         -> Gen Prop         -- ^ Test result
360
prop_Utils_select_undefv lst1 (NonEmpty lst2) =
361
  Utils.select undefined cndlist ==? head lst2
362
  where flist = map (\e -> (False, e)) lst1
363
        tlist = map (\e -> (True, e)) lst2
364
        cndlist = flist ++ tlist ++ [undefined]
365

    
366
prop_Utils_parseUnit (NonNegative n) =
367
    Utils.parseUnit (show n) == Types.Ok n &&
368
    Utils.parseUnit (show n ++ "m") == Types.Ok n &&
369
    (case Utils.parseUnit (show n ++ "M") of
370
      Types.Ok m -> if n > 0
371
                    then m < n  -- for positive values, X MB is less than X MiB
372
                    else m == 0 -- but for 0, 0 MB == 0 MiB
373
      Types.Bad _ -> False) &&
374
    Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
375
    Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
376
    Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
377
    where _types = n::Int
378

    
379
-- | Test list for the Utils module.
380
testSuite "Utils"
381
              [ 'prop_Utils_commaJoinSplit
382
              , 'prop_Utils_commaSplitJoin
383
              , 'prop_Utils_fromObjWithDefault
384
              , 'prop_Utils_if'if
385
              , 'prop_Utils_select
386
              , 'prop_Utils_select_undefd
387
              , 'prop_Utils_select_undefv
388
              , 'prop_Utils_parseUnit
389
              ]
390

    
391
-- ** PeerMap tests
392

    
393
-- | Make sure add is idempotent.
394
prop_PeerMap_addIdempotent pmap key em =
395
    fn puniq ==? fn (fn puniq)
396
    where _types = (pmap::PeerMap.PeerMap,
397
                    key::PeerMap.Key, em::PeerMap.Elem)
398
          fn = PeerMap.add key em
399
          puniq = PeerMap.accumArray const pmap
400

    
401
-- | Make sure remove is idempotent.
402
prop_PeerMap_removeIdempotent pmap key =
403
    fn puniq ==? fn (fn puniq)
404
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
405
          fn = PeerMap.remove key
406
          puniq = PeerMap.accumArray const pmap
407

    
408
-- | Make sure a missing item returns 0.
409
prop_PeerMap_findMissing pmap key =
410
    PeerMap.find key (PeerMap.remove key puniq) ==? 0
411
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
412
          puniq = PeerMap.accumArray const pmap
413

    
414
-- | Make sure an added item is found.
415
prop_PeerMap_addFind pmap key em =
416
    PeerMap.find key (PeerMap.add key em puniq) ==? em
417
    where _types = (pmap::PeerMap.PeerMap,
418
                    key::PeerMap.Key, em::PeerMap.Elem)
419
          puniq = PeerMap.accumArray const pmap
420

    
421
-- | Manual check that maxElem returns the maximum indeed, or 0 for null.
422
prop_PeerMap_maxElem pmap =
423
    PeerMap.maxElem puniq ==? if null puniq then 0
424
                              else (maximum . snd . unzip) puniq
425
    where _types = pmap::PeerMap.PeerMap
426
          puniq = PeerMap.accumArray const pmap
427

    
428
-- | List of tests for the PeerMap module.
429
testSuite "PeerMap"
430
              [ 'prop_PeerMap_addIdempotent
431
              , 'prop_PeerMap_removeIdempotent
432
              , 'prop_PeerMap_maxElem
433
              , 'prop_PeerMap_addFind
434
              , 'prop_PeerMap_findMissing
435
              ]
436

    
437
-- ** Container tests
438

    
439
prop_Container_addTwo cdata i1 i2 =
440
    fn i1 i2 cont == fn i2 i1 cont &&
441
       fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
442
    where _types = (cdata::[Int],
443
                    i1::Int, i2::Int)
444
          cont = foldl (\c x -> Container.add x x c) Container.empty cdata
445
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
446

    
447
prop_Container_nameOf node =
448
  let nl = makeSmallCluster node 1
449
      fnode = head (Container.elems nl)
450
  in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
451

    
452
-- | We test that in a cluster, given a random node, we can find it by
453
-- its name and alias, as long as all names and aliases are unique,
454
-- and that we fail to find a non-existing name.
455
prop_Container_findByName node othername =
456
  forAll (choose (1, 20)) $ \ cnt ->
457
  forAll (choose (0, cnt - 1)) $ \ fidx ->
458
  forAll (vector cnt) $ \ names ->
459
  (length . nub) (map fst names ++ map snd names) ==
460
  length names * 2 &&
461
  not (othername `elem` (map fst names ++ map snd names)) ==>
462
  let nl = makeSmallCluster node cnt
463
      nodes = Container.elems nl
464
      nodes' = map (\((name, alias), nn) -> (Node.idx nn,
465
                                             nn { Node.name = name,
466
                                                  Node.alias = alias }))
467
               $ zip names nodes
468
      nl' = Container.fromList nodes'
469
      target = snd (nodes' !! fidx)
470
  in Container.findByName nl' (Node.name target) == Just target &&
471
     Container.findByName nl' (Node.alias target) == Just target &&
472
     Container.findByName nl' othername == Nothing
473

    
474
testSuite "Container"
475
              [ 'prop_Container_addTwo
476
              , 'prop_Container_nameOf
477
              , 'prop_Container_findByName
478
              ]
479

    
480
-- ** Instance tests
481

    
482
-- Simple instance tests, we only have setter/getters
483

    
484
prop_Instance_creat inst =
485
    Instance.name inst ==? Instance.alias inst
486

    
487
prop_Instance_setIdx inst idx =
488
    Instance.idx (Instance.setIdx inst idx) ==? idx
489
    where _types = (inst::Instance.Instance, idx::Types.Idx)
490

    
491
prop_Instance_setName inst name =
492
    Instance.name newinst == name &&
493
    Instance.alias newinst == name
494
    where _types = (inst::Instance.Instance, name::String)
495
          newinst = Instance.setName inst name
496

    
497
prop_Instance_setAlias inst name =
498
    Instance.name newinst == Instance.name inst &&
499
    Instance.alias newinst == name
500
    where _types = (inst::Instance.Instance, name::String)
501
          newinst = Instance.setAlias inst name
502

    
503
prop_Instance_setPri inst pdx =
504
    Instance.pNode (Instance.setPri inst pdx) ==? pdx
505
    where _types = (inst::Instance.Instance, pdx::Types.Ndx)
506

    
507
prop_Instance_setSec inst sdx =
508
    Instance.sNode (Instance.setSec inst sdx) ==? sdx
509
    where _types = (inst::Instance.Instance, sdx::Types.Ndx)
510

    
511
prop_Instance_setBoth inst pdx sdx =
512
    Instance.pNode si == pdx && Instance.sNode si == sdx
513
    where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
514
          si = Instance.setBoth inst pdx sdx
515

    
516
prop_Instance_runStatus_True =
517
    forAll (arbitrary `suchThat`
518
            ((`elem` Instance.runningStates) . Instance.runSt))
519
    Instance.running
520

    
521
prop_Instance_runStatus_False inst =
522
    let run_st = Instance.running inst
523
        run_tx = Instance.runSt inst
524
    in
525
      run_tx `notElem` Instance.runningStates ==> not run_st
526

    
527
prop_Instance_shrinkMG inst =
528
    Instance.mem inst >= 2 * Types.unitMem ==>
529
        case Instance.shrinkByType inst Types.FailMem of
530
          Types.Ok inst' ->
531
              Instance.mem inst' == Instance.mem inst - Types.unitMem
532
          _ -> False
533

    
534
prop_Instance_shrinkMF inst =
535
    forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
536
    let inst' = inst { Instance.mem = mem}
537
    in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
538

    
539
prop_Instance_shrinkCG inst =
540
    Instance.vcpus inst >= 2 * Types.unitCpu ==>
541
        case Instance.shrinkByType inst Types.FailCPU of
542
          Types.Ok inst' ->
543
              Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
544
          _ -> False
545

    
546
prop_Instance_shrinkCF inst =
547
    forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
548
    let inst' = inst { Instance.vcpus = vcpus }
549
    in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
550

    
551
prop_Instance_shrinkDG inst =
552
    Instance.dsk inst >= 2 * Types.unitDsk ==>
553
        case Instance.shrinkByType inst Types.FailDisk of
554
          Types.Ok inst' ->
555
              Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
556
          _ -> False
557

    
558
prop_Instance_shrinkDF inst =
559
    forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
560
    let inst' = inst { Instance.dsk = dsk }
561
    in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
562

    
563
prop_Instance_setMovable inst m =
564
    Instance.movable inst' ==? m
565
    where inst' = Instance.setMovable inst m
566

    
567
testSuite "Instance"
568
              [ 'prop_Instance_creat
569
              , 'prop_Instance_setIdx
570
              , 'prop_Instance_setName
571
              , 'prop_Instance_setAlias
572
              , 'prop_Instance_setPri
573
              , 'prop_Instance_setSec
574
              , 'prop_Instance_setBoth
575
              , 'prop_Instance_runStatus_True
576
              , 'prop_Instance_runStatus_False
577
              , 'prop_Instance_shrinkMG
578
              , 'prop_Instance_shrinkMF
579
              , 'prop_Instance_shrinkCG
580
              , 'prop_Instance_shrinkCF
581
              , 'prop_Instance_shrinkDG
582
              , 'prop_Instance_shrinkDF
583
              , 'prop_Instance_setMovable
584
              ]
585

    
586
-- ** Text backend tests
587

    
588
-- Instance text loader tests
589

    
590
prop_Text_Load_Instance name mem dsk vcpus status
591
                        (NonEmpty pnode) snode
592
                        (NonNegative pdx) (NonNegative sdx) autobal dt =
593
    pnode /= snode && pdx /= sdx ==>
594
    let vcpus_s = show vcpus
595
        dsk_s = show dsk
596
        mem_s = show mem
597
        status_s = Types.instanceStatusToRaw status
598
        ndx = if null snode
599
              then [(pnode, pdx)]
600
              else [(pnode, pdx), (snode, sdx)]
601
        nl = Data.Map.fromList ndx
602
        tags = ""
603
        sbal = if autobal then "Y" else "N"
604
        sdt = Types.diskTemplateToRaw dt
605
        inst = Text.loadInst nl
606
               [name, mem_s, dsk_s, vcpus_s, status_s,
607
                sbal, pnode, snode, sdt, tags]
608
        fail1 = Text.loadInst nl
609
               [name, mem_s, dsk_s, vcpus_s, status_s,
610
                sbal, pnode, pnode, tags]
611
        _types = ( name::String, mem::Int, dsk::Int
612
                 , vcpus::Int, status::Types.InstanceStatus
613
                 , snode::String
614
                 , autobal::Bool)
615
    in
616
      case inst of
617
        Types.Bad msg -> printTestCase ("Failed to load instance: " ++ msg)
618
                         False
619
        Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
620
                                         \ loading the instance" $
621
            Instance.name i == name &&
622
            Instance.vcpus i == vcpus &&
623
            Instance.mem i == mem &&
624
            Instance.pNode i == pdx &&
625
            Instance.sNode i == (if null snode
626
                                 then Node.noSecondary
627
                                 else sdx) &&
628
            Instance.autoBalance i == autobal &&
629
            Types.isBad fail1
630

    
631
prop_Text_Load_InstanceFail ktn fields =
632
    length fields /= 10 ==>
633
    case Text.loadInst nl fields of
634
      Types.Ok _ -> printTestCase "Managed to load instance from invalid\
635
                                  \ data" False
636
      Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
637
                       "Invalid/incomplete instance data: '" `isPrefixOf` msg
638
    where nl = Data.Map.fromList ktn
639

    
640
prop_Text_Load_Node name tm nm fm td fd tc fo =
641
    let conv v = if v < 0
642
                    then "?"
643
                    else show v
644
        tm_s = conv tm
645
        nm_s = conv nm
646
        fm_s = conv fm
647
        td_s = conv td
648
        fd_s = conv fd
649
        tc_s = conv tc
650
        fo_s = if fo
651
               then "Y"
652
               else "N"
653
        any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
654
        gid = Group.uuid defGroup
655
    in case Text.loadNode defGroupAssoc
656
           [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
657
         Nothing -> False
658
         Just (name', node) ->
659
             if fo || any_broken
660
             then Node.offline node
661
             else Node.name node == name' && name' == name &&
662
                  Node.alias node == name &&
663
                  Node.tMem node == fromIntegral tm &&
664
                  Node.nMem node == nm &&
665
                  Node.fMem node == fm &&
666
                  Node.tDsk node == fromIntegral td &&
667
                  Node.fDsk node == fd &&
668
                  Node.tCpu node == fromIntegral tc
669

    
670
prop_Text_Load_NodeFail fields =
671
    length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
672

    
673
prop_Text_NodeLSIdempotent node =
674
    (Text.loadNode defGroupAssoc.
675
         Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
676
    Just (Node.name n, n)
677
    -- override failN1 to what loadNode returns by default
678
    where n = node { Node.failN1 = True, Node.offline = False }
679

    
680
testSuite "Text"
681
              [ 'prop_Text_Load_Instance
682
              , 'prop_Text_Load_InstanceFail
683
              , 'prop_Text_Load_Node
684
              , 'prop_Text_Load_NodeFail
685
              , 'prop_Text_NodeLSIdempotent
686
              ]
687

    
688
-- ** Node tests
689

    
690
prop_Node_setAlias node name =
691
    Node.name newnode == Node.name node &&
692
    Node.alias newnode == name
693
    where _types = (node::Node.Node, name::String)
694
          newnode = Node.setAlias node name
695

    
696
prop_Node_setOffline node status =
697
    Node.offline newnode ==? status
698
    where newnode = Node.setOffline node status
699

    
700
prop_Node_setXmem node xm =
701
    Node.xMem newnode ==? xm
702
    where newnode = Node.setXmem node xm
703

    
704
prop_Node_setMcpu node mc =
705
    Node.mCpu newnode ==? mc
706
    where newnode = Node.setMcpu node mc
707

    
708
-- | Check that an instance add with too high memory or disk will be
709
-- rejected.
710
prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node &&
711
                               not (Node.failN1 node)
712
                               ==>
713
                               case Node.addPri node inst'' of
714
                                 Types.OpFail Types.FailMem -> True
715
                                 _ -> False
716
    where _types = (node::Node.Node, inst::Instance.Instance)
717
          inst' = setInstanceSmallerThanNode node inst
718
          inst'' = inst' { Instance.mem = Instance.mem inst }
719

    
720
prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
721
                               not (Node.failN1 node)
722
                               ==>
723
                               case Node.addPri node inst'' of
724
                                 Types.OpFail Types.FailDisk -> True
725
                                 _ -> False
726
    where _types = (node::Node.Node, inst::Instance.Instance)
727
          inst' = setInstanceSmallerThanNode node inst
728
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
729

    
730
prop_Node_addPriFC node inst (Positive extra) =
731
    not (Node.failN1 node) ==>
732
        case Node.addPri node inst'' of
733
          Types.OpFail Types.FailCPU -> True
734
          _ -> False
735
    where _types = (node::Node.Node, inst::Instance.Instance)
736
          inst' = setInstanceSmallerThanNode node inst
737
          inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
738

    
739
-- | Check that an instance add with too high memory or disk will be
740
-- rejected.
741
prop_Node_addSec node inst pdx =
742
    (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
743
     Instance.dsk inst >= Node.fDsk node) &&
744
    not (Node.failN1 node)
745
    ==> isFailure (Node.addSec node inst pdx)
746
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
747

    
748
-- | Checks for memory reservation changes.
749
prop_Node_rMem inst =
750
    forAll (arbitrary `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
751
    -- ab = auto_balance, nb = non-auto_balance
752
    -- we use -1 as the primary node of the instance
753
    let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
754
        inst_ab = setInstanceSmallerThanNode node inst'
755
        inst_nb = inst_ab { Instance.autoBalance = False }
756
        -- now we have the two instances, identical except the
757
        -- autoBalance attribute
758
        orig_rmem = Node.rMem node
759
        inst_idx = Instance.idx inst_ab
760
        node_add_ab = Node.addSec node inst_ab (-1)
761
        node_add_nb = Node.addSec node inst_nb (-1)
762
        node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
763
        node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
764
    in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
765
         (Types.OpGood a_ab, Types.OpGood a_nb,
766
          Types.OpGood d_ab, Types.OpGood d_nb) ->
767
             printTestCase "Consistency checks failed" $
768
             Node.rMem a_ab >  orig_rmem &&
769
             Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
770
             Node.rMem a_nb == orig_rmem &&
771
             Node.rMem d_ab == orig_rmem &&
772
             Node.rMem d_nb == orig_rmem &&
773
             -- this is not related to rMem, but as good a place to
774
             -- test as any
775
             inst_idx `elem` Node.sList a_ab &&
776
             not (inst_idx `elem` Node.sList d_ab)
777
         x -> printTestCase ("Failed to add/remove instances: " ++ show x)
778
              False
779

    
780
-- | Check mdsk setting.
781
prop_Node_setMdsk node mx =
782
    Node.loDsk node' >= 0 &&
783
    fromIntegral (Node.loDsk node') <= Node.tDsk node &&
784
    Node.availDisk node' >= 0 &&
785
    Node.availDisk node' <= Node.fDsk node' &&
786
    fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
787
    Node.mDsk node' == mx'
788
    where _types = (node::Node.Node, mx::SmallRatio)
789
          node' = Node.setMdsk node mx'
790
          SmallRatio mx' = mx
791

    
792
-- Check tag maps
793
prop_Node_tagMaps_idempotent tags =
794
    Node.delTags (Node.addTags m tags) tags ==? m
795
    where m = Data.Map.empty
796

    
797
prop_Node_tagMaps_reject tags =
798
    not (null tags) ==>
799
    all (\t -> Node.rejectAddTags m [t]) tags
800
    where m = Node.addTags Data.Map.empty tags
801

    
802
prop_Node_showField node =
803
  forAll (elements Node.defaultFields) $ \ field ->
804
  fst (Node.showHeader field) /= Types.unknownField &&
805
  Node.showField node field /= Types.unknownField
806

    
807
prop_Node_computeGroups nodes =
808
  let ng = Node.computeGroups nodes
809
      onlyuuid = map fst ng
810
  in length nodes == sum (map (length . snd) ng) &&
811
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
812
     length (nub onlyuuid) == length onlyuuid &&
813
     (null nodes || not (null ng))
814

    
815
testSuite "Node"
816
              [ 'prop_Node_setAlias
817
              , 'prop_Node_setOffline
818
              , 'prop_Node_setMcpu
819
              , 'prop_Node_setXmem
820
              , 'prop_Node_addPriFM
821
              , 'prop_Node_addPriFD
822
              , 'prop_Node_addPriFC
823
              , 'prop_Node_addSec
824
              , 'prop_Node_rMem
825
              , 'prop_Node_setMdsk
826
              , 'prop_Node_tagMaps_idempotent
827
              , 'prop_Node_tagMaps_reject
828
              , 'prop_Node_showField
829
              , 'prop_Node_computeGroups
830
              ]
831

    
832
-- ** Cluster tests
833

    
834
-- | Check that the cluster score is close to zero for a homogeneous
835
-- cluster.
836
prop_Score_Zero node =
837
    forAll (choose (1, 1024)) $ \count ->
838
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
839
     (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
840
    let fn = Node.buildPeers node Container.empty
841
        nlst = replicate count fn
842
        score = Cluster.compCVNodes nlst
843
    -- we can't say == 0 here as the floating point errors accumulate;
844
    -- this should be much lower than the default score in CLI.hs
845
    in score <= 1e-12
846

    
847
-- | Check that cluster stats are sane.
848
prop_CStats_sane node =
849
    forAll (choose (1, 1024)) $ \count ->
850
    (not (Node.offline node) && not (Node.failN1 node) &&
851
     (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
852
    let fn = Node.buildPeers node Container.empty
853
        nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
854
        nl = Container.fromList nlst
855
        cstats = Cluster.totalResources nl
856
    in Cluster.csAdsk cstats >= 0 &&
857
       Cluster.csAdsk cstats <= Cluster.csFdsk cstats
858

    
859
-- | Check that one instance is allocated correctly, without
860
-- rebalances needed.
861
prop_ClusterAlloc_sane node inst =
862
    forAll (choose (5, 20)) $ \count ->
863
    not (Node.offline node)
864
            && not (Node.failN1 node)
865
            && Node.availDisk node > 0
866
            && Node.availMem node > 0
867
            ==>
868
    let nl = makeSmallCluster node count
869
        il = Container.empty
870
        inst' = setInstanceSmallerThanNode node inst
871
    in case Cluster.genAllocNodes defGroupList nl 2 True >>=
872
       Cluster.tryAlloc nl il inst' of
873
         Types.Bad _ -> False
874
         Types.Ok as ->
875
             case Cluster.asSolution as of
876
               Nothing -> False
877
               Just (xnl, xi, _, cv) ->
878
                   let il' = Container.add (Instance.idx xi) xi il
879
                       tbl = Cluster.Table xnl il' cv []
880
                   in not (canBalance tbl True True False)
881

    
882
-- | Checks that on a 2-5 node cluster, we can allocate a random
883
-- instance spec via tiered allocation (whatever the original instance
884
-- spec), on either one or two nodes.
885
prop_ClusterCanTieredAlloc node inst =
886
    forAll (choose (2, 5)) $ \count ->
887
    forAll (choose (1, 2)) $ \rqnodes ->
888
    not (Node.offline node)
889
            && not (Node.failN1 node)
890
            && isNodeBig node 4
891
            ==>
892
    let nl = makeSmallCluster node count
893
        il = Container.empty
894
        allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
895
    in case allocnodes >>= \allocnodes' ->
896
        Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
897
         Types.Bad _ -> False
898
         Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
899
                                      IntMap.size il' == length ixes &&
900
                                      length ixes == length cstats
901

    
902
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
903
-- we can also evacuate it.
904
prop_ClusterAllocEvac node inst =
905
    forAll (choose (4, 8)) $ \count ->
906
    not (Node.offline node)
907
            && not (Node.failN1 node)
908
            && isNodeBig node 4
909
            ==>
910
    let nl = makeSmallCluster node count
911
        il = Container.empty
912
        inst' = setInstanceSmallerThanNode node inst
913
    in case Cluster.genAllocNodes defGroupList nl 2 True >>=
914
       Cluster.tryAlloc nl il inst' of
915
         Types.Bad _ -> False
916
         Types.Ok as ->
917
             case Cluster.asSolution as of
918
               Nothing -> False
919
               Just (xnl, xi, _, _) ->
920
                   let sdx = Instance.sNode xi
921
                       il' = Container.add (Instance.idx xi) xi il
922
                   in case IAlloc.processRelocate defGroupList xnl il'
923
                          (Instance.idx xi) 1 [sdx] of
924
                        Types.Ok _ -> True
925
                        _ -> False
926

    
927
-- | Check that allocating multiple instances on a cluster, then
928
-- adding an empty node, results in a valid rebalance.
929
prop_ClusterAllocBalance =
930
    forAll (genNode (Just 5) (Just 128)) $ \node ->
931
    forAll (choose (3, 5)) $ \count ->
932
    not (Node.offline node) && not (Node.failN1 node) ==>
933
    let nl = makeSmallCluster node count
934
        (hnode, nl') = IntMap.deleteFindMax nl
935
        il = Container.empty
936
        allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
937
        i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
938
    in case allocnodes >>= \allocnodes' ->
939
        Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
940
         Types.Bad _ -> False
941
         Types.Ok (_, xnl, il', _, _) ->
942
                   let ynl = Container.add (Node.idx hnode) hnode xnl
943
                       cv = Cluster.compCV ynl
944
                       tbl = Cluster.Table ynl il' cv []
945
                   in canBalance tbl True True False
946

    
947
-- | Checks consistency.
948
prop_ClusterCheckConsistency node inst =
949
  let nl = makeSmallCluster node 3
950
      [node1, node2, node3] = Container.elems nl
951
      node3' = node3 { Node.group = 1 }
952
      nl' = Container.add (Node.idx node3') node3' nl
953
      inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
954
      inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
955
      inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
956
      ccheck = Cluster.findSplitInstances nl' . Container.fromList
957
  in null (ccheck [(0, inst1)]) &&
958
     null (ccheck [(0, inst2)]) &&
959
     (not . null $ ccheck [(0, inst3)])
960

    
961
-- | For now, we only test that we don't lose instances during the split.
962
prop_ClusterSplitCluster node inst =
963
  forAll (choose (0, 100)) $ \icnt ->
964
  let nl = makeSmallCluster node 2
965
      (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
966
                   (nl, Container.empty) [1..icnt]
967
      gni = Cluster.splitCluster nl' il'
968
  in sum (map (Container.size . snd . snd) gni) == icnt &&
969
     all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
970
                                 (Container.elems nl'')) gni
971

    
972
testSuite "Cluster"
973
              [ 'prop_Score_Zero
974
              , 'prop_CStats_sane
975
              , 'prop_ClusterAlloc_sane
976
              , 'prop_ClusterCanTieredAlloc
977
              , 'prop_ClusterAllocEvac
978
              , 'prop_ClusterAllocBalance
979
              , 'prop_ClusterCheckConsistency
980
              , 'prop_ClusterSplitCluster
981
              ]
982

    
983
-- ** OpCodes tests
984

    
985
-- | Check that opcode serialization is idempotent.
986
prop_OpCodes_serialization op =
987
  case J.readJSON (J.showJSON op) of
988
    J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
989
    J.Ok op' -> op ==? op'
990
  where _types = op::OpCodes.OpCode
991

    
992
testSuite "OpCodes"
993
              [ 'prop_OpCodes_serialization ]
994

    
995
-- ** Jobs tests
996

    
997
-- | Check that (queued) job\/opcode status serialization is idempotent.
998
prop_OpStatus_serialization os =
999
  case J.readJSON (J.showJSON os) of
1000
    J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
1001
    J.Ok os' -> os ==? os'
1002
  where _types = os::Jobs.OpStatus
1003

    
1004
prop_JobStatus_serialization js =
1005
  case J.readJSON (J.showJSON js) of
1006
    J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
1007
    J.Ok js' -> js ==? js'
1008
  where _types = js::Jobs.JobStatus
1009

    
1010
testSuite "Jobs"
1011
              [ 'prop_OpStatus_serialization
1012
              , 'prop_JobStatus_serialization
1013
              ]
1014

    
1015
-- ** Loader tests
1016

    
1017
prop_Loader_lookupNode ktn inst node =
1018
  Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
1019
  where nl = Data.Map.fromList ktn
1020

    
1021
prop_Loader_lookupInstance kti inst =
1022
  Loader.lookupInstance il inst ==? Data.Map.lookup inst il
1023
  where il = Data.Map.fromList kti
1024

    
1025
prop_Loader_assignIndices nodes =
1026
  Data.Map.size nassoc == length nodes &&
1027
  Container.size kt == length nodes &&
1028
  (if not (null nodes)
1029
   then maximum (IntMap.keys kt) == length nodes - 1
1030
   else True)
1031
  where (nassoc, kt) = Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1032

    
1033
-- | Checks that the number of primary instances recorded on the nodes
1034
-- is zero.
1035
prop_Loader_mergeData ns =
1036
  let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1037
  in case Loader.mergeData [] [] [] []
1038
         (Loader.emptyCluster {Loader.cdNodes = na}) of
1039
    Types.Bad _ -> False
1040
    Types.Ok (Loader.ClusterData _ nl il _) ->
1041
      let nodes = Container.elems nl
1042
          instances = Container.elems il
1043
      in (sum . map (length . Node.pList)) nodes == 0 &&
1044
         null instances
1045

    
1046
-- | Check that compareNameComponent on equal strings works.
1047
prop_Loader_compareNameComponent_equal :: String -> Bool
1048
prop_Loader_compareNameComponent_equal s =
1049
  Loader.compareNameComponent s s ==
1050
    Loader.LookupResult Loader.ExactMatch s
1051

    
1052
-- | Check that compareNameComponent on prefix strings works.
1053
prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1054
prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1055
  Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1056
    Loader.LookupResult Loader.PartialMatch s1
1057

    
1058
testSuite "Loader"
1059
              [ 'prop_Loader_lookupNode
1060
              , 'prop_Loader_lookupInstance
1061
              , 'prop_Loader_assignIndices
1062
              , 'prop_Loader_mergeData
1063
              , 'prop_Loader_compareNameComponent_equal
1064
              , 'prop_Loader_compareNameComponent_prefix
1065
              ]
1066

    
1067
-- ** Types tests
1068

    
1069
prop_Types_AllocPolicy_serialisation apol =
1070
    case J.readJSON (J.showJSON apol) of
1071
      J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1072
                p == apol
1073
      J.Error s -> printTestCase ("failed to deserialise: " ++ s) False
1074
    where _types = apol::Types.AllocPolicy
1075

    
1076
prop_Types_DiskTemplate_serialisation dt =
1077
    case J.readJSON (J.showJSON dt) of
1078
      J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1079
                p == dt
1080
      J.Error s -> printTestCase ("failed to deserialise: " ++ s)
1081
                   False
1082
    where _types = dt::Types.DiskTemplate
1083

    
1084
prop_Types_opToResult op =
1085
    case op of
1086
      Types.OpFail _ -> Types.isBad r
1087
      Types.OpGood v -> case r of
1088
                          Types.Bad _ -> False
1089
                          Types.Ok v' -> v == v'
1090
    where r = Types.opToResult op
1091
          _types = op::Types.OpResult Int
1092

    
1093
prop_Types_eitherToResult ei =
1094
    case ei of
1095
      Left _ -> Types.isBad r
1096
      Right v -> case r of
1097
                   Types.Bad _ -> False
1098
                   Types.Ok v' -> v == v'
1099
    where r = Types.eitherToResult ei
1100
          _types = ei::Either String Int
1101

    
1102
testSuite "Types"
1103
              [ 'prop_Types_AllocPolicy_serialisation
1104
              , 'prop_Types_DiskTemplate_serialisation
1105
              , 'prop_Types_opToResult
1106
              , 'prop_Types_eitherToResult
1107
              ]