Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / HTools / QC.hs @ 468b828e

History | View | Annotate | Download (57.7 kB)

1
{-# LANGUAGE TemplateHaskell #-}
2

    
3
{-| Unittests for ganeti-htools.
4

    
5
-}
6

    
7
{-
8

    
9
Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
10

    
11
This program is free software; you can redistribute it and/or modify
12
it under the terms of the GNU General Public License as published by
13
the Free Software Foundation; either version 2 of the License, or
14
(at your option) any later version.
15

    
16
This program is distributed in the hope that it will be useful, but
17
WITHOUT ANY WARRANTY; without even the implied warranty of
18
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
19
General Public License for more details.
20

    
21
You should have received a copy of the GNU General Public License
22
along with this program; if not, write to the Free Software
23
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
24
02110-1301, USA.
25

    
26
-}
27

    
28
module Ganeti.HTools.QC
29
  ( testUtils
30
  , testPeerMap
31
  , testContainer
32
  , testInstance
33
  , testNode
34
  , testText
35
  , testSimu
36
  , testOpCodes
37
  , testJobs
38
  , testCluster
39
  , testLoader
40
  , testTypes
41
  , testCLI
42
  ) where
43

    
44
import Test.QuickCheck
45
import Text.Printf (printf)
46
import Data.List (findIndex, intercalate, nub, isPrefixOf)
47
import qualified Data.Set as Set
48
import Data.Maybe
49
import Control.Monad
50
import qualified System.Console.GetOpt as GetOpt
51
import qualified Text.JSON as J
52
import qualified Data.Map
53
import qualified Data.IntMap as IntMap
54

    
55
import qualified Ganeti.OpCodes as OpCodes
56
import qualified Ganeti.Jobs as Jobs
57
import qualified Ganeti.Luxi
58
import qualified Ganeti.HTools.CLI as CLI
59
import qualified Ganeti.HTools.Cluster as Cluster
60
import qualified Ganeti.HTools.Container as Container
61
import qualified Ganeti.HTools.ExtLoader
62
import qualified Ganeti.HTools.IAlloc as IAlloc
63
import qualified Ganeti.HTools.Instance as Instance
64
import qualified Ganeti.HTools.JSON as JSON
65
import qualified Ganeti.HTools.Loader as Loader
66
import qualified Ganeti.HTools.Luxi
67
import qualified Ganeti.HTools.Node as Node
68
import qualified Ganeti.HTools.Group as Group
69
import qualified Ganeti.HTools.PeerMap as PeerMap
70
import qualified Ganeti.HTools.Rapi
71
import qualified Ganeti.HTools.Simu as Simu
72
import qualified Ganeti.HTools.Text as Text
73
import qualified Ganeti.HTools.Types as Types
74
import qualified Ganeti.HTools.Utils as Utils
75
import qualified Ganeti.HTools.Version
76
import qualified Ganeti.Constants as C
77

    
78
import qualified Ganeti.HTools.Program as Program
79
import qualified Ganeti.HTools.Program.Hail
80
import qualified Ganeti.HTools.Program.Hbal
81
import qualified Ganeti.HTools.Program.Hscan
82
import qualified Ganeti.HTools.Program.Hspace
83

    
84
import Ganeti.HTools.QCHelper (testSuite)
85

    
86
-- * Constants
87

    
88
-- | Maximum memory (1TiB, somewhat random value).
89
maxMem :: Int
90
maxMem = 1024 * 1024
91

    
92
-- | Maximum disk (8TiB, somewhat random value).
93
maxDsk :: Int
94
maxDsk = 1024 * 1024 * 8
95

    
96
-- | Max CPUs (1024, somewhat random value).
97
maxCpu :: Int
98
maxCpu = 1024
99

    
100
-- | All disk templates (used later)
101
allDiskTemplates :: [Types.DiskTemplate]
102
allDiskTemplates = [minBound..maxBound]
103

    
104
-- | Null iPolicy, and by null we mean very liberal.
105
nullIPolicy = Types.IPolicy
106
  { Types.iPolicyMinSpec = Types.ISpec { Types.iSpecMemorySize = 0
107
                                       , Types.iSpecCpuCount   = 0
108
                                       , Types.iSpecDiskSize   = 0
109
                                       , Types.iSpecDiskCount  = 0
110
                                       , Types.iSpecNicCount   = 0
111
                                       }
112
  , Types.iPolicyMaxSpec = Types.ISpec { Types.iSpecMemorySize = maxBound
113
                                       , Types.iSpecCpuCount   = maxBound
114
                                       , Types.iSpecDiskSize   = maxBound
115
                                       , Types.iSpecDiskCount  = C.maxDisks
116
                                       , Types.iSpecNicCount   = C.maxNics
117
                                       }
118
  , Types.iPolicyStdSpec = Types.ISpec { Types.iSpecMemorySize = Types.unitMem
119
                                       , Types.iSpecCpuCount   = Types.unitCpu
120
                                       , Types.iSpecDiskSize   = Types.unitDsk
121
                                       , Types.iSpecDiskCount  = 1
122
                                       , Types.iSpecNicCount   = 1
123
                                       }
124
  , Types.iPolicyDiskTemplates = [Types.DTDrbd8, Types.DTPlain]
125
  }
126

    
127

    
128
defGroup :: Group.Group
129
defGroup = flip Group.setIdx 0 $
130
             Group.create "default" Types.defaultGroupID Types.AllocPreferred
131
                  nullIPolicy
132

    
133
defGroupList :: Group.List
134
defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
135

    
136
defGroupAssoc :: Data.Map.Map String Types.Gdx
137
defGroupAssoc = Data.Map.singleton (Group.uuid defGroup) (Group.idx defGroup)
138

    
139
-- * Helper functions
140

    
141
-- | Simple checker for whether OpResult is fail or pass.
142
isFailure :: Types.OpResult a -> Bool
143
isFailure (Types.OpFail _) = True
144
isFailure _ = False
145

    
146
-- | Checks for equality with proper annotation.
147
(==?) :: (Show a, Eq a) => a -> a -> Property
148
(==?) x y = printTestCase
149
            ("Expected equality, but '" ++
150
             show x ++ "' /= '" ++ show y ++ "'") (x == y)
151
infix 3 ==?
152

    
153
-- | Show a message and fail the test.
154
failTest :: String -> Property
155
failTest msg = printTestCase msg False
156

    
157
-- | Update an instance to be smaller than a node.
158
setInstanceSmallerThanNode node inst =
159
  inst { Instance.mem = Node.availMem node `div` 2
160
       , Instance.dsk = Node.availDisk node `div` 2
161
       , Instance.vcpus = Node.availCpu node `div` 2
162
       }
163

    
164
-- | Create an instance given its spec.
165
createInstance mem dsk vcpus =
166
  Instance.create "inst-unnamed" mem dsk vcpus Types.Running [] True (-1) (-1)
167
    Types.DTDrbd8
168

    
169
-- | Create a small cluster by repeating a node spec.
170
makeSmallCluster :: Node.Node -> Int -> Node.List
171
makeSmallCluster node count =
172
  let origname = Node.name node
173
      origalias = Node.alias node
174
      nodes = map (\idx -> node { Node.name = origname ++ "-" ++ show idx
175
                                , Node.alias = origalias ++ "-" ++ show idx })
176
              [1..count]
177
      fn = flip Node.buildPeers Container.empty
178
      namelst = map (\n -> (Node.name n, fn n)) nodes
179
      (_, nlst) = Loader.assignIndices namelst
180
  in nlst
181

    
182
-- | Make a small cluster, both nodes and instances.
183
makeSmallEmptyCluster :: Node.Node -> Int -> Instance.Instance
184
                      -> (Node.List, Instance.List, Instance.Instance)
185
makeSmallEmptyCluster node count inst =
186
  (makeSmallCluster node count, Container.empty,
187
   setInstanceSmallerThanNode node inst)
188

    
189
-- | Checks if a node is "big" enough.
190
isNodeBig :: Int -> Node.Node -> Bool
191
isNodeBig size node = Node.availDisk node > size * Types.unitDsk
192
                      && Node.availMem node > size * Types.unitMem
193
                      && Node.availCpu node > size * Types.unitCpu
194

    
195
canBalance :: Cluster.Table -> Bool -> Bool -> Bool -> Bool
196
canBalance tbl dm im evac = isJust $ Cluster.tryBalance tbl dm im evac 0 0
197

    
198
-- | Assigns a new fresh instance to a cluster; this is not
199
-- allocation, so no resource checks are done.
200
assignInstance :: Node.List -> Instance.List -> Instance.Instance ->
201
                  Types.Idx -> Types.Idx ->
202
                  (Node.List, Instance.List)
203
assignInstance nl il inst pdx sdx =
204
  let pnode = Container.find pdx nl
205
      snode = Container.find sdx nl
206
      maxiidx = if Container.null il
207
                  then 0
208
                  else fst (Container.findMax il) + 1
209
      inst' = inst { Instance.idx = maxiidx,
210
                     Instance.pNode = pdx, Instance.sNode = sdx }
211
      pnode' = Node.setPri pnode inst'
212
      snode' = Node.setSec snode inst'
213
      nl' = Container.addTwo pdx pnode' sdx snode' nl
214
      il' = Container.add maxiidx inst' il
215
  in (nl', il')
216

    
217
-- | Generates a list of a given size with non-duplicate elements.
218
genUniquesList :: (Eq a, Arbitrary a) => Int -> Gen [a]
219
genUniquesList cnt =
220
  foldM (\lst _ -> do
221
           newelem <- arbitrary `suchThat` (`notElem` lst)
222
           return (newelem:lst)) [] [1..cnt]
223

    
224
-- * Arbitrary instances
225

    
226
-- | Defines a DNS name.
227
newtype DNSChar = DNSChar { dnsGetChar::Char }
228

    
229
instance Arbitrary DNSChar where
230
  arbitrary = do
231
    x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
232
    return (DNSChar x)
233

    
234
-- | Generates a single name component.
235
getName :: Gen String
236
getName = do
237
  n <- choose (1, 64)
238
  dn <- vector n::Gen [DNSChar]
239
  return (map dnsGetChar dn)
240

    
241
-- | Generates an entire FQDN.
242
getFQDN :: Gen String
243
getFQDN = do
244
  ncomps <- choose (1, 4)
245
  names <- mapM (const getName) [1..ncomps::Int]
246
  return $ intercalate "." names
247

    
248
-- | Defines a tag type.
249
newtype TagChar = TagChar { tagGetChar :: Char }
250

    
251
-- | All valid tag chars. This doesn't need to match _exactly_
252
-- Ganeti's own tag regex, just enough for it to be close.
253
tagChar :: [Char]
254
tagChar = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ ".+*/:@-"
255

    
256
instance Arbitrary TagChar where
257
  arbitrary = do
258
    c <- elements tagChar
259
    return (TagChar c)
260

    
261
-- | Generates a tag
262
genTag :: Gen [TagChar]
263
genTag = do
264
  -- the correct value would be C.maxTagLen, but that's way too
265
  -- verbose in unittests, and at the moment I don't see any possible
266
  -- bugs with longer tags and the way we use tags in htools
267
  n <- choose (1, 10)
268
  vector n
269

    
270
-- | Generates a list of tags (correctly upper bounded).
271
genTags :: Gen [String]
272
genTags = do
273
  -- the correct value would be C.maxTagsPerObj, but per the comment
274
  -- in genTag, we don't use tags enough in htools to warrant testing
275
  -- such big values
276
  n <- choose (0, 10::Int)
277
  tags <- mapM (const genTag) [1..n]
278
  return $ map (map tagGetChar) tags
279

    
280
instance Arbitrary Types.InstanceStatus where
281
    arbitrary = elements [minBound..maxBound]
282

    
283
-- | Generates a random instance with maximum disk/mem/cpu values.
284
genInstanceSmallerThan :: Int -> Int -> Int -> Gen Instance.Instance
285
genInstanceSmallerThan lim_mem lim_dsk lim_cpu = do
286
  name <- getFQDN
287
  mem <- choose (0, lim_mem)
288
  dsk <- choose (0, lim_dsk)
289
  run_st <- arbitrary
290
  pn <- arbitrary
291
  sn <- arbitrary
292
  vcpus <- choose (0, lim_cpu)
293
  return $ Instance.create name mem dsk vcpus run_st [] True pn sn
294
         Types.DTDrbd8
295

    
296
-- | Generates an instance smaller than a node.
297
genInstanceSmallerThanNode :: Node.Node -> Gen Instance.Instance
298
genInstanceSmallerThanNode node =
299
  genInstanceSmallerThan (Node.availMem node `div` 2)
300
                         (Node.availDisk node `div` 2)
301
                         (Node.availCpu node `div` 2)
302

    
303
-- let's generate a random instance
304
instance Arbitrary Instance.Instance where
305
  arbitrary = genInstanceSmallerThan maxMem maxDsk maxCpu
306

    
307
-- | Generas an arbitrary node based on sizing information.
308
genNode :: Maybe Int -- ^ Minimum node size in terms of units
309
        -> Maybe Int -- ^ Maximum node size (when Nothing, bounded
310
                     -- just by the max... constants)
311
        -> Gen Node.Node
312
genNode min_multiplier max_multiplier = do
313
  let (base_mem, base_dsk, base_cpu) =
314
        case min_multiplier of
315
          Just mm -> (mm * Types.unitMem,
316
                      mm * Types.unitDsk,
317
                      mm * Types.unitCpu)
318
          Nothing -> (0, 0, 0)
319
      (top_mem, top_dsk, top_cpu)  =
320
        case max_multiplier of
321
          Just mm -> (mm * Types.unitMem,
322
                      mm * Types.unitDsk,
323
                      mm * Types.unitCpu)
324
          Nothing -> (maxMem, maxDsk, maxCpu)
325
  name  <- getFQDN
326
  mem_t <- choose (base_mem, top_mem)
327
  mem_f <- choose (base_mem, mem_t)
328
  mem_n <- choose (0, mem_t - mem_f)
329
  dsk_t <- choose (base_dsk, top_dsk)
330
  dsk_f <- choose (base_dsk, dsk_t)
331
  cpu_t <- choose (base_cpu, top_cpu)
332
  offl  <- arbitrary
333
  let n = Node.create name (fromIntegral mem_t) mem_n mem_f
334
          (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
335
      n' = Node.setPolicy nullIPolicy n
336
  return $ Node.buildPeers n' Container.empty
337

    
338
-- | Helper function to generate a sane node.
339
genOnlineNode :: Gen Node.Node
340
genOnlineNode = do
341
  arbitrary `suchThat` (\n -> not (Node.offline n) &&
342
                              not (Node.failN1 n) &&
343
                              Node.availDisk n > 0 &&
344
                              Node.availMem n > 0 &&
345
                              Node.availCpu n > 0)
346

    
347
-- and a random node
348
instance Arbitrary Node.Node where
349
  arbitrary = genNode Nothing Nothing
350

    
351
-- replace disks
352
instance Arbitrary OpCodes.ReplaceDisksMode where
353
  arbitrary = elements [minBound..maxBound]
354

    
355
instance Arbitrary OpCodes.OpCode where
356
  arbitrary = do
357
    op_id <- elements [ "OP_TEST_DELAY"
358
                      , "OP_INSTANCE_REPLACE_DISKS"
359
                      , "OP_INSTANCE_FAILOVER"
360
                      , "OP_INSTANCE_MIGRATE"
361
                      ]
362
    case op_id of
363
      "OP_TEST_DELAY" ->
364
        liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
365
      "OP_INSTANCE_REPLACE_DISKS" ->
366
        liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
367
          arbitrary arbitrary arbitrary
368
      "OP_INSTANCE_FAILOVER" ->
369
        liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
370
          arbitrary
371
      "OP_INSTANCE_MIGRATE" ->
372
        liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
373
          arbitrary arbitrary arbitrary
374
      _ -> fail "Wrong opcode"
375

    
376
instance Arbitrary Jobs.OpStatus where
377
  arbitrary = elements [minBound..maxBound]
378

    
379
instance Arbitrary Jobs.JobStatus where
380
  arbitrary = elements [minBound..maxBound]
381

    
382
newtype SmallRatio = SmallRatio Double deriving Show
383
instance Arbitrary SmallRatio where
384
  arbitrary = do
385
    v <- choose (0, 1)
386
    return $ SmallRatio v
387

    
388
instance Arbitrary Types.AllocPolicy where
389
  arbitrary = elements [minBound..maxBound]
390

    
391
instance Arbitrary Types.DiskTemplate where
392
  arbitrary = elements [minBound..maxBound]
393

    
394
instance Arbitrary Types.FailMode where
395
  arbitrary = elements [minBound..maxBound]
396

    
397
instance Arbitrary Types.EvacMode where
398
  arbitrary = elements [minBound..maxBound]
399

    
400
instance Arbitrary a => Arbitrary (Types.OpResult a) where
401
  arbitrary = arbitrary >>= \c ->
402
              if c
403
                then liftM Types.OpGood arbitrary
404
                else liftM Types.OpFail arbitrary
405

    
406
instance Arbitrary Types.ISpec where
407
  arbitrary = do
408
    mem_s <- arbitrary::Gen (NonNegative Int)
409
    dsk_c <- arbitrary::Gen (NonNegative Int)
410
    dsk_s <- arbitrary::Gen (NonNegative Int)
411
    cpu_c <- arbitrary::Gen (NonNegative Int)
412
    nic_c <- arbitrary::Gen (NonNegative Int)
413
    return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
414
                       , Types.iSpecCpuCount   = fromIntegral cpu_c
415
                       , Types.iSpecDiskSize   = fromIntegral dsk_s
416
                       , Types.iSpecDiskCount  = fromIntegral dsk_c
417
                       , Types.iSpecNicCount   = fromIntegral nic_c
418
                       }
419

    
420
-- | Generates an ispec bigger than the given one.
421
genBiggerISpec :: Types.ISpec -> Gen Types.ISpec
422
genBiggerISpec imin = do
423
  mem_s <- choose (Types.iSpecMemorySize imin, maxBound)
424
  dsk_c <- choose (Types.iSpecDiskCount imin, maxBound)
425
  dsk_s <- choose (Types.iSpecDiskSize imin, maxBound)
426
  cpu_c <- choose (Types.iSpecCpuCount imin, maxBound)
427
  nic_c <- choose (Types.iSpecNicCount imin, maxBound)
428
  return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
429
                     , Types.iSpecCpuCount   = fromIntegral cpu_c
430
                     , Types.iSpecDiskSize   = fromIntegral dsk_s
431
                     , Types.iSpecDiskCount  = fromIntegral dsk_c
432
                     , Types.iSpecNicCount   = fromIntegral nic_c
433
                     }
434

    
435
instance Arbitrary Types.IPolicy where
436
  arbitrary = do
437
    imin <- arbitrary
438
    istd <- genBiggerISpec imin
439
    imax <- genBiggerISpec istd
440
    num_tmpl <- choose (0, length allDiskTemplates)
441
    dts  <- genUniquesList num_tmpl
442
    return Types.IPolicy { Types.iPolicyMinSpec = imin
443
                         , Types.iPolicyStdSpec = istd
444
                         , Types.iPolicyMaxSpec = imax
445
                         , Types.iPolicyDiskTemplates = dts
446
                         }
447

    
448
-- * Actual tests
449

    
450
-- ** Utils tests
451

    
452
-- | Helper to generate a small string that doesn't contain commas.
453
genNonCommaString = do
454
  size <- choose (0, 20) -- arbitrary max size
455
  vectorOf size (arbitrary `suchThat` ((/=) ','))
456

    
457
-- | If the list is not just an empty element, and if the elements do
458
-- not contain commas, then join+split should be idempotent.
459
prop_Utils_commaJoinSplit =
460
  forAll (choose (0, 20)) $ \llen ->
461
  forAll (vectorOf llen genNonCommaString `suchThat` ((/=) [""])) $ \lst ->
462
  Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
463

    
464
-- | Split and join should always be idempotent.
465
prop_Utils_commaSplitJoin s =
466
  Utils.commaJoin (Utils.sepSplit ',' s) ==? s
467

    
468
-- | fromObjWithDefault, we test using the Maybe monad and an integer
469
-- value.
470
prop_Utils_fromObjWithDefault def_value random_key =
471
  -- a missing key will be returned with the default
472
  JSON.fromObjWithDefault [] random_key def_value == Just def_value &&
473
  -- a found key will be returned as is, not with default
474
  JSON.fromObjWithDefault [(random_key, J.showJSON def_value)]
475
       random_key (def_value+1) == Just def_value
476
    where _types = def_value :: Integer
477

    
478
-- | Test that functional if' behaves like the syntactic sugar if.
479
prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
480
prop_Utils_if'if cnd a b =
481
  Utils.if' cnd a b ==? if cnd then a else b
482

    
483
-- | Test basic select functionality
484
prop_Utils_select :: Int      -- ^ Default result
485
                  -> [Int]    -- ^ List of False values
486
                  -> [Int]    -- ^ List of True values
487
                  -> Gen Prop -- ^ Test result
488
prop_Utils_select def lst1 lst2 =
489
  Utils.select def (flist ++ tlist) ==? expectedresult
490
    where expectedresult = Utils.if' (null lst2) def (head lst2)
491
          flist = zip (repeat False) lst1
492
          tlist = zip (repeat True)  lst2
493

    
494
-- | Test basic select functionality with undefined default
495
prop_Utils_select_undefd :: [Int]            -- ^ List of False values
496
                         -> NonEmptyList Int -- ^ List of True values
497
                         -> Gen Prop         -- ^ Test result
498
prop_Utils_select_undefd lst1 (NonEmpty lst2) =
499
  Utils.select undefined (flist ++ tlist) ==? head lst2
500
    where flist = zip (repeat False) lst1
501
          tlist = zip (repeat True)  lst2
502

    
503
-- | Test basic select functionality with undefined list values
504
prop_Utils_select_undefv :: [Int]            -- ^ List of False values
505
                         -> NonEmptyList Int -- ^ List of True values
506
                         -> Gen Prop         -- ^ Test result
507
prop_Utils_select_undefv lst1 (NonEmpty lst2) =
508
  Utils.select undefined cndlist ==? head lst2
509
    where flist = zip (repeat False) lst1
510
          tlist = zip (repeat True)  lst2
511
          cndlist = flist ++ tlist ++ [undefined]
512

    
513
prop_Utils_parseUnit (NonNegative n) =
514
  Utils.parseUnit (show n) == Types.Ok n &&
515
  Utils.parseUnit (show n ++ "m") == Types.Ok n &&
516
  (case Utils.parseUnit (show n ++ "M") of
517
     Types.Ok m -> if n > 0
518
                     then m < n  -- for positive values, X MB is < than X MiB
519
                     else m == 0 -- but for 0, 0 MB == 0 MiB
520
     Types.Bad _ -> False) &&
521
  Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
522
  Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
523
  Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
524
    where _types = n::Int
525

    
526
-- | Test list for the Utils module.
527
testSuite "Utils"
528
            [ 'prop_Utils_commaJoinSplit
529
            , 'prop_Utils_commaSplitJoin
530
            , 'prop_Utils_fromObjWithDefault
531
            , 'prop_Utils_if'if
532
            , 'prop_Utils_select
533
            , 'prop_Utils_select_undefd
534
            , 'prop_Utils_select_undefv
535
            , 'prop_Utils_parseUnit
536
            ]
537

    
538
-- ** PeerMap tests
539

    
540
-- | Make sure add is idempotent.
541
prop_PeerMap_addIdempotent pmap key em =
542
  fn puniq ==? fn (fn puniq)
543
    where _types = (pmap::PeerMap.PeerMap,
544
                    key::PeerMap.Key, em::PeerMap.Elem)
545
          fn = PeerMap.add key em
546
          puniq = PeerMap.accumArray const pmap
547

    
548
-- | Make sure remove is idempotent.
549
prop_PeerMap_removeIdempotent pmap key =
550
  fn puniq ==? fn (fn puniq)
551
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
552
          fn = PeerMap.remove key
553
          puniq = PeerMap.accumArray const pmap
554

    
555
-- | Make sure a missing item returns 0.
556
prop_PeerMap_findMissing pmap key =
557
  PeerMap.find key (PeerMap.remove key puniq) ==? 0
558
    where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
559
          puniq = PeerMap.accumArray const pmap
560

    
561
-- | Make sure an added item is found.
562
prop_PeerMap_addFind pmap key em =
563
  PeerMap.find key (PeerMap.add key em puniq) ==? em
564
    where _types = (pmap::PeerMap.PeerMap,
565
                    key::PeerMap.Key, em::PeerMap.Elem)
566
          puniq = PeerMap.accumArray const pmap
567

    
568
-- | Manual check that maxElem returns the maximum indeed, or 0 for null.
569
prop_PeerMap_maxElem pmap =
570
  PeerMap.maxElem puniq ==? if null puniq then 0
571
                              else (maximum . snd . unzip) puniq
572
    where _types = pmap::PeerMap.PeerMap
573
          puniq = PeerMap.accumArray const pmap
574

    
575
-- | List of tests for the PeerMap module.
576
testSuite "PeerMap"
577
            [ 'prop_PeerMap_addIdempotent
578
            , 'prop_PeerMap_removeIdempotent
579
            , 'prop_PeerMap_maxElem
580
            , 'prop_PeerMap_addFind
581
            , 'prop_PeerMap_findMissing
582
            ]
583

    
584
-- ** Container tests
585

    
586
-- we silence the following due to hlint bug fixed in later versions
587
{-# ANN prop_Container_addTwo "HLint: ignore Avoid lambda" #-}
588
prop_Container_addTwo cdata i1 i2 =
589
  fn i1 i2 cont == fn i2 i1 cont &&
590
  fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
591
    where _types = (cdata::[Int],
592
                    i1::Int, i2::Int)
593
          cont = foldl (\c x -> Container.add x x c) Container.empty cdata
594
          fn x1 x2 = Container.addTwo x1 x1 x2 x2
595

    
596
prop_Container_nameOf node =
597
  let nl = makeSmallCluster node 1
598
      fnode = head (Container.elems nl)
599
  in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
600

    
601
-- | We test that in a cluster, given a random node, we can find it by
602
-- its name and alias, as long as all names and aliases are unique,
603
-- and that we fail to find a non-existing name.
604
prop_Container_findByName node =
605
  forAll (choose (1, 20)) $ \ cnt ->
606
  forAll (choose (0, cnt - 1)) $ \ fidx ->
607
  forAll (genUniquesList (cnt * 2)) $ \ allnames ->
608
  forAll (arbitrary `suchThat` (`notElem` allnames)) $ \ othername ->
609
  let names = zip (take cnt allnames) (drop cnt allnames)
610
      nl = makeSmallCluster node cnt
611
      nodes = Container.elems nl
612
      nodes' = map (\((name, alias), nn) -> (Node.idx nn,
613
                                             nn { Node.name = name,
614
                                                  Node.alias = alias }))
615
               $ zip names nodes
616
      nl' = Container.fromList nodes'
617
      target = snd (nodes' !! fidx)
618
  in Container.findByName nl' (Node.name target) == Just target &&
619
     Container.findByName nl' (Node.alias target) == Just target &&
620
     isNothing (Container.findByName nl' othername)
621

    
622
testSuite "Container"
623
            [ 'prop_Container_addTwo
624
            , 'prop_Container_nameOf
625
            , 'prop_Container_findByName
626
            ]
627

    
628
-- ** Instance tests
629

    
630
-- Simple instance tests, we only have setter/getters
631

    
632
prop_Instance_creat inst =
633
  Instance.name inst ==? Instance.alias inst
634

    
635
prop_Instance_setIdx inst idx =
636
  Instance.idx (Instance.setIdx inst idx) ==? idx
637
    where _types = (inst::Instance.Instance, idx::Types.Idx)
638

    
639
prop_Instance_setName inst name =
640
  Instance.name newinst == name &&
641
  Instance.alias newinst == name
642
    where _types = (inst::Instance.Instance, name::String)
643
          newinst = Instance.setName inst name
644

    
645
prop_Instance_setAlias inst name =
646
  Instance.name newinst == Instance.name inst &&
647
  Instance.alias newinst == name
648
    where _types = (inst::Instance.Instance, name::String)
649
          newinst = Instance.setAlias inst name
650

    
651
prop_Instance_setPri inst pdx =
652
  Instance.pNode (Instance.setPri inst pdx) ==? pdx
653
    where _types = (inst::Instance.Instance, pdx::Types.Ndx)
654

    
655
prop_Instance_setSec inst sdx =
656
  Instance.sNode (Instance.setSec inst sdx) ==? sdx
657
    where _types = (inst::Instance.Instance, sdx::Types.Ndx)
658

    
659
prop_Instance_setBoth inst pdx sdx =
660
  Instance.pNode si == pdx && Instance.sNode si == sdx
661
    where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
662
          si = Instance.setBoth inst pdx sdx
663

    
664
prop_Instance_shrinkMG inst =
665
  Instance.mem inst >= 2 * Types.unitMem ==>
666
    case Instance.shrinkByType inst Types.FailMem of
667
      Types.Ok inst' -> Instance.mem inst' == Instance.mem inst - Types.unitMem
668
      _ -> False
669

    
670
prop_Instance_shrinkMF inst =
671
  forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
672
    let inst' = inst { Instance.mem = mem}
673
    in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
674

    
675
prop_Instance_shrinkCG inst =
676
  Instance.vcpus inst >= 2 * Types.unitCpu ==>
677
    case Instance.shrinkByType inst Types.FailCPU of
678
      Types.Ok inst' ->
679
        Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
680
      _ -> False
681

    
682
prop_Instance_shrinkCF inst =
683
  forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
684
    let inst' = inst { Instance.vcpus = vcpus }
685
    in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
686

    
687
prop_Instance_shrinkDG inst =
688
  Instance.dsk inst >= 2 * Types.unitDsk ==>
689
    case Instance.shrinkByType inst Types.FailDisk of
690
      Types.Ok inst' ->
691
        Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
692
      _ -> False
693

    
694
prop_Instance_shrinkDF inst =
695
  forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
696
    let inst' = inst { Instance.dsk = dsk }
697
    in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
698

    
699
prop_Instance_setMovable inst m =
700
  Instance.movable inst' ==? m
701
    where inst' = Instance.setMovable inst m
702

    
703
testSuite "Instance"
704
            [ 'prop_Instance_creat
705
            , 'prop_Instance_setIdx
706
            , 'prop_Instance_setName
707
            , 'prop_Instance_setAlias
708
            , 'prop_Instance_setPri
709
            , 'prop_Instance_setSec
710
            , 'prop_Instance_setBoth
711
            , 'prop_Instance_shrinkMG
712
            , 'prop_Instance_shrinkMF
713
            , 'prop_Instance_shrinkCG
714
            , 'prop_Instance_shrinkCF
715
            , 'prop_Instance_shrinkDG
716
            , 'prop_Instance_shrinkDF
717
            , 'prop_Instance_setMovable
718
            ]
719

    
720
-- ** Backends
721

    
722
-- *** Text backend tests
723

    
724
-- Instance text loader tests
725

    
726
prop_Text_Load_Instance name mem dsk vcpus status
727
                        (NonEmpty pnode) snode
728
                        (NonNegative pdx) (NonNegative sdx) autobal dt =
729
  pnode /= snode && pdx /= sdx ==>
730
  let vcpus_s = show vcpus
731
      dsk_s = show dsk
732
      mem_s = show mem
733
      status_s = Types.instanceStatusToRaw status
734
      ndx = if null snode
735
              then [(pnode, pdx)]
736
              else [(pnode, pdx), (snode, sdx)]
737
      nl = Data.Map.fromList ndx
738
      tags = ""
739
      sbal = if autobal then "Y" else "N"
740
      sdt = Types.diskTemplateToRaw dt
741
      inst = Text.loadInst nl
742
             [name, mem_s, dsk_s, vcpus_s, status_s,
743
              sbal, pnode, snode, sdt, tags]
744
      fail1 = Text.loadInst nl
745
              [name, mem_s, dsk_s, vcpus_s, status_s,
746
               sbal, pnode, pnode, tags]
747
      _types = ( name::String, mem::Int, dsk::Int
748
               , vcpus::Int, status::Types.InstanceStatus
749
               , snode::String
750
               , autobal::Bool)
751
  in case inst of
752
       Types.Bad msg -> failTest $ "Failed to load instance: " ++ msg
753
       Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
754
                                        \ loading the instance" $
755
               Instance.name i == name &&
756
               Instance.vcpus i == vcpus &&
757
               Instance.mem i == mem &&
758
               Instance.pNode i == pdx &&
759
               Instance.sNode i == (if null snode
760
                                      then Node.noSecondary
761
                                      else sdx) &&
762
               Instance.autoBalance i == autobal &&
763
               Types.isBad fail1
764

    
765
prop_Text_Load_InstanceFail ktn fields =
766
  length fields /= 10 ==>
767
    case Text.loadInst nl fields of
768
      Types.Ok _ -> failTest "Managed to load instance from invalid data"
769
      Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
770
                       "Invalid/incomplete instance data: '" `isPrefixOf` msg
771
    where nl = Data.Map.fromList ktn
772

    
773
prop_Text_Load_Node name tm nm fm td fd tc fo =
774
  let conv v = if v < 0
775
                 then "?"
776
                 else show v
777
      tm_s = conv tm
778
      nm_s = conv nm
779
      fm_s = conv fm
780
      td_s = conv td
781
      fd_s = conv fd
782
      tc_s = conv tc
783
      fo_s = if fo
784
               then "Y"
785
               else "N"
786
      any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
787
      gid = Group.uuid defGroup
788
  in case Text.loadNode defGroupAssoc
789
       [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
790
       Nothing -> False
791
       Just (name', node) ->
792
         if fo || any_broken
793
           then Node.offline node
794
           else Node.name node == name' && name' == name &&
795
                Node.alias node == name &&
796
                Node.tMem node == fromIntegral tm &&
797
                Node.nMem node == nm &&
798
                Node.fMem node == fm &&
799
                Node.tDsk node == fromIntegral td &&
800
                Node.fDsk node == fd &&
801
                Node.tCpu node == fromIntegral tc
802

    
803
prop_Text_Load_NodeFail fields =
804
  length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
805

    
806
prop_Text_NodeLSIdempotent node =
807
  (Text.loadNode defGroupAssoc.
808
       Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
809
  Just (Node.name n, n)
810
    -- override failN1 to what loadNode returns by default
811
    where n = node { Node.failN1 = True, Node.offline = False
812
                   , Node.iPolicy = Types.defIPolicy }
813

    
814
prop_Text_ISpecIdempotent ispec =
815
  case Text.loadISpec "dummy" . Utils.sepSplit ',' .
816
       Text.serializeISpec $ ispec of
817
    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
818
    Types.Ok ispec' -> ispec ==? ispec'
819

    
820
prop_Text_IPolicyIdempotent ipol =
821
  case Text.loadIPolicy . Utils.sepSplit '|' $
822
       Text.serializeIPolicy owner ipol of
823
    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
824
    Types.Ok res -> (owner, ipol) ==? res
825
  where owner = "dummy"
826

    
827
-- | This property, while being in the text tests, does more than just
828
-- test end-to-end the serialisation and loading back workflow; it
829
-- also tests the Loader.mergeData and the actuall
830
-- Cluster.iterateAlloc (for well-behaving w.r.t. instance
831
-- allocations, not for the business logic). As such, it's a quite
832
-- complex and slow test, and that's the reason we restrict it to
833
-- small cluster sizes.
834
prop_Text_CreateSerialise =
835
  forAll genTags $ \ctags ->
836
  forAll (choose (1, 2)) $ \reqnodes ->
837
  forAll (choose (1, 20)) $ \maxiter ->
838
  forAll (choose (2, 10)) $ \count ->
839
  forAll genOnlineNode $ \node ->
840
  forAll (genInstanceSmallerThanNode node) $ \inst ->
841
  let inst' = Instance.setMovable inst (reqnodes == 2)
842
      nl = makeSmallCluster node count
843
  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>= \allocn ->
844
     Cluster.iterateAlloc nl Container.empty (Just maxiter) inst' allocn [] []
845
     of
846
       Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
847
       Types.Ok (_, _, _, [], _) -> printTestCase
848
                                    "Failed to allocate: no allocations" False
849
       Types.Ok (_, nl', il', _, _) ->
850
         let cdata = Loader.ClusterData defGroupList nl' il' ctags
851
                     Types.defIPolicy
852
             saved = Text.serializeCluster cdata
853
         in case Text.parseData saved >>= Loader.mergeData [] [] [] [] of
854
              Types.Bad msg -> failTest $ "Failed to load/merge: " ++ msg
855
              Types.Ok (Loader.ClusterData gl2 nl2 il2 ctags2 cpol2) ->
856
                ctags ==? ctags2 .&&.
857
                Types.defIPolicy ==? cpol2 .&&.
858
                il' ==? il2 .&&.
859
                defGroupList ==? gl2 .&&.
860
                nl' ==? nl2
861

    
862
testSuite "Text"
863
            [ 'prop_Text_Load_Instance
864
            , 'prop_Text_Load_InstanceFail
865
            , 'prop_Text_Load_Node
866
            , 'prop_Text_Load_NodeFail
867
            , 'prop_Text_NodeLSIdempotent
868
            , 'prop_Text_ISpecIdempotent
869
            , 'prop_Text_IPolicyIdempotent
870
            , 'prop_Text_CreateSerialise
871
            ]
872

    
873
-- *** Simu backend
874

    
875
-- | Generates a tuple of specs for simulation.
876
genSimuSpec :: Gen (String, Int, Int, Int, Int)
877
genSimuSpec = do
878
  pol <- elements [C.allocPolicyPreferred,
879
                   C.allocPolicyLastResort, C.allocPolicyUnallocable,
880
                  "p", "a", "u"]
881
 -- should be reasonable (nodes/group), bigger values only complicate
882
 -- the display of failed tests, and we don't care (in this particular
883
 -- test) about big node groups
884
  nodes <- choose (0, 20)
885
  dsk <- choose (0, maxDsk)
886
  mem <- choose (0, maxMem)
887
  cpu <- choose (0, maxCpu)
888
  return (pol, nodes, dsk, mem, cpu)
889

    
890
-- | Checks that given a set of corrects specs, we can load them
891
-- successfully, and that at high-level the values look right.
892
prop_SimuLoad =
893
  forAll (choose (0, 10)) $ \ngroups ->
894
  forAll (replicateM ngroups genSimuSpec) $ \specs ->
895
  let strspecs = map (\(p, n, d, m, c) -> printf "%s,%d,%d,%d,%d"
896
                                          p n d m c::String) specs
897
      totnodes = sum $ map (\(_, n, _, _, _) -> n) specs
898
      mdc_in = concatMap (\(_, n, d, m, c) ->
899
                            replicate n (fromIntegral m, fromIntegral d,
900
                                         fromIntegral c,
901
                                         fromIntegral m, fromIntegral d)) specs
902
  in case Simu.parseData strspecs of
903
       Types.Bad msg -> failTest $ "Failed to load specs: " ++ msg
904
       Types.Ok (Loader.ClusterData gl nl il tags ipol) ->
905
         let nodes = map snd $ IntMap.toAscList nl
906
             nidx = map Node.idx nodes
907
             mdc_out = map (\n -> (Node.tMem n, Node.tDsk n, Node.tCpu n,
908
                                   Node.fMem n, Node.fDsk n)) nodes
909
         in
910
         Container.size gl ==? ngroups .&&.
911
         Container.size nl ==? totnodes .&&.
912
         Container.size il ==? 0 .&&.
913
         length tags ==? 0 .&&.
914
         ipol ==? Types.defIPolicy .&&.
915
         nidx ==? [1..totnodes] .&&.
916
         mdc_in ==? mdc_out .&&.
917
         map Group.iPolicy (Container.elems gl) ==?
918
             replicate ngroups Types.defIPolicy
919

    
920
testSuite "Simu"
921
            [ 'prop_SimuLoad
922
            ]
923

    
924
-- ** Node tests
925

    
926
prop_Node_setAlias node name =
927
  Node.name newnode == Node.name node &&
928
  Node.alias newnode == name
929
    where _types = (node::Node.Node, name::String)
930
          newnode = Node.setAlias node name
931

    
932
prop_Node_setOffline node status =
933
  Node.offline newnode ==? status
934
    where newnode = Node.setOffline node status
935

    
936
prop_Node_setXmem node xm =
937
  Node.xMem newnode ==? xm
938
    where newnode = Node.setXmem node xm
939

    
940
prop_Node_setMcpu node mc =
941
  Node.mCpu newnode ==? mc
942
    where newnode = Node.setMcpu node mc
943

    
944
-- | Check that an instance add with too high memory or disk will be
945
-- rejected.
946
prop_Node_addPriFM node inst =
947
  Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
948
  not (Instance.instanceOffline inst) ==>
949
  case Node.addPri node inst'' of
950
    Types.OpFail Types.FailMem -> True
951
    _ -> False
952
  where _types = (node::Node.Node, inst::Instance.Instance)
953
        inst' = setInstanceSmallerThanNode node inst
954
        inst'' = inst' { Instance.mem = Instance.mem inst }
955

    
956
prop_Node_addPriFD node inst =
957
  Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
958
    case Node.addPri node inst'' of
959
      Types.OpFail Types.FailDisk -> True
960
      _ -> False
961
    where _types = (node::Node.Node, inst::Instance.Instance)
962
          inst' = setInstanceSmallerThanNode node inst
963
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
964

    
965
prop_Node_addPriFC (Positive extra) =
966
  forAll genOnlineNode $ \node ->
967
  forAll (arbitrary `suchThat` Instance.instanceNotOffline) $ \inst ->
968
  let inst' = setInstanceSmallerThanNode node inst
969
      inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
970
  in case Node.addPri node inst'' of
971
       Types.OpFail Types.FailCPU -> property True
972
       v -> failTest $ "Expected OpFail FailCPU, but got " ++ show v
973

    
974
-- | Check that an instance add with too high memory or disk will be
975
-- rejected.
976
prop_Node_addSec node inst pdx =
977
  ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
978
    not (Instance.instanceOffline inst)) ||
979
   Instance.dsk inst >= Node.fDsk node) &&
980
  not (Node.failN1 node) ==>
981
      isFailure (Node.addSec node inst pdx)
982
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
983

    
984
-- | Check that an offline instance with reasonable disk size but
985
-- extra mem/cpu can always be added.
986
prop_Node_addOffline (NonNegative extra_mem) (NonNegative extra_cpu) pdx =
987
  forAll genOnlineNode $ \node ->
988
  forAll (genInstanceSmallerThanNode node) $ \inst ->
989
  let inst' = inst { Instance.runSt = Types.AdminOffline
990
                   , Instance.mem = Node.availMem node + extra_mem
991
                   , Instance.vcpus = Node.availCpu node + extra_cpu }
992
  in case (Node.addPri node inst', Node.addSec node inst' pdx) of
993
       (Types.OpGood _, Types.OpGood _) -> property True
994
       v -> failTest $ "Expected OpGood/OpGood, but got: " ++ show v
995

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

    
1028
-- | Check mdsk setting.
1029
prop_Node_setMdsk node mx =
1030
  Node.loDsk node' >= 0 &&
1031
  fromIntegral (Node.loDsk node') <= Node.tDsk node &&
1032
  Node.availDisk node' >= 0 &&
1033
  Node.availDisk node' <= Node.fDsk node' &&
1034
  fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
1035
  Node.mDsk node' == mx'
1036
    where _types = (node::Node.Node, mx::SmallRatio)
1037
          node' = Node.setMdsk node mx'
1038
          SmallRatio mx' = mx
1039

    
1040
-- Check tag maps
1041
prop_Node_tagMaps_idempotent =
1042
  forAll genTags $ \tags ->
1043
  Node.delTags (Node.addTags m tags) tags ==? m
1044
    where m = Data.Map.empty
1045

    
1046
prop_Node_tagMaps_reject =
1047
  forAll (genTags `suchThat` (not . null)) $ \tags ->
1048
  let m = Node.addTags Data.Map.empty tags
1049
  in all (\t -> Node.rejectAddTags m [t]) tags
1050

    
1051
prop_Node_showField node =
1052
  forAll (elements Node.defaultFields) $ \ field ->
1053
  fst (Node.showHeader field) /= Types.unknownField &&
1054
  Node.showField node field /= Types.unknownField
1055

    
1056
prop_Node_computeGroups nodes =
1057
  let ng = Node.computeGroups nodes
1058
      onlyuuid = map fst ng
1059
  in length nodes == sum (map (length . snd) ng) &&
1060
     all (\(guuid, ns) -> all ((== guuid) . Node.group) ns) ng &&
1061
     length (nub onlyuuid) == length onlyuuid &&
1062
     (null nodes || not (null ng))
1063

    
1064
testSuite "Node"
1065
            [ 'prop_Node_setAlias
1066
            , 'prop_Node_setOffline
1067
            , 'prop_Node_setMcpu
1068
            , 'prop_Node_setXmem
1069
            , 'prop_Node_addPriFM
1070
            , 'prop_Node_addPriFD
1071
            , 'prop_Node_addPriFC
1072
            , 'prop_Node_addSec
1073
            , 'prop_Node_addOffline
1074
            , 'prop_Node_rMem
1075
            , 'prop_Node_setMdsk
1076
            , 'prop_Node_tagMaps_idempotent
1077
            , 'prop_Node_tagMaps_reject
1078
            , 'prop_Node_showField
1079
            , 'prop_Node_computeGroups
1080
            ]
1081

    
1082
-- ** Cluster tests
1083

    
1084
-- | Check that the cluster score is close to zero for a homogeneous
1085
-- cluster.
1086
prop_Score_Zero node =
1087
  forAll (choose (1, 1024)) $ \count ->
1088
    (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
1089
     (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
1090
  let fn = Node.buildPeers node Container.empty
1091
      nlst = replicate count fn
1092
      score = Cluster.compCVNodes nlst
1093
  -- we can't say == 0 here as the floating point errors accumulate;
1094
  -- this should be much lower than the default score in CLI.hs
1095
  in score <= 1e-12
1096

    
1097
-- | Check that cluster stats are sane.
1098
prop_CStats_sane =
1099
  forAll (choose (1, 1024)) $ \count ->
1100
  forAll genOnlineNode $ \node ->
1101
  let fn = Node.buildPeers node Container.empty
1102
      nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
1103
      nl = Container.fromList nlst
1104
      cstats = Cluster.totalResources nl
1105
  in Cluster.csAdsk cstats >= 0 &&
1106
     Cluster.csAdsk cstats <= Cluster.csFdsk cstats
1107

    
1108
-- | Check that one instance is allocated correctly, without
1109
-- rebalances needed.
1110
prop_ClusterAlloc_sane inst =
1111
  forAll (choose (5, 20)) $ \count ->
1112
  forAll genOnlineNode $ \node ->
1113
  let (nl, il, inst') = makeSmallEmptyCluster node count inst
1114
  in case Cluster.genAllocNodes defGroupList nl 2 True >>=
1115
     Cluster.tryAlloc nl il inst' of
1116
       Types.Bad _ -> False
1117
       Types.Ok as ->
1118
         case Cluster.asSolution as of
1119
           Nothing -> False
1120
           Just (xnl, xi, _, cv) ->
1121
             let il' = Container.add (Instance.idx xi) xi il
1122
                 tbl = Cluster.Table xnl il' cv []
1123
             in not (canBalance tbl True True False)
1124

    
1125
-- | Checks that on a 2-5 node cluster, we can allocate a random
1126
-- instance spec via tiered allocation (whatever the original instance
1127
-- spec), on either one or two nodes.
1128
prop_ClusterCanTieredAlloc inst =
1129
  forAll (choose (2, 5)) $ \count ->
1130
  forAll (choose (1, 2)) $ \rqnodes ->
1131
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1132
  let nl = makeSmallCluster node count
1133
      il = Container.empty
1134
      allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
1135
  in case allocnodes >>= \allocnodes' ->
1136
    Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
1137
       Types.Bad _ -> False
1138
       Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
1139
                                             IntMap.size il' == length ixes &&
1140
                                             length ixes == length cstats
1141

    
1142
-- | Helper function to create a cluster with the given range of nodes
1143
-- and allocate an instance on it.
1144
genClusterAlloc count node inst =
1145
  let nl = makeSmallCluster node count
1146
  in case Cluster.genAllocNodes defGroupList nl 2 True >>=
1147
     Cluster.tryAlloc nl Container.empty inst of
1148
       Types.Bad _ -> Types.Bad "Can't allocate"
1149
       Types.Ok as ->
1150
         case Cluster.asSolution as of
1151
           Nothing -> Types.Bad "Empty solution?"
1152
           Just (xnl, xi, _, _) ->
1153
             let xil = Container.add (Instance.idx xi) xi Container.empty
1154
             in Types.Ok (xnl, xil, xi)
1155

    
1156
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
1157
-- we can also relocate it.
1158
prop_ClusterAllocRelocate =
1159
  forAll (choose (4, 8)) $ \count ->
1160
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1161
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1162
  case genClusterAlloc count node inst of
1163
    Types.Bad msg -> failTest msg
1164
    Types.Ok (nl, il, inst') ->
1165
      case IAlloc.processRelocate defGroupList nl il
1166
             (Instance.idx inst) 1 [Instance.sNode inst'] of
1167
        Types.Ok _ -> printTestCase "??" True  -- huh, how to make
1168
                                               -- this nicer...
1169
        Types.Bad msg -> failTest $ "Failed to relocate: " ++ msg
1170

    
1171
-- | Helper property checker for the result of a nodeEvac or
1172
-- changeGroup operation.
1173
check_EvacMode grp inst result =
1174
  case result of
1175
    Types.Bad msg -> failTest $ "Couldn't evacuate/change group:" ++ msg
1176
    Types.Ok (_, _, es) ->
1177
      let moved = Cluster.esMoved es
1178
          failed = Cluster.esFailed es
1179
          opcodes = not . null $ Cluster.esOpCodes es
1180
      in failmsg ("'failed' not empty: " ++ show failed) (null failed) .&&.
1181
         failmsg "'opcodes' is null" opcodes .&&.
1182
         case moved of
1183
           [(idx', gdx, _)] -> failmsg "invalid instance moved" (idx == idx')
1184
                               .&&.
1185
                               failmsg "wrong target group"
1186
                                         (gdx == Group.idx grp)
1187
           v -> failmsg  ("invalid solution: " ++ show v) False
1188
  where failmsg = \msg -> printTestCase ("Failed to evacuate: " ++ msg)
1189
        idx = Instance.idx inst
1190

    
1191
-- | Checks that on a 4-8 node cluster, once we allocate an instance,
1192
-- we can also node-evacuate it.
1193
prop_ClusterAllocEvacuate =
1194
  forAll (choose (4, 8)) $ \count ->
1195
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1196
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1197
  case genClusterAlloc count node inst of
1198
    Types.Bad msg -> failTest msg
1199
    Types.Ok (nl, il, inst') ->
1200
      conjoin $ map (\mode -> check_EvacMode defGroup inst' $
1201
                              Cluster.tryNodeEvac defGroupList nl il mode
1202
                                [Instance.idx inst']) [minBound..maxBound]
1203

    
1204
-- | Checks that on a 4-8 node cluster with two node groups, once we
1205
-- allocate an instance on the first node group, we can also change
1206
-- its group.
1207
prop_ClusterAllocChangeGroup =
1208
  forAll (choose (4, 8)) $ \count ->
1209
  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
1210
  forAll (genInstanceSmallerThanNode node) $ \inst ->
1211
  case genClusterAlloc count node inst of
1212
    Types.Bad msg -> failTest msg
1213
    Types.Ok (nl, il, inst') ->
1214
      -- we need to add a second node group and nodes to the cluster
1215
      let nl2 = Container.elems $ makeSmallCluster node count
1216
          grp2 = Group.setIdx defGroup (Group.idx defGroup + 1)
1217
          maxndx = maximum . map Node.idx $ nl2
1218
          nl3 = map (\n -> n { Node.group = Group.idx grp2
1219
                             , Node.idx = Node.idx n + maxndx }) nl2
1220
          nl4 = Container.fromList . map (\n -> (Node.idx n, n)) $ nl3
1221
          gl' = Container.add (Group.idx grp2) grp2 defGroupList
1222
          nl' = IntMap.union nl nl4
1223
      in check_EvacMode grp2 inst' $
1224
         Cluster.tryChangeGroup gl' nl' il [] [Instance.idx inst']
1225

    
1226
-- | Check that allocating multiple instances on a cluster, then
1227
-- adding an empty node, results in a valid rebalance.
1228
prop_ClusterAllocBalance =
1229
  forAll (genNode (Just 5) (Just 128)) $ \node ->
1230
  forAll (choose (3, 5)) $ \count ->
1231
  not (Node.offline node) && not (Node.failN1 node) ==>
1232
  let nl = makeSmallCluster node count
1233
      (hnode, nl') = IntMap.deleteFindMax nl
1234
      il = Container.empty
1235
      allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
1236
      i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
1237
  in case allocnodes >>= \allocnodes' ->
1238
    Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
1239
       Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
1240
       Types.Ok (_, _, _, [], _) -> failTest "Failed to allocate: no instances"
1241
       Types.Ok (_, xnl, il', _, _) ->
1242
         let ynl = Container.add (Node.idx hnode) hnode xnl
1243
             cv = Cluster.compCV ynl
1244
             tbl = Cluster.Table ynl il' cv []
1245
         in printTestCase "Failed to rebalance" $
1246
            canBalance tbl True True False
1247

    
1248
-- | Checks consistency.
1249
prop_ClusterCheckConsistency node inst =
1250
  let nl = makeSmallCluster node 3
1251
      [node1, node2, node3] = Container.elems nl
1252
      node3' = node3 { Node.group = 1 }
1253
      nl' = Container.add (Node.idx node3') node3' nl
1254
      inst1 = Instance.setBoth inst (Node.idx node1) (Node.idx node2)
1255
      inst2 = Instance.setBoth inst (Node.idx node1) Node.noSecondary
1256
      inst3 = Instance.setBoth inst (Node.idx node1) (Node.idx node3)
1257
      ccheck = Cluster.findSplitInstances nl' . Container.fromList
1258
  in null (ccheck [(0, inst1)]) &&
1259
     null (ccheck [(0, inst2)]) &&
1260
     (not . null $ ccheck [(0, inst3)])
1261

    
1262
-- | For now, we only test that we don't lose instances during the split.
1263
prop_ClusterSplitCluster node inst =
1264
  forAll (choose (0, 100)) $ \icnt ->
1265
  let nl = makeSmallCluster node 2
1266
      (nl', il') = foldl (\(ns, is) _ -> assignInstance ns is inst 0 1)
1267
                   (nl, Container.empty) [1..icnt]
1268
      gni = Cluster.splitCluster nl' il'
1269
  in sum (map (Container.size . snd . snd) gni) == icnt &&
1270
     all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
1271
                                 (Container.elems nl'')) gni
1272

    
1273
-- | Helper function to check if we can allocate an instance on a
1274
-- given node list.
1275
canAllocOn :: Node.List -> Int -> Instance.Instance -> Bool
1276
canAllocOn nl reqnodes inst =
1277
  case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
1278
       Cluster.tryAlloc nl (Container.empty) inst of
1279
       Types.Bad _ -> False
1280
       Types.Ok as ->
1281
         case Cluster.asSolution as of
1282
           Nothing -> False
1283
           Just _ -> True
1284

    
1285
-- | Checks that allocation obeys minimum and maximum instance
1286
-- policies. The unittest generates a random node, duplicates it count
1287
-- times, and generates a random instance that can be allocated on
1288
-- this mini-cluster; it then checks that after applying a policy that
1289
-- the instance doesn't fits, the allocation fails.
1290
prop_ClusterAllocPolicy node =
1291
  -- rqn is the required nodes (1 or 2)
1292
  forAll (choose (1, 2)) $ \rqn ->
1293
  forAll (choose (5, 20)) $ \count ->
1294
  forAll (arbitrary `suchThat` (canAllocOn (makeSmallCluster node count) rqn))
1295
         $ \inst ->
1296
  forAll (arbitrary `suchThat` (isFailure .
1297
                                Instance.instMatchesPolicy inst)) $ \ipol ->
1298
  let node' = Node.setPolicy ipol node
1299
      nl = makeSmallCluster node' count
1300
  in not $ canAllocOn nl rqn inst
1301

    
1302
testSuite "Cluster"
1303
            [ 'prop_Score_Zero
1304
            , 'prop_CStats_sane
1305
            , 'prop_ClusterAlloc_sane
1306
            , 'prop_ClusterCanTieredAlloc
1307
            , 'prop_ClusterAllocRelocate
1308
            , 'prop_ClusterAllocEvacuate
1309
            , 'prop_ClusterAllocChangeGroup
1310
            , 'prop_ClusterAllocBalance
1311
            , 'prop_ClusterCheckConsistency
1312
            , 'prop_ClusterSplitCluster
1313
            , 'prop_ClusterAllocPolicy
1314
            ]
1315

    
1316
-- ** OpCodes tests
1317

    
1318
-- | Check that opcode serialization is idempotent.
1319
prop_OpCodes_serialization op =
1320
  case J.readJSON (J.showJSON op) of
1321
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1322
    J.Ok op' -> op ==? op'
1323
  where _types = op::OpCodes.OpCode
1324

    
1325
testSuite "OpCodes"
1326
            [ 'prop_OpCodes_serialization ]
1327

    
1328
-- ** Jobs tests
1329

    
1330
-- | Check that (queued) job\/opcode status serialization is idempotent.
1331
prop_OpStatus_serialization os =
1332
  case J.readJSON (J.showJSON os) of
1333
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1334
    J.Ok os' -> os ==? os'
1335
  where _types = os::Jobs.OpStatus
1336

    
1337
prop_JobStatus_serialization js =
1338
  case J.readJSON (J.showJSON js) of
1339
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1340
    J.Ok js' -> js ==? js'
1341
  where _types = js::Jobs.JobStatus
1342

    
1343
testSuite "Jobs"
1344
            [ 'prop_OpStatus_serialization
1345
            , 'prop_JobStatus_serialization
1346
            ]
1347

    
1348
-- ** Loader tests
1349

    
1350
prop_Loader_lookupNode ktn inst node =
1351
  Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
1352
    where nl = Data.Map.fromList ktn
1353

    
1354
prop_Loader_lookupInstance kti inst =
1355
  Loader.lookupInstance il inst ==? Data.Map.lookup inst il
1356
    where il = Data.Map.fromList kti
1357

    
1358
prop_Loader_assignIndices =
1359
  -- generate nodes with unique names
1360
  forAll (arbitrary `suchThat`
1361
          (\nodes ->
1362
             let names = map Node.name nodes
1363
             in length names == length (nub names))) $ \nodes ->
1364
  let (nassoc, kt) =
1365
        Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1366
  in Data.Map.size nassoc == length nodes &&
1367
     Container.size kt == length nodes &&
1368
     if not (null nodes)
1369
       then maximum (IntMap.keys kt) == length nodes - 1
1370
       else True
1371

    
1372
-- | Checks that the number of primary instances recorded on the nodes
1373
-- is zero.
1374
prop_Loader_mergeData ns =
1375
  let na = Container.fromList $ map (\n -> (Node.idx n, n)) ns
1376
  in case Loader.mergeData [] [] [] []
1377
         (Loader.emptyCluster {Loader.cdNodes = na}) of
1378
    Types.Bad _ -> False
1379
    Types.Ok (Loader.ClusterData _ nl il _ _) ->
1380
      let nodes = Container.elems nl
1381
          instances = Container.elems il
1382
      in (sum . map (length . Node.pList)) nodes == 0 &&
1383
         null instances
1384

    
1385
-- | Check that compareNameComponent on equal strings works.
1386
prop_Loader_compareNameComponent_equal :: String -> Bool
1387
prop_Loader_compareNameComponent_equal s =
1388
  Loader.compareNameComponent s s ==
1389
    Loader.LookupResult Loader.ExactMatch s
1390

    
1391
-- | Check that compareNameComponent on prefix strings works.
1392
prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1393
prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1394
  Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1395
    Loader.LookupResult Loader.PartialMatch s1
1396

    
1397
testSuite "Loader"
1398
            [ 'prop_Loader_lookupNode
1399
            , 'prop_Loader_lookupInstance
1400
            , 'prop_Loader_assignIndices
1401
            , 'prop_Loader_mergeData
1402
            , 'prop_Loader_compareNameComponent_equal
1403
            , 'prop_Loader_compareNameComponent_prefix
1404
            ]
1405

    
1406
-- ** Types tests
1407

    
1408
prop_Types_AllocPolicy_serialisation apol =
1409
  case J.readJSON (J.showJSON apol) of
1410
    J.Ok p -> p ==? apol
1411
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1412
      where _types = apol::Types.AllocPolicy
1413

    
1414
prop_Types_DiskTemplate_serialisation dt =
1415
  case J.readJSON (J.showJSON dt) of
1416
    J.Ok p -> p ==? dt
1417
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1418
      where _types = dt::Types.DiskTemplate
1419

    
1420
prop_Types_ISpec_serialisation ispec =
1421
  case J.readJSON (J.showJSON ispec) of
1422
    J.Ok p -> p ==? ispec
1423
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1424
      where _types = ispec::Types.ISpec
1425

    
1426
prop_Types_IPolicy_serialisation ipol =
1427
  case J.readJSON (J.showJSON ipol) of
1428
    J.Ok p -> p ==? ipol
1429
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1430
      where _types = ipol::Types.IPolicy
1431

    
1432
prop_Types_EvacMode_serialisation em =
1433
  case J.readJSON (J.showJSON em) of
1434
    J.Ok p -> p ==? em
1435
    J.Error s -> failTest $ "Failed to deserialise: " ++ s
1436
      where _types = em::Types.EvacMode
1437

    
1438
prop_Types_opToResult op =
1439
  case op of
1440
    Types.OpFail _ -> Types.isBad r
1441
    Types.OpGood v -> case r of
1442
                        Types.Bad _ -> False
1443
                        Types.Ok v' -> v == v'
1444
  where r = Types.opToResult op
1445
        _types = op::Types.OpResult Int
1446

    
1447
prop_Types_eitherToResult ei =
1448
  case ei of
1449
    Left _ -> Types.isBad r
1450
    Right v -> case r of
1451
                 Types.Bad _ -> False
1452
                 Types.Ok v' -> v == v'
1453
    where r = Types.eitherToResult ei
1454
          _types = ei::Either String Int
1455

    
1456
testSuite "Types"
1457
            [ 'prop_Types_AllocPolicy_serialisation
1458
            , 'prop_Types_DiskTemplate_serialisation
1459
            , 'prop_Types_ISpec_serialisation
1460
            , 'prop_Types_IPolicy_serialisation
1461
            , 'prop_Types_EvacMode_serialisation
1462
            , 'prop_Types_opToResult
1463
            , 'prop_Types_eitherToResult
1464
            ]
1465

    
1466
-- ** CLI tests
1467

    
1468
-- | Test correct parsing.
1469
prop_CLI_parseISpec descr dsk mem cpu =
1470
  let str = printf "%d,%d,%d" dsk mem cpu
1471
  in CLI.parseISpecString descr str ==? Types.Ok (Types.RSpec cpu mem dsk)
1472

    
1473
-- | Test parsing failure due to wrong section count.
1474
prop_CLI_parseISpecFail descr =
1475
  forAll (choose (0,100) `suchThat` ((/=) 3)) $ \nelems ->
1476
  forAll (replicateM nelems arbitrary) $ \values ->
1477
  let str = intercalate "," $ map show (values::[Int])
1478
  in case CLI.parseISpecString descr str of
1479
       Types.Ok v -> failTest $ "Expected failure, got " ++ show v
1480
       _ -> property True
1481

    
1482
-- | Test parseYesNo.
1483
prop_CLI_parseYesNo def testval val =
1484
  forAll (elements [val, "yes", "no"]) $ \actual_val ->
1485
  if testval
1486
    then CLI.parseYesNo def Nothing ==? Types.Ok def
1487
    else let result = CLI.parseYesNo def (Just actual_val)
1488
         in if actual_val `elem` ["yes", "no"]
1489
              then result ==? Types.Ok (actual_val == "yes")
1490
              else property $ Types.isBad result
1491

    
1492
-- | Helper to check for correct parsing of string arg.
1493
checkStringArg val (opt, fn) =
1494
  let GetOpt.Option _ longs _ _ = opt
1495
  in case longs of
1496
       [] -> failTest "no long options?"
1497
       cmdarg:_ ->
1498
         case CLI.parseOptsInner ["--" ++ cmdarg ++ "=" ++ val] "prog" [opt] of
1499
           Left e -> failTest $ "Failed to parse option: " ++ show e
1500
           Right (options, _) -> fn options ==? Just val
1501

    
1502
-- | Test a few string arguments.
1503
prop_CLI_StringArg argument =
1504
  let args = [ (CLI.oDataFile,      CLI.optDataFile)
1505
             , (CLI.oDynuFile,      CLI.optDynuFile)
1506
             , (CLI.oSaveCluster,   CLI.optSaveCluster)
1507
             , (CLI.oReplay,        CLI.optReplay)
1508
             , (CLI.oPrintCommands, CLI.optShowCmds)
1509
             , (CLI.oLuxiSocket,    CLI.optLuxi)
1510
             ]
1511
  in conjoin $ map (checkStringArg argument) args
1512

    
1513
-- | Helper to test that a given option is accepted OK with quick exit.
1514
checkEarlyExit name options param =
1515
  case CLI.parseOptsInner [param] name options of
1516
    Left (code, _) -> if code == 0
1517
                          then property True
1518
                          else failTest $ "Program " ++ name ++
1519
                                 " returns invalid code " ++ show code ++
1520
                                 " for option " ++ param
1521
    _ -> failTest $ "Program " ++ name ++ " doesn't consider option " ++
1522
         param ++ " as early exit one"
1523

    
1524
-- | Test that all binaries support some common options. There is
1525
-- nothing actually random about this test...
1526
prop_CLI_stdopts =
1527
  let params = ["-h", "--help", "-V", "--version"]
1528
      opts = map (\(name, (_, o)) -> (name, o)) Program.personalities
1529
      -- apply checkEarlyExit across the cartesian product of params and opts
1530
  in conjoin [checkEarlyExit n o p | p <- params, (n, o) <- opts]
1531

    
1532
testSuite "CLI"
1533
          [ 'prop_CLI_parseISpec
1534
          , 'prop_CLI_parseISpecFail
1535
          , 'prop_CLI_parseYesNo
1536
          , 'prop_CLI_StringArg
1537
          , 'prop_CLI_stdopts
1538
          ]