## root / Ganeti / HTools / QC.hs @ 8fcf251f

History | View | Annotate | Download (13.8 kB)

1 |
{-| Unittests for ganeti-htools |
---|---|

2 | |

3 |
-} |

4 | |

5 |
{- |

6 | |

7 |
Copyright (C) 2009 Google Inc. |

8 | |

9 |
This program is free software; you can redistribute it and/or modify |

10 |
it under the terms of the GNU General Public License as published by |

11 |
the Free Software Foundation; either version 2 of the License, or |

12 |
(at your option) any later version. |

13 | |

14 |
This program is distributed in the hope that it will be useful, but |

15 |
WITHOUT ANY WARRANTY; without even the implied warranty of |

16 |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |

17 |
General Public License for more details. |

18 | |

19 |
You should have received a copy of the GNU General Public License |

20 |
along with this program; if not, write to the Free Software |

21 |
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |

22 |
02110-1301, USA. |

23 | |

24 |
-} |

25 | |

26 |
module Ganeti.HTools.QC |

27 |
( testPeerMap |

28 |
, testContainer |

29 |
, testInstance |

30 |
, testNode |

31 |
, testText |

32 |
, testCluster |

33 |
) where |

34 | |

35 |
import Test.QuickCheck |

36 |
import Test.QuickCheck.Batch |

37 |
import Data.Maybe |

38 |
import qualified Data.Map |

39 |
import qualified Ganeti.HTools.CLI as CLI |

40 |
import qualified Ganeti.HTools.Cluster as Cluster |

41 |
import qualified Ganeti.HTools.Container as Container |

42 |
import qualified Ganeti.HTools.IAlloc as IAlloc |

43 |
import qualified Ganeti.HTools.Instance as Instance |

44 |
import qualified Ganeti.HTools.Loader as Loader |

45 |
import qualified Ganeti.HTools.Node as Node |

46 |
import qualified Ganeti.HTools.PeerMap as PeerMap |

47 |
import qualified Ganeti.HTools.Text as Text |

48 |
import qualified Ganeti.HTools.Types as Types |

49 |
import qualified Ganeti.HTools.Utils as Utils |

50 | |

51 |
-- | Maximum memory (1TiB, somewhat random value) |

52 |
maxMem :: Int |

53 |
maxMem = 1024 * 1024 |

54 | |

55 |
-- | Maximum disk (1PiB, somewhat random value) |

56 |
maxDsk :: Int |

57 |
maxDsk = 1024 * 1024 * 1024 |

58 | |

59 |
-- | Max CPUs (1024, somewhat random value) |

60 |
maxCpu :: Int |

61 |
maxCpu = 1024 |

62 | |

63 |
-- | Simple checker for whether OpResult is fail or pass |

64 |
isFailure :: Types.OpResult a -> Bool |

65 |
isFailure (Types.OpFail _) = True |

66 |
isFailure _ = False |

67 | |

68 |
-- | Simple checker for whether Result is fail or pass |

69 |
isOk :: Types.Result a -> Bool |

70 |
isOk (Types.Ok _ ) = True |

71 |
isOk _ = False |

72 | |

73 |
-- copied from the introduction to quickcheck |

74 |
instance Arbitrary Char where |

75 |
arbitrary = choose ('\32', '\128') |

76 | |

77 |
-- let's generate a random instance |

78 |
instance Arbitrary Instance.Instance where |

79 |
arbitrary = do |

80 |
name <- arbitrary |

81 |
mem <- choose (0, maxMem) |

82 |
dsk <- choose (0, maxDsk) |

83 |
run_st <- elements ["ERROR_up", "ERROR_down", "ADMIN_down" |

84 |
, "ERROR_nodedown", "ERROR_nodeoffline" |

85 |
, "running" |

86 |
, "no_such_status1", "no_such_status2"] |

87 |
pn <- arbitrary |

88 |
sn <- arbitrary |

89 |
vcpus <- choose (0, maxCpu) |

90 |
return $ Instance.create name mem dsk vcpus run_st [] pn sn |

91 | |

92 |
-- and a random node |

93 |
instance Arbitrary Node.Node where |

94 |
arbitrary = do |

95 |
name <- arbitrary |

96 |
mem_t <- choose (0, maxMem) |

97 |
mem_f <- choose (0, mem_t) |

98 |
mem_n <- choose (0, mem_t - mem_f) |

99 |
dsk_t <- choose (0, maxDsk) |

100 |
dsk_f <- choose (0, dsk_t) |

101 |
cpu_t <- choose (0, maxCpu) |

102 |
offl <- arbitrary |

103 |
let n = Node.create name (fromIntegral mem_t) mem_n mem_f |

104 |
(fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl |

105 |
n' = Node.buildPeers n Container.empty |

106 |
return n' |

107 | |

108 |
setInstanceSmallerThanNode node inst = |

109 |
inst { Instance.mem = (Node.availMem node) `div` 2 |

110 |
, Instance.dsk = (Node.availDisk node) `div` 2 |

111 |
, Instance.vcpus = (Node.availCpu node) `div` 2 |

112 |
} |

113 | |

114 |
-- | Make sure add is idempotent |

115 |
prop_PeerMap_addIdempotent pmap key em = |

116 |
fn puniq == fn (fn puniq) |

117 |
where _types = (pmap::PeerMap.PeerMap, |

118 |
key::PeerMap.Key, em::PeerMap.Elem) |

119 |
fn = PeerMap.add key em |

120 |
puniq = PeerMap.accumArray const pmap |

121 | |

122 |
-- | Make sure remove is idempotent |

123 |
prop_PeerMap_removeIdempotent pmap key = |

124 |
fn puniq == fn (fn puniq) |

125 |
where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key) |

126 |
fn = PeerMap.remove key |

127 |
puniq = PeerMap.accumArray const pmap |

128 | |

129 |
-- | Make sure a missing item returns 0 |

130 |
prop_PeerMap_findMissing pmap key = |

131 |
PeerMap.find key (PeerMap.remove key puniq) == 0 |

132 |
where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key) |

133 |
puniq = PeerMap.accumArray const pmap |

134 | |

135 |
-- | Make sure an added item is found |

136 |
prop_PeerMap_addFind pmap key em = |

137 |
PeerMap.find key (PeerMap.add key em puniq) == em |

138 |
where _types = (pmap::PeerMap.PeerMap, |

139 |
key::PeerMap.Key, em::PeerMap.Elem) |

140 |
puniq = PeerMap.accumArray const pmap |

141 | |

142 |
-- | Manual check that maxElem returns the maximum indeed, or 0 for null |

143 |
prop_PeerMap_maxElem pmap = |

144 |
PeerMap.maxElem puniq == if null puniq then 0 |

145 |
else (maximum . snd . unzip) puniq |

146 |
where _types = pmap::PeerMap.PeerMap |

147 |
puniq = PeerMap.accumArray const pmap |

148 | |

149 |
testPeerMap = |

150 |
[ run prop_PeerMap_addIdempotent |

151 |
, run prop_PeerMap_removeIdempotent |

152 |
, run prop_PeerMap_maxElem |

153 |
, run prop_PeerMap_addFind |

154 |
, run prop_PeerMap_findMissing |

155 |
] |

156 | |

157 |
-- Container tests |

158 | |

159 |
prop_Container_addTwo cdata i1 i2 = |

160 |
fn i1 i2 cont == fn i2 i1 cont && |

161 |
fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont) |

162 |
where _types = (cdata::[Int], |

163 |
i1::Int, i2::Int) |

164 |
cont = foldl (\c x -> Container.add x x c) Container.empty cdata |

165 |
fn x1 x2 = Container.addTwo x1 x1 x2 x2 |

166 | |

167 |
testContainer = |

168 |
[ run prop_Container_addTwo ] |

169 | |

170 |
-- Simple instance tests, we only have setter/getters |

171 | |

172 |
prop_Instance_setIdx inst idx = |

173 |
Instance.idx (Instance.setIdx inst idx) == idx |

174 |
where _types = (inst::Instance.Instance, idx::Types.Idx) |

175 | |

176 |
prop_Instance_setName inst name = |

177 |
Instance.name (Instance.setName inst name) == name |

178 |
where _types = (inst::Instance.Instance, name::String) |

179 | |

180 |
prop_Instance_setPri inst pdx = |

181 |
Instance.pNode (Instance.setPri inst pdx) == pdx |

182 |
where _types = (inst::Instance.Instance, pdx::Types.Ndx) |

183 | |

184 |
prop_Instance_setSec inst sdx = |

185 |
Instance.sNode (Instance.setSec inst sdx) == sdx |

186 |
where _types = (inst::Instance.Instance, sdx::Types.Ndx) |

187 | |

188 |
prop_Instance_setBoth inst pdx sdx = |

189 |
Instance.pNode si == pdx && Instance.sNode si == sdx |

190 |
where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx) |

191 |
si = Instance.setBoth inst pdx sdx |

192 | |

193 |
prop_Instance_runStatus_True inst = |

194 |
let run_st = Instance.running inst |

195 |
run_tx = Instance.runSt inst |

196 |
in |

197 |
run_tx `elem` Instance.runningStates ==> run_st |

198 | |

199 |
prop_Instance_runStatus_False inst = |

200 |
let run_st = Instance.running inst |

201 |
run_tx = Instance.runSt inst |

202 |
in |

203 |
run_tx `notElem` Instance.runningStates ==> not run_st |

204 | |

205 |
prop_Instance_shrinkMG inst = |

206 |
Instance.mem inst >= 2 * Types.unitMem ==> |

207 |
case Instance.shrinkByType inst Types.FailMem of |

208 |
Types.Ok inst' -> |

209 |
Instance.mem inst' == Instance.mem inst - Types.unitMem |

210 |
_ -> False |

211 |
where _types = (inst::Instance.Instance) |

212 | |

213 |
prop_Instance_shrinkMF inst = |

214 |
Instance.mem inst < 2 * Types.unitMem ==> |

215 |
not . isOk $ Instance.shrinkByType inst Types.FailMem |

216 |
where _types = (inst::Instance.Instance) |

217 | |

218 |
prop_Instance_shrinkCG inst = |

219 |
Instance.vcpus inst >= 2 * Types.unitCpu ==> |

220 |
case Instance.shrinkByType inst Types.FailCPU of |

221 |
Types.Ok inst' -> |

222 |
Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu |

223 |
_ -> False |

224 |
where _types = (inst::Instance.Instance) |

225 | |

226 |
prop_Instance_shrinkCF inst = |

227 |
Instance.vcpus inst < 2 * Types.unitCpu ==> |

228 |
not . isOk $ Instance.shrinkByType inst Types.FailCPU |

229 |
where _types = (inst::Instance.Instance) |

230 | |

231 |
prop_Instance_shrinkDG inst = |

232 |
Instance.dsk inst >= 2 * Types.unitDsk ==> |

233 |
case Instance.shrinkByType inst Types.FailDisk of |

234 |
Types.Ok inst' -> |

235 |
Instance.dsk inst' == Instance.dsk inst - Types.unitDsk |

236 |
_ -> False |

237 |
where _types = (inst::Instance.Instance) |

238 | |

239 |
prop_Instance_shrinkDF inst = |

240 |
Instance.dsk inst < 2 * Types.unitDsk ==> |

241 |
not . isOk $ Instance.shrinkByType inst Types.FailDisk |

242 |
where _types = (inst::Instance.Instance) |

243 | |

244 |
prop_Instance_setMovable inst m = |

245 |
Instance.movable inst' == m |

246 |
where _types = (inst::Instance.Instance, m::Bool) |

247 |
inst' = Instance.setMovable inst m |

248 | |

249 |
testInstance = |

250 |
[ run prop_Instance_setIdx |

251 |
, run prop_Instance_setName |

252 |
, run prop_Instance_setPri |

253 |
, run prop_Instance_setSec |

254 |
, run prop_Instance_setBoth |

255 |
, run prop_Instance_runStatus_True |

256 |
, run prop_Instance_runStatus_False |

257 |
, run prop_Instance_shrinkMG |

258 |
, run prop_Instance_shrinkMF |

259 |
, run prop_Instance_shrinkCG |

260 |
, run prop_Instance_shrinkCF |

261 |
, run prop_Instance_shrinkDG |

262 |
, run prop_Instance_shrinkDF |

263 |
, run prop_Instance_setMovable |

264 |
] |

265 | |

266 |
-- Instance text loader tests |

267 | |

268 |
prop_Text_Load_Instance name mem dsk vcpus status pnode snode pdx sdx = |

269 |
let vcpus_s = show vcpus |

270 |
dsk_s = show dsk |

271 |
mem_s = show mem |

272 |
rsnode = snode ++ "a" -- non-empty secondary node |

273 |
rsdx = if pdx == sdx |

274 |
then sdx + 1 |

275 |
else sdx |

276 |
ndx = [(pnode, pdx), (rsnode, rsdx)] |

277 |
tags = "" |

278 |
inst = Text.loadInst ndx |

279 |
[name, mem_s, dsk_s, vcpus_s, status, pnode, rsnode, tags]:: |

280 |
Maybe (String, Instance.Instance) |

281 |
_types = ( name::String, mem::Int, dsk::Int |

282 |
, vcpus::Int, status::String |

283 |
, pnode::String, snode::String |

284 |
, pdx::Types.Ndx, sdx::Types.Ndx) |

285 |
in |

286 |
case inst of |

287 |
Nothing -> False |

288 |
Just (_, i) -> |

289 |
(Instance.name i == name && |

290 |
Instance.vcpus i == vcpus && |

291 |
Instance.mem i == mem && |

292 |
Instance.pNode i == pdx && |

293 |
Instance.sNode i == rsdx) |

294 | |

295 |
testText = |

296 |
[ run prop_Text_Load_Instance |

297 |
] |

298 | |

299 |
-- Node tests |

300 | |

301 |
-- | Check that an instance add with too high memory or disk will be rejected |

302 |
prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node && |

303 |
not (Node.failN1 node) |

304 |
==> |

305 |
case Node.addPri node inst'' of |

306 |
Types.OpFail Types.FailMem -> True |

307 |
_ -> False |

308 |
where _types = (node::Node.Node, inst::Instance.Instance) |

309 |
inst' = setInstanceSmallerThanNode node inst |

310 |
inst'' = inst' { Instance.mem = Instance.mem inst } |

311 | |

312 |
prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node && |

313 |
not (Node.failN1 node) |

314 |
==> |

315 |
case Node.addPri node inst'' of |

316 |
Types.OpFail Types.FailDisk -> True |

317 |
_ -> False |

318 |
where _types = (node::Node.Node, inst::Instance.Instance) |

319 |
inst' = setInstanceSmallerThanNode node inst |

320 |
inst'' = inst' { Instance.dsk = Instance.dsk inst } |

321 | |

322 |
prop_Node_addPriFC node inst = Instance.vcpus inst > Node.availCpu node && |

323 |
not (Node.failN1 node) |

324 |
==> |

325 |
case Node.addPri node inst'' of |

326 |
Types.OpFail Types.FailCPU -> True |

327 |
_ -> False |

328 |
where _types = (node::Node.Node, inst::Instance.Instance) |

329 |
inst' = setInstanceSmallerThanNode node inst |

330 |
inst'' = inst' { Instance.vcpus = Instance.vcpus inst } |

331 | |

332 |
-- | Check that an instance add with too high memory or disk will be rejected |

333 |
prop_Node_addSec node inst pdx = |

334 |
(Instance.mem inst >= (Node.fMem node - Node.rMem node) || |

335 |
Instance.dsk inst >= Node.fDsk node) && |

336 |
not (Node.failN1 node) |

337 |
==> isFailure (Node.addSec node inst pdx) |

338 |
where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int) |

339 | |

340 |
newtype SmallRatio = SmallRatio Double deriving Show |

341 |
instance Arbitrary SmallRatio where |

342 |
arbitrary = do |

343 |
v <- choose (0, 1) |

344 |
return $ SmallRatio v |

345 | |

346 |
-- | Check mdsk setting |

347 |
prop_Node_setMdsk node mx = |

348 |
Node.loDsk node' >= 0 && |

349 |
fromIntegral (Node.loDsk node') <= Node.tDsk node && |

350 |
Node.availDisk node' >= 0 && |

351 |
Node.availDisk node' <= Node.fDsk node' && |

352 |
fromIntegral (Node.availDisk node') <= Node.tDsk node' |

353 |
where _types = (node::Node.Node, mx::SmallRatio) |

354 |
node' = Node.setMdsk node mx' |

355 |
SmallRatio mx' = mx |

356 | |

357 |
-- Check tag maps |

358 |
prop_Node_tagMaps_idempotent tags = |

359 |
Node.delTags (Node.addTags m tags) tags == m |

360 |
where _types = (tags::[String]) |

361 |
m = Data.Map.empty |

362 | |

363 |
prop_Node_tagMaps_reject tags = |

364 |
not (null tags) ==> |

365 |
any (\t -> Node.rejectAddTags m [t]) tags |

366 |
where _types = (tags::[String]) |

367 |
m = Node.addTags (Data.Map.empty) tags |

368 | |

369 |
testNode = |

370 |
[ run prop_Node_addPriFM |

371 |
, run prop_Node_addPriFD |

372 |
, run prop_Node_addPriFC |

373 |
, run prop_Node_addSec |

374 |
, run prop_Node_setMdsk |

375 |
, run prop_Node_tagMaps_idempotent |

376 |
, run prop_Node_tagMaps_reject |

377 |
] |

378 | |

379 | |

380 |
-- Cluster tests |

381 | |

382 |
-- | Check that the cluster score is close to zero for a homogeneous cluster |

383 |
prop_Score_Zero node count = |

384 |
(not (Node.offline node) && not (Node.failN1 node) && (count > 0) && |

385 |
(Node.tDsk node > 0) && (Node.tMem node > 0)) ==> |

386 |
let fn = Node.buildPeers node Container.empty |

387 |
nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)] |

388 |
nl = Container.fromAssocList nlst |

389 |
score = Cluster.compCV nl |

390 |
-- we can't say == 0 here as the floating point errors accumulate; |

391 |
-- this should be much lower than the default score in CLI.hs |

392 |
in score <= 1e-15 |

393 | |

394 |
-- | Check that cluster stats are sane |

395 |
prop_CStats_sane node count = |

396 |
(not (Node.offline node) && not (Node.failN1 node) && (count > 0) && |

397 |
(Node.tDsk node > 0) && (Node.tMem node > 0)) ==> |

398 |
let fn = Node.buildPeers node Container.empty |

399 |
nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)] |

400 |
nl = Container.fromAssocList nlst |

401 |
cstats = Cluster.totalResources nl |

402 |
in Cluster.csAdsk cstats >= 0 && |

403 |
Cluster.csAdsk cstats <= Cluster.csFdsk cstats |

404 | |

405 |
testCluster = |

406 |
[ run prop_Score_Zero |

407 |
, run prop_CStats_sane |

408 |
] |