diff --git a/htest/Test/Ganeti/Network.hs b/htest/Test/Ganeti/Network.hs index 6df8b82c2dd710593fe95dce6bf254f4b8b53510..4867830c95d90433483e542ccabcee2d08444a15 100644 --- a/htest/Test/Ganeti/Network.hs +++ b/htest/Test/Ganeti/Network.hs @@ -3,75 +3,34 @@ module Test.Ganeti.Network ( testNetwork + , genBitStringMaxLen + , genNetworkType ) where import Test.QuickCheck -import Control.Monad - import Ganeti.Network as Network import Ganeti.Objects as Objects -import Ganeti.Types -import Test.Ganeti.Query.Language (genJSValue) +import Test.Ganeti.Objects + ( genBitStringMaxLen + , genNetworkType + , genValidNetwork ) import Test.Ganeti.TestHelper import Test.Ganeti.TestCommon import qualified Data.Vector.Unboxed as V -import qualified Data.Set as S -- * Generators and arbitrary instances --- | Generate an arbitrary string consisting of '0' and '1' of the given length. -genBitString :: Int -> Gen String -genBitString len = vectorOf len (elements "01") - --- | Generate an arbitrary string consisting of '0' and '1' of the maximum given --- length. -genBitStringMaxLen :: Int -> Gen String -genBitStringMaxLen maxLen = choose (0, maxLen) >>= genBitString - --- | Generates an arbitrary bit vector of the given length. -genBitVector :: Int -> Gen (V.Vector Bool) -genBitVector len = do - boolList <- vector len::Gen [Bool] - return $ V.fromList boolList - --- | Generates a network instance with bit vectors of the given lengths for --- reservations and external reservations. -genValidNetwork :: Int -> Gen Objects.Network -genValidNetwork maxLenBitStr = do - lenBitStr <- choose (0, maxLenBitStr) - name <- genName >>= mkNonEmpty - network_type <- genMaybe genNetworkType - mac_prefix <- genMaybe genName - fam <- arbitrary - net <- genName >>= mkNonEmpty - net6 <- genMaybe genName - gateway <- genMaybe genName - gateway6 <- genMaybe genName - size <- genMaybe genJSValue - res <- liftM Just (genBitString lenBitStr) - ext_res <- liftM Just (genBitString lenBitStr) - let n = Network name network_type mac_prefix fam net net6 gateway - gateway6 size res ext_res 0 S.empty - return n - --- | Generates an arbitrary network type. -genNetworkType :: Gen NetworkType -genNetworkType = elements [ PrivateNetwork, PublicNetwork ] - --- | Network instances are generated arbitrarily only with short bit strings to --- not slow down the test execution too much. -instance Arbitrary Objects.Network where - arbitrary = genValidNetwork 256 - -- | Generates address pools. The size of the network is intentionally -- decoupled from the size of the bit vectors, to avoid slowing down -- the tests by generating unnecessary bit strings. genAddressPool :: Int -> Gen AddressPool genAddressPool maxLenBitVec = do - net <- arbitrary + -- Generating networks with netmask of minimum /24 to avoid too long + -- bit strings being generated. + net <- genValidNetwork lenBitVec <- choose (0, maxLenBitVec) res <- genBitVector lenBitVec ext_res <- genBitVector lenBitVec @@ -79,6 +38,12 @@ genAddressPool maxLenBitVec = do , reservations = res , extReservations = ext_res } +-- | Generates an arbitrary bit vector of the given length. +genBitVector :: Int -> Gen (V.Vector Bool) +genBitVector len = do + boolList <- vector len::Gen [Bool] + return $ V.fromList boolList + instance Arbitrary AddressPool where arbitrary = genAddressPool ((2::Int)^(8::Int)) diff --git a/htest/Test/Ganeti/Objects.hs b/htest/Test/Ganeti/Objects.hs index 7e3e9919f44a54811cbd4ee146a5a56430ab757b..03da9e7df54041817310a3eef41262df552d5cb1 100644 --- a/htest/Test/Ganeti/Objects.hs +++ b/htest/Test/Ganeti/Objects.hs @@ -30,13 +30,20 @@ module Test.Ganeti.Objects ( testObjects , Node(..) , genEmptyCluster + , genValidNetwork + , genNetworkType + , genBitStringMaxLen ) where import Test.QuickCheck +import qualified Test.HUnit as HUnit import Control.Applicative +import Control.Monad +import Data.Char import qualified Data.Map as Map import qualified Data.Set as Set +import qualified Text.JSON as J import Test.Ganeti.Query.Language (genJSValue) import Test.Ganeti.TestHelper @@ -44,8 +51,10 @@ import Test.Ganeti.TestCommon import Test.Ganeti.Types () import qualified Ganeti.Constants as C +import Ganeti.Network import Ganeti.Objects as Objects import Ganeti.JSON +import Ganeti.Types {-# ANN module "HLint: ignore Use camelCase" #-} @@ -164,33 +173,42 @@ instance Arbitrary TagSet where $(genArbitrary ''Cluster) instance Arbitrary Network where - arbitrary = Network <$> - -- name - arbitrary - -- network_type - <*> arbitrary - -- mac_prefix - <*> arbitrary - -- family - <*> arbitrary - -- network - <*> arbitrary - -- network6 - <*> arbitrary - -- gateway - <*> arbitrary - -- gateway6 - <*> arbitrary - -- size - <*> genMaybe genJSValue - -- reservations - <*> arbitrary - -- external reservations - <*> arbitrary - -- serial - <*> arbitrary - -- tags - <*> (Set.fromList <$> genTags) + arbitrary = genValidNetwork + +-- | Generates a network instance with minimum netmasks of /24. Generating +-- bigger networks slows down the tests, because long bit strings are generated +-- for the reservations. +genValidNetwork :: Gen Objects.Network +genValidNetwork = do + -- generate netmask for the IPv4 network + netmask <- choose (24::Int, 30) + name <- genName >>= mkNonEmpty + network_type <- genMaybe genNetworkType + mac_prefix <- genMaybe genName + fam <- arbitrary + net <- genIp4NetWithNetmask netmask + net6 <- genMaybe genIp6Net + gateway <- genMaybe genIp4AddrStr + gateway6 <- genMaybe genIp6Addr + size <- genMaybe genJSValue + res <- liftM Just (genBitString $ netmask2NumHosts netmask) + ext_res <- liftM Just (genBitString $ netmask2NumHosts netmask) + let n = Network name network_type mac_prefix fam net net6 gateway + gateway6 size res ext_res 0 Set.empty + return n + +-- | Generates an arbitrary network type. +genNetworkType :: Gen NetworkType +genNetworkType = elements [ PrivateNetwork, PublicNetwork ] + +-- | Generate an arbitrary string consisting of '0' and '1' of the given length. +genBitString :: Int -> Gen String +genBitString len = vectorOf len (elements "01") + +-- | Generate an arbitrary string consisting of '0' and '1' of the maximum given +-- length. +genBitStringMaxLen :: Int -> Gen String +genBitStringMaxLen maxLen = choose (0, maxLen) >>= genBitString -- | Generator for config data with an empty cluster (no instances), -- with N defined nodes. @@ -261,6 +279,58 @@ prop_Config_serialisation :: Property prop_Config_serialisation = forAll (choose (0, maxNodes `div` 4) >>= genEmptyCluster) testSerialisation +-- | Custom HUnit test to check the correspondence between Haskell-generated +-- networks and their Python decoded, validated and re-encoded version. +-- For the technical background of this unit test, check the documentation +-- of "case_py_compat_types" of htest/Test/Ganeti/Opcodes.hs +case_py_compat_networks :: HUnit.Assertion +case_py_compat_networks = do + let num_networks = 500::Int + sample_networks <- sample' (vectorOf num_networks genValidNetwork) + let networks = head sample_networks + networks_with_properties = map getNetworkProperties networks + serialized = J.encode networks + -- check for non-ASCII fields, usually due to 'arbitrary :: String' + mapM_ (\net -> when (any (not . isAscii) (J.encode net)) . + HUnit.assertFailure $ + "Network has non-ASCII fields: " ++ show net + ) networks + py_stdout <- + runPython "from ganeti import network\n\ + \from ganeti import objects\n\ + \from ganeti import serializer\n\ + \import sys\n\ + \net_data = serializer.Load(sys.stdin.read())\n\ + \decoded = [objects.Network.FromDict(n) for n in net_data]\n\ + \encoded = []\n\ + \for net in decoded:\n\ + \ a = network.AddressPool(net)\n\ + \ encoded.append((a.GetFreeCount(), a.GetReservedCount(), \\\n\ + \ net.ToDict()))\n\ + \print serializer.Dump(encoded)" serialized + >>= checkPythonResult + let deserialised = J.decode py_stdout::J.Result [(Int, Int, Network)] + decoded <- case deserialised of + J.Ok ops -> return ops + J.Error msg -> + HUnit.assertFailure ("Unable to decode networks: " ++ msg) + -- this already raised an expection, but we need it + -- for proper types + >> fail "Unable to decode networks" + HUnit.assertEqual "Mismatch in number of returned networks" + (length decoded) (length networks_with_properties) + mapM_ (uncurry (HUnit.assertEqual "Different result after encoding/decoding") + ) $ zip decoded networks_with_properties + +-- | Creates a tuple of the given network combined with some of its properties +-- to be compared against the same properties generated by the python code. +getNetworkProperties :: Network -> (Int, Int, Network) +getNetworkProperties net = + let maybePool = createAddressPool net + in case maybePool of + (Just pool) -> (getFreeCount pool, getReservedCount pool, net) + Nothing -> (-1, -1, net) + testSuite "Objects" [ 'prop_fillDict , 'prop_Disk_serialisation @@ -268,4 +338,5 @@ testSuite "Objects" , 'prop_Network_serialisation , 'prop_Node_serialisation , 'prop_Config_serialisation + , 'case_py_compat_networks ] diff --git a/htest/Test/Ganeti/OpCodes.hs b/htest/Test/Ganeti/OpCodes.hs index 58107b2787a6eee1d200374ef970cf57bdf411fa..931b2063ea61d59021129e9a483918180c41f865 100644 --- a/htest/Test/Ganeti/OpCodes.hs +++ b/htest/Test/Ganeti/OpCodes.hs @@ -386,22 +386,6 @@ genNamesNE = resize maxNodes (listOf genNameNE) genFieldsNE :: Gen [NonEmptyString] genFieldsNE = genFields >>= mapM mkNonEmpty --- | Generate an arbitrary IPv4 address in textual form. -genIp4Addr :: Gen NonEmptyString -genIp4Addr = do - a <- choose (1::Int, 255) - b <- choose (0::Int, 255) - c <- choose (0::Int, 255) - d <- choose (0::Int, 255) - mkNonEmpty $ intercalate "." (map show [a, b, c, d]) - --- | Generate an arbitrary IPv4 network address in textual form. -genIp4Net :: Gen NonEmptyString -genIp4Net = do - netmask <- choose (8::Int, 30) - ip <- genIp4Addr - mkNonEmpty $ fromNonEmpty ip ++ "/" ++ show netmask - -- | Generate a 3-byte MAC prefix. genMacPrefix :: Gen NonEmptyString genMacPrefix = do diff --git a/htest/Test/Ganeti/TestCommon.hs b/htest/Test/Ganeti/TestCommon.hs index 13878297740afbf91f47a5226ca21fd5035c77af..0a33931da74fd21ae0535f0917fe5cab7dba9dcf 100644 --- a/htest/Test/Ganeti/TestCommon.hs +++ b/htest/Test/Ganeti/TestCommon.hs @@ -38,8 +38,10 @@ import qualified Test.HUnit as HUnit import Test.QuickCheck import Test.QuickCheck.Monadic import qualified Text.JSON as J +import Numeric import qualified Ganeti.BasicTypes as BasicTypes +import Ganeti.Types -- * Constants @@ -215,6 +217,52 @@ genSetHelper candidates size = do genSet :: (Ord a, Bounded a, Enum a) => Maybe Int -> Gen (Set.Set a) genSet = genSetHelper [minBound..maxBound] +-- | Generate an arbitrary IPv4 address in textual form (non empty). +genIp4Addr :: Gen NonEmptyString +genIp4Addr = genIp4AddrStr >>= mkNonEmpty + +-- | Generate an arbitrary IPv4 address in textual form. +genIp4AddrStr :: Gen String +genIp4AddrStr = do + a <- choose (1::Int, 255) + b <- choose (0::Int, 255) + c <- choose (0::Int, 255) + d <- choose (0::Int, 255) + return $ intercalate "." (map show [a, b, c, d]) + +-- | Generates an arbitrary IPv4 address with a given netmask in textual form. +genIp4NetWithNetmask :: Int -> Gen NonEmptyString +genIp4NetWithNetmask netmask = do + ip <- genIp4AddrStr + mkNonEmpty $ ip ++ "/" ++ show netmask + +-- | Generate an arbitrary IPv4 network in textual form. +genIp4Net :: Gen NonEmptyString +genIp4Net = do + netmask <- choose (8::Int, 30) + genIp4NetWithNetmask netmask + +-- | Helper function to compute the number of hosts in a network +-- given the netmask. (For IPv4 only.) +netmask2NumHosts :: Int -> Int +netmask2NumHosts n = (2::Int)^((32::Int)-n) + +-- | Generates an arbitrary IPv6 network address in textual form. +-- The generated address is not simpflified, e. g. an address like +-- "2607:f0d0:1002:0051:0000:0000:0000:0004" does not become +-- "2607:f0d0:1002:51::4" +genIp6Addr :: Gen String +genIp6Addr = do + rawIp <- vectorOf 8 $ choose (0::Integer, 65535) + return $ intercalate ":" (map (`showHex` "") rawIp) + +-- | Generates an arbitrary IPv6 network in textual form. +genIp6Net :: Gen String +genIp6Net = do + netmask <- choose (8::Int, 126) + ip <- genIp6Addr + return $ ip ++ "/" ++ show netmask + -- * Helper functions -- | Checks for serialisation idempotence.