Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / HTools / QC.hs @ 8a8ed513

History | View | Annotate | Download (39.7 kB)

1
{-# LANGUAGE TemplateHaskell #-}
2

    
3
{-| Unittests for ganeti-htools.
4

    
5
-}
6

    
7
{-
8

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

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

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

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

    
26
-}
27

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

    
42
import Test.QuickCheck
43
import Data.List (findIndex, intercalate, nub, isPrefixOf)
44
import Data.Maybe
45
import Control.Monad
46
import qualified Text.JSON as J
47
import qualified Data.Map
48
import qualified Data.IntMap as IntMap
49
import qualified Ganeti.OpCodes as OpCodes
50
import qualified Ganeti.Jobs as Jobs
51
import qualified Ganeti.Luxi
52
import qualified Ganeti.HTools.CLI as CLI
53
import qualified Ganeti.HTools.Cluster as Cluster
54
import qualified Ganeti.HTools.Container as Container
55
import qualified Ganeti.HTools.ExtLoader
56
import qualified Ganeti.HTools.IAlloc as IAlloc
57
import qualified Ganeti.HTools.Instance as Instance
58
import qualified Ganeti.HTools.Loader as Loader
59
import qualified Ganeti.HTools.Luxi
60
import qualified Ganeti.HTools.Node as Node
61
import qualified Ganeti.HTools.Group as Group
62
import qualified Ganeti.HTools.PeerMap as PeerMap
63
import qualified Ganeti.HTools.Rapi
64
import qualified Ganeti.HTools.Simu
65
import qualified Ganeti.HTools.Text as Text
66
import qualified Ganeti.HTools.Types as Types
67
import qualified Ganeti.HTools.Utils as Utils
68
import qualified Ganeti.HTools.Version
69
import qualified Ganeti.Constants as C
70

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

    
76
import Ganeti.HTools.QCHelper (testSuite)
77

    
78
-- * Constants
79

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

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

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

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

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

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

    
103
-- * Helper functions
104

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

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

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

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

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

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

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

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

    
165
-- * Arbitrary instances
166

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

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

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

    
181

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
304
-- * Actual tests
305

    
306
-- ** Utils tests
307

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

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

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

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

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

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

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

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

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

    
391
-- ** PeerMap tests
392

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

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

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

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

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

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

    
437
-- ** Container tests
438

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

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

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

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

    
480
-- ** Instance tests
481

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

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

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

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

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

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

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

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

    
516
prop_Instance_shrinkMG inst =
517
    Instance.mem inst >= 2 * Types.unitMem ==>
518
        case Instance.shrinkByType inst Types.FailMem of
519
          Types.Ok inst' ->
520
              Instance.mem inst' == Instance.mem inst - Types.unitMem
521
          _ -> False
522

    
523
prop_Instance_shrinkMF inst =
524
    forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
525
    let inst' = inst { Instance.mem = mem}
526
    in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
527

    
528
prop_Instance_shrinkCG inst =
529
    Instance.vcpus inst >= 2 * Types.unitCpu ==>
530
        case Instance.shrinkByType inst Types.FailCPU of
531
          Types.Ok inst' ->
532
              Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
533
          _ -> False
534

    
535
prop_Instance_shrinkCF inst =
536
    forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
537
    let inst' = inst { Instance.vcpus = vcpus }
538
    in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
539

    
540
prop_Instance_shrinkDG inst =
541
    Instance.dsk inst >= 2 * Types.unitDsk ==>
542
        case Instance.shrinkByType inst Types.FailDisk of
543
          Types.Ok inst' ->
544
              Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
545
          _ -> False
546

    
547
prop_Instance_shrinkDF inst =
548
    forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
549
    let inst' = inst { Instance.dsk = dsk }
550
    in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
551

    
552
prop_Instance_setMovable inst m =
553
    Instance.movable inst' ==? m
554
    where inst' = Instance.setMovable inst m
555

    
556
testSuite "Instance"
557
              [ 'prop_Instance_creat
558
              , 'prop_Instance_setIdx
559
              , 'prop_Instance_setName
560
              , 'prop_Instance_setAlias
561
              , 'prop_Instance_setPri
562
              , 'prop_Instance_setSec
563
              , 'prop_Instance_setBoth
564
              , 'prop_Instance_shrinkMG
565
              , 'prop_Instance_shrinkMF
566
              , 'prop_Instance_shrinkCG
567
              , 'prop_Instance_shrinkCF
568
              , 'prop_Instance_shrinkDG
569
              , 'prop_Instance_shrinkDF
570
              , 'prop_Instance_setMovable
571
              ]
572

    
573
-- ** Text backend tests
574

    
575
-- Instance text loader tests
576

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

    
618
prop_Text_Load_InstanceFail ktn fields =
619
    length fields /= 10 ==>
620
    case Text.loadInst nl fields of
621
      Types.Ok _ -> printTestCase "Managed to load instance from invalid\
622
                                  \ data" False
623
      Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
624
                       "Invalid/incomplete instance data: '" `isPrefixOf` msg
625
    where nl = Data.Map.fromList ktn
626

    
627
prop_Text_Load_Node name tm nm fm td fd tc fo =
628
    let conv v = if v < 0
629
                    then "?"
630
                    else show v
631
        tm_s = conv tm
632
        nm_s = conv nm
633
        fm_s = conv fm
634
        td_s = conv td
635
        fd_s = conv fd
636
        tc_s = conv tc
637
        fo_s = if fo
638
               then "Y"
639
               else "N"
640
        any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
641
        gid = Group.uuid defGroup
642
    in case Text.loadNode defGroupAssoc
643
           [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
644
         Nothing -> False
645
         Just (name', node) ->
646
             if fo || any_broken
647
             then Node.offline node
648
             else Node.name node == name' && name' == name &&
649
                  Node.alias node == name &&
650
                  Node.tMem node == fromIntegral tm &&
651
                  Node.nMem node == nm &&
652
                  Node.fMem node == fm &&
653
                  Node.tDsk node == fromIntegral td &&
654
                  Node.fDsk node == fd &&
655
                  Node.tCpu node == fromIntegral tc
656

    
657
prop_Text_Load_NodeFail fields =
658
    length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
659

    
660
prop_Text_NodeLSIdempotent node =
661
    (Text.loadNode defGroupAssoc.
662
         Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
663
    Just (Node.name n, n)
664
    -- override failN1 to what loadNode returns by default
665
    where n = node { Node.failN1 = True, Node.offline = False }
666

    
667
testSuite "Text"
668
              [ 'prop_Text_Load_Instance
669
              , 'prop_Text_Load_InstanceFail
670
              , 'prop_Text_Load_Node
671
              , 'prop_Text_Load_NodeFail
672
              , 'prop_Text_NodeLSIdempotent
673
              ]
674

    
675
-- ** Node tests
676

    
677
prop_Node_setAlias node name =
678
    Node.name newnode == Node.name node &&
679
    Node.alias newnode == name
680
    where _types = (node::Node.Node, name::String)
681
          newnode = Node.setAlias node name
682

    
683
prop_Node_setOffline node status =
684
    Node.offline newnode ==? status
685
    where newnode = Node.setOffline node status
686

    
687
prop_Node_setXmem node xm =
688
    Node.xMem newnode ==? xm
689
    where newnode = Node.setXmem node xm
690

    
691
prop_Node_setMcpu node mc =
692
    Node.mCpu newnode ==? mc
693
    where newnode = Node.setMcpu node mc
694

    
695
-- | Check that an instance add with too high memory or disk will be
696
-- rejected.
697
prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node &&
698
                               not (Node.failN1 node)
699
                               ==>
700
                               case Node.addPri node inst'' of
701
                                 Types.OpFail Types.FailMem -> True
702
                                 _ -> False
703
    where _types = (node::Node.Node, inst::Instance.Instance)
704
          inst' = setInstanceSmallerThanNode node inst
705
          inst'' = inst' { Instance.mem = Instance.mem inst }
706

    
707
prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
708
                               not (Node.failN1 node)
709
                               ==>
710
                               case Node.addPri node inst'' of
711
                                 Types.OpFail Types.FailDisk -> True
712
                                 _ -> False
713
    where _types = (node::Node.Node, inst::Instance.Instance)
714
          inst' = setInstanceSmallerThanNode node inst
715
          inst'' = inst' { Instance.dsk = Instance.dsk inst }
716

    
717
prop_Node_addPriFC node inst (Positive extra) =
718
    not (Node.failN1 node) ==>
719
        case Node.addPri node inst'' of
720
          Types.OpFail Types.FailCPU -> True
721
          _ -> False
722
    where _types = (node::Node.Node, inst::Instance.Instance)
723
          inst' = setInstanceSmallerThanNode node inst
724
          inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
725

    
726
-- | Check that an instance add with too high memory or disk will be
727
-- rejected.
728
prop_Node_addSec node inst pdx =
729
    (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
730
     Instance.dsk inst >= Node.fDsk node) &&
731
    not (Node.failN1 node)
732
    ==> isFailure (Node.addSec node inst pdx)
733
        where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
734

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

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

    
779
-- Check tag maps
780
prop_Node_tagMaps_idempotent tags =
781
    Node.delTags (Node.addTags m tags) tags ==? m
782
    where m = Data.Map.empty
783

    
784
prop_Node_tagMaps_reject tags =
785
    not (null tags) ==>
786
    all (\t -> Node.rejectAddTags m [t]) tags
787
    where m = Node.addTags Data.Map.empty tags
788

    
789
prop_Node_showField node =
790
  forAll (elements Node.defaultFields) $ \ field ->
791
  fst (Node.showHeader field) /= Types.unknownField &&
792
  Node.showField node field /= Types.unknownField
793

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

    
802
testSuite "Node"
803
              [ 'prop_Node_setAlias
804
              , 'prop_Node_setOffline
805
              , 'prop_Node_setMcpu
806
              , 'prop_Node_setXmem
807
              , 'prop_Node_addPriFM
808
              , 'prop_Node_addPriFD
809
              , 'prop_Node_addPriFC
810
              , 'prop_Node_addSec
811
              , 'prop_Node_rMem
812
              , 'prop_Node_setMdsk
813
              , 'prop_Node_tagMaps_idempotent
814
              , 'prop_Node_tagMaps_reject
815
              , 'prop_Node_showField
816
              , 'prop_Node_computeGroups
817
              ]
818

    
819
-- ** Cluster tests
820

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

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

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

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

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

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

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

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

    
959
testSuite "Cluster"
960
              [ 'prop_Score_Zero
961
              , 'prop_CStats_sane
962
              , 'prop_ClusterAlloc_sane
963
              , 'prop_ClusterCanTieredAlloc
964
              , 'prop_ClusterAllocEvac
965
              , 'prop_ClusterAllocBalance
966
              , 'prop_ClusterCheckConsistency
967
              , 'prop_ClusterSplitCluster
968
              ]
969

    
970
-- ** OpCodes tests
971

    
972
-- | Check that opcode serialization is idempotent.
973
prop_OpCodes_serialization op =
974
  case J.readJSON (J.showJSON op) of
975
    J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
976
    J.Ok op' -> op ==? op'
977
  where _types = op::OpCodes.OpCode
978

    
979
testSuite "OpCodes"
980
              [ 'prop_OpCodes_serialization ]
981

    
982
-- ** Jobs tests
983

    
984
-- | Check that (queued) job\/opcode status serialization is idempotent.
985
prop_OpStatus_serialization os =
986
  case J.readJSON (J.showJSON os) of
987
    J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
988
    J.Ok os' -> os ==? os'
989
  where _types = os::Jobs.OpStatus
990

    
991
prop_JobStatus_serialization js =
992
  case J.readJSON (J.showJSON js) of
993
    J.Error e -> printTestCase ("Cannot deserialise: " ++ e) False
994
    J.Ok js' -> js ==? js'
995
  where _types = js::Jobs.JobStatus
996

    
997
testSuite "Jobs"
998
              [ 'prop_OpStatus_serialization
999
              , 'prop_JobStatus_serialization
1000
              ]
1001

    
1002
-- ** Loader tests
1003

    
1004
prop_Loader_lookupNode ktn inst node =
1005
  Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
1006
  where nl = Data.Map.fromList ktn
1007

    
1008
prop_Loader_lookupInstance kti inst =
1009
  Loader.lookupInstance il inst ==? Data.Map.lookup inst il
1010
  where il = Data.Map.fromList kti
1011

    
1012
prop_Loader_assignIndices nodes =
1013
  Data.Map.size nassoc == length nodes &&
1014
  Container.size kt == length nodes &&
1015
  (if not (null nodes)
1016
   then maximum (IntMap.keys kt) == length nodes - 1
1017
   else True)
1018
  where (nassoc, kt) = Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
1019

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

    
1033
-- | Check that compareNameComponent on equal strings works.
1034
prop_Loader_compareNameComponent_equal :: String -> Bool
1035
prop_Loader_compareNameComponent_equal s =
1036
  Loader.compareNameComponent s s ==
1037
    Loader.LookupResult Loader.ExactMatch s
1038

    
1039
-- | Check that compareNameComponent on prefix strings works.
1040
prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
1041
prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
1042
  Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
1043
    Loader.LookupResult Loader.PartialMatch s1
1044

    
1045
testSuite "Loader"
1046
              [ 'prop_Loader_lookupNode
1047
              , 'prop_Loader_lookupInstance
1048
              , 'prop_Loader_assignIndices
1049
              , 'prop_Loader_mergeData
1050
              , 'prop_Loader_compareNameComponent_equal
1051
              , 'prop_Loader_compareNameComponent_prefix
1052
              ]
1053

    
1054
-- ** Types tests
1055

    
1056
prop_Types_AllocPolicy_serialisation apol =
1057
    case J.readJSON (J.showJSON apol) of
1058
      J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
1059
                p == apol
1060
      J.Error s -> printTestCase ("failed to deserialise: " ++ s) False
1061
    where _types = apol::Types.AllocPolicy
1062

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

    
1071
prop_Types_opToResult op =
1072
    case op of
1073
      Types.OpFail _ -> Types.isBad r
1074
      Types.OpGood v -> case r of
1075
                          Types.Bad _ -> False
1076
                          Types.Ok v' -> v == v'
1077
    where r = Types.opToResult op
1078
          _types = op::Types.OpResult Int
1079

    
1080
prop_Types_eitherToResult ei =
1081
    case ei of
1082
      Left _ -> Types.isBad r
1083
      Right v -> case r of
1084
                   Types.Bad _ -> False
1085
                   Types.Ok v' -> v == v'
1086
    where r = Types.eitherToResult ei
1087
          _types = ei::Either String Int
1088

    
1089
testSuite "Types"
1090
              [ 'prop_Types_AllocPolicy_serialisation
1091
              , 'prop_Types_DiskTemplate_serialisation
1092
              , 'prop_Types_opToResult
1093
              , 'prop_Types_eitherToResult
1094
              ]