From aed2325fcc63e0e0794db5f30e97a07218573548 Mon Sep 17 00:00:00 2001 From: Iustin Pop <iustin@google.com> Date: Wed, 29 Aug 2012 22:34:09 +0200 Subject: [PATCH] Split Luxi, Qlang, Ssconf and OpCodes tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit β¦ from QC.hs into their own files, again mirroring the production code source tree. Signed-off-by: Iustin Pop <iustin@google.com> Reviewed-by: RenΓ© Nussbaumer <rn@google.com> --- Makefile.am | 9 +- htest/Test/Ganeti/Luxi.hs | 142 +++++++++++++ htest/Test/Ganeti/OpCodes.hs | 149 ++++++++++++++ htest/Test/Ganeti/Query/Language.hs | 98 +++++++++ htest/Test/Ganeti/Ssconf.hs | 51 +++++ htest/Test/Ganeti/TestCommon.hs | 34 ++++ htest/test.hs | 4 + htools/Ganeti/HTools/QC.hs | 303 ---------------------------- 8 files changed, 485 insertions(+), 305 deletions(-) create mode 100644 htest/Test/Ganeti/Luxi.hs create mode 100644 htest/Test/Ganeti/OpCodes.hs create mode 100644 htest/Test/Ganeti/Query/Language.hs create mode 100644 htest/Test/Ganeti/Ssconf.hs diff --git a/Makefile.am b/Makefile.am index 348e61bb6..fbd1fa636 100644 --- a/Makefile.am +++ b/Makefile.am @@ -63,7 +63,8 @@ HTOOLS_DIRS = \ htest \ htest/Test \ htest/Test/Ganeti \ - htest/Test/Ganeti/Confd + htest/Test/Ganeti/Confd \ + htest/Test/Ganeti/Query DIRS = \ autotools \ @@ -434,8 +435,12 @@ HS_LIB_SRCS = \ htest/Test/Ganeti/TestHelper.hs \ htest/Test/Ganeti/TestCommon.hs \ htest/Test/Ganeti/Confd/Utils.hs \ + htest/Test/Ganeti/Luxi.hs \ htest/Test/Ganeti/Objects.hs \ - htest/Test/Ganeti/Rpc.hs + htest/Test/Ganeti/OpCodes.hs \ + htest/Test/Ganeti/Query/Language.hs \ + htest/Test/Ganeti/Rpc.hs \ + htest/Test/Ganeti/Ssconf.hs HS_BUILT_SRCS = htools/Ganeti/HTools/Version.hs htools/Ganeti/Constants.hs diff --git a/htest/Test/Ganeti/Luxi.hs b/htest/Test/Ganeti/Luxi.hs new file mode 100644 index 000000000..b40cf3451 --- /dev/null +++ b/htest/Test/Ganeti/Luxi.hs @@ -0,0 +1,142 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +{-| Unittests for ganeti-htools. + +-} + +{- + +Copyright (C) 2009, 2010, 2011, 2012 Google Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301, USA. + +-} + +module Test.Ganeti.Luxi (testLuxi) where + +import Test.QuickCheck +import Test.QuickCheck.Monadic (monadicIO, run, stop) + +import Control.Applicative +import Control.Concurrent (forkIO) +import Control.Exception (bracket) +import System.Directory (getTemporaryDirectory, removeFile) +import System.IO (hClose, openTempFile) +import qualified Text.JSON as J + +import Test.Ganeti.TestHelper +import Test.Ganeti.TestCommon +import Test.Ganeti.Query.Language (genFilter) +import Test.Ganeti.OpCodes () + +import Ganeti.BasicTypes +import qualified Ganeti.Luxi as Luxi + +-- * Luxi tests + +instance Arbitrary Luxi.TagObject where + arbitrary = elements [minBound..maxBound] + +instance Arbitrary Luxi.LuxiReq where + arbitrary = elements [minBound..maxBound] + +instance Arbitrary Luxi.LuxiOp where + arbitrary = do + lreq <- arbitrary + case lreq of + Luxi.ReqQuery -> Luxi.Query <$> arbitrary <*> getFields <*> genFilter + Luxi.ReqQueryFields -> Luxi.QueryFields <$> arbitrary <*> getFields + Luxi.ReqQueryNodes -> Luxi.QueryNodes <$> (listOf getFQDN) <*> + getFields <*> arbitrary + Luxi.ReqQueryGroups -> Luxi.QueryGroups <$> arbitrary <*> + arbitrary <*> arbitrary + Luxi.ReqQueryInstances -> Luxi.QueryInstances <$> (listOf getFQDN) <*> + getFields <*> arbitrary + Luxi.ReqQueryJobs -> Luxi.QueryJobs <$> arbitrary <*> getFields + Luxi.ReqQueryExports -> Luxi.QueryExports <$> + (listOf getFQDN) <*> arbitrary + Luxi.ReqQueryConfigValues -> Luxi.QueryConfigValues <$> getFields + Luxi.ReqQueryClusterInfo -> pure Luxi.QueryClusterInfo + Luxi.ReqQueryTags -> Luxi.QueryTags <$> arbitrary <*> getFQDN + Luxi.ReqSubmitJob -> Luxi.SubmitJob <$> (resize maxOpCodes arbitrary) + Luxi.ReqSubmitManyJobs -> Luxi.SubmitManyJobs <$> + (resize maxOpCodes arbitrary) + Luxi.ReqWaitForJobChange -> Luxi.WaitForJobChange <$> arbitrary <*> + getFields <*> pure J.JSNull <*> + pure J.JSNull <*> arbitrary + Luxi.ReqArchiveJob -> Luxi.ArchiveJob <$> arbitrary + Luxi.ReqAutoArchiveJobs -> Luxi.AutoArchiveJobs <$> arbitrary <*> + arbitrary + Luxi.ReqCancelJob -> Luxi.CancelJob <$> arbitrary + Luxi.ReqSetDrainFlag -> Luxi.SetDrainFlag <$> arbitrary + Luxi.ReqSetWatcherPause -> Luxi.SetWatcherPause <$> arbitrary + +-- | Simple check that encoding/decoding of LuxiOp works. +prop_Luxi_CallEncoding :: Luxi.LuxiOp -> Property +prop_Luxi_CallEncoding op = + (Luxi.validateCall (Luxi.buildCall op) >>= Luxi.decodeCall) ==? Ok op + +-- | Helper to a get a temporary file name. +getTempFileName :: IO FilePath +getTempFileName = do + tempdir <- getTemporaryDirectory + (fpath, handle) <- openTempFile tempdir "luxitest" + _ <- hClose handle + removeFile fpath + return fpath + +-- | Server ping-pong helper. +luxiServerPong :: Luxi.Client -> IO () +luxiServerPong c = do + msg <- Luxi.recvMsgExt c + case msg of + Luxi.RecvOk m -> Luxi.sendMsg c m >> luxiServerPong c + _ -> return () + +-- | Client ping-pong helper. +luxiClientPong :: Luxi.Client -> [String] -> IO [String] +luxiClientPong c = + mapM (\m -> Luxi.sendMsg c m >> Luxi.recvMsg c) + +-- | Monadic check that, given a server socket, we can connect via a +-- client to it, and that we can send a list of arbitrary messages and +-- get back what we sent. +prop_Luxi_ClientServer :: [[DNSChar]] -> Property +prop_Luxi_ClientServer dnschars = monadicIO $ do + let msgs = map (map dnsGetChar) dnschars + fpath <- run $ getTempFileName + -- we need to create the server first, otherwise (if we do it in the + -- forked thread) the client could try to connect to it before it's + -- ready + server <- run $ Luxi.getServer fpath + -- fork the server responder + _ <- run . forkIO $ + bracket + (Luxi.acceptClient server) + (\c -> Luxi.closeClient c >> Luxi.closeServer fpath server) + luxiServerPong + replies <- run $ + bracket + (Luxi.getClient fpath) + Luxi.closeClient + (\c -> luxiClientPong c msgs) + stop $ replies ==? msgs + +testSuite "Luxi" + [ 'prop_Luxi_CallEncoding + , 'prop_Luxi_ClientServer + ] diff --git a/htest/Test/Ganeti/OpCodes.hs b/htest/Test/Ganeti/OpCodes.hs new file mode 100644 index 000000000..f4878d3bf --- /dev/null +++ b/htest/Test/Ganeti/OpCodes.hs @@ -0,0 +1,149 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +{-| Unittests for ganeti-htools. + +-} + +{- + +Copyright (C) 2009, 2010, 2011, 2012 Google Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301, USA. + +-} + +module Test.Ganeti.OpCodes + ( testOpCodes + , OpCodes.OpCode(..) + ) where + +import qualified Test.HUnit as HUnit +import Test.QuickCheck + +import Control.Applicative +import Data.List +import qualified Text.JSON as J + +import Test.Ganeti.TestHelper +import Test.Ganeti.TestCommon + +import qualified Ganeti.Constants as C +import qualified Ganeti.OpCodes as OpCodes + +-- * Arbitrary instances + +instance Arbitrary OpCodes.ReplaceDisksMode where + arbitrary = elements [minBound..maxBound] + +instance Arbitrary OpCodes.DiskIndex where + arbitrary = choose (0, C.maxDisks - 1) >>= OpCodes.mkDiskIndex + +instance Arbitrary OpCodes.OpCode where + arbitrary = do + op_id <- elements OpCodes.allOpIDs + case op_id of + "OP_TEST_DELAY" -> + OpCodes.OpTestDelay <$> arbitrary <*> arbitrary + <*> resize maxNodes (listOf getFQDN) + "OP_INSTANCE_REPLACE_DISKS" -> + OpCodes.OpInstanceReplaceDisks <$> getFQDN <*> getMaybe getFQDN <*> + arbitrary <*> resize C.maxDisks arbitrary <*> getMaybe getName + "OP_INSTANCE_FAILOVER" -> + OpCodes.OpInstanceFailover <$> getFQDN <*> arbitrary <*> + getMaybe getFQDN + "OP_INSTANCE_MIGRATE" -> + OpCodes.OpInstanceMigrate <$> getFQDN <*> arbitrary <*> + arbitrary <*> arbitrary <*> getMaybe getFQDN + _ -> fail "Wrong opcode" + +-- * Test cases + +-- | Check that opcode serialization is idempotent. +prop_OpCodes_serialization :: OpCodes.OpCode -> Property +prop_OpCodes_serialization op = + case J.readJSON (J.showJSON op) of + J.Error e -> failTest $ "Cannot deserialise: " ++ e + J.Ok op' -> op ==? op' + +-- | Check that Python and Haskell defined the same opcode list. +case_OpCodes_AllDefined :: HUnit.Assertion +case_OpCodes_AllDefined = do + py_stdout <- runPython "from ganeti import opcodes\n\ + \print '\\n'.join(opcodes.OP_MAPPING.keys())" "" >>= + checkPythonResult + let py_ops = sort $ lines py_stdout + hs_ops = OpCodes.allOpIDs + -- extra_py = py_ops \\ hs_ops + extra_hs = hs_ops \\ py_ops + -- FIXME: uncomment when we have parity + -- HUnit.assertBool ("OpCodes missing from Haskell code:\n" ++ + -- unlines extra_py) (null extra_py) + HUnit.assertBool ("Extra OpCodes in the Haskell code code:\n" ++ + unlines extra_hs) (null extra_hs) + +-- | Custom HUnit test case that forks a Python process and checks +-- correspondence between Haskell-generated OpCodes and their Python +-- decoded, validated and re-encoded version. +-- +-- Note that we have a strange beast here: since launching Python is +-- expensive, we don't do this via a usual QuickProperty, since that's +-- slow (I've tested it, and it's indeed quite slow). Rather, we use a +-- single HUnit assertion, and in it we manually use QuickCheck to +-- generate 500 opcodes times the number of defined opcodes, which +-- then we pass in bulk to Python. The drawbacks to this method are +-- two fold: we cannot control the number of generated opcodes, since +-- HUnit assertions don't get access to the test options, and for the +-- same reason we can't run a repeatable seed. We should probably find +-- a better way to do this, for example by having a +-- separately-launched Python process (if not running the tests would +-- be skipped). +case_OpCodes_py_compat :: HUnit.Assertion +case_OpCodes_py_compat = do + let num_opcodes = length OpCodes.allOpIDs * 500 + sample_opcodes <- sample' (vectorOf num_opcodes + (arbitrary::Gen OpCodes.OpCode)) + let opcodes = head sample_opcodes + serialized = J.encode opcodes + py_stdout <- + runPython "from ganeti import opcodes\n\ + \import sys\n\ + \from ganeti import serializer\n\ + \op_data = serializer.Load(sys.stdin.read())\n\ + \decoded = [opcodes.OpCode.LoadOpCode(o) for o in op_data]\n\ + \for op in decoded:\n\ + \ op.Validate(True)\n\ + \encoded = [op.__getstate__() for op in decoded]\n\ + \print serializer.Dump(encoded)" serialized + >>= checkPythonResult + let deserialised = (J.decode py_stdout::J.Result [OpCodes.OpCode]) + decoded <- case deserialised of + J.Ok ops -> return ops + J.Error msg -> + HUnit.assertFailure ("Unable to decode opcodes: " ++ msg) + -- this already raised an expection, but we need it + -- for proper types + >> fail "Unable to decode opcodes" + HUnit.assertEqual "Mismatch in number of returned opcodes" + (length opcodes) (length decoded) + mapM_ (uncurry (HUnit.assertEqual "Different result after encoding/decoding") + ) $ zip opcodes decoded + +testSuite "OpCodes" + [ 'prop_OpCodes_serialization + , 'case_OpCodes_AllDefined + , 'case_OpCodes_py_compat + ] diff --git a/htest/Test/Ganeti/Query/Language.hs b/htest/Test/Ganeti/Query/Language.hs new file mode 100644 index 000000000..8c33e24a0 --- /dev/null +++ b/htest/Test/Ganeti/Query/Language.hs @@ -0,0 +1,98 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +{-| Unittests for ganeti-htools. + +-} + +{- + +Copyright (C) 2009, 2010, 2011, 2012 Google Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301, USA. + +-} + +module Test.Ganeti.Query.Language + ( testQlang + , genFilter + ) where + +import Test.QuickCheck + +import Control.Applicative +import qualified Text.JSON as J + +import Test.Ganeti.TestHelper +import Test.Ganeti.TestCommon + +import qualified Ganeti.Query.Language as Qlang + +-- | Custom 'Qlang.Filter' generator (top-level), which enforces a +-- (sane) limit on the depth of the generated filters. +genFilter :: Gen (Qlang.Filter Qlang.FilterField) +genFilter = choose (0, 10) >>= genFilter' + +-- | Custom generator for filters that correctly halves the state of +-- the generators at each recursive step, per the QuickCheck +-- documentation, in order not to run out of memory. +genFilter' :: Int -> Gen (Qlang.Filter Qlang.FilterField) +genFilter' 0 = + oneof [ return Qlang.EmptyFilter + , Qlang.TrueFilter <$> getName + , Qlang.EQFilter <$> getName <*> value + , Qlang.LTFilter <$> getName <*> value + , Qlang.GTFilter <$> getName <*> value + , Qlang.LEFilter <$> getName <*> value + , Qlang.GEFilter <$> getName <*> value + , Qlang.RegexpFilter <$> getName <*> arbitrary + , Qlang.ContainsFilter <$> getName <*> value + ] + where value = oneof [ Qlang.QuotedString <$> getName + , Qlang.NumericValue <$> arbitrary + ] +genFilter' n = do + oneof [ Qlang.AndFilter <$> vectorOf n'' (genFilter' n') + , Qlang.OrFilter <$> vectorOf n'' (genFilter' n') + , Qlang.NotFilter <$> genFilter' n' + ] + where n' = n `div` 2 -- sub-filter generator size + n'' = max n' 2 -- but we don't want empty or 1-element lists, + -- so use this for and/or filter list length + +instance Arbitrary Qlang.ItemType where + arbitrary = elements [minBound..maxBound] + +instance Arbitrary Qlang.FilterRegex where + arbitrary = getName >>= Qlang.mkRegex -- a name should be a good regex + +-- | Tests that serialisation/deserialisation of filters is +-- idempotent. +prop_Qlang_Serialisation :: Property +prop_Qlang_Serialisation = + forAll genFilter $ \flt -> + J.readJSON (J.showJSON flt) ==? J.Ok flt + +prop_Qlang_FilterRegex_instances :: Qlang.FilterRegex -> Property +prop_Qlang_FilterRegex_instances rex = + printTestCase "failed JSON encoding" + (J.readJSON (J.showJSON rex) ==? J.Ok rex) .&&. + printTestCase "failed read/show instances" (read (show rex) ==? rex) + +testSuite "Qlang" + [ 'prop_Qlang_Serialisation + , 'prop_Qlang_FilterRegex_instances + ] diff --git a/htest/Test/Ganeti/Ssconf.hs b/htest/Test/Ganeti/Ssconf.hs new file mode 100644 index 000000000..8139e2dd8 --- /dev/null +++ b/htest/Test/Ganeti/Ssconf.hs @@ -0,0 +1,51 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +{-| Unittests for ganeti-htools. + +-} + +{- + +Copyright (C) 2009, 2010, 2011, 2012 Google Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301, USA. + +-} + +module Test.Ganeti.Ssconf (testSsconf) where + +import Test.QuickCheck + +import Data.List + +import Test.Ganeti.TestHelper + +import qualified Ganeti.Ssconf as Ssconf + +-- * Ssconf tests + +instance Arbitrary Ssconf.SSKey where + arbitrary = elements [minBound..maxBound] + +prop_Ssconf_filename :: Ssconf.SSKey -> Property +prop_Ssconf_filename key = + printTestCase "Key doesn't start with correct prefix" $ + Ssconf.sSFilePrefix `isPrefixOf` Ssconf.keyToFilename (Just "") key + +testSuite "Ssconf" + [ 'prop_Ssconf_filename + ] diff --git a/htest/Test/Ganeti/TestCommon.hs b/htest/Test/Ganeti/TestCommon.hs index 7417af1dc..6b92452d4 100644 --- a/htest/Test/Ganeti/TestCommon.hs +++ b/htest/Test/Ganeti/TestCommon.hs @@ -26,8 +26,15 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA module Test.Ganeti.TestCommon where import Control.Applicative +import Control.Exception (catchJust) +import Control.Monad import Data.List +import qualified Test.HUnit as HUnit import Test.QuickCheck +import System.Environment (getEnv) +import System.Exit (ExitCode(..)) +import System.IO.Error (isDoesNotExistError) +import System.Process (readProcessWithExitCode) -- * Constants @@ -73,6 +80,26 @@ infix 3 ==? failTest :: String -> Property failTest msg = printTestCase msg False +-- | Return the python binary to use. If the PYTHON environment +-- variable is defined, use its value, otherwise use just \"python\". +pythonCmd :: IO String +pythonCmd = catchJust (guard . isDoesNotExistError) + (getEnv "PYTHON") (const (return "python")) + +-- | Run Python with an expression, returning the exit code, standard +-- output and error. +runPython :: String -> String -> IO (ExitCode, String, String) +runPython expr stdin = do + py_binary <- pythonCmd + readProcessWithExitCode py_binary ["-c", expr] stdin + +-- | Check python exit code, and fail via HUnit assertions if +-- non-zero. Otherwise, return the standard output. +checkPythonResult :: (ExitCode, String, String) -> IO String +checkPythonResult (py_code, py_stdout, py_stderr) = do + HUnit.assertEqual ("python exited with error: " ++ py_stderr) + ExitSuccess py_code + return py_stdout -- * Arbitrary instances @@ -140,3 +167,10 @@ genTags = do n <- choose (0, 10::Int) tags <- mapM (const genTag) [1..n] return $ map (map tagGetChar) tags + +-- | Generates a fields list. This uses the same character set as a +-- DNS name (just for simplicity). +getFields :: Gen [String] +getFields = do + n <- choose (1, 32) + vectorOf n getName diff --git a/htest/test.hs b/htest/test.hs index 67ab776c3..e8c410cbd 100644 --- a/htest/test.hs +++ b/htest/test.hs @@ -31,8 +31,12 @@ import System.Environment (getArgs) import Ganeti.HTools.QC import Test.Ganeti.Confd.Utils +import Test.Ganeti.Luxi import Test.Ganeti.Objects +import Test.Ganeti.OpCodes +import Test.Ganeti.Query.Language import Test.Ganeti.Rpc +import Test.Ganeti.Ssconf -- | Our default test options, overring the built-in test-framework -- ones. diff --git a/htools/Ganeti/HTools/QC.hs b/htools/Ganeti/HTools/QC.hs index 5bd926a8e..8d2360f78 100644 --- a/htools/Ganeti/HTools/QC.hs +++ b/htools/Ganeti/HTools/QC.hs @@ -39,16 +39,12 @@ module Ganeti.HTools.QC , testNode , testText , testSimu - , testOpCodes , testJobs , testCluster , testLoader , testTypes , testCLI , testJSON - , testLuxi - , testSsconf - , testQlang ) where import qualified Test.HUnit as HUnit @@ -87,7 +83,6 @@ import qualified Ganeti.Objects as Objects import qualified Ganeti.OpCodes as OpCodes import qualified Ganeti.Query.Language as Qlang import qualified Ganeti.Runtime as Runtime -import qualified Ganeti.Ssconf as Ssconf import qualified Ganeti.HTools.CLI as CLI import qualified Ganeti.HTools.Cluster as Cluster import qualified Ganeti.HTools.Container as Container @@ -170,27 +165,6 @@ isFailure :: Types.OpResult a -> Bool isFailure (Types.OpFail _) = True isFailure _ = False --- | Return the python binary to use. If the PYTHON environment --- variable is defined, use its value, otherwise use just \"python\". -pythonCmd :: IO String -pythonCmd = catchJust (guard . isDoesNotExistError) - (getEnv "PYTHON") (const (return "python")) - --- | Run Python with an expression, returning the exit code, standard --- output and error. -runPython :: String -> String -> IO (ExitCode, String, String) -runPython expr stdin = do - py_binary <- pythonCmd - readProcessWithExitCode py_binary ["-c", expr] stdin - --- | Check python exit code, and fail via HUnit assertions if --- non-zero. Otherwise, return the standard output. -checkPythonResult :: (ExitCode, String, String) -> IO String -checkPythonResult (py_code, py_stdout, py_stderr) = do - HUnit.assertEqual ("python exited with error: " ++ py_stderr) - ExitSuccess py_code - return py_stdout - -- | Update an instance to be smaller than a node. setInstanceSmallerThanNode :: Node.Node -> Instance.Instance -> Instance.Instance @@ -271,13 +245,6 @@ evacModeOptions Types.MirrorNone = [] evacModeOptions Types.MirrorInternal = [minBound..maxBound] -- DRBD can do all evacModeOptions Types.MirrorExternal = [Types.ChangePrimary, Types.ChangeAll] --- | Generates a fields list. This uses the same character set as a --- DNS name (just for simplicity). -getFields :: Gen [String] -getFields = do - n <- choose (1, 32) - vectorOf n getName - instance Arbitrary Types.InstanceStatus where arbitrary = elements [minBound..maxBound] @@ -349,31 +316,6 @@ genOnlineNode = do instance Arbitrary Node.Node where arbitrary = genNode Nothing Nothing --- replace disks -instance Arbitrary OpCodes.ReplaceDisksMode where - arbitrary = elements [minBound..maxBound] - -instance Arbitrary OpCodes.DiskIndex where - arbitrary = choose (0, C.maxDisks - 1) >>= OpCodes.mkDiskIndex - -instance Arbitrary OpCodes.OpCode where - arbitrary = do - op_id <- elements OpCodes.allOpIDs - case op_id of - "OP_TEST_DELAY" -> - OpCodes.OpTestDelay <$> arbitrary <*> arbitrary - <*> resize maxNodes (listOf getFQDN) - "OP_INSTANCE_REPLACE_DISKS" -> - OpCodes.OpInstanceReplaceDisks <$> getFQDN <*> getMaybe getFQDN <*> - arbitrary <*> resize C.maxDisks arbitrary <*> getMaybe getName - "OP_INSTANCE_FAILOVER" -> - OpCodes.OpInstanceFailover <$> getFQDN <*> arbitrary <*> - getMaybe getFQDN - "OP_INSTANCE_MIGRATE" -> - OpCodes.OpInstanceMigrate <$> getFQDN <*> arbitrary <*> - arbitrary <*> arbitrary <*> getMaybe getFQDN - _ -> fail "Wrong opcode" - instance Arbitrary Jobs.OpStatus where arbitrary = elements [minBound..maxBound] @@ -454,44 +396,6 @@ instance Arbitrary Types.IPolicy where , Types.iPolicySpindleRatio = spindle_ratio } --- | Custom 'Qlang.Filter' generator (top-level), which enforces a --- (sane) limit on the depth of the generated filters. -genFilter :: Gen (Qlang.Filter Qlang.FilterField) -genFilter = choose (0, 10) >>= genFilter' - --- | Custom generator for filters that correctly halves the state of --- the generators at each recursive step, per the QuickCheck --- documentation, in order not to run out of memory. -genFilter' :: Int -> Gen (Qlang.Filter Qlang.FilterField) -genFilter' 0 = - oneof [ return Qlang.EmptyFilter - , Qlang.TrueFilter <$> getName - , Qlang.EQFilter <$> getName <*> value - , Qlang.LTFilter <$> getName <*> value - , Qlang.GTFilter <$> getName <*> value - , Qlang.LEFilter <$> getName <*> value - , Qlang.GEFilter <$> getName <*> value - , Qlang.RegexpFilter <$> getName <*> arbitrary - , Qlang.ContainsFilter <$> getName <*> value - ] - where value = oneof [ Qlang.QuotedString <$> getName - , Qlang.NumericValue <$> arbitrary - ] -genFilter' n = do - oneof [ Qlang.AndFilter <$> vectorOf n'' (genFilter' n') - , Qlang.OrFilter <$> vectorOf n'' (genFilter' n') - , Qlang.NotFilter <$> genFilter' n' - ] - where n' = n `div` 2 -- sub-filter generator size - n'' = max n' 2 -- but we don't want empty or 1-element lists, - -- so use this for and/or filter list length - -instance Arbitrary Qlang.ItemType where - arbitrary = elements [minBound..maxBound] - -instance Arbitrary Qlang.FilterRegex where - arbitrary = getName >>= Qlang.mkRegex -- a name should be a good regex - -- * Actual tests -- ** Utils tests @@ -1484,84 +1388,6 @@ testSuite "Cluster" , 'prop_Cluster_AllocPolicy ] --- ** OpCodes tests - --- | Check that opcode serialization is idempotent. -prop_OpCodes_serialization :: OpCodes.OpCode -> Property -prop_OpCodes_serialization op = - case J.readJSON (J.showJSON op) of - J.Error e -> failTest $ "Cannot deserialise: " ++ e - J.Ok op' -> op ==? op' - --- | Check that Python and Haskell defined the same opcode list. -case_OpCodes_AllDefined :: HUnit.Assertion -case_OpCodes_AllDefined = do - py_stdout <- runPython "from ganeti import opcodes\n\ - \print '\\n'.join(opcodes.OP_MAPPING.keys())" "" >>= - checkPythonResult - let py_ops = sort $ lines py_stdout - hs_ops = OpCodes.allOpIDs - -- extra_py = py_ops \\ hs_ops - extra_hs = hs_ops \\ py_ops - -- FIXME: uncomment when we have parity - -- HUnit.assertBool ("OpCodes missing from Haskell code:\n" ++ - -- unlines extra_py) (null extra_py) - HUnit.assertBool ("Extra OpCodes in the Haskell code code:\n" ++ - unlines extra_hs) (null extra_hs) - --- | Custom HUnit test case that forks a Python process and checks --- correspondence between Haskell-generated OpCodes and their Python --- decoded, validated and re-encoded version. --- --- Note that we have a strange beast here: since launching Python is --- expensive, we don't do this via a usual QuickProperty, since that's --- slow (I've tested it, and it's indeed quite slow). Rather, we use a --- single HUnit assertion, and in it we manually use QuickCheck to --- generate 500 opcodes times the number of defined opcodes, which --- then we pass in bulk to Python. The drawbacks to this method are --- two fold: we cannot control the number of generated opcodes, since --- HUnit assertions don't get access to the test options, and for the --- same reason we can't run a repeatable seed. We should probably find --- a better way to do this, for example by having a --- separately-launched Python process (if not running the tests would --- be skipped). -case_OpCodes_py_compat :: HUnit.Assertion -case_OpCodes_py_compat = do - let num_opcodes = length OpCodes.allOpIDs * 500 - sample_opcodes <- sample' (vectorOf num_opcodes - (arbitrary::Gen OpCodes.OpCode)) - let opcodes = head sample_opcodes - serialized = J.encode opcodes - py_stdout <- - runPython "from ganeti import opcodes\n\ - \import sys\n\ - \from ganeti import serializer\n\ - \op_data = serializer.Load(sys.stdin.read())\n\ - \decoded = [opcodes.OpCode.LoadOpCode(o) for o in op_data]\n\ - \for op in decoded:\n\ - \ op.Validate(True)\n\ - \encoded = [op.__getstate__() for op in decoded]\n\ - \print serializer.Dump(encoded)" serialized - >>= checkPythonResult - let deserialised = (J.decode py_stdout::J.Result [OpCodes.OpCode]) - decoded <- case deserialised of - J.Ok ops -> return ops - J.Error msg -> - HUnit.assertFailure ("Unable to decode opcodes: " ++ msg) - -- this already raised an expection, but we need it - -- for proper types - >> fail "Unable to decode opcodes" - HUnit.assertEqual "Mismatch in number of returned opcodes" - (length opcodes) (length decoded) - mapM_ (uncurry (HUnit.assertEqual "Different result after encoding/decoding") - ) $ zip opcodes decoded - -testSuite "OpCodes" - [ 'prop_OpCodes_serialization - , 'case_OpCodes_AllDefined - , 'case_OpCodes_py_compat - ] - -- ** Jobs tests -- | Check that (queued) job\/opcode status serialization is idempotent. @@ -1809,132 +1635,3 @@ testSuite "JSON" [ 'prop_JSON_toArray , 'prop_JSON_toArrayFail ] - --- * Luxi tests - -instance Arbitrary Luxi.TagObject where - arbitrary = elements [minBound..maxBound] - -instance Arbitrary Luxi.LuxiReq where - arbitrary = elements [minBound..maxBound] - -instance Arbitrary Luxi.LuxiOp where - arbitrary = do - lreq <- arbitrary - case lreq of - Luxi.ReqQuery -> Luxi.Query <$> arbitrary <*> getFields <*> genFilter - Luxi.ReqQueryFields -> Luxi.QueryFields <$> arbitrary <*> getFields - Luxi.ReqQueryNodes -> Luxi.QueryNodes <$> (listOf getFQDN) <*> - getFields <*> arbitrary - Luxi.ReqQueryGroups -> Luxi.QueryGroups <$> arbitrary <*> - arbitrary <*> arbitrary - Luxi.ReqQueryInstances -> Luxi.QueryInstances <$> (listOf getFQDN) <*> - getFields <*> arbitrary - Luxi.ReqQueryJobs -> Luxi.QueryJobs <$> arbitrary <*> getFields - Luxi.ReqQueryExports -> Luxi.QueryExports <$> - (listOf getFQDN) <*> arbitrary - Luxi.ReqQueryConfigValues -> Luxi.QueryConfigValues <$> getFields - Luxi.ReqQueryClusterInfo -> pure Luxi.QueryClusterInfo - Luxi.ReqQueryTags -> Luxi.QueryTags <$> arbitrary <*> getFQDN - Luxi.ReqSubmitJob -> Luxi.SubmitJob <$> (resize maxOpCodes arbitrary) - Luxi.ReqSubmitManyJobs -> Luxi.SubmitManyJobs <$> - (resize maxOpCodes arbitrary) - Luxi.ReqWaitForJobChange -> Luxi.WaitForJobChange <$> arbitrary <*> - getFields <*> pure J.JSNull <*> - pure J.JSNull <*> arbitrary - Luxi.ReqArchiveJob -> Luxi.ArchiveJob <$> arbitrary - Luxi.ReqAutoArchiveJobs -> Luxi.AutoArchiveJobs <$> arbitrary <*> - arbitrary - Luxi.ReqCancelJob -> Luxi.CancelJob <$> arbitrary - Luxi.ReqSetDrainFlag -> Luxi.SetDrainFlag <$> arbitrary - Luxi.ReqSetWatcherPause -> Luxi.SetWatcherPause <$> arbitrary - --- | Simple check that encoding/decoding of LuxiOp works. -prop_Luxi_CallEncoding :: Luxi.LuxiOp -> Property -prop_Luxi_CallEncoding op = - (Luxi.validateCall (Luxi.buildCall op) >>= Luxi.decodeCall) ==? Types.Ok op - --- | Helper to a get a temporary file name. -getTempFileName :: IO FilePath -getTempFileName = do - tempdir <- getTemporaryDirectory - (fpath, handle) <- openTempFile tempdir "luxitest" - _ <- hClose handle - removeFile fpath - return fpath - --- | Server ping-pong helper. -luxiServerPong :: Luxi.Client -> IO () -luxiServerPong c = do - msg <- Luxi.recvMsgExt c - case msg of - Luxi.RecvOk m -> Luxi.sendMsg c m >> luxiServerPong c - _ -> return () - --- | Client ping-pong helper. -luxiClientPong :: Luxi.Client -> [String] -> IO [String] -luxiClientPong c = - mapM (\m -> Luxi.sendMsg c m >> Luxi.recvMsg c) - --- | Monadic check that, given a server socket, we can connect via a --- client to it, and that we can send a list of arbitrary messages and --- get back what we sent. -prop_Luxi_ClientServer :: [[DNSChar]] -> Property -prop_Luxi_ClientServer dnschars = monadicIO $ do - let msgs = map (map dnsGetChar) dnschars - fpath <- run $ getTempFileName - -- we need to create the server first, otherwise (if we do it in the - -- forked thread) the client could try to connect to it before it's - -- ready - server <- run $ Luxi.getServer fpath - -- fork the server responder - _ <- run . forkIO $ - bracket - (Luxi.acceptClient server) - (\c -> Luxi.closeClient c >> Luxi.closeServer fpath server) - luxiServerPong - replies <- run $ - bracket - (Luxi.getClient fpath) - Luxi.closeClient - (\c -> luxiClientPong c msgs) - assert $ replies == msgs - -testSuite "Luxi" - [ 'prop_Luxi_CallEncoding - , 'prop_Luxi_ClientServer - ] - --- * Ssconf tests - -instance Arbitrary Ssconf.SSKey where - arbitrary = elements [minBound..maxBound] - -prop_Ssconf_filename :: Ssconf.SSKey -> Property -prop_Ssconf_filename key = - printTestCase "Key doesn't start with correct prefix" $ - Ssconf.sSFilePrefix `isPrefixOf` Ssconf.keyToFilename (Just "") key - -testSuite "Ssconf" - [ 'prop_Ssconf_filename - ] - --- * Qlang tests - --- | Tests that serialisation/deserialisation of filters is --- idempotent. -prop_Qlang_Serialisation :: Property -prop_Qlang_Serialisation = - forAll genFilter $ \flt -> - J.readJSON (J.showJSON flt) ==? J.Ok flt - -prop_Qlang_FilterRegex_instances :: Qlang.FilterRegex -> Property -prop_Qlang_FilterRegex_instances rex = - printTestCase "failed JSON encoding" - (J.readJSON (J.showJSON rex) ==? J.Ok rex) .&&. - printTestCase "failed read/show instances" (read (show rex) ==? rex) - -testSuite "Qlang" - [ 'prop_Qlang_Serialisation - , 'prop_Qlang_FilterRegex_instances - ] -- GitLab