Revision e1ee7d5a htools/Ganeti/HTools/QC.hs

b/htools/Ganeti/HTools/QC.hs
32 32
-}
33 33

  
34 34
module Ganeti.HTools.QC
35
  ( testUtils
36
  , testPeerMap
37
  , testContainer
38
  , testInstance
39
  , testNode
40
  , testText
41
  , testSimu
42
  , testJobs
43
  , testCluster
44
  , testLoader
45
  , testTypes
46
  , testCLI
35
  ( testJobs
47 36
  , testJSON
48 37
  ) where
49 38

  
......
112 101
import Test.Ganeti.TestHelper (testSuite)
113 102
import Test.Ganeti.TestCommon
114 103

  
115
-- | All disk templates (used later)
116
allDiskTemplates :: [Types.DiskTemplate]
117
allDiskTemplates = [minBound..maxBound]
118

  
119
-- | Null iPolicy, and by null we mean very liberal.
120
nullIPolicy :: Types.IPolicy
121
nullIPolicy = Types.IPolicy
122
  { Types.iPolicyMinSpec = Types.ISpec { Types.iSpecMemorySize = 0
123
                                       , Types.iSpecCpuCount   = 0
124
                                       , Types.iSpecDiskSize   = 0
125
                                       , Types.iSpecDiskCount  = 0
126
                                       , Types.iSpecNicCount   = 0
127
                                       , Types.iSpecSpindleUse = 0
128
                                       }
129
  , Types.iPolicyMaxSpec = Types.ISpec { Types.iSpecMemorySize = maxBound
130
                                       , Types.iSpecCpuCount   = maxBound
131
                                       , Types.iSpecDiskSize   = maxBound
132
                                       , Types.iSpecDiskCount  = C.maxDisks
133
                                       , Types.iSpecNicCount   = C.maxNics
134
                                       , Types.iSpecSpindleUse = maxBound
135
                                       }
136
  , Types.iPolicyStdSpec = Types.ISpec { Types.iSpecMemorySize = Types.unitMem
137
                                       , Types.iSpecCpuCount   = Types.unitCpu
138
                                       , Types.iSpecDiskSize   = Types.unitDsk
139
                                       , Types.iSpecDiskCount  = 1
140
                                       , Types.iSpecNicCount   = 1
141
                                       , Types.iSpecSpindleUse = 1
142
                                       }
143
  , Types.iPolicyDiskTemplates = [minBound..maxBound]
144
  , Types.iPolicyVcpuRatio = maxVcpuRatio -- somewhat random value, high
145
                                          -- enough to not impact us
146
  , Types.iPolicySpindleRatio = maxSpindleRatio
147
  }
148

  
149

  
150
defGroup :: Group.Group
151
defGroup = flip Group.setIdx 0 $
152
             Group.create "default" Types.defaultGroupID Types.AllocPreferred
153
                  nullIPolicy
154

  
155
defGroupList :: Group.List
156
defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
157

  
158
defGroupAssoc :: Map.Map String Types.Gdx
159
defGroupAssoc = Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
160

  
161 104
-- * Helper functions
162 105

  
163
-- | Simple checker for whether OpResult is fail or pass.
164
isFailure :: Types.OpResult a -> Bool
165
isFailure (Types.OpFail _) = True
166
isFailure _ = False
167

  
168
-- | Update an instance to be smaller than a node.
169
setInstanceSmallerThanNode :: Node.Node
170
                           -> Instance.Instance -> Instance.Instance
171
setInstanceSmallerThanNode node inst =
172
  inst { Instance.mem = Node.availMem node `div` 2
173
       , Instance.dsk = Node.availDisk node `div` 2
174
       , Instance.vcpus = Node.availCpu node `div` 2
175
       }
176

  
177
-- | Create an instance given its spec.
178
createInstance :: Int -> Int -> Int -> Instance.Instance
179
createInstance mem dsk vcpus =
180
  Instance.create "inst-unnamed" mem dsk vcpus Types.Running [] True (-1) (-1)
181
    Types.DTDrbd8 1
182

  
183
-- | Create a small cluster by repeating a node spec.
184
makeSmallCluster :: Node.Node -> Int -> Node.List
185
makeSmallCluster node count =
186
  let origname = Node.name node
187
      origalias = Node.alias node
188
      nodes = map (\idx -> node { Node.name = origname ++ "-" ++ show idx
189
                                , Node.alias = origalias ++ "-" ++ show idx })
190
              [1..count]
191
      fn = flip Node.buildPeers Container.empty
192
      namelst = map (\n -> (Node.name n, fn n)) nodes
193
      (_, nlst) = Loader.assignIndices namelst
194
  in nlst
195

  
196
-- | Make a small cluster, both nodes and instances.
197
makeSmallEmptyCluster :: Node.Node -> Int -> Instance.Instance
198
                      -> (Node.List, Instance.List, Instance.Instance)
199
makeSmallEmptyCluster node count inst =
200
  (makeSmallCluster node count, Container.empty,
201
   setInstanceSmallerThanNode node inst)
202

  
203
-- | Checks if a node is "big" enough.
204
isNodeBig :: Int -> Node.Node -> Bool
205
isNodeBig size node = Node.availDisk node > size * Types.unitDsk
206
                      && Node.availMem node > size * Types.unitMem
207
                      && Node.availCpu node > size * Types.unitCpu
208

  
209
canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
210
canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
211

  
212
-- | Assigns a new fresh instance to a cluster; this is not
213
-- allocation, so no resource checks are done.
214
assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
215
                  Types.Idx -> Types.Idx ->
216
                  (Node.List, Instance.List)
217
assignInstance nl il inst pdx sdx =
218
  let pnode = Container.find pdx nl
219
      snode = Container.find sdx nl
220
      maxiidx = if Container.null il
221
                  then 0
222
                  else fst (Container.findMax il) + 1
223
      inst' = inst { Instance.idx = maxiidx,
224
                     Instance.pNode = pdx, Instance.sNode = sdx }
225
      pnode' = Node.setPri pnode inst'
226
      snode' = Node.setSec snode inst'
227
      nl' = Container.addTwo pdx pnode' sdx snode' nl
228
      il' = Container.add maxiidx inst' il
229
  in (nl', il')
230

  
231
-- | Generates a list of a given size with non-duplicate elements.
232
genUniquesList :: (Eq a, Arbitrary a) => Int -> Gen [a]
233
genUniquesList cnt =
234
  foldM (\lst _ -> do
235
           newelem <- arbitrary `suchThat` (`notElem` lst)
236
           return (newelem:lst)) [] [1..cnt]
237

  
238
-- | Checks if an instance is mirrored.
239
isMirrored :: Instance.Instance -> Bool
240
isMirrored = (/= Types.MirrorNone) . Instance.mirrorType
241

  
242
-- | Returns the possible change node types for a disk template.
243
evacModeOptions :: Types.MirrorType -> [Types.EvacMode]
244
evacModeOptions Types.MirrorNone     = []
245
evacModeOptions Types.MirrorInternal = [minBound..maxBound] -- DRBD can do all
246
evacModeOptions Types.MirrorExternal = [Types.ChangePrimary, Types.ChangeAll]
247

  
248
instance Arbitrary Types.InstanceStatus where
249
    arbitrary = elements [minBound..maxBound]
250

  
251
-- | Generates a random instance with maximum disk/mem/cpu values.
252
genInstanceSmallerThan :: Int -> Int -> Int -> Gen Instance.Instance
253
genInstanceSmallerThan lim_mem lim_dsk lim_cpu = do
254
  name <- getFQDN
255
  mem <- choose (0, lim_mem)
256
  dsk <- choose (0, lim_dsk)
257
  run_st <- arbitrary
258
  pn <- arbitrary
259
  sn <- arbitrary
260
  vcpus <- choose (0, lim_cpu)
261
  dt <- arbitrary
262
  return $ Instance.create name mem dsk vcpus run_st [] True pn sn dt 1
263

  
264
-- | Generates an instance smaller than a node.
265
genInstanceSmallerThanNode :: Node.Node -> Gen Instance.Instance
266
genInstanceSmallerThanNode node =
267
  genInstanceSmallerThan (Node.availMem node `div` 2)
268
                         (Node.availDisk node `div` 2)
269
                         (Node.availCpu node `div` 2)
270

  
271
-- let's generate a random instance
272
instance Arbitrary Instance.Instance where
273
  arbitrary = genInstanceSmallerThan maxMem maxDsk maxCpu
274

  
275
-- | Generas an arbitrary node based on sizing information.
276
genNode :: Maybe Int -- ^ Minimum node size in terms of units
277
        -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
278
                     -- just by the max... constants)
279
        -> Gen Node.Node
280
genNode min_multiplier max_multiplier = do
281
  let (base_mem, base_dsk, base_cpu) =
282
        case min_multiplier of
283
          Just mm -> (mm * Types.unitMem,
284
                      mm * Types.unitDsk,
285
                      mm * Types.unitCpu)
286
          Nothing -> (0, 0, 0)
287
      (top_mem, top_dsk, top_cpu)  =
288
        case max_multiplier of
289
          Just mm -> (mm * Types.unitMem,
290
                      mm * Types.unitDsk,
291
                      mm * Types.unitCpu)
292
          Nothing -> (maxMem, maxDsk, maxCpu)
293
  name  <- getFQDN
294
  mem_t <- choose (base_mem, top_mem)
295
  mem_f <- choose (base_mem, mem_t)
296
  mem_n <- choose (0, mem_t - mem_f)
297
  dsk_t <- choose (base_dsk, top_dsk)
298
  dsk_f <- choose (base_dsk, dsk_t)
299
  cpu_t <- choose (base_cpu, top_cpu)
300
  offl  <- arbitrary
301
  let n = Node.create name (fromIntegral mem_t) mem_n mem_f
302
          (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 1 0
303
      n' = Node.setPolicy nullIPolicy n
304
  return $ Node.buildPeers n' Container.empty
305

  
306
-- | Helper function to generate a sane node.
307
genOnlineNode :: Gen Node.Node
308
genOnlineNode = do
309
  arbitrary `suchThat` (\n -> not (Node.offline n) &&
310
                              not (Node.failN1 n) &&
311
                              Node.availDisk n > 0 &&
312
                              Node.availMem n > 0 &&
313
                              Node.availCpu n > 0)
314

  
315
-- and a random node
316
instance Arbitrary Node.Node where
317
  arbitrary = genNode Nothing Nothing
318 106

  
319 107
instance Arbitrary Jobs.OpStatus where
320 108
  arbitrary = elements [minBound..maxBound]
......
322 110
instance Arbitrary Jobs.JobStatus where
323 111
  arbitrary = elements [minBound..maxBound]
324 112

  
325
newtype SmallRatio = SmallRatio Double deriving Show
326
instance Arbitrary SmallRatio where
327
  arbitrary = do
328
    v <- choose (0, 1)
329
    return $ SmallRatio v
330

  
331
instance Arbitrary Types.AllocPolicy where
332
  arbitrary = elements [minBound..maxBound]
333

  
334
instance Arbitrary Types.DiskTemplate where
335
  arbitrary = elements [minBound..maxBound]
336

  
337
instance Arbitrary Types.FailMode where
338
  arbitrary = elements [minBound..maxBound]
339

  
340
instance Arbitrary Types.EvacMode where
341
  arbitrary = elements [minBound..maxBound]
342

  
343
instance Arbitrary a => Arbitrary (Types.OpResult a) where
344
  arbitrary = arbitrary >>= \c ->
345
              if c
346
                then Types.OpGood <$> arbitrary
347
                else Types.OpFail <$> arbitrary
348

  
349
instance Arbitrary Types.ISpec where
350
  arbitrary = do
351
    mem_s <- arbitrary::Gen (NonNegative Int)
352
    dsk_c <- arbitrary::Gen (NonNegative Int)
353
    dsk_s <- arbitrary::Gen (NonNegative Int)
354
    cpu_c <- arbitrary::Gen (NonNegative Int)
355
    nic_c <- arbitrary::Gen (NonNegative Int)
356
    su    <- arbitrary::Gen (NonNegative Int)
357
    return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
358
                       , Types.iSpecCpuCount   = fromIntegral cpu_c
359
                       , Types.iSpecDiskSize   = fromIntegral dsk_s
360
                       , Types.iSpecDiskCount  = fromIntegral dsk_c
361
                       , Types.iSpecNicCount   = fromIntegral nic_c
362
                       , Types.iSpecSpindleUse = fromIntegral su
363
                       }
364

  
365
-- | Generates an ispec bigger than the given one.
366
genBiggerISpec :: Types.ISpec -> Gen Types.ISpec
367
genBiggerISpec imin = do
368
  mem_s <- choose (Types.iSpecMemorySize imin, maxBound)
369
  dsk_c <- choose (Types.iSpecDiskCount imin, maxBound)
370
  dsk_s <- choose (Types.iSpecDiskSize imin, maxBound)
371
  cpu_c <- choose (Types.iSpecCpuCount imin, maxBound)
372
  nic_c <- choose (Types.iSpecNicCount imin, maxBound)
373
  su    <- choose (Types.iSpecSpindleUse imin, maxBound)
374
  return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
375
                     , Types.iSpecCpuCount   = fromIntegral cpu_c
376
                     , Types.iSpecDiskSize   = fromIntegral dsk_s
377
                     , Types.iSpecDiskCount  = fromIntegral dsk_c
378
                     , Types.iSpecNicCount   = fromIntegral nic_c
379
                     , Types.iSpecSpindleUse = fromIntegral su
380
                     }
381

  
382
instance Arbitrary Types.IPolicy where
383
  arbitrary = do
384
    imin <- arbitrary
385
    istd <- genBiggerISpec imin
386
    imax <- genBiggerISpec istd
387
    num_tmpl <- choose (0, length allDiskTemplates)
388
    dts  <- genUniquesList num_tmpl
389
    vcpu_ratio <- choose (1.0, maxVcpuRatio)
390
    spindle_ratio <- choose (1.0, maxSpindleRatio)
391
    return Types.IPolicy { Types.iPolicyMinSpec = imin
392
                         , Types.iPolicyStdSpec = istd
393
                         , Types.iPolicyMaxSpec = imax
394
                         , Types.iPolicyDiskTemplates = dts
395
                         , Types.iPolicyVcpuRatio = vcpu_ratio
396
                         , Types.iPolicySpindleRatio = spindle_ratio
397
                         }
398

  
399 113
-- * Actual tests
400 114

  
401
-- ** Utils tests
402

  
403
-- | Helper to generate a small string that doesn't contain commas.
404
genNonCommaString :: Gen [Char]
405
genNonCommaString = do
406
  size <- choose (0, 20) -- arbitrary max size
407
  vectorOf size (arbitrary `suchThat` ((/=) ','))
408

  
409
-- | If the list is not just an empty element, and if the elements do
410
-- not contain commas, then join+split should be idempotent.
411
prop_Utils_commaJoinSplit :: Property
412
prop_Utils_commaJoinSplit =
413
  forAll (choose (0, 20)) $ \llen ->
414
  forAll (vectorOf llen genNonCommaString `suchThat` ((/=) [""])) $ \lst ->
415
  Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
416

  
417
-- | Split and join should always be idempotent.
418
prop_Utils_commaSplitJoin :: [Char] -> Property
419
prop_Utils_commaSplitJoin s =
420
  Utils.commaJoin (Utils.sepSplit ',' s) ==? s
421

  
422
-- | fromObjWithDefault, we test using the Maybe monad and an integer
423
-- value.
424
prop_Utils_fromObjWithDefault :: Integer -> String -> Bool
425
prop_Utils_fromObjWithDefault def_value random_key =
426
  -- a missing key will be returned with the default
427
  JSON.fromObjWithDefault [] random_key def_value == Just def_value &&
428
  -- a found key will be returned as is, not with default
429
  JSON.fromObjWithDefault [(random_key, J.showJSON def_value)]
430
       random_key (def_value+1) == Just def_value
431

  
432
-- | Test that functional if' behaves like the syntactic sugar if.
433
prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
434
prop_Utils_if'if cnd a b =
435
  Utils.if' cnd a b ==? if cnd then a else b
436

  
437
-- | Test basic select functionality
438
prop_Utils_select :: Int      -- ^ Default result
439
                  -> [Int]    -- ^ List of False values
440
                  -> [Int]    -- ^ List of True values
441
                  -> Gen Prop -- ^ Test result
442
prop_Utils_select def lst1 lst2 =
443
  Utils.select def (flist ++ tlist) ==? expectedresult
444
    where expectedresult = Utils.if' (null lst2) def (head lst2)
445
          flist = zip (repeat False) lst1
446
          tlist = zip (repeat True)  lst2
447

  
448
-- | Test basic select functionality with undefined default
449
prop_Utils_select_undefd :: [Int]            -- ^ List of False values
450
                         -> NonEmptyList Int -- ^ List of True values
451
                         -> Gen Prop         -- ^ Test result
452
prop_Utils_select_undefd lst1 (NonEmpty lst2) =
453
  Utils.select undefined (flist ++ tlist) ==? head lst2
454
    where flist = zip (repeat False) lst1
455
          tlist = zip (repeat True)  lst2
456

  
457
-- | Test basic select functionality with undefined list values
458
prop_Utils_select_undefv :: [Int]            -- ^ List of False values
459
                         -> NonEmptyList Int -- ^ List of True values
460
                         -> Gen Prop         -- ^ Test result
461
prop_Utils_select_undefv lst1 (NonEmpty lst2) =
462
  Utils.select undefined cndlist ==? head lst2
463
    where flist = zip (repeat False) lst1
464
          tlist = zip (repeat True)  lst2
465
          cndlist = flist ++ tlist ++ [undefined]
466

  
467
prop_Utils_parseUnit :: NonNegative Int -> Property
468
prop_Utils_parseUnit (NonNegative n) =
469
  Utils.parseUnit (show n) ==? Types.Ok n .&&.
470
  Utils.parseUnit (show n ++ "m") ==? Types.Ok n .&&.
471
  Utils.parseUnit (show n ++ "M") ==? Types.Ok (truncate n_mb::Int) .&&.
472
  Utils.parseUnit (show n ++ "g") ==? Types.Ok (n*1024) .&&.
473
  Utils.parseUnit (show n ++ "G") ==? Types.Ok (truncate n_gb::Int) .&&.
474
  Utils.parseUnit (show n ++ "t") ==? Types.Ok (n*1048576) .&&.
475
  Utils.parseUnit (show n ++ "T") ==? Types.Ok (truncate n_tb::Int) .&&.
476
  printTestCase "Internal error/overflow?"
477
    (n_mb >=0 && n_gb >= 0 && n_tb >= 0) .&&.
478
  property (Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int))
479
  where n_mb = (fromIntegral n::Rational) * 1000 * 1000 / 1024 / 1024
480
        n_gb = n_mb * 1000
481
        n_tb = n_gb * 1000
482

  
483
-- | Test list for the Utils module.
484
testSuite "Utils"
485
            [ 'prop_Utils_commaJoinSplit
486
            , 'prop_Utils_commaSplitJoin
487
            , 'prop_Utils_fromObjWithDefault
488
            , 'prop_Utils_if'if
489
            , 'prop_Utils_select
490
            , 'prop_Utils_select_undefd
491
            , 'prop_Utils_select_undefv
492
            , 'prop_Utils_parseUnit
493
            ]
494

  
495
-- ** PeerMap tests
496

  
497
-- | Make sure add is idempotent.
498
prop_PeerMap_addIdempotent :: PeerMap.PeerMap
499
                           -> PeerMap.Key -> PeerMap.Elem -> Property
500
prop_PeerMap_addIdempotent pmap key em =
501
  fn puniq ==? fn (fn puniq)
502
    where fn = PeerMap.add key em
503
          puniq = PeerMap.accumArray const pmap
504

  
505
-- | Make sure remove is idempotent.
506
prop_PeerMap_removeIdempotent :: PeerMap.PeerMap -> PeerMap.Key -> Property
507
prop_PeerMap_removeIdempotent pmap key =
508
  fn puniq ==? fn (fn puniq)
509
    where fn = PeerMap.remove key
510
          puniq = PeerMap.accumArray const pmap
511

  
512
-- | Make sure a missing item returns 0.
513
prop_PeerMap_findMissing :: PeerMap.PeerMap -> PeerMap.Key -> Property
514
prop_PeerMap_findMissing pmap key =
515
  PeerMap.find key (PeerMap.remove key puniq) ==? 0
516
    where puniq = PeerMap.accumArray const pmap
517

  
518
-- | Make sure an added item is found.
519
prop_PeerMap_addFind :: PeerMap.PeerMap
520
                     -> PeerMap.Key -> PeerMap.Elem -> Property
521
prop_PeerMap_addFind pmap key em =
522
  PeerMap.find key (PeerMap.add key em puniq) ==? em
523
    where puniq = PeerMap.accumArray const pmap
524

  
525
-- | Manual check that maxElem returns the maximum indeed, or 0 for null.
526
prop_PeerMap_maxElem :: PeerMap.PeerMap -> Property
527
prop_PeerMap_maxElem pmap =
528
  PeerMap.maxElem puniq ==? if null puniq then 0
529
                              else (maximum . snd . unzip) puniq
530
    where puniq = PeerMap.accumArray const pmap
531

  
532
-- | List of tests for the PeerMap module.
533
testSuite "PeerMap"
534
            [ 'prop_PeerMap_addIdempotent
535
            , 'prop_PeerMap_removeIdempotent
536
            , 'prop_PeerMap_maxElem
537
            , 'prop_PeerMap_addFind
538
            , 'prop_PeerMap_findMissing
539
            ]
540

  
541
-- ** Container tests
542

  
543
-- we silence the following due to hlint bug fixed in later versions
544
{-# ANN prop_Container_addTwo "HLint: ignore Avoid lambda" #-}
545
prop_Container_addTwo :: [Container.Key] -> Int -> Int -> Bool
546
prop_Container_addTwo cdata i1 i2 =
547
  fn i1 i2 cont == fn i2 i1 cont &&
548
  fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
549
    where cont = foldl (\c x -> Container.add x x c) Container.empty cdata
550
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
551

  
552
prop_Container_nameOf :: Node.Node -> Property
553
prop_Container_nameOf node =
554
  let nl = makeSmallCluster node 1
555
      fnode = head (Container.elems nl)
556
  in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
557

  
558
-- | We test that in a cluster, given a random node, we can find it by
559
-- its name and alias, as long as all names and aliases are unique,
560
-- and that we fail to find a non-existing name.
561
prop_Container_findByName :: Property
562
prop_Container_findByName =
563
  forAll (genNode (Just 1) Nothing) $ \node ->
564
  forAll (choose (1, 20)) $ \ cnt ->
565
  forAll (choose (0, cnt - 1)) $ \ fidx ->
566
  forAll (genUniquesList (cnt * 2)) $ \ allnames ->
567
  forAll (arbitrary `suchThat` (`notElem` allnames)) $ \ othername ->
568
  let names = zip (take cnt allnames) (drop cnt allnames)
569
      nl = makeSmallCluster node cnt
570
      nodes = Container.elems nl
571
      nodes' = map (\((name, alias), nn) -> (Node.idx nn,
572
                                             nn { Node.name = name,
573
                                                  Node.alias = alias }))
574
               $ zip names nodes
575
      nl' = Container.fromList nodes'
576
      target = snd (nodes' !! fidx)
577
  in Container.findByName nl' (Node.name target) ==? Just target .&&.
578
     Container.findByName nl' (Node.alias target) ==? Just target .&&.
579
     printTestCase "Found non-existing name"
580
       (isNothing (Container.findByName nl' othername))
581

  
582
testSuite "Container"
583
            [ 'prop_Container_addTwo
584
            , 'prop_Container_nameOf
585
            , 'prop_Container_findByName
586
            ]
587

  
588
-- ** Instance tests
589

  
590
-- Simple instance tests, we only have setter/getters
591

  
592
prop_Instance_creat :: Instance.Instance -> Property
593
prop_Instance_creat inst =
594
  Instance.name inst ==? Instance.alias inst
595

  
596
prop_Instance_setIdx :: Instance.Instance -> Types.Idx -> Property
597
prop_Instance_setIdx inst idx =
598
  Instance.idx (Instance.setIdx inst idx) ==? idx
599

  
600
prop_Instance_setName :: Instance.Instance -> String -> Bool
601
prop_Instance_setName inst name =
602
  Instance.name newinst == name &&
603
  Instance.alias newinst == name
604
    where newinst = Instance.setName inst name
605

  
606
prop_Instance_setAlias :: Instance.Instance -> String -> Bool
607
prop_Instance_setAlias inst name =
608
  Instance.name newinst == Instance.name inst &&
609
  Instance.alias newinst == name
610
    where newinst = Instance.setAlias inst name
611

  
612
prop_Instance_setPri :: Instance.Instance -> Types.Ndx -> Property
613
prop_Instance_setPri inst pdx =
614
  Instance.pNode (Instance.setPri inst pdx) ==? pdx
615

  
616
prop_Instance_setSec :: Instance.Instance -> Types.Ndx -> Property
617
prop_Instance_setSec inst sdx =
618
  Instance.sNode (Instance.setSec inst sdx) ==? sdx
619

  
620
prop_Instance_setBoth :: Instance.Instance -> Types.Ndx -> Types.Ndx -> Bool
621
prop_Instance_setBoth inst pdx sdx =
622
  Instance.pNode si == pdx && Instance.sNode si == sdx
623
    where si = Instance.setBoth inst pdx sdx
624

  
625
prop_Instance_shrinkMG :: Instance.Instance -> Property
626
prop_Instance_shrinkMG inst =
627
  Instance.mem inst >= 2 * Types.unitMem ==>
628
    case Instance.shrinkByType inst Types.FailMem of
629
      Types.Ok inst' -> Instance.mem inst' == Instance.mem inst - Types.unitMem
630
      _ -> False
631

  
632
prop_Instance_shrinkMF :: Instance.Instance -> Property
633
prop_Instance_shrinkMF inst =
634
  forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
635
    let inst' = inst { Instance.mem = mem}
636
    in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
637

  
638
prop_Instance_shrinkCG :: Instance.Instance -> Property
639
prop_Instance_shrinkCG inst =
640
  Instance.vcpus inst >= 2 * Types.unitCpu ==>
641
    case Instance.shrinkByType inst Types.FailCPU of
642
      Types.Ok inst' ->
643
        Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
644
      _ -> False
645

  
646
prop_Instance_shrinkCF :: Instance.Instance -> Property
647
prop_Instance_shrinkCF inst =
648
  forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
649
    let inst' = inst { Instance.vcpus = vcpus }
650
    in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
651

  
652
prop_Instance_shrinkDG :: Instance.Instance -> Property
653
prop_Instance_shrinkDG inst =
654
  Instance.dsk inst >= 2 * Types.unitDsk ==>
655
    case Instance.shrinkByType inst Types.FailDisk of
656
      Types.Ok inst' ->
657
        Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
658
      _ -> False
659

  
660
prop_Instance_shrinkDF :: Instance.Instance -> Property
661
prop_Instance_shrinkDF inst =
662
  forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
663
    let inst' = inst { Instance.dsk = dsk }
664
    in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
665

  
666
prop_Instance_setMovable :: Instance.Instance -> Bool -> Property
667
prop_Instance_setMovable inst m =
668
  Instance.movable inst' ==? m
669
    where inst' = Instance.setMovable inst m
670

  
671
testSuite "Instance"
672
            [ 'prop_Instance_creat
673
            , 'prop_Instance_setIdx
674
            , 'prop_Instance_setName
675
            , 'prop_Instance_setAlias
676
            , 'prop_Instance_setPri
677
            , 'prop_Instance_setSec
678
            , 'prop_Instance_setBoth
679
            , 'prop_Instance_shrinkMG
680
            , 'prop_Instance_shrinkMF
681
            , 'prop_Instance_shrinkCG
682
            , 'prop_Instance_shrinkCF
683
            , 'prop_Instance_shrinkDG
684
            , 'prop_Instance_shrinkDF
685
            , 'prop_Instance_setMovable
686
            ]
687

  
688
-- ** Backends
689

  
690
-- *** Text backend tests
691

  
692
-- Instance text loader tests
693

  
694
prop_Text_Load_Instance :: String -> Int -> Int -> Int -> Types.InstanceStatus
695
                        -> NonEmptyList Char -> [Char]
696
                        -> NonNegative Int -> NonNegative Int -> Bool
697
                        -> Types.DiskTemplate -> Int -> Property
698
prop_Text_Load_Instance name mem dsk vcpus status
699
                        (NonEmpty pnode) snode
700
                        (NonNegative pdx) (NonNegative sdx) autobal dt su =
701
  pnode /= snode && pdx /= sdx ==>
702
  let vcpus_s = show vcpus
703
      dsk_s = show dsk
704
      mem_s = show mem
705
      su_s = show su
706
      status_s = Types.instanceStatusToRaw status
707
      ndx = if null snode
708
              then [(pnode, pdx)]
709
              else [(pnode, pdx), (snode, sdx)]
710
      nl = Map.fromList ndx
711
      tags = ""
712
      sbal = if autobal then "Y" else "N"
713
      sdt = Types.diskTemplateToRaw dt
714
      inst = Text.loadInst nl
715
             [name, mem_s, dsk_s, vcpus_s, status_s,
716
              sbal, pnode, snode, sdt, tags, su_s]
717
      fail1 = Text.loadInst nl
718
              [name, mem_s, dsk_s, vcpus_s, status_s,
719
               sbal, pnode, pnode, tags]
720
  in case inst of
721
       Types.Bad msg -> failTest $ "Failed to load instance: " ++ msg
722
       Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
723
                                        \ loading the instance" $
724
               Instance.name i == name &&
725
               Instance.vcpus i == vcpus &&
726
               Instance.mem i == mem &&
727
               Instance.pNode i == pdx &&
728
               Instance.sNode i == (if null snode
729
                                      then Node.noSecondary
730
                                      else sdx) &&
731
               Instance.autoBalance i == autobal &&
732
               Instance.spindleUse i == su &&
733
               Types.isBad fail1
734

  
735
prop_Text_Load_InstanceFail :: [(String, Int)] -> [String] -> Property
736
prop_Text_Load_InstanceFail ktn fields =
737
  length fields /= 10 && length fields /= 11 ==>
738
    case Text.loadInst nl fields of
739
      Types.Ok _ -> failTest "Managed to load instance from invalid data"
740
      Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
741
                       "Invalid/incomplete instance data: '" `isPrefixOf` msg
742
    where nl = Map.fromList ktn
743

  
744
prop_Text_Load_Node :: String -> Int -> Int -> Int -> Int -> Int
745
                    -> Int -> Bool -> Bool
746
prop_Text_Load_Node name tm nm fm td fd tc fo =
747
  let conv v = if v < 0
748
                 then "?"
749
                 else show v
750
      tm_s = conv tm
751
      nm_s = conv nm
752
      fm_s = conv fm
753
      td_s = conv td
754
      fd_s = conv fd
755
      tc_s = conv tc
756
      fo_s = if fo
757
               then "Y"
758
               else "N"
759
      any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
760
      gid = Group.uuid defGroup
761
  in case Text.loadNode defGroupAssoc
762
       [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
763
       Nothing -> False
764
       Just (name', node) ->
765
         if fo || any_broken
766
           then Node.offline node
767
           else Node.name node == name' && name' == name &&
768
                Node.alias node == name &&
769
                Node.tMem node == fromIntegral tm &&
770
                Node.nMem node == nm &&
771
                Node.fMem node == fm &&
772
                Node.tDsk node == fromIntegral td &&
773
                Node.fDsk node == fd &&
774
                Node.tCpu node == fromIntegral tc
775

  
776
prop_Text_Load_NodeFail :: [String] -> Property
777
prop_Text_Load_NodeFail fields =
778
  length fields /= 8 ==> isNothing $ Text.loadNode Map.empty fields
779

  
780
prop_Text_NodeLSIdempotent :: Property
781
prop_Text_NodeLSIdempotent =
782
  forAll (genNode (Just 1) Nothing) $ \node ->
783
  -- override failN1 to what loadNode returns by default
784
  let n = Node.setPolicy Types.defIPolicy $
785
          node { Node.failN1 = True, Node.offline = False }
786
  in
787
    (Text.loadNode defGroupAssoc.
788
         Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==?
789
    Just (Node.name n, n)
790

  
791
prop_Text_ISpecIdempotent :: Types.ISpec -> Property
792
prop_Text_ISpecIdempotent ispec =
793
  case Text.loadISpec "dummy" . Utils.sepSplit ',' .
794
       Text.serializeISpec $ ispec of
795
    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
796
    Types.Ok ispec' -> ispec ==? ispec'
797

  
798
prop_Text_IPolicyIdempotent :: Types.IPolicy -> Property
799
prop_Text_IPolicyIdempotent ipol =
800
  case Text.loadIPolicy . Utils.sepSplit '|' $
801
       Text.serializeIPolicy owner ipol of
802
    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
803
    Types.Ok res -> (owner, ipol) ==? res
804
  where owner = "dummy"
805

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

  
841
testSuite "Text"
842
            [ 'prop_Text_Load_Instance
843
            , 'prop_Text_Load_InstanceFail
844
            , 'prop_Text_Load_Node
845
            , 'prop_Text_Load_NodeFail
846
            , 'prop_Text_NodeLSIdempotent
847
            , 'prop_Text_ISpecIdempotent
848
            , 'prop_Text_IPolicyIdempotent
849
            , 'prop_Text_CreateSerialise
850
            ]
851

  
852
-- *** Simu backend
853

  
854
-- | Generates a tuple of specs for simulation.
855
genSimuSpec :: Gen (String, Int, Int, Int, Int)
856
genSimuSpec = do
857
  pol <- elements [C.allocPolicyPreferred,
858
                   C.allocPolicyLastResort, C.allocPolicyUnallocable,
859
                  "p", "a", "u"]
860
 -- should be reasonable (nodes/group), bigger values only complicate
861
 -- the display of failed tests, and we don't care (in this particular
862
 -- test) about big node groups
863
  nodes <- choose (0, 20)
864
  dsk <- choose (0, maxDsk)
865
  mem <- choose (0, maxMem)
866
  cpu <- choose (0, maxCpu)
867
  return (pol, nodes, dsk, mem, cpu)
868

  
869
-- | Checks that given a set of corrects specs, we can load them
870
-- successfully, and that at high-level the values look right.
871
prop_Simu_Load :: Property
872
prop_Simu_Load =
873
  forAll (choose (0, 10)) $ \ngroups ->
874
  forAll (replicateM ngroups genSimuSpec) $ \specs ->
875
  let strspecs = map (\(p, n, d, m, c) -> printf "%s,%d,%d,%d,%d"
876
                                          p n d m c::String) specs
877
      totnodes = sum $ map (\(_, n, _, _, _) -> n) specs
878
      mdc_in = concatMap (\(_, n, d, m, c) ->
879
                            replicate n (fromIntegral m, fromIntegral d,
880
                                         fromIntegral c,
881
                                         fromIntegral m, fromIntegral d))
882
               specs :: [(Double, Double, Double, Int, Int)]
883
  in case Simu.parseData strspecs of
884
       Types.Bad msg -> failTest $ "Failed to load specs: " ++ msg
885
       Types.Ok (Loader.ClusterData gl nl il tags ipol) ->
886
         let nodes = map snd $ IntMap.toAscList nl
887
             nidx = map Node.idx nodes
888
             mdc_out = map (\n -> (Node.tMem n, Node.tDsk n, Node.tCpu n,
889
                                   Node.fMem n, Node.fDsk n)) nodes
890
         in
891
         Container.size gl ==? ngroups .&&.
892
         Container.size nl ==? totnodes .&&.
893
         Container.size il ==? 0 .&&.
894
         length tags ==? 0 .&&.
895
         ipol ==? Types.defIPolicy .&&.
896
         nidx ==? [1..totnodes] .&&.
897
         mdc_in ==? mdc_out .&&.
898
         map Group.iPolicy (Container.elems gl) ==?
899
             replicate ngroups Types.defIPolicy
900

  
901
testSuite "Simu"
902
            [ 'prop_Simu_Load
903
            ]
904

  
905
-- ** Node tests
906

  
907
prop_Node_setAlias :: Node.Node -> String -> Bool
908
prop_Node_setAlias node name =
909
  Node.name newnode == Node.name node &&
910
  Node.alias newnode == name
911
    where newnode = Node.setAlias node name
912

  
913
prop_Node_setOffline :: Node.Node -> Bool -> Property
914
prop_Node_setOffline node status =
915
  Node.offline newnode ==? status
916
    where newnode = Node.setOffline node status
917

  
918
prop_Node_setXmem :: Node.Node -> Int -> Property
919
prop_Node_setXmem node xm =
920
  Node.xMem newnode ==? xm
921
    where newnode = Node.setXmem node xm
922

  
923
prop_Node_setMcpu :: Node.Node -> Double -> Property
924
prop_Node_setMcpu node mc =
925
  Types.iPolicyVcpuRatio (Node.iPolicy newnode) ==? mc
926
    where newnode = Node.setMcpu node mc
927

  
928
-- | Check that an instance add with too high memory or disk will be
929
-- rejected.
930
prop_Node_addPriFM :: Node.Node -> Instance.Instance -> Property
931
prop_Node_addPriFM node inst =
932
  Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
933
  not (Instance.isOffline inst) ==>
934
  case Node.addPri node inst'' of
935
    Types.OpFail Types.FailMem -> True
936
    _ -> False
937
  where inst' = setInstanceSmallerThanNode node inst
938
        inst'' = inst' { Instance.mem = Instance.mem inst }
939

  
940
-- | Check that adding a primary instance with too much disk fails
941
-- with type FailDisk.
942
prop_Node_addPriFD :: Node.Node -> Instance.Instance -> Property
943
prop_Node_addPriFD node inst =
944
  forAll (elements Instance.localStorageTemplates) $ \dt ->
945
  Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
946
  let inst' = setInstanceSmallerThanNode node inst
947
      inst'' = inst' { Instance.dsk = Instance.dsk inst
948
                     , Instance.diskTemplate = dt }
949
  in case Node.addPri node inst'' of
950
       Types.OpFail Types.FailDisk -> True
951
       _ -> False
952

  
953
-- | Check that adding a primary instance with too many VCPUs fails
954
-- with type FailCPU.
955
prop_Node_addPriFC :: Property
956
prop_Node_addPriFC =
957
  forAll (choose (1, maxCpu)) $ \extra ->
958
  forAll genOnlineNode $ \node ->
959
  forAll (arbitrary `suchThat` Instance.notOffline) $ \inst ->
960
  let inst' = setInstanceSmallerThanNode node inst
961
      inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
962
  in case Node.addPri node inst'' of
963
       Types.OpFail Types.FailCPU -> property True
964
       v -> failTest $ "Expected OpFail FailCPU, but got " ++ show v
965

  
966
-- | Check that an instance add with too high memory or disk will be
967
-- rejected.
968
prop_Node_addSec :: Node.Node -> Instance.Instance -> Int -> Property
969
prop_Node_addSec node inst pdx =
970
  ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
971
    not (Instance.isOffline inst)) ||
972
   Instance.dsk inst >= Node.fDsk node) &&
973
  not (Node.failN1 node) ==>
974
      isFailure (Node.addSec node inst pdx)
975

  
976
-- | Check that an offline instance with reasonable disk size but
977
-- extra mem/cpu can always be added.
978
prop_Node_addOfflinePri :: NonNegative Int -> NonNegative Int -> Property
979
prop_Node_addOfflinePri (NonNegative extra_mem) (NonNegative extra_cpu) =
980
  forAll genOnlineNode $ \node ->
981
  forAll (genInstanceSmallerThanNode node) $ \inst ->
982
  let inst' = inst { Instance.runSt = Types.AdminOffline
983
                   , Instance.mem = Node.availMem node + extra_mem
984
                   , Instance.vcpus = Node.availCpu node + extra_cpu }
985
  in case Node.addPri node inst' of
986
       Types.OpGood _ -> property True
987
       v -> failTest $ "Expected OpGood, but got: " ++ show v
988

  
989
-- | Check that an offline instance with reasonable disk size but
990
-- extra mem/cpu can always be added.
991
prop_Node_addOfflineSec :: NonNegative Int -> NonNegative Int
992
                        -> Types.Ndx -> Property
993
prop_Node_addOfflineSec (NonNegative extra_mem) (NonNegative extra_cpu) pdx =
994
  forAll genOnlineNode $ \node ->
995
  forAll (genInstanceSmallerThanNode node) $ \inst ->
996
  let inst' = inst { Instance.runSt = Types.AdminOffline
997
                   , Instance.mem = Node.availMem node + extra_mem
998
                   , Instance.vcpus = Node.availCpu node + extra_cpu
999
                   , Instance.diskTemplate = Types.DTDrbd8 }
1000
  in case Node.addSec node inst' pdx of
1001
       Types.OpGood _ -> property True
1002
       v -> failTest $ "Expected OpGood/OpGood, but got: " ++ show v
1003

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

  
1038
-- | Check mdsk setting.
1039
prop_Node_setMdsk :: Node.Node -> SmallRatio -> Bool
1040
prop_Node_setMdsk node mx =
1041
  Node.loDsk node' >= 0 &&
1042
  fromIntegral (Node.loDsk node') <= Node.tDsk node &&
1043
  Node.availDisk node' >= 0 &&
1044
  Node.availDisk node' <= Node.fDsk node' &&
1045
  fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
1046
  Node.mDsk node' == mx'
1047
    where node' = Node.setMdsk node mx'
1048
          SmallRatio mx' = mx
1049

  
1050
-- Check tag maps
1051
prop_Node_tagMaps_idempotent :: Property
1052
prop_Node_tagMaps_idempotent =
1053
  forAll genTags $ \tags ->
1054
  Node.delTags (Node.addTags m tags) tags ==? m
1055
    where m = Map.empty
1056

  
1057
prop_Node_tagMaps_reject :: Property
1058
prop_Node_tagMaps_reject =
1059
  forAll (genTags `suchThat` (not . null)) $ \tags ->
1060
  let m = Node.addTags Map.empty tags
1061
  in all (\t -> Node.rejectAddTags m [t]) tags
1062

  
1063
prop_Node_showField :: Node.Node -> Property
1064
prop_Node_showField node =
1065
  forAll (elements Node.defaultFields) $ \ field ->
1066
  fst (Node.showHeader field) /= Types.unknownField &&
1067
  Node.showField node field /= Types.unknownField
1068

  
1069
prop_Node_computeGroups :: [Node.Node] -> Bool
1070
prop_Node_computeGroups nodes =
1071
  let ng = Node.computeGroups nodes
1072
      onlyuuid = map fst ng
1073
  in length nodes == sum (map (length . snd) ng) &&
1074
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
1075
     length (nub onlyuuid) == length onlyuuid &&
1076
     (null nodes || not (null ng))
1077

  
1078
-- Check idempotence of add/remove operations
1079
prop_Node_addPri_idempotent :: Property
1080
prop_Node_addPri_idempotent =
1081
  forAll genOnlineNode $ \node ->
1082
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1083
  case Node.addPri node inst of
1084
    Types.OpGood node' -> Node.removePri node' inst ==? node
1085
    _ -> failTest "Can't add instance"
1086

  
1087
prop_Node_addSec_idempotent :: Property
1088
prop_Node_addSec_idempotent =
1089
  forAll genOnlineNode $ \node ->
1090
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1091
  let pdx = Node.idx node + 1
1092
      inst' = Instance.setPri inst pdx
1093
      inst'' = inst' { Instance.diskTemplate = Types.DTDrbd8 }
1094
  in case Node.addSec node inst'' pdx of
1095
       Types.OpGood node' -> Node.removeSec node' inst'' ==? node
1096
       _ -> failTest "Can't add instance"
1097

  
1098
testSuite "Node"
1099
            [ 'prop_Node_setAlias
1100
            , 'prop_Node_setOffline
1101
            , 'prop_Node_setMcpu
1102
            , 'prop_Node_setXmem
1103
            , 'prop_Node_addPriFM
1104
            , 'prop_Node_addPriFD
1105
            , 'prop_Node_addPriFC
1106
            , 'prop_Node_addSec
1107
            , 'prop_Node_addOfflinePri
1108
            , 'prop_Node_addOfflineSec
1109
            , 'prop_Node_rMem
1110
            , 'prop_Node_setMdsk
1111
            , 'prop_Node_tagMaps_idempotent
1112
            , 'prop_Node_tagMaps_reject
1113
            , 'prop_Node_showField
1114
            , 'prop_Node_computeGroups
1115
            , 'prop_Node_addPri_idempotent
1116
            , 'prop_Node_addSec_idempotent
1117
            ]
1118

  
1119
-- ** Cluster tests
1120

  
1121
-- | Check that the cluster score is close to zero for a homogeneous
1122
-- cluster.
1123
prop_Cluster_Score_Zero :: Node.Node -> Property
1124
prop_Cluster_Score_Zero node =
1125
  forAll (choose (1, 1024)) $ \count ->
1126
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
1127
     (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
1128
  let fn = Node.buildPeers node Container.empty
1129
      nlst = replicate count fn
1130
      score = Cluster.compCVNodes nlst
1131
  -- we can't say == 0 here as the floating point errors accumulate;
1132
  -- this should be much lower than the default score in CLI.hs
1133
  in score <= 1e-12
1134

  
1135
-- | Check that cluster stats are sane.
1136
prop_Cluster_CStats_sane :: Property
1137
prop_Cluster_CStats_sane =
1138
  forAll (choose (1, 1024)) $ \count ->
1139
  forAll genOnlineNode $ \node ->
1140
  let fn = Node.buildPeers node Container.empty
1141
      nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
1142
      nl = Container.fromList nlst
1143
      cstats = Cluster.totalResources nl
1144
  in Cluster.csAdsk cstats >= 0 &&
1145
     Cluster.csAdsk cstats <= Cluster.csFdsk cstats
1146

  
1147
-- | Check that one instance is allocated correctly, without
1148
-- rebalances needed.
1149
prop_Cluster_Alloc_sane :: Instance.Instance -> Property
1150
prop_Cluster_Alloc_sane inst =
1151
  forAll (choose (5, 20)) $ \count ->
1152
  forAll genOnlineNode $ \node ->
1153
  let (nl, il, inst') = makeSmallEmptyCluster node count inst
1154
      reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
1155
  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1156
     Cluster.tryAlloc nl il inst' of
1157
       Types.Bad _ -> False
1158
       Types.Ok as ->
1159
         case Cluster.asSolution as of
1160
           Nothing -> False
1161
           Just (xnl, xi, _, cv) ->
1162
             let il' = Container.add (Instance.idx xi) xi il
1163
                 tbl = Cluster.Table xnl il' cv []
1164
             in not (canBalance tbl True True False)
1165

  
1166
-- | Checks that on a 2-5 node cluster, we can allocate a random
1167
-- instance spec via tiered allocation (whatever the original instance
1168
-- spec), on either one or two nodes. Furthermore, we test that
1169
-- computed allocation statistics are correct.
1170
prop_Cluster_CanTieredAlloc :: Instance.Instance -> Property
1171
prop_Cluster_CanTieredAlloc inst =
1172
  forAll (choose (2, 5)) $ \count ->
1173
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1174
  let nl = makeSmallCluster node count
1175
      il = Container.empty
1176
      rqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
1177
      allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
1178
  in case allocnodes >>= \allocnodes' ->
1179
    Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
1180
       Types.Bad msg -> failTest $ "Failed to tiered alloc: " ++ msg
1181
       Types.Ok (_, nl', il', ixes, cstats) ->
1182
         let (ai_alloc, ai_pool, ai_unav) =
1183
               Cluster.computeAllocationDelta
1184
                (Cluster.totalResources nl)
1185
                (Cluster.totalResources nl')
1186
             all_nodes = Container.elems nl
1187
         in property (not (null ixes)) .&&.
1188
            IntMap.size il' ==? length ixes .&&.
1189
            length ixes ==? length cstats .&&.
1190
            sum (map Types.allocInfoVCpus [ai_alloc, ai_pool, ai_unav]) ==?
1191
              sum (map Node.hiCpu all_nodes) .&&.
1192
            sum (map Types.allocInfoNCpus [ai_alloc, ai_pool, ai_unav]) ==?
1193
              sum (map Node.tCpu all_nodes) .&&.
1194
            sum (map Types.allocInfoMem [ai_alloc, ai_pool, ai_unav]) ==?
1195
              truncate (sum (map Node.tMem all_nodes)) .&&.
1196
            sum (map Types.allocInfoDisk [ai_alloc, ai_pool, ai_unav]) ==?
1197
              truncate (sum (map Node.tDsk all_nodes))
1198

  
1199
-- | Helper function to create a cluster with the given range of nodes
1200
-- and allocate an instance on it.
1201
genClusterAlloc :: Int -> Node.Node -> Instance.Instance
1202
                -> Types.Result (Node.List, Instance.List, Instance.Instance)
1203
genClusterAlloc count node inst =
1204
  let nl = makeSmallCluster node count
1205
      reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
1206
  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1207
     Cluster.tryAlloc nl Container.empty inst of
1208
       Types.Bad _ -> Types.Bad "Can't allocate"
1209
       Types.Ok as ->
1210
         case Cluster.asSolution as of
1211
           Nothing -> Types.Bad "Empty solution?"
1212
           Just (xnl, xi, _, _) ->
1213
             let xil = Container.add (Instance.idx xi) xi Container.empty
1214
             in Types.Ok (xnl, xil, xi)
1215

  
1216
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
1217
-- we can also relocate it.
1218
prop_Cluster_AllocRelocate :: Property
1219
prop_Cluster_AllocRelocate =
1220
  forAll (choose (4, 8)) $ \count ->
1221
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1222
  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
1223
  case genClusterAlloc count node inst of
1224
    Types.Bad msg -> failTest msg
1225
    Types.Ok (nl, il, inst') ->
1226
      case IAlloc.processRelocate defGroupList nl il
1227
             (Instance.idx inst) 1
1228
             [(if Instance.diskTemplate inst' == Types.DTDrbd8
1229
                 then Instance.sNode
1230
                 else Instance.pNode) inst'] of
1231
        Types.Ok _ -> property True
1232
        Types.Bad msg -> failTest $ "Failed to relocate: " ++ msg
1233

  
1234
-- | Helper property checker for the result of a nodeEvac or
1235
-- changeGroup operation.
1236
check_EvacMode :: Group.Group -> Instance.Instance
1237
               -> Types.Result (Node.List, Instance.List, Cluster.EvacSolution)
1238
               -> Property
1239
check_EvacMode grp inst result =
1240
  case result of
1241
    Types.Bad msg -> failTest $ "Couldn't evacuate/change group:" ++ msg
1242
    Types.Ok (_, _, es) ->
1243
      let moved = Cluster.esMoved es
1244
          failed = Cluster.esFailed es
1245
          opcodes = not . null $ Cluster.esOpCodes es
1246
      in failmsg ("'failed' not empty: " ++ show failed) (null failed) .&&.
1247
         failmsg "'opcodes' is null" opcodes .&&.
1248
         case moved of
1249
           [(idx', gdx, _)] -> failmsg "invalid instance moved" (idx == idx')
1250
                               .&&.
1251
                               failmsg "wrong target group"
1252
                                         (gdx == Group.idx grp)
1253
           v -> failmsg  ("invalid solution: " ++ show v) False
1254
  where failmsg :: String -> Bool -> Property
1255
        failmsg = \msg -> printTestCase ("Failed to evacuate: " ++ msg)
1256
        idx = Instance.idx inst
1257

  
1258
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
1259
-- we can also node-evacuate it.
1260
prop_Cluster_AllocEvacuate :: Property
1261
prop_Cluster_AllocEvacuate =
1262
  forAll (choose (4, 8)) $ \count ->
1263
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1264
  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
1265
  case genClusterAlloc count node inst of
1266
    Types.Bad msg -> failTest msg
1267
    Types.Ok (nl, il, inst') ->
1268
      conjoin . map (\mode -> check_EvacMode defGroup inst' $
1269
                              Cluster.tryNodeEvac defGroupList nl il mode
1270
                                [Instance.idx inst']) .
1271
                              evacModeOptions .
1272
                              Instance.mirrorType $ inst'
1273

  
1274
-- | Checks that on a 4-8 node cluster with two node groups, once we
1275
-- allocate an instance on the first node group, we can also change
1276
-- its group.
1277
prop_Cluster_AllocChangeGroup :: Property
1278
prop_Cluster_AllocChangeGroup =
1279
  forAll (choose (4, 8)) $ \count ->
1280
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1281
  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
1282
  case genClusterAlloc count node inst of
1283
    Types.Bad msg -> failTest msg
1284
    Types.Ok (nl, il, inst') ->
1285
      -- we need to add a second node group and nodes to the cluster
1286
      let nl2 = Container.elems $ makeSmallCluster node count
1287
          grp2 = Group.setIdx defGroup (Group.idx defGroup + 1)
1288
          maxndx = maximum . map Node.idx $ nl2
1289
          nl3 = map (\n -> n { Node.group = Group.idx grp2
1290
                             , Node.idx = Node.idx n + maxndx }) nl2
1291
          nl4 = Container.fromList . map (\n -> (Node.idx n, n)) $ nl3
1292
          gl' = Container.add (Group.idx grp2) grp2 defGroupList
1293
          nl' = IntMap.union nl nl4
1294
      in check_EvacMode grp2 inst' $
1295
         Cluster.tryChangeGroup gl' nl' il [] [Instance.idx inst']
1296

  
1297
-- | Check that allocating multiple instances on a cluster, then
1298
-- adding an empty node, results in a valid rebalance.
1299
prop_Cluster_AllocBalance :: Property
1300
prop_Cluster_AllocBalance =
1301
  forAll (genNode (Just 5) (Just 128)) $ \node ->
1302
  forAll (choose (3, 5)) $ \count ->
1303
  not (Node.offline node) && not (Node.failN1 node) ==>
1304
  let nl = makeSmallCluster node count
1305
      (hnode, nl') = IntMap.deleteFindMax nl
1306
      il = Container.empty
1307
      allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
1308
      i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
1309
  in case allocnodes >>= \allocnodes' ->
1310
    Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
1311
       Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
1312
       Types.Ok (_, _, _, [], _) -> failTest "Failed to allocate: no instances"
1313
       Types.Ok (_, xnl, il', _, _) ->
1314
         let ynl = Container.add (Node.idx hnode) hnode xnl
1315
             cv = Cluster.compCV ynl
1316
             tbl = Cluster.Table ynl il' cv []
1317
         in printTestCase "Failed to rebalance" $
1318
            canBalance tbl True True False
1319

  
1320
-- | Checks consistency.
1321
prop_Cluster_CheckConsistency :: Node.Node -> Instance.Instance -> Bool
1322
prop_Cluster_CheckConsistency node inst =
1323
  let nl = makeSmallCluster node 3
1324
      [node1, node2, node3] = Container.elems nl
1325
      node3' = node3 { Node.group = 1 }
1326
      nl' = Container.add (Node.idx node3') node3' nl
1327
      inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
1328
      inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
1329
      inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
1330
      ccheck = Cluster.findSplitInstances nl' . Container.fromList
1331
  in null (ccheck [(0, inst1)]) &&
1332
     null (ccheck [(0, inst2)]) &&
1333
     (not . null $ ccheck [(0, inst3)])
1334

  
1335
-- | For now, we only test that we don't lose instances during the split.
1336
prop_Cluster_SplitCluster :: Node.Node -> Instance.Instance -> Property
1337
prop_Cluster_SplitCluster node inst =
1338
  forAll (choose (0, 100)) $ \icnt ->
1339
  let nl = makeSmallCluster node 2
1340
      (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
1341
                   (nl, Container.empty) [1..icnt]
1342
      gni = Cluster.splitCluster nl' il'
1343
  in sum (map (Container.size . snd . snd) gni) == icnt &&
1344
     all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
1345
                                 (Container.elems nl'')) gni
1346

  
1347
-- | Helper function to check if we can allocate an instance on a
1348
-- given node list.
1349
canAllocOn :: Node.List -> Int -> Instance.Instance -> Bool
1350
canAllocOn nl reqnodes inst =
1351
  case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1352
       Cluster.tryAlloc nl (Container.empty) inst of
1353
       Types.Bad _ -> False
1354
       Types.Ok as ->
1355
         case Cluster.asSolution as of
1356
           Nothing -> False
1357
           Just _ -> True
1358

  
1359
-- | Checks that allocation obeys minimum and maximum instance
1360
-- policies. The unittest generates a random node, duplicates it /count/
1361
-- times, and generates a random instance that can be allocated on
1362
-- this mini-cluster; it then checks that after applying a policy that
1363
-- the instance doesn't fits, the allocation fails.
1364
prop_Cluster_AllocPolicy :: Node.Node -> Property
1365
prop_Cluster_AllocPolicy node =
1366
  -- rqn is the required nodes (1 or 2)
1367
  forAll (choose (1, 2)) $ \rqn ->
1368
  forAll (choose (5, 20)) $ \count ->
1369
  forAll (arbitrary `suchThat` (canAllocOn (makeSmallCluster node count) rqn))
1370
         $ \inst ->
1371
  forAll (arbitrary `suchThat` (isFailure .
1372
                                Instance.instMatchesPolicy inst)) $ \ipol ->
1373
  let node' = Node.setPolicy ipol node
1374
      nl = makeSmallCluster node' count
1375
  in not $ canAllocOn nl rqn inst
1376

  
1377
testSuite "Cluster"
1378
            [ 'prop_Cluster_Score_Zero
1379
            , 'prop_Cluster_CStats_sane
1380
            , 'prop_Cluster_Alloc_sane
1381
            , 'prop_Cluster_CanTieredAlloc
1382
            , 'prop_Cluster_AllocRelocate
1383
            , 'prop_Cluster_AllocEvacuate
1384
            , 'prop_Cluster_AllocChangeGroup
1385
            , 'prop_Cluster_AllocBalance
1386
            , 'prop_Cluster_CheckConsistency
1387
            , 'prop_Cluster_SplitCluster
1388
            , 'prop_Cluster_AllocPolicy
1389
            ]
1390 115

  
1391 116
-- ** Jobs tests
1392 117

  
......
1408 133
            , 'prop_Jobs_JobStatus_serialization
1409 134
            ]
1410 135

  
1411
-- ** Loader tests
1412

  
1413
prop_Loader_lookupNode :: [(String, Int)] -> String -> String -> Property
1414
prop_Loader_lookupNode ktn inst node =
1415
  Loader.lookupNode nl inst node ==? Map.lookup node nl
1416
    where nl = Map.fromList ktn
1417

  
1418
prop_Loader_lookupInstance :: [(String, Int)] -> String -> Property
1419
prop_Loader_lookupInstance kti inst =
1420
  Loader.lookupInstance il inst ==? Map.lookup inst il
1421
    where il = Map.fromList kti
1422

  
1423
prop_Loader_assignIndices :: Property
1424
prop_Loader_assignIndices =
1425
  -- generate nodes with unique names
1426
  forAll (arbitrary `suchThat`
1427
          (\nodes ->
1428
             let names = map Node.name nodes
1429
             in length names == length (nub names))) $ \nodes ->
1430
  let (nassoc, kt) =
1431
        Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1432
  in Map.size nassoc == length nodes &&
1433
     Container.size kt == length nodes &&
1434
     if not (null nodes)
1435
       then maximum (IntMap.keys kt) == length nodes - 1
1436
       else True
1437

  
1438
-- | Checks that the number of primary instances recorded on the nodes
1439
-- is zero.
1440
prop_Loader_mergeData :: [Node.Node] -> Bool
1441
prop_Loader_mergeData ns =
1442
  let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1443
  in case Loader.mergeData [] [] [] []
1444
         (Loader.emptyCluster {Loader.cdNodes = na}) of
1445
    Types.Bad _ -> False
1446
    Types.Ok (Loader.ClusterData _ nl il _ _) ->
1447
      let nodes = Container.elems nl
1448
          instances = Container.elems il
1449
      in (sum . map (length . Node.pList)) nodes == 0 &&
1450
         null instances
1451

  
1452
-- | Check that compareNameComponent on equal strings works.
1453
prop_Loader_compareNameComponent_equal :: String -> Bool
1454
prop_Loader_compareNameComponent_equal s =
1455
  BasicTypes.compareNameComponent s s ==
1456
    BasicTypes.LookupResult BasicTypes.ExactMatch s
1457

  
1458
-- | Check that compareNameComponent on prefix strings works.
1459
prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1460
prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1461
  BasicTypes.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1462
    BasicTypes.LookupResult BasicTypes.PartialMatch s1
1463

  
1464
testSuite "Loader"
1465
            [ 'prop_Loader_lookupNode
1466
            , 'prop_Loader_lookupInstance
1467
            , 'prop_Loader_assignIndices
1468
            , 'prop_Loader_mergeData
1469
            , 'prop_Loader_compareNameComponent_equal
1470
            , 'prop_Loader_compareNameComponent_prefix
1471
            ]
1472

  
1473
-- ** Types tests
1474

  
1475
prop_Types_AllocPolicy_serialisation :: Types.AllocPolicy -> Property
1476
prop_Types_AllocPolicy_serialisation apol =
1477
  case J.readJSON (J.showJSON apol) of
1478
    J.Ok p -> p ==? apol
1479
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1480

  
1481
prop_Types_DiskTemplate_serialisation :: Types.DiskTemplate -> Property
1482
prop_Types_DiskTemplate_serialisation dt =
1483
  case J.readJSON (J.showJSON dt) of
1484
    J.Ok p -> p ==? dt
1485
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1486

  
1487
prop_Types_ISpec_serialisation :: Types.ISpec -> Property
1488
prop_Types_ISpec_serialisation ispec =
1489
  case J.readJSON (J.showJSON ispec) of
1490
    J.Ok p -> p ==? ispec
1491
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1492

  
1493
prop_Types_IPolicy_serialisation :: Types.IPolicy -> Property
1494
prop_Types_IPolicy_serialisation ipol =
1495
  case J.readJSON (J.showJSON ipol) of
1496
    J.Ok p -> p ==? ipol
1497
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1498

  
1499
prop_Types_EvacMode_serialisation :: Types.EvacMode -> Property
1500
prop_Types_EvacMode_serialisation em =
1501
  case J.readJSON (J.showJSON em) of
1502
    J.Ok p -> p ==? em
1503
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1504

  
1505
prop_Types_opToResult :: Types.OpResult Int -> Bool
1506
prop_Types_opToResult op =
1507
  case op of
1508
    Types.OpFail _ -> Types.isBad r
1509
    Types.OpGood v -> case r of
1510
                        Types.Bad _ -> False
1511
                        Types.Ok v' -> v == v'
1512
  where r = Types.opToResult op
1513

  
1514
prop_Types_eitherToResult :: Either String Int -> Bool
1515
prop_Types_eitherToResult ei =
1516
  case ei of
1517
    Left _ -> Types.isBad r
1518
    Right v -> case r of
1519
                 Types.Bad _ -> False
1520
                 Types.Ok v' -> v == v'
1521
    where r = Types.eitherToResult ei
1522

  
1523
testSuite "Types"
1524
            [ 'prop_Types_AllocPolicy_serialisation
1525
            , 'prop_Types_DiskTemplate_serialisation
1526
            , 'prop_Types_ISpec_serialisation
1527
            , 'prop_Types_IPolicy_serialisation
1528
            , 'prop_Types_EvacMode_serialisation
1529
            , 'prop_Types_opToResult
1530
            , 'prop_Types_eitherToResult
1531
            ]
1532

  
1533
-- ** CLI tests
1534

  
1535
-- | Test correct parsing.
1536
prop_CLI_parseISpec :: String -> Int -> Int -> Int -> Property
1537
prop_CLI_parseISpec descr dsk mem cpu =
1538
  let str = printf "%d,%d,%d" dsk mem cpu::String
1539
  in CLI.parseISpecString descr str ==? Types.Ok (Types.RSpec cpu mem dsk)
1540

  
1541
-- | Test parsing failure due to wrong section count.
1542
prop_CLI_parseISpecFail :: String -> Property
1543
prop_CLI_parseISpecFail descr =
1544
  forAll (choose (0,100) `suchThat` ((/=) 3)) $ \nelems ->
1545
  forAll (replicateM nelems arbitrary) $ \values ->
1546
  let str = intercalate "," $ map show (values::[Int])
1547
  in case CLI.parseISpecString descr str of
1548
       Types.Ok v -> failTest $ "Expected failure, got " ++ show v
1549
       _ -> property True
1550

  
1551
-- | Test parseYesNo.
1552
prop_CLI_parseYesNo :: Bool -> Bool -> [Char] -> Property
1553
prop_CLI_parseYesNo def testval val =
1554
  forAll (elements [val, "yes", "no"]) $ \actual_val ->
1555
  if testval
1556
    then CLI.parseYesNo def Nothing ==? Types.Ok def
1557
    else let result = CLI.parseYesNo def (Just actual_val)
1558
         in if actual_val `elem` ["yes", "no"]
1559
              then result ==? Types.Ok (actual_val == "yes")
1560
              else property $ Types.isBad result
1561

  
1562
-- | Helper to check for correct parsing of string arg.
1563
checkStringArg :: [Char]
1564
               -> (GetOpt.OptDescr (CLI.Options -> Types.Result CLI.Options),
1565
                   CLI.Options -> Maybe [Char])
1566
               -> Property
1567
checkStringArg val (opt, fn) =
1568
  let GetOpt.Option _ longs _ _ = opt
1569
  in case longs of
1570
       [] -> failTest "no long options?"
1571
       cmdarg:_ ->
1572
         case CLI.parseOptsInner ["--" ++ cmdarg ++ "=" ++ val] "prog" [opt] of
1573
           Left e -> failTest $ "Failed to parse option: " ++ show e
1574
           Right (options, _) -> fn options ==? Just val
1575

  
1576
-- | Test a few string arguments.
1577
prop_CLI_StringArg :: [Char] -> Property
1578
prop_CLI_StringArg argument =
1579
  let args = [ (CLI.oDataFile,      CLI.optDataFile)
1580
             , (CLI.oDynuFile,      CLI.optDynuFile)
1581
             , (CLI.oSaveCluster,   CLI.optSaveCluster)
1582
             , (CLI.oReplay,        CLI.optReplay)
1583
             , (CLI.oPrintCommands, CLI.optShowCmds)
1584
             , (CLI.oLuxiSocket,    CLI.optLuxi)
1585
             ]
1586
  in conjoin $ map (checkStringArg argument) args
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff