Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (39 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 Types.AllocPreferred
95

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

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

    
102
-- * Helper functions
103

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

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

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

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

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

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

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

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

    
164
-- * Arbitrary instances
165

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

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

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

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

    
188
instance Arbitrary Types.InstanceStatus where
189
    arbitrary = elements [minBound..maxBound]
190

    
191
-- let's generate a random instance
192
instance Arbitrary Instance.Instance where
193
  arbitrary = do
194
    name <- getFQDN
195
    mem <- choose (0, maxMem)
196
    dsk <- choose (0, maxDsk)
197
    run_st <- arbitrary
198
    pn <- arbitrary
199
    sn <- arbitrary
200
    vcpus <- choose (0, maxCpu)
201
    return $ Instance.create name mem dsk vcpus run_st [] True pn sn
202
              Types.DTDrbd8
203

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

    
234
-- and a random node
235
instance Arbitrary Node.Node where
236
  arbitrary = genNode Nothing Nothing
237

    
238
-- replace disks
239
instance Arbitrary OpCodes.ReplaceDisksMode where
240
  arbitrary = elements [minBound..maxBound]
241

    
242
instance Arbitrary OpCodes.OpCode where
243
  arbitrary = do
244
    op_id <- elements [ "OP_TEST_DELAY"
245
                      , "OP_INSTANCE_REPLACE_DISKS"
246
                      , "OP_INSTANCE_FAILOVER"
247
                      , "OP_INSTANCE_MIGRATE"
248
                      ]
249
    (case op_id of
250
       "OP_TEST_DELAY" ->
251
         liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
252
       "OP_INSTANCE_REPLACE_DISKS" ->
253
         liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
254
         arbitrary arbitrary arbitrary
255
       "OP_INSTANCE_FAILOVER" ->
256
         liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
257
                arbitrary
258
       "OP_INSTANCE_MIGRATE" ->
259
         liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
260
                arbitrary arbitrary arbitrary
261
       _ -> fail "Wrong opcode")
262

    
263
instance Arbitrary Jobs.OpStatus where
264
  arbitrary = elements [minBound..maxBound]
265

    
266
instance Arbitrary Jobs.JobStatus where
267
  arbitrary = elements [minBound..maxBound]
268

    
269
newtype SmallRatio = SmallRatio Double deriving Show
270
instance Arbitrary SmallRatio where
271
  arbitrary = do
272
    v <- choose (0, 1)
273
    return $ SmallRatio v
274

    
275
instance Arbitrary Types.AllocPolicy where
276
  arbitrary = elements [minBound..maxBound]
277

    
278
instance Arbitrary Types.DiskTemplate where
279
  arbitrary = elements [minBound..maxBound]
280

    
281
instance Arbitrary Types.FailMode where
282
  arbitrary = elements [minBound..maxBound]
283

    
284
instance Arbitrary a => Arbitrary (Types.OpResult a) where
285
  arbitrary = arbitrary >>= \c ->
286
              case c of
287
                False -> liftM Types.OpFail arbitrary
288
                True -> liftM Types.OpGood arbitrary
289

    
290
-- * Actual tests
291

    
292
-- ** Utils tests
293

    
294
-- | If the list is not just an empty element, and if the elements do
295
-- not contain commas, then join+split should be idempotent.
296
prop_Utils_commaJoinSplit =
297
  forAll (arbitrary `suchThat`
298
          (\l -> l /= [""] && all (not . elem ',') l )) $ \lst ->
299
  Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
300

    
301
-- | Split and join should always be idempotent.
302
prop_Utils_commaSplitJoin s =
303
  Utils.commaJoin (Utils.sepSplit ',' s) ==? s
304

    
305
-- | fromObjWithDefault, we test using the Maybe monad and an integer
306
-- value.
307
prop_Utils_fromObjWithDefault def_value random_key =
308
  -- a missing key will be returned with the default
309
  Utils.fromObjWithDefault [] random_key def_value == Just def_value &&
310
  -- a found key will be returned as is, not with default
311
  Utils.fromObjWithDefault [(random_key, J.showJSON def_value)]
312
       random_key (def_value+1) == Just def_value
313
    where _types = def_value :: Integer
314

    
315
-- | Test that functional if' behaves like the syntactic sugar if.
316
prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
317
prop_Utils_if'if cnd a b =
318
  Utils.if' cnd a b ==? if cnd then a else b
319

    
320
-- | Test basic select functionality
321
prop_Utils_select :: Int      -- ^ Default result
322
                  -> [Int]    -- ^ List of False values
323
                  -> [Int]    -- ^ List of True values
324
                  -> Gen Prop -- ^ Test result
325
prop_Utils_select def lst1 lst2 =
326
  Utils.select def cndlist ==? expectedresult
327
  where expectedresult = Utils.if' (null lst2) def (head lst2)
328
        flist = map (\e -> (False, e)) lst1
329
        tlist = map (\e -> (True, e)) lst2
330
        cndlist = flist ++ tlist
331

    
332
-- | Test basic select functionality with undefined default
333
prop_Utils_select_undefd :: [Int]            -- ^ List of False values
334
                         -> NonEmptyList Int -- ^ List of True values
335
                         -> Gen Prop         -- ^ Test result
336
prop_Utils_select_undefd lst1 (NonEmpty lst2) =
337
  Utils.select undefined cndlist ==? head lst2
338
  where flist = map (\e -> (False, e)) lst1
339
        tlist = map (\e -> (True, e)) lst2
340
        cndlist = flist ++ tlist
341

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

    
352
prop_Utils_parseUnit (NonNegative n) =
353
  Utils.parseUnit (show n) == Types.Ok n &&
354
  Utils.parseUnit (show n ++ "m") == Types.Ok n &&
355
  (case Utils.parseUnit (show n ++ "M") of
356
     Types.Ok m -> if n > 0
357
                     then m < n  -- for positive values, X MB is < than X MiB
358
                     else m == 0 -- but for 0, 0 MB == 0 MiB
359
     Types.Bad _ -> False) &&
360
  Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
361
  Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
362
  Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
363
    where _types = n::Int
364

    
365
-- | Test list for the Utils module.
366
testSuite "Utils"
367
            [ 'prop_Utils_commaJoinSplit
368
            , 'prop_Utils_commaSplitJoin
369
            , 'prop_Utils_fromObjWithDefault
370
            , 'prop_Utils_if'if
371
            , 'prop_Utils_select
372
            , 'prop_Utils_select_undefd
373
            , 'prop_Utils_select_undefv
374
            , 'prop_Utils_parseUnit
375
            ]
376

    
377
-- ** PeerMap tests
378

    
379
-- | Make sure add is idempotent.
380
prop_PeerMap_addIdempotent pmap key em =
381
  fn puniq ==? fn (fn puniq)
382
    where _types = (pmap::PeerMap.PeerMap,
383
                    key::PeerMap.Key, em::PeerMap.Elem)
384
          fn = PeerMap.add key em
385
          puniq = PeerMap.accumArray const pmap
386

    
387
-- | Make sure remove is idempotent.
388
prop_PeerMap_removeIdempotent pmap key =
389
  fn puniq ==? fn (fn puniq)
390
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
391
          fn = PeerMap.remove key
392
          puniq = PeerMap.accumArray const pmap
393

    
394
-- | Make sure a missing item returns 0.
395
prop_PeerMap_findMissing pmap key =
396
  PeerMap.find key (PeerMap.remove key puniq) ==? 0
397
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
398
          puniq = PeerMap.accumArray const pmap
399

    
400
-- | Make sure an added item is found.
401
prop_PeerMap_addFind pmap key em =
402
  PeerMap.find key (PeerMap.add key em puniq) ==? em
403
    where _types = (pmap::PeerMap.PeerMap,
404
                    key::PeerMap.Key, em::PeerMap.Elem)
405
          puniq = PeerMap.accumArray const pmap
406

    
407
-- | Manual check that maxElem returns the maximum indeed, or 0 for null.
408
prop_PeerMap_maxElem pmap =
409
  PeerMap.maxElem puniq ==? if null puniq then 0
410
                              else (maximum . snd . unzip) puniq
411
    where _types = pmap::PeerMap.PeerMap
412
          puniq = PeerMap.accumArray const pmap
413

    
414
-- | List of tests for the PeerMap module.
415
testSuite "PeerMap"
416
            [ 'prop_PeerMap_addIdempotent
417
            , 'prop_PeerMap_removeIdempotent
418
            , 'prop_PeerMap_maxElem
419
            , 'prop_PeerMap_addFind
420
            , 'prop_PeerMap_findMissing
421
            ]
422

    
423
-- ** Container tests
424

    
425
prop_Container_addTwo cdata i1 i2 =
426
  fn i1 i2 cont == fn i2 i1 cont &&
427
  fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
428
    where _types = (cdata::[Int],
429
                    i1::Int, i2::Int)
430
          cont = foldl (\c x -> Container.add x x c) Container.empty cdata
431
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
432

    
433
prop_Container_nameOf node =
434
  let nl = makeSmallCluster node 1
435
      fnode = head (Container.elems nl)
436
  in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
437

    
438
-- | We test that in a cluster, given a random node, we can find it by
439
-- its name and alias, as long as all names and aliases are unique,
440
-- and that we fail to find a non-existing name.
441
prop_Container_findByName node othername =
442
  forAll (choose (1, 20)) $ \ cnt ->
443
  forAll (choose (0, cnt - 1)) $ \ fidx ->
444
  forAll (vector cnt) $ \ names ->
445
  (length . nub) (map fst names ++ map snd names) ==
446
  length names * 2 &&
447
  not (othername `elem` (map fst names ++ map snd names)) ==>
448
  let nl = makeSmallCluster node cnt
449
      nodes = Container.elems nl
450
      nodes' = map (\((name, alias), nn) -> (Node.idx nn,
451
                                             nn { Node.name = name,
452
                                                  Node.alias = alias }))
453
               $ zip names nodes
454
      nl' = Container.fromList nodes'
455
      target = snd (nodes' !! fidx)
456
  in Container.findByName nl' (Node.name target) == Just target &&
457
     Container.findByName nl' (Node.alias target) == Just target &&
458
     Container.findByName nl' othername == Nothing
459

    
460
testSuite "Container"
461
            [ 'prop_Container_addTwo
462
            , 'prop_Container_nameOf
463
            , 'prop_Container_findByName
464
            ]
465

    
466
-- ** Instance tests
467

    
468
-- Simple instance tests, we only have setter/getters
469

    
470
prop_Instance_creat inst =
471
  Instance.name inst ==? Instance.alias inst
472

    
473
prop_Instance_setIdx inst idx =
474
  Instance.idx (Instance.setIdx inst idx) ==? idx
475
    where _types = (inst::Instance.Instance, idx::Types.Idx)
476

    
477
prop_Instance_setName inst name =
478
  Instance.name newinst == name &&
479
  Instance.alias newinst == name
480
    where _types = (inst::Instance.Instance, name::String)
481
          newinst = Instance.setName inst name
482

    
483
prop_Instance_setAlias inst name =
484
  Instance.name newinst == Instance.name inst &&
485
  Instance.alias newinst == name
486
    where _types = (inst::Instance.Instance, name::String)
487
          newinst = Instance.setAlias inst name
488

    
489
prop_Instance_setPri inst pdx =
490
  Instance.pNode (Instance.setPri inst pdx) ==? pdx
491
    where _types = (inst::Instance.Instance, pdx::Types.Ndx)
492

    
493
prop_Instance_setSec inst sdx =
494
  Instance.sNode (Instance.setSec inst sdx) ==? sdx
495
    where _types = (inst::Instance.Instance, sdx::Types.Ndx)
496

    
497
prop_Instance_setBoth inst pdx sdx =
498
  Instance.pNode si == pdx && Instance.sNode si == sdx
499
    where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
500
          si = Instance.setBoth inst pdx sdx
501

    
502
prop_Instance_shrinkMG inst =
503
  Instance.mem inst >= 2 * Types.unitMem ==>
504
    case Instance.shrinkByType inst Types.FailMem of
505
      Types.Ok inst' -> Instance.mem inst' == Instance.mem inst - Types.unitMem
506
      _ -> False
507

    
508
prop_Instance_shrinkMF inst =
509
  forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
510
    let inst' = inst { Instance.mem = mem}
511
    in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
512

    
513
prop_Instance_shrinkCG inst =
514
  Instance.vcpus inst >= 2 * Types.unitCpu ==>
515
    case Instance.shrinkByType inst Types.FailCPU of
516
      Types.Ok inst' ->
517
        Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
518
      _ -> False
519

    
520
prop_Instance_shrinkCF inst =
521
  forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
522
    let inst' = inst { Instance.vcpus = vcpus }
523
    in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
524

    
525
prop_Instance_shrinkDG inst =
526
  Instance.dsk inst >= 2 * Types.unitDsk ==>
527
    case Instance.shrinkByType inst Types.FailDisk of
528
      Types.Ok inst' ->
529
        Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
530
      _ -> False
531

    
532
prop_Instance_shrinkDF inst =
533
  forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
534
    let inst' = inst { Instance.dsk = dsk }
535
    in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
536

    
537
prop_Instance_setMovable inst m =
538
  Instance.movable inst' ==? m
539
    where inst' = Instance.setMovable inst m
540

    
541
testSuite "Instance"
542
            [ 'prop_Instance_creat
543
            , 'prop_Instance_setIdx
544
            , 'prop_Instance_setName
545
            , 'prop_Instance_setAlias
546
            , 'prop_Instance_setPri
547
            , 'prop_Instance_setSec
548
            , 'prop_Instance_setBoth
549
            , 'prop_Instance_shrinkMG
550
            , 'prop_Instance_shrinkMF
551
            , 'prop_Instance_shrinkCG
552
            , 'prop_Instance_shrinkCF
553
            , 'prop_Instance_shrinkDG
554
            , 'prop_Instance_shrinkDF
555
            , 'prop_Instance_setMovable
556
            ]
557

    
558
-- ** Text backend tests
559

    
560
-- Instance text loader tests
561

    
562
prop_Text_Load_Instance name mem dsk vcpus status
563
                        (NonEmpty pnode) snode
564
                        (NonNegative pdx) (NonNegative sdx) autobal dt =
565
  pnode /= snode && pdx /= sdx ==>
566
  let vcpus_s = show vcpus
567
      dsk_s = show dsk
568
      mem_s = show mem
569
      status_s = Types.instanceStatusToRaw status
570
      ndx = if null snode
571
              then [(pnode, pdx)]
572
              else [(pnode, pdx), (snode, sdx)]
573
      nl = Data.Map.fromList ndx
574
      tags = ""
575
      sbal = if autobal then "Y" else "N"
576
      sdt = Types.diskTemplateToRaw dt
577
      inst = Text.loadInst nl
578
             [name, mem_s, dsk_s, vcpus_s, status_s,
579
              sbal, pnode, snode, sdt, tags]
580
      fail1 = Text.loadInst nl
581
              [name, mem_s, dsk_s, vcpus_s, status_s,
582
               sbal, pnode, pnode, tags]
583
      _types = ( name::String, mem::Int, dsk::Int
584
               , vcpus::Int, status::Types.InstanceStatus
585
               , snode::String
586
               , autobal::Bool)
587
  in case inst of
588
       Types.Bad msg -> printTestCase ("Failed to load instance: " ++ msg)
589
                        False
590
       Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
591
                                        \ loading the instance" $
592
               Instance.name i == name &&
593
               Instance.vcpus i == vcpus &&
594
               Instance.mem i == mem &&
595
               Instance.pNode i == pdx &&
596
               Instance.sNode i == (if null snode
597
                                      then Node.noSecondary
598
                                      else sdx) &&
599
               Instance.autoBalance i == autobal &&
600
               Types.isBad fail1
601

    
602
prop_Text_Load_InstanceFail ktn fields =
603
  length fields /= 10 ==>
604
    case Text.loadInst nl fields of
605
      Types.Ok _ -> printTestCase "Managed to load instance from invalid\
606
                                  \ data" False
607
      Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
608
                       "Invalid/incomplete instance data: '" `isPrefixOf` msg
609
    where nl = Data.Map.fromList ktn
610

    
611
prop_Text_Load_Node name tm nm fm td fd tc fo =
612
  let conv v = if v < 0
613
                 then "?"
614
                 else show v
615
      tm_s = conv tm
616
      nm_s = conv nm
617
      fm_s = conv fm
618
      td_s = conv td
619
      fd_s = conv fd
620
      tc_s = conv tc
621
      fo_s = if fo
622
               then "Y"
623
               else "N"
624
      any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
625
      gid = Group.uuid defGroup
626
  in case Text.loadNode defGroupAssoc
627
       [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
628
       Nothing -> False
629
       Just (name', node) ->
630
         if fo || any_broken
631
           then Node.offline node
632
           else Node.name node == name' && name' == name &&
633
                Node.alias node == name &&
634
                Node.tMem node == fromIntegral tm &&
635
                Node.nMem node == nm &&
636
                Node.fMem node == fm &&
637
                Node.tDsk node == fromIntegral td &&
638
                Node.fDsk node == fd &&
639
                Node.tCpu node == fromIntegral tc
640

    
641
prop_Text_Load_NodeFail fields =
642
  length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
643

    
644
prop_Text_NodeLSIdempotent node =
645
  (Text.loadNode defGroupAssoc.
646
       Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
647
  Just (Node.name n, n)
648
    -- override failN1 to what loadNode returns by default
649
    where n = node { Node.failN1 = True, Node.offline = False }
650

    
651
testSuite "Text"
652
            [ 'prop_Text_Load_Instance
653
            , 'prop_Text_Load_InstanceFail
654
            , 'prop_Text_Load_Node
655
            , 'prop_Text_Load_NodeFail
656
            , 'prop_Text_NodeLSIdempotent
657
            ]
658

    
659
-- ** Node tests
660

    
661
prop_Node_setAlias node name =
662
  Node.name newnode == Node.name node &&
663
  Node.alias newnode == name
664
    where _types = (node::Node.Node, name::String)
665
          newnode = Node.setAlias node name
666

    
667
prop_Node_setOffline node status =
668
  Node.offline newnode ==? status
669
    where newnode = Node.setOffline node status
670

    
671
prop_Node_setXmem node xm =
672
  Node.xMem newnode ==? xm
673
    where newnode = Node.setXmem node xm
674

    
675
prop_Node_setMcpu node mc =
676
  Node.mCpu newnode ==? mc
677
    where newnode = Node.setMcpu node mc
678

    
679
-- | Check that an instance add with too high memory or disk will be
680
-- rejected.
681
prop_Node_addPriFM node inst =
682
  Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
683
  not (Instance.instanceOffline inst) ==>
684
  case Node.addPri node inst'' of
685
    Types.OpFail Types.FailMem -> True
686
    _ -> False
687
  where _types = (node::Node.Node, inst::Instance.Instance)
688
        inst' = setInstanceSmallerThanNode node inst
689
        inst'' = inst' { Instance.mem = Instance.mem inst }
690

    
691
prop_Node_addPriFD node inst =
692
  Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
693
    case Node.addPri node inst'' of
694
      Types.OpFail Types.FailDisk -> True
695
      _ -> False
696
    where _types = (node::Node.Node, inst::Instance.Instance)
697
          inst' = setInstanceSmallerThanNode node inst
698
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
699

    
700
prop_Node_addPriFC node inst (Positive extra) =
701
  not (Node.failN1 node) && not (Instance.instanceOffline inst) ==>
702
      case Node.addPri node inst'' of
703
        Types.OpFail Types.FailCPU -> True
704
        _ -> False
705
    where _types = (node::Node.Node, inst::Instance.Instance)
706
          inst' = setInstanceSmallerThanNode node inst
707
          inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
708

    
709
-- | Check that an instance add with too high memory or disk will be
710
-- rejected.
711
prop_Node_addSec node inst pdx =
712
  ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
713
    not (Instance.instanceOffline inst)) ||
714
   Instance.dsk inst >= Node.fDsk node) &&
715
  not (Node.failN1 node) ==>
716
      isFailure (Node.addSec node inst pdx)
717
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
718

    
719
-- | Check that an offline instance with reasonable disk size can always
720
-- be added.
721
prop_Node_addPriOffline =
722
  forAll (arbitrary `suchThat` ((> 0) . Node.fMem)) $ \node ->
723
  forAll (arbitrary `suchThat`
724
          (\ x ->  (Instance.dsk x  < Node.fDsk node) &&
725
                   Instance.instanceOffline x)) $ \inst ->
726
  case Node.addPri node inst of
727
    Types.OpGood _ -> True
728
    _ -> False
729

    
730
prop_Node_addSecOffline pdx =
731
  forAll (arbitrary `suchThat` ((> 0) . Node.fMem)) $ \node ->
732
  forAll (arbitrary `suchThat`
733
          (\ x ->  (Instance.dsk x  < Node.fDsk node) &&
734
                   Instance.instanceOffline x)) $ \inst ->
735
  case Node.addSec node inst pdx of
736
    Types.OpGood _ -> True
737
    _ -> False
738

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

    
771
-- | Check mdsk setting.
772
prop_Node_setMdsk node mx =
773
  Node.loDsk node' >= 0 &&
774
  fromIntegral (Node.loDsk node') <= Node.tDsk node &&
775
  Node.availDisk node' >= 0 &&
776
  Node.availDisk node' <= Node.fDsk node' &&
777
  fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
778
  Node.mDsk node' == mx'
779
    where _types = (node::Node.Node, mx::SmallRatio)
780
          node' = Node.setMdsk node mx'
781
          SmallRatio mx' = mx
782

    
783
-- Check tag maps
784
prop_Node_tagMaps_idempotent tags =
785
  Node.delTags (Node.addTags m tags) tags ==? m
786
    where m = Data.Map.empty
787

    
788
prop_Node_tagMaps_reject tags =
789
  not (null tags) ==>
790
  all (\t -> Node.rejectAddTags m [t]) tags
791
    where m = Node.addTags Data.Map.empty tags
792

    
793
prop_Node_showField node =
794
  forAll (elements Node.defaultFields) $ \ field ->
795
  fst (Node.showHeader field) /= Types.unknownField &&
796
  Node.showField node field /= Types.unknownField
797

    
798
prop_Node_computeGroups nodes =
799
  let ng = Node.computeGroups nodes
800
      onlyuuid = map fst ng
801
  in length nodes == sum (map (length . snd) ng) &&
802
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
803
     length (nub onlyuuid) == length onlyuuid &&
804
     (null nodes || not (null ng))
805

    
806
testSuite "Node"
807
            [ 'prop_Node_setAlias
808
            , 'prop_Node_setOffline
809
            , 'prop_Node_setMcpu
810
            , 'prop_Node_setXmem
811
            , 'prop_Node_addPriFM
812
            , 'prop_Node_addPriFD
813
            , 'prop_Node_addPriFC
814
            , 'prop_Node_addSec
815
            , 'prop_Node_addPriOffline
816
            , 'prop_Node_addSecOffline
817
            , 'prop_Node_rMem
818
            , 'prop_Node_setMdsk
819
            , 'prop_Node_tagMaps_idempotent
820
            , 'prop_Node_tagMaps_reject
821
            , 'prop_Node_showField
822
            , 'prop_Node_computeGroups
823
            ]
824

    
825
-- ** Cluster tests
826

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

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

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

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

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

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

    
940
-- | Checks consistency.
941
prop_ClusterCheckConsistency node inst =
942
  let nl = makeSmallCluster node 3
943
      [node1, node2, node3] = Container.elems nl
944
      node3' = node3 { Node.group = 1 }
945
      nl' = Container.add (Node.idx node3') node3' nl
946
      inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
947
      inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
948
      inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
949
      ccheck = Cluster.findSplitInstances nl' . Container.fromList
950
  in null (ccheck [(0, inst1)]) &&
951
     null (ccheck [(0, inst2)]) &&
952
     (not . null $ ccheck [(0, inst3)])
953

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

    
965
testSuite "Cluster"
966
            [ 'prop_Score_Zero
967
            , 'prop_CStats_sane
968
            , 'prop_ClusterAlloc_sane
969
            , 'prop_ClusterCanTieredAlloc
970
            , 'prop_ClusterAllocEvac
971
            , 'prop_ClusterAllocBalance
972
            , 'prop_ClusterCheckConsistency
973
            , 'prop_ClusterSplitCluster
974
            ]
975

    
976
-- ** OpCodes tests
977

    
978
-- | Check that opcode serialization is idempotent.
979
prop_OpCodes_serialization op =
980
  case J.readJSON (J.showJSON op) of
981
    J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
982
    J.Ok op' -> op ==? op'
983
  where _types = op::OpCodes.OpCode
984

    
985
testSuite "OpCodes"
986
            [ 'prop_OpCodes_serialization ]
987

    
988
-- ** Jobs tests
989

    
990
-- | Check that (queued) job\/opcode status serialization is idempotent.
991
prop_OpStatus_serialization os =
992
  case J.readJSON (J.showJSON os) of
993
    J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
994
    J.Ok os' -> os ==? os'
995
  where _types = os::Jobs.OpStatus
996

    
997
prop_JobStatus_serialization js =
998
  case J.readJSON (J.showJSON js) of
999
    J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
1000
    J.Ok js' -> js ==? js'
1001
  where _types = js::Jobs.JobStatus
1002

    
1003
testSuite "Jobs"
1004
            [ 'prop_OpStatus_serialization
1005
            , 'prop_JobStatus_serialization
1006
            ]
1007

    
1008
-- ** Loader tests
1009

    
1010
prop_Loader_lookupNode ktn inst node =
1011
  Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
1012
    where nl = Data.Map.fromList ktn
1013

    
1014
prop_Loader_lookupInstance kti inst =
1015
  Loader.lookupInstance il inst ==? Data.Map.lookup inst il
1016
    where il = Data.Map.fromList kti
1017

    
1018
prop_Loader_assignIndices nodes =
1019
  Data.Map.size nassoc == length nodes &&
1020
  Container.size kt == length nodes &&
1021
  (if not (null nodes)
1022
   then maximum (IntMap.keys kt) == length nodes - 1
1023
   else True)
1024
    where (nassoc, kt) =
1025
            Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1026

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

    
1040
-- | Check that compareNameComponent on equal strings works.
1041
prop_Loader_compareNameComponent_equal :: String -> Bool
1042
prop_Loader_compareNameComponent_equal s =
1043
  Loader.compareNameComponent s s ==
1044
    Loader.LookupResult Loader.ExactMatch s
1045

    
1046
-- | Check that compareNameComponent on prefix strings works.
1047
prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1048
prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1049
  Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1050
    Loader.LookupResult Loader.PartialMatch s1
1051

    
1052
testSuite "Loader"
1053
            [ 'prop_Loader_lookupNode
1054
            , 'prop_Loader_lookupInstance
1055
            , 'prop_Loader_assignIndices
1056
            , 'prop_Loader_mergeData
1057
            , 'prop_Loader_compareNameComponent_equal
1058
            , 'prop_Loader_compareNameComponent_prefix
1059
            ]
1060

    
1061
-- ** Types tests
1062

    
1063
prop_Types_AllocPolicy_serialisation apol =
1064
  case J.readJSON (J.showJSON apol) of
1065
    J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1066
              p == apol
1067
    J.Error s -> printTestCase ("failed to deserialise: " ++ s) False
1068
      where _types = apol::Types.AllocPolicy
1069

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

    
1078
prop_Types_opToResult op =
1079
  case op of
1080
    Types.OpFail _ -> Types.isBad r
1081
    Types.OpGood v -> case r of
1082
                        Types.Bad _ -> False
1083
                        Types.Ok v' -> v == v'
1084
  where r = Types.opToResult op
1085
        _types = op::Types.OpResult Int
1086

    
1087
prop_Types_eitherToResult ei =
1088
  case ei of
1089
    Left _ -> Types.isBad r
1090
    Right v -> case r of
1091
                 Types.Bad _ -> False
1092
                 Types.Ok v' -> v == v'
1093
    where r = Types.eitherToResult ei
1094
          _types = ei::Either String Int
1095

    
1096
testSuite "Types"
1097
            [ 'prop_Types_AllocPolicy_serialisation
1098
            , 'prop_Types_DiskTemplate_serialisation
1099
            , 'prop_Types_opToResult
1100
            , 'prop_Types_eitherToResult
1101
            ]