1 {-| Unittest helpers for ganeti-htools.
7 Copyright (C) 2009, 2010, 2011, 2012, 2013 Google Inc.
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.
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.
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
26 module Test.Ganeti.TestCommon
71 import Control.Applicative
72 import Control.Exception (catchJust)
74 import Data.Attoparsec.Text (Parser, parseOnly)
76 import Data.Text (pack)
78 import qualified Data.Set as Set
79 import System.Environment (getEnv)
80 import System.Exit (ExitCode(..))
81 import System.IO.Error (isDoesNotExistError)
82 import System.Process (readProcessWithExitCode)
83 import qualified Test.HUnit as HUnit
84 import Test.QuickCheck
85 import Test.QuickCheck.Monadic
86 import qualified Text.JSON as J
89 import qualified Ganeti.BasicTypes as BasicTypes
94 -- | Maximum memory (1TiB, somewhat random value).
98 -- | Maximum disk (8TiB, somewhat random value).
100 maxDsk = 1024 * 1024 * 8
102 -- | Max CPUs (1024, somewhat random value).
106 -- | Max spindles (1024, somewhat random value).
110 -- | Max vcpu ratio (random value).
111 maxVcpuRatio :: Double
112 maxVcpuRatio = 1024.0
114 -- | Max spindle ratio (random value).
115 maxSpindleRatio :: Double
116 maxSpindleRatio = 1024.0
118 -- | Max nodes, used just to limit arbitrary instances for smaller
119 -- opcode definitions (e.g. list of nodes in OpTestDelay).
123 -- | Max opcodes or jobs in a submit job and submit many jobs.
127 -- * Helper functions
129 -- | Checks for equality with proper annotation. The first argument is
130 -- the computed value, the second one the expected value.
131 (==?) :: (Show a, Eq a) => a -> a -> Property
132 (==?) x y = printTestCase
133 ("Expected equality, but got mismatch\nexpected: " ++
134 show y ++ "\n but got: " ++ show x) (x == y)
137 -- | Checks for inequality with proper annotation. The first argument
138 -- is the computed value, the second one the expected (not equal)
140 (/=?) :: (Show a, Eq a) => a -> a -> Property
141 (/=?) x y = printTestCase
142 ("Expected inequality, but got equality: '" ++
143 show x ++ "'.") (x /= y)
146 -- | Show a message and fail the test.
147 failTest :: String -> Property
148 failTest msg = printTestCase msg False
150 -- | A 'True' property.
152 passTest = property True
154 -- | Return the python binary to use. If the PYTHON environment
155 -- variable is defined, use its value, otherwise use just \"python\".
156 pythonCmd :: IO String
157 pythonCmd = catchJust (guard . isDoesNotExistError)
158 (getEnv "PYTHON") (const (return "python"))
160 -- | Run Python with an expression, returning the exit code, standard
162 runPython :: String -> String -> IO (ExitCode, String, String)
163 runPython expr stdin = do
164 py_binary <- pythonCmd
165 readProcessWithExitCode py_binary ["-c", expr] stdin
167 -- | Check python exit code, and fail via HUnit assertions if
168 -- non-zero. Otherwise, return the standard output.
169 checkPythonResult :: (ExitCode, String, String) -> IO String
170 checkPythonResult (py_code, py_stdout, py_stderr) = do
171 HUnit.assertEqual ("python exited with error: " ++ py_stderr)
175 -- * Arbitrary instances
177 -- | Defines a DNS name.
178 newtype DNSChar = DNSChar { dnsGetChar::Char }
180 instance Arbitrary DNSChar where
181 arbitrary = liftM DNSChar $ elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
183 instance Show DNSChar where
184 show = show . dnsGetChar
186 -- | Generates a single name component.
187 genName :: Gen String
191 return (map dnsGetChar dn)
193 -- | Generates an entire FQDN.
194 genFQDN :: Gen String
196 ncomps <- choose (1, 4)
197 names <- vectorOf ncomps genName
198 return $ intercalate "." names
200 -- | Generates a UUID-like string.
202 -- Only to be used for QuickCheck testing. For obtaining actual UUIDs use
203 -- the newUUID function in Ganeti.Utils
204 genUUID :: Gen String
213 return $ map dnsGetChar c1 ++ "-" ++ map dnsGetChar c2 ++ "-" ++
214 map dnsGetChar c3 ++ "-" ++ map dnsGetChar c4 ++ "-" ++
215 map dnsGetChar c5 ++ "-" ++ map dnsGetChar c6 ++ "-" ++
218 -- | Combinator that generates a 'Maybe' using a sub-combinator.
219 genMaybe :: Gen a -> Gen (Maybe a)
220 genMaybe subgen = frequency [ (1, pure Nothing), (3, Just <$> subgen) ]
222 -- | Defines a tag type.
223 newtype TagChar = TagChar { tagGetChar :: Char }
225 -- | All valid tag chars. This doesn't need to match _exactly_
226 -- Ganeti's own tag regex, just enough for it to be close.
228 tagChar = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ ".+*/:@-"
230 instance Arbitrary TagChar where
231 arbitrary = liftM TagChar $ elements tagChar
234 genTag :: Gen [TagChar]
236 -- the correct value would be C.maxTagLen, but that's way too
237 -- verbose in unittests, and at the moment I don't see any possible
238 -- bugs with longer tags and the way we use tags in htools
242 -- | Generates a list of tags (correctly upper bounded).
243 genTags :: Gen [String]
245 -- the correct value would be C.maxTagsPerObj, but per the comment
246 -- in genTag, we don't use tags enough in htools to warrant testing
248 n <- choose (0, 10::Int)
249 tags <- mapM (const genTag) [1..n]
250 return $ map (map tagGetChar) tags
252 -- | Generates a fields list. This uses the same character set as a
253 -- DNS name (just for simplicity).
254 genFields :: Gen [String]
259 -- | Generates a list of a given size with non-duplicate elements.
260 genUniquesList :: (Eq a, Arbitrary a, Ord a) => Int -> Gen a -> Gen [a]
261 genUniquesList cnt generator = do
262 set <- foldM (\set _ -> do
263 newelem <- generator `suchThat` (`Set.notMember` set)
264 return (Set.insert newelem set)) Set.empty [1..cnt]
265 return $ Set.toList set
267 newtype SmallRatio = SmallRatio Double deriving Show
268 instance Arbitrary SmallRatio where
269 arbitrary = liftM SmallRatio $ choose (0, 1)
271 -- | Helper for 'genSet', declared separately due to type constraints.
272 genSetHelper :: (Ord a) => [a] -> Maybe Int -> Gen (Set.Set a)
273 genSetHelper candidates size = do
274 size' <- case size of
275 Nothing -> choose (0, length candidates)
276 Just s | s > length candidates ->
277 error $ "Invalid size " ++ show s ++ ", maximum is " ++
278 show (length candidates)
279 | otherwise -> return s
281 newelem <- elements candidates `suchThat` (`Set.notMember` set)
282 return (Set.insert newelem set)) Set.empty [1..size']
284 -- | Generates a 'Set' of arbitrary elements.
285 genSet :: (Ord a, Bounded a, Enum a) => Maybe Int -> Gen (Set.Set a)
286 genSet = genSetHelper [minBound..maxBound]
288 -- | Generates a 'Set' of arbitrary elements wrapped in a 'ListSet'
289 genListSet :: (Ord a, Bounded a, Enum a) => Maybe Int
290 -> Gen (BasicTypes.ListSet a)
291 genListSet is = BasicTypes.ListSet <$> genSet is
293 -- | Generate an arbitrary IPv4 address in textual form.
294 genIPv4 :: Gen String
296 a <- choose (1::Int, 255)
297 b <- choose (0::Int, 255)
298 c <- choose (0::Int, 255)
299 d <- choose (0::Int, 255)
300 return . intercalate "." $ map show [a, b, c, d]
302 genIPv4Address :: Gen IPv4Address
303 genIPv4Address = mkIPv4Address =<< genIPv4
305 -- | Generate an arbitrary IPv4 network in textual form.
306 genIPv4AddrRange :: Gen String
307 genIPv4AddrRange = do
309 netmask <- choose (8::Int, 30)
310 return $ ip ++ "/" ++ show netmask
312 genIPv4Network :: Gen IPv4Network
313 genIPv4Network = mkIPv4Network =<< genIPv4AddrRange
315 -- | Helper function to compute the number of hosts in a network
316 -- given the netmask. (For IPv4 only.)
317 netmask2NumHosts :: Word8 -> Int
318 netmask2NumHosts n = 2^(32-n)
320 -- | Generates an arbitrary IPv6 network address in textual form.
321 -- The generated address is not simpflified, e. g. an address like
322 -- "2607:f0d0:1002:0051:0000:0000:0000:0004" does not become
323 -- "2607:f0d0:1002:51::4"
324 genIp6Addr :: Gen String
326 rawIp <- vectorOf 8 $ choose (0::Integer, 65535)
327 return $ intercalate ":" (map (`showHex` "") rawIp)
329 -- | Generates an arbitrary IPv6 network in textual form.
330 genIp6Net :: Gen String
332 netmask <- choose (8::Int, 126)
334 return $ ip ++ "/" ++ show netmask
336 -- | Generates a valid, arbitrary tag name with respect to the given
337 -- 'TagKind' for opcodes.
338 genOpCodesTagName :: TagKind -> Gen (Maybe String)
339 genOpCodesTagName TagKindCluster = return Nothing
340 genOpCodesTagName _ = Just <$> genFQDN
342 -- | Generates a valid, arbitrary tag name with respect to the given
343 -- 'TagKind' for Luxi.
344 genLuxiTagName :: TagKind -> Gen String
345 genLuxiTagName TagKindCluster = return ""
346 genLuxiTagName _ = genFQDN
348 -- * Helper functions
350 -- | Checks for serialisation idempotence.
351 testSerialisation :: (Eq a, Show a, J.JSON a) => a -> Property
352 testSerialisation a =
353 case J.readJSON (J.showJSON a) of
354 J.Error msg -> failTest $ "Failed to deserialise: " ++ msg
357 -- | Result to PropertyM IO.
358 resultProp :: (Show a) => BasicTypes.GenericResult a b -> PropertyM IO b
359 resultProp (BasicTypes.Bad err) = stop . failTest $ show err
360 resultProp (BasicTypes.Ok val) = return val
362 -- | Return the source directory of Ganeti.
363 getSourceDir :: IO FilePath
364 getSourceDir = catchJust (guard . isDoesNotExistError)
365 (getEnv "TOP_SRCDIR")
368 -- | Returns the path of a file in the test data directory, given its name.
369 testDataFilename :: String -> String -> IO FilePath
370 testDataFilename datadir name = do
372 return $ src ++ datadir ++ name
374 -- | Returns the content of the specified haskell test data file.
375 readTestData :: String -> IO String
376 readTestData filename = do
377 name <- testDataFilename "/test/data/" filename
380 -- | Generate arbitrary values in the IO monad. This is a simple
381 -- wrapper over 'sample''.
382 genSample :: Gen a -> IO a
384 values <- sample' gen
386 [] -> error "sample' returned an empty list of values??"
389 -- | Function for testing whether a file is parsed correctly.
390 testParser :: (Show a, Eq a) => Parser a -> String -> a -> HUnit.Assertion
391 testParser parser fileName expectedContent = do
392 fileContent <- readTestData fileName
393 case parseOnly parser $ pack fileContent of
394 Left msg -> HUnit.assertFailure $ "Parsing failed: " ++ msg
395 Right obtained -> HUnit.assertEqual fileName expectedContent obtained
397 -- | Generate a property test for parsers.
398 genPropParser :: (Show a, Eq a) => Parser a -> String -> a -> Property
399 genPropParser parser s expected =
400 case parseOnly parser $ pack s of
401 Left msg -> failTest $ "Parsing failed: " ++ msg
402 Right obtained -> expected ==? obtained
404 -- | Generate an arbitrary non negative integer number
405 genNonNegative :: Gen Int
407 fmap fromIntegral (arbitrary::Gen (Test.QuickCheck.NonNegative Int))
409 -- | Computes the relative error of two 'Double' numbers.
411 -- This is the \"relative error\" algorithm in
412 -- http:\/\/randomascii.wordpress.com\/2012\/02\/25\/
413 -- comparing-floating-point-numbers-2012-edition (URL split due to too
415 relativeError :: Double -> Double -> Double
416 relativeError d1 d2 =
417 let delta = abs $ d1 - d2
423 else delta / greatest