Statistics
| Branch: | Tag: | Revision:

root / test / hs / Test / Ganeti / TestCommon.hs @ 105266b2

History | View | Annotate | Download (10.7 kB)

1
{-| Unittest helpers for ganeti-htools.
2

    
3
-}
4

    
5
{-
6

    
7
Copyright (C) 2009, 2010, 2011, 2012, 2013 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 Test.Ganeti.TestCommon
27
  ( maxMem
28
  , maxDsk
29
  , maxCpu
30
  , maxVcpuRatio
31
  , maxSpindleRatio
32
  , maxNodes
33
  , maxOpCodes
34
  , (==?)
35
  , (/=?)
36
  , failTest
37
  , passTest
38
  , pythonCmd
39
  , runPython
40
  , checkPythonResult
41
  , DNSChar(..)
42
  , genName
43
  , genFQDN
44
  , genMaybe
45
  , genTags
46
  , genFields
47
  , genUniquesList
48
  , SmallRatio(..)
49
  , genSetHelper
50
  , genSet
51
  , genIp4AddrStr
52
  , genIp4Addr
53
  , genIp4NetWithNetmask
54
  , genIp4Net
55
  , genIp6Addr
56
  , genIp6Net
57
  , netmask2NumHosts
58
  , testSerialisation
59
  , resultProp
60
  , readTestData
61
  , genSample
62
  , testParser
63
  ) where
64

    
65
import Control.Applicative
66
import Control.Exception (catchJust)
67
import Control.Monad
68
import Data.Attoparsec.Text (Parser, parseOnly)
69
import Data.List
70
import Data.Text (pack)
71
import Data.Word
72
import qualified Data.Set as Set
73
import System.Environment (getEnv)
74
import System.Exit (ExitCode(..))
75
import System.IO.Error (isDoesNotExistError)
76
import System.Process (readProcessWithExitCode)
77
import qualified Test.HUnit as HUnit
78
import Test.QuickCheck
79
import Test.QuickCheck.Monadic
80
import qualified Text.JSON as J
81
import Numeric
82

    
83
import qualified Ganeti.BasicTypes as BasicTypes
84
import Ganeti.Types
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
-- | Max vcpu ratio (random value).
101
maxVcpuRatio :: Double
102
maxVcpuRatio = 1024.0
103

    
104
-- | Max spindle ratio (random value).
105
maxSpindleRatio :: Double
106
maxSpindleRatio = 1024.0
107

    
108
-- | Max nodes, used just to limit arbitrary instances for smaller
109
-- opcode definitions (e.g. list of nodes in OpTestDelay).
110
maxNodes :: Int
111
maxNodes = 32
112

    
113
-- | Max opcodes or jobs in a submit job and submit many jobs.
114
maxOpCodes :: Int
115
maxOpCodes = 16
116

    
117
-- * Helper functions
118

    
119
-- | Checks for equality with proper annotation. The first argument is
120
-- the computed value, the second one the expected value.
121
(==?) :: (Show a, Eq a) => a -> a -> Property
122
(==?) x y = printTestCase
123
            ("Expected equality, but got mismatch\nexpected: " ++
124
             show y ++ "\n but got: " ++ show x) (x == y)
125
infix 3 ==?
126

    
127
-- | Checks for inequality with proper annotation. The first argument
128
-- is the computed value, the second one the expected (not equal)
129
-- value.
130
(/=?) :: (Show a, Eq a) => a -> a -> Property
131
(/=?) x y = printTestCase
132
            ("Expected inequality, but got equality: '" ++
133
             show x ++ "'.") (x /= y)
134
infix 3 /=?
135

    
136
-- | Show a message and fail the test.
137
failTest :: String -> Property
138
failTest msg = printTestCase msg False
139

    
140
-- | A 'True' property.
141
passTest :: Property
142
passTest = property True
143

    
144
-- | Return the python binary to use. If the PYTHON environment
145
-- variable is defined, use its value, otherwise use just \"python\".
146
pythonCmd :: IO String
147
pythonCmd = catchJust (guard . isDoesNotExistError)
148
            (getEnv "PYTHON") (const (return "python"))
149

    
150
-- | Run Python with an expression, returning the exit code, standard
151
-- output and error.
152
runPython :: String -> String -> IO (ExitCode, String, String)
153
runPython expr stdin = do
154
  py_binary <- pythonCmd
155
  readProcessWithExitCode py_binary ["-c", expr] stdin
156

    
157
-- | Check python exit code, and fail via HUnit assertions if
158
-- non-zero. Otherwise, return the standard output.
159
checkPythonResult :: (ExitCode, String, String) -> IO String
160
checkPythonResult (py_code, py_stdout, py_stderr) = do
161
  HUnit.assertEqual ("python exited with error: " ++ py_stderr)
162
       ExitSuccess py_code
163
  return py_stdout
164

    
165
-- * Arbitrary instances
166

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

    
170
instance Arbitrary DNSChar where
171
  arbitrary = liftM DNSChar $ elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
172

    
173
instance Show DNSChar where
174
  show = show . dnsGetChar
175

    
176
-- | Generates a single name component.
177
genName :: Gen String
178
genName = do
179
  n <- choose (1, 16)
180
  dn <- vector n
181
  return (map dnsGetChar dn)
182

    
183
-- | Generates an entire FQDN.
184
genFQDN :: Gen String
185
genFQDN = do
186
  ncomps <- choose (1, 4)
187
  names <- vectorOf ncomps genName
188
  return $ intercalate "." names
189

    
190
-- | Combinator that generates a 'Maybe' using a sub-combinator.
191
genMaybe :: Gen a -> Gen (Maybe a)
192
genMaybe subgen = frequency [ (1, pure Nothing), (3, Just <$> subgen) ]
193

    
194
-- | Defines a tag type.
195
newtype TagChar = TagChar { tagGetChar :: Char }
196

    
197
-- | All valid tag chars. This doesn't need to match _exactly_
198
-- Ganeti's own tag regex, just enough for it to be close.
199
tagChar :: String
200
tagChar = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ ".+*/:@-"
201

    
202
instance Arbitrary TagChar where
203
  arbitrary = liftM TagChar $ elements tagChar
204

    
205
-- | Generates a tag
206
genTag :: Gen [TagChar]
207
genTag = do
208
  -- the correct value would be C.maxTagLen, but that's way too
209
  -- verbose in unittests, and at the moment I don't see any possible
210
  -- bugs with longer tags and the way we use tags in htools
211
  n <- choose (1, 10)
212
  vector n
213

    
214
-- | Generates a list of tags (correctly upper bounded).
215
genTags :: Gen [String]
216
genTags = do
217
  -- the correct value would be C.maxTagsPerObj, but per the comment
218
  -- in genTag, we don't use tags enough in htools to warrant testing
219
  -- such big values
220
  n <- choose (0, 10::Int)
221
  tags <- mapM (const genTag) [1..n]
222
  return $ map (map tagGetChar) tags
223

    
224
-- | Generates a fields list. This uses the same character set as a
225
-- DNS name (just for simplicity).
226
genFields :: Gen [String]
227
genFields = do
228
  n <- choose (1, 32)
229
  vectorOf n genName
230

    
231
-- | Generates a list of a given size with non-duplicate elements.
232
genUniquesList :: (Eq a, Arbitrary a, Ord a) => Int -> Gen a -> Gen [a]
233
genUniquesList cnt generator = do
234
  set <- foldM (\set _ -> do
235
                  newelem <- generator `suchThat` (`Set.notMember` set)
236
                  return (Set.insert newelem set)) Set.empty [1..cnt]
237
  return $ Set.toList set
238

    
239
newtype SmallRatio = SmallRatio Double deriving Show
240
instance Arbitrary SmallRatio where
241
  arbitrary = liftM SmallRatio $ choose (0, 1)
242

    
243
-- | Helper for 'genSet', declared separately due to type constraints.
244
genSetHelper :: (Ord a) => [a] -> Maybe Int -> Gen (Set.Set a)
245
genSetHelper candidates size = do
246
  size' <- case size of
247
             Nothing -> choose (0, length candidates)
248
             Just s | s > length candidates ->
249
                        error $ "Invalid size " ++ show s ++ ", maximum is " ++
250
                                show (length candidates)
251
                    | otherwise -> return s
252
  foldM (\set _ -> do
253
           newelem <- elements candidates `suchThat` (`Set.notMember` set)
254
           return (Set.insert newelem set)) Set.empty [1..size']
255

    
256
-- | Generates a set of arbitrary elements.
257
genSet :: (Ord a, Bounded a, Enum a) => Maybe Int -> Gen (Set.Set a)
258
genSet = genSetHelper [minBound..maxBound]
259

    
260
-- | Generate an arbitrary IPv4 address in textual form (non empty).
261
genIp4Addr :: Gen NonEmptyString
262
genIp4Addr = genIp4AddrStr >>= mkNonEmpty
263

    
264
-- | Generate an arbitrary IPv4 address in textual form.
265
genIp4AddrStr :: Gen String
266
genIp4AddrStr = do
267
  a <- choose (1::Int, 255)
268
  b <- choose (0::Int, 255)
269
  c <- choose (0::Int, 255)
270
  d <- choose (0::Int, 255)
271
  return $ intercalate "." (map show [a, b, c, d])
272

    
273
-- | Generates an arbitrary IPv4 address with a given netmask in textual form.
274
genIp4NetWithNetmask :: Int -> Gen NonEmptyString
275
genIp4NetWithNetmask netmask = do
276
  ip <- genIp4AddrStr
277
  mkNonEmpty $ ip ++ "/" ++ show netmask
278

    
279
-- | Generate an arbitrary IPv4 network in textual form.
280
genIp4Net :: Gen NonEmptyString
281
genIp4Net = do
282
  netmask <- choose (8::Int, 30)
283
  genIp4NetWithNetmask netmask
284

    
285
-- | Helper function to compute the number of hosts in a network
286
-- given the netmask. (For IPv4 only.)
287
netmask2NumHosts :: Word8 -> Int
288
netmask2NumHosts n = 2^(32-n)
289

    
290
-- | Generates an arbitrary IPv6 network address in textual form.
291
-- The generated address is not simpflified, e. g. an address like
292
-- "2607:f0d0:1002:0051:0000:0000:0000:0004" does not become
293
-- "2607:f0d0:1002:51::4"
294
genIp6Addr :: Gen String
295
genIp6Addr = do
296
  rawIp <- vectorOf 8 $ choose (0::Integer, 65535)
297
  return $ intercalate ":" (map (`showHex` "") rawIp)
298

    
299
-- | Generates an arbitrary IPv6 network in textual form.
300
genIp6Net :: Gen String
301
genIp6Net = do
302
  netmask <- choose (8::Int, 126)
303
  ip <- genIp6Addr
304
  return $ ip ++ "/" ++ show netmask
305

    
306
-- * Helper functions
307

    
308
-- | Checks for serialisation idempotence.
309
testSerialisation :: (Eq a, Show a, J.JSON a) => a -> Property
310
testSerialisation a =
311
  case J.readJSON (J.showJSON a) of
312
    J.Error msg -> failTest $ "Failed to deserialise: " ++ msg
313
    J.Ok a' -> a ==? a'
314

    
315
-- | Result to PropertyM IO.
316
resultProp :: (Show a) => BasicTypes.GenericResult a b -> PropertyM IO b
317
resultProp (BasicTypes.Bad err) = stop . failTest $ show err
318
resultProp (BasicTypes.Ok  val) = return val
319

    
320
-- | Return the source directory of Ganeti.
321
getSourceDir :: IO FilePath
322
getSourceDir = catchJust (guard . isDoesNotExistError)
323
            (getEnv "TOP_SRCDIR")
324
            (const (return "."))
325

    
326
-- | Returns the path of a file in the test data directory, given its name.
327
testDataFilename :: String -> String -> IO FilePath
328
testDataFilename datadir name = do
329
        src <- getSourceDir
330
        return $ src ++ datadir ++ name
331

    
332
-- | Returns the content of the specified haskell test data file.
333
readTestData :: String -> IO String
334
readTestData filename = do
335
    name <- testDataFilename "/test/data/" filename
336
    readFile name
337

    
338
-- | Generate arbitrary values in the IO monad. This is a simple
339
-- wrapper over 'sample''.
340
genSample :: Gen a -> IO a
341
genSample gen = do
342
  values <- sample' gen
343
  case values of
344
    [] -> error "sample' returned an empty list of values??"
345
    x:_ -> return x
346

    
347
-- | Function for testing whether a file is parsed correctly.
348
testParser :: (Show a, Eq a) => Parser a -> String -> a -> HUnit.Assertion
349
testParser parser fileName expectedContent = do
350
  fileContent <- readTestData fileName
351
  case parseOnly parser $ pack fileContent of
352
    Left msg -> HUnit.assertFailure $ "Parsing failed: " ++ msg
353
    Right obtained -> HUnit.assertEqual fileName expectedContent obtained