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
53 , genIp4NetWithNetmask
64 import Control.Applicative
65 import Control.Exception (catchJust)
68 import qualified Data.Set as Set
69 import System.Environment (getEnv)
70 import System.Exit (ExitCode(..))
71 import System.IO.Error (isDoesNotExistError)
72 import System.Process (readProcessWithExitCode)
73 import qualified Test.HUnit as HUnit
74 import Test.QuickCheck
75 import Test.QuickCheck.Monadic
76 import qualified Text.JSON as J
79 import qualified Ganeti.BasicTypes as BasicTypes
84 -- | Maximum memory (1TiB, somewhat random value).
88 -- | Maximum disk (8TiB, somewhat random value).
90 maxDsk = 1024 * 1024 * 8
92 -- | Max CPUs (1024, somewhat random value).
96 -- | Max vcpu ratio (random value).
97 maxVcpuRatio :: Double
100 -- | Max spindle ratio (random value).
101 maxSpindleRatio :: Double
102 maxSpindleRatio = 1024.0
104 -- | Max nodes, used just to limit arbitrary instances for smaller
105 -- opcode definitions (e.g. list of nodes in OpTestDelay).
109 -- | Max opcodes or jobs in a submit job and submit many jobs.
113 -- * Helper functions
115 -- | Checks for equality with proper annotation. The first argument is
116 -- the computed value, the second one the expected value.
117 (==?) :: (Show a, Eq a) => a -> a -> Property
118 (==?) x y = printTestCase
119 ("Expected equality, but got mismatch\nexpected: " ++
120 show y ++ "\n but got: " ++ show x) (x == y)
123 -- | Checks for inequality with proper annotation. The first argument
124 -- is the computed value, the second one the expected (not equal)
126 (/=?) :: (Show a, Eq a) => a -> a -> Property
127 (/=?) x y = printTestCase
128 ("Expected inequality, but got equality: '" ++
129 show x ++ "'.") (x /= y)
132 -- | Show a message and fail the test.
133 failTest :: String -> Property
134 failTest msg = printTestCase msg False
136 -- | A 'True' property.
138 passTest = property True
140 -- | Return the python binary to use. If the PYTHON environment
141 -- variable is defined, use its value, otherwise use just \"python\".
142 pythonCmd :: IO String
143 pythonCmd = catchJust (guard . isDoesNotExistError)
144 (getEnv "PYTHON") (const (return "python"))
146 -- | Run Python with an expression, returning the exit code, standard
148 runPython :: String -> String -> IO (ExitCode, String, String)
149 runPython expr stdin = do
150 py_binary <- pythonCmd
151 readProcessWithExitCode py_binary ["-c", expr] stdin
153 -- | Check python exit code, and fail via HUnit assertions if
154 -- non-zero. Otherwise, return the standard output.
155 checkPythonResult :: (ExitCode, String, String) -> IO String
156 checkPythonResult (py_code, py_stdout, py_stderr) = do
157 HUnit.assertEqual ("python exited with error: " ++ py_stderr)
161 -- * Arbitrary instances
163 -- | Defines a DNS name.
164 newtype DNSChar = DNSChar { dnsGetChar::Char }
166 instance Arbitrary DNSChar where
167 arbitrary = liftM DNSChar $ elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
169 instance Show DNSChar where
170 show = show . dnsGetChar
172 -- | Generates a single name component.
173 genName :: Gen String
177 return (map dnsGetChar dn)
179 -- | Generates an entire FQDN.
180 genFQDN :: Gen String
182 ncomps <- choose (1, 4)
183 names <- vectorOf ncomps genName
184 return $ intercalate "." names
186 -- | Combinator that generates a 'Maybe' using a sub-combinator.
187 genMaybe :: Gen a -> Gen (Maybe a)
188 genMaybe subgen = frequency [ (1, pure Nothing), (3, Just <$> subgen) ]
190 -- | Defines a tag type.
191 newtype TagChar = TagChar { tagGetChar :: Char }
193 -- | All valid tag chars. This doesn't need to match _exactly_
194 -- Ganeti's own tag regex, just enough for it to be close.
196 tagChar = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ ".+*/:@-"
198 instance Arbitrary TagChar where
199 arbitrary = liftM TagChar $ elements tagChar
202 genTag :: Gen [TagChar]
204 -- the correct value would be C.maxTagLen, but that's way too
205 -- verbose in unittests, and at the moment I don't see any possible
206 -- bugs with longer tags and the way we use tags in htools
210 -- | Generates a list of tags (correctly upper bounded).
211 genTags :: Gen [String]
213 -- the correct value would be C.maxTagsPerObj, but per the comment
214 -- in genTag, we don't use tags enough in htools to warrant testing
216 n <- choose (0, 10::Int)
217 tags <- mapM (const genTag) [1..n]
218 return $ map (map tagGetChar) tags
220 -- | Generates a fields list. This uses the same character set as a
221 -- DNS name (just for simplicity).
222 genFields :: Gen [String]
227 -- | Generates a list of a given size with non-duplicate elements.
228 genUniquesList :: (Eq a, Arbitrary a, Ord a) => Int -> Gen a -> Gen [a]
229 genUniquesList cnt generator = do
230 set <- foldM (\set _ -> do
231 newelem <- generator `suchThat` (`Set.notMember` set)
232 return (Set.insert newelem set)) Set.empty [1..cnt]
233 return $ Set.toList set
235 newtype SmallRatio = SmallRatio Double deriving Show
236 instance Arbitrary SmallRatio where
237 arbitrary = liftM SmallRatio $ choose (0, 1)
239 -- | Helper for 'genSet', declared separately due to type constraints.
240 genSetHelper :: (Ord a) => [a] -> Maybe Int -> Gen (Set.Set a)
241 genSetHelper candidates size = do
242 size' <- case size of
243 Nothing -> choose (0, length candidates)
244 Just s | s > length candidates ->
245 error $ "Invalid size " ++ show s ++ ", maximum is " ++
246 show (length candidates)
247 | otherwise -> return s
249 newelem <- elements candidates `suchThat` (`Set.notMember` set)
250 return (Set.insert newelem set)) Set.empty [1..size']
252 -- | Generates a set of arbitrary elements.
253 genSet :: (Ord a, Bounded a, Enum a) => Maybe Int -> Gen (Set.Set a)
254 genSet = genSetHelper [minBound..maxBound]
256 -- | Generate an arbitrary IPv4 address in textual form (non empty).
257 genIp4Addr :: Gen NonEmptyString
258 genIp4Addr = genIp4AddrStr >>= mkNonEmpty
260 -- | Generate an arbitrary IPv4 address in textual form.
261 genIp4AddrStr :: Gen String
263 a <- choose (1::Int, 255)
264 b <- choose (0::Int, 255)
265 c <- choose (0::Int, 255)
266 d <- choose (0::Int, 255)
267 return $ intercalate "." (map show [a, b, c, d])
269 -- | Generates an arbitrary IPv4 address with a given netmask in textual form.
270 genIp4NetWithNetmask :: Int -> Gen NonEmptyString
271 genIp4NetWithNetmask netmask = do
273 mkNonEmpty $ ip ++ "/" ++ show netmask
275 -- | Generate an arbitrary IPv4 network in textual form.
276 genIp4Net :: Gen NonEmptyString
278 netmask <- choose (8::Int, 30)
279 genIp4NetWithNetmask netmask
281 -- | Helper function to compute the number of hosts in a network
282 -- given the netmask. (For IPv4 only.)
283 netmask2NumHosts :: Int -> Int
284 netmask2NumHosts n = 2^(32-n)
286 -- | Generates an arbitrary IPv6 network address in textual form.
287 -- The generated address is not simpflified, e. g. an address like
288 -- "2607:f0d0:1002:0051:0000:0000:0000:0004" does not become
289 -- "2607:f0d0:1002:51::4"
290 genIp6Addr :: Gen String
292 rawIp <- vectorOf 8 $ choose (0::Integer, 65535)
293 return $ intercalate ":" (map (`showHex` "") rawIp)
295 -- | Generates an arbitrary IPv6 network in textual form.
296 genIp6Net :: Gen String
298 netmask <- choose (8::Int, 126)
300 return $ ip ++ "/" ++ show netmask
302 -- * Helper functions
304 -- | Checks for serialisation idempotence.
305 testSerialisation :: (Eq a, Show a, J.JSON a) => a -> Property
306 testSerialisation a =
307 case J.readJSON (J.showJSON a) of
308 J.Error msg -> failTest $ "Failed to deserialise: " ++ msg
311 -- | Result to PropertyM IO.
312 resultProp :: (Show a) => BasicTypes.GenericResult a b -> PropertyM IO b
313 resultProp (BasicTypes.Bad err) = stop . failTest $ show err
314 resultProp (BasicTypes.Ok val) = return val
316 -- | Return the source directory of Ganeti.
317 getSourceDir :: IO FilePath
318 getSourceDir = catchJust (guard . isDoesNotExistError)
319 (getEnv "TOP_SRCDIR")
322 -- | Returns the path of a file in the test data directory, given its name.
323 testDataFilename :: String -> String -> IO FilePath
324 testDataFilename datadir name = do
326 return $ src ++ datadir ++ name
328 -- | Returns the content of the specified haskell test data file.
329 readTestData :: String -> IO String
330 readTestData filename = do
331 name <- testDataFilename "/test/data/" filename
334 -- | Generate arbitrary values in the IO monad. This is a simple
335 -- wrapper over 'sample''.
336 genSample :: Gen a -> IO a
338 values <- sample' gen
340 [] -> error "sample' returned an empty list of values??"