Revision aed2325f

b/Makefile.am
63 63
	htest \
64 64
	htest/Test \
65 65
	htest/Test/Ganeti \
66
	htest/Test/Ganeti/Confd
66
	htest/Test/Ganeti/Confd \
67
	htest/Test/Ganeti/Query
67 68

  
68 69
DIRS = \
69 70
	autotools \
......
434 435
	htest/Test/Ganeti/TestHelper.hs \
435 436
	htest/Test/Ganeti/TestCommon.hs \
436 437
	htest/Test/Ganeti/Confd/Utils.hs \
438
	htest/Test/Ganeti/Luxi.hs \
437 439
	htest/Test/Ganeti/Objects.hs \
438
	htest/Test/Ganeti/Rpc.hs
440
	htest/Test/Ganeti/OpCodes.hs \
441
	htest/Test/Ganeti/Query/Language.hs \
442
	htest/Test/Ganeti/Rpc.hs \
443
	htest/Test/Ganeti/Ssconf.hs
439 444

  
440 445

  
441 446
HS_BUILT_SRCS = htools/Ganeti/HTools/Version.hs htools/Ganeti/Constants.hs
b/htest/Test/Ganeti/Luxi.hs
1
{-# LANGUAGE TemplateHaskell #-}
2
{-# OPTIONS_GHC -fno-warn-orphans #-}
3

  
4
{-| Unittests for ganeti-htools.
5

  
6
-}
7

  
8
{-
9

  
10
Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
11

  
12
This program is free software; you can redistribute it and/or modify
13
it under the terms of the GNU General Public License as published by
14
the Free Software Foundation; either version 2 of the License, or
15
(at your option) any later version.
16

  
17
This program is distributed in the hope that it will be useful, but
18
WITHOUT ANY WARRANTY; without even the implied warranty of
19
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
20
General Public License for more details.
21

  
22
You should have received a copy of the GNU General Public License
23
along with this program; if not, write to the Free Software
24
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
25
02110-1301, USA.
26

  
27
-}
28

  
29
module Test.Ganeti.Luxi (testLuxi) where
30

  
31
import Test.QuickCheck
32
import Test.QuickCheck.Monadic (monadicIO, run, stop)
33

  
34
import Control.Applicative
35
import Control.Concurrent (forkIO)
36
import Control.Exception (bracket)
37
import System.Directory (getTemporaryDirectory, removeFile)
38
import System.IO (hClose, openTempFile)
39
import qualified Text.JSON as J
40

  
41
import Test.Ganeti.TestHelper
42
import Test.Ganeti.TestCommon
43
import Test.Ganeti.Query.Language (genFilter)
44
import Test.Ganeti.OpCodes ()
45

  
46
import Ganeti.BasicTypes
47
import qualified Ganeti.Luxi as Luxi
48

  
49
-- * Luxi tests
50

  
51
instance Arbitrary Luxi.TagObject where
52
  arbitrary = elements [minBound..maxBound]
53

  
54
instance Arbitrary Luxi.LuxiReq where
55
  arbitrary = elements [minBound..maxBound]
56

  
57
instance Arbitrary Luxi.LuxiOp where
58
  arbitrary = do
59
    lreq <- arbitrary
60
    case lreq of
61
      Luxi.ReqQuery -> Luxi.Query <$> arbitrary <*> getFields <*> genFilter
62
      Luxi.ReqQueryFields -> Luxi.QueryFields <$> arbitrary <*> getFields
63
      Luxi.ReqQueryNodes -> Luxi.QueryNodes <$> (listOf getFQDN) <*>
64
                            getFields <*> arbitrary
65
      Luxi.ReqQueryGroups -> Luxi.QueryGroups <$> arbitrary <*>
66
                             arbitrary <*> arbitrary
67
      Luxi.ReqQueryInstances -> Luxi.QueryInstances <$> (listOf getFQDN) <*>
68
                                getFields <*> arbitrary
69
      Luxi.ReqQueryJobs -> Luxi.QueryJobs <$> arbitrary <*> getFields
70
      Luxi.ReqQueryExports -> Luxi.QueryExports <$>
71
                              (listOf getFQDN) <*> arbitrary
72
      Luxi.ReqQueryConfigValues -> Luxi.QueryConfigValues <$> getFields
73
      Luxi.ReqQueryClusterInfo -> pure Luxi.QueryClusterInfo
74
      Luxi.ReqQueryTags -> Luxi.QueryTags <$> arbitrary <*> getFQDN
75
      Luxi.ReqSubmitJob -> Luxi.SubmitJob <$> (resize maxOpCodes arbitrary)
76
      Luxi.ReqSubmitManyJobs -> Luxi.SubmitManyJobs <$>
77
                                (resize maxOpCodes arbitrary)
78
      Luxi.ReqWaitForJobChange -> Luxi.WaitForJobChange <$> arbitrary <*>
79
                                  getFields <*> pure J.JSNull <*>
80
                                  pure J.JSNull <*> arbitrary
81
      Luxi.ReqArchiveJob -> Luxi.ArchiveJob <$> arbitrary
82
      Luxi.ReqAutoArchiveJobs -> Luxi.AutoArchiveJobs <$> arbitrary <*>
83
                                 arbitrary
84
      Luxi.ReqCancelJob -> Luxi.CancelJob <$> arbitrary
85
      Luxi.ReqSetDrainFlag -> Luxi.SetDrainFlag <$> arbitrary
86
      Luxi.ReqSetWatcherPause -> Luxi.SetWatcherPause <$> arbitrary
87

  
88
-- | Simple check that encoding/decoding of LuxiOp works.
89
prop_Luxi_CallEncoding :: Luxi.LuxiOp -> Property
90
prop_Luxi_CallEncoding op =
91
  (Luxi.validateCall (Luxi.buildCall op) >>= Luxi.decodeCall) ==? Ok op
92

  
93
-- | Helper to a get a temporary file name.
94
getTempFileName :: IO FilePath
95
getTempFileName = do
96
  tempdir <- getTemporaryDirectory
97
  (fpath, handle) <- openTempFile tempdir "luxitest"
98
  _ <- hClose handle
99
  removeFile fpath
100
  return fpath
101

  
102
-- | Server ping-pong helper.
103
luxiServerPong :: Luxi.Client -> IO ()
104
luxiServerPong c = do
105
  msg <- Luxi.recvMsgExt c
106
  case msg of
107
    Luxi.RecvOk m -> Luxi.sendMsg c m >> luxiServerPong c
108
    _ -> return ()
109

  
110
-- | Client ping-pong helper.
111
luxiClientPong :: Luxi.Client -> [String] -> IO [String]
112
luxiClientPong c =
113
  mapM (\m -> Luxi.sendMsg c m >> Luxi.recvMsg c)
114

  
115
-- | Monadic check that, given a server socket, we can connect via a
116
-- client to it, and that we can send a list of arbitrary messages and
117
-- get back what we sent.
118
prop_Luxi_ClientServer :: [[DNSChar]] -> Property
119
prop_Luxi_ClientServer dnschars = monadicIO $ do
120
  let msgs = map (map dnsGetChar) dnschars
121
  fpath <- run $ getTempFileName
122
  -- we need to create the server first, otherwise (if we do it in the
123
  -- forked thread) the client could try to connect to it before it's
124
  -- ready
125
  server <- run $ Luxi.getServer fpath
126
  -- fork the server responder
127
  _ <- run . forkIO $
128
    bracket
129
      (Luxi.acceptClient server)
130
      (\c -> Luxi.closeClient c >> Luxi.closeServer fpath server)
131
      luxiServerPong
132
  replies <- run $
133
    bracket
134
      (Luxi.getClient fpath)
135
      Luxi.closeClient
136
      (\c -> luxiClientPong c msgs)
137
  stop $ replies ==? msgs
138

  
139
testSuite "Luxi"
140
          [ 'prop_Luxi_CallEncoding
141
          , 'prop_Luxi_ClientServer
142
          ]
b/htest/Test/Ganeti/OpCodes.hs
1
{-# LANGUAGE TemplateHaskell #-}
2
{-# OPTIONS_GHC -fno-warn-orphans #-}
3

  
4
{-| Unittests for ganeti-htools.
5

  
6
-}
7

  
8
{-
9

  
10
Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
11

  
12
This program is free software; you can redistribute it and/or modify
13
it under the terms of the GNU General Public License as published by
14
the Free Software Foundation; either version 2 of the License, or
15
(at your option) any later version.
16

  
17
This program is distributed in the hope that it will be useful, but
18
WITHOUT ANY WARRANTY; without even the implied warranty of
19
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
20
General Public License for more details.
21

  
22
You should have received a copy of the GNU General Public License
23
along with this program; if not, write to the Free Software
24
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
25
02110-1301, USA.
26

  
27
-}
28

  
29
module Test.Ganeti.OpCodes
30
  ( testOpCodes
31
  , OpCodes.OpCode(..)
32
  ) where
33

  
34
import qualified Test.HUnit as HUnit
35
import Test.QuickCheck
36

  
37
import Control.Applicative
38
import Data.List
39
import qualified Text.JSON as J
40

  
41
import Test.Ganeti.TestHelper
42
import Test.Ganeti.TestCommon
43

  
44
import qualified Ganeti.Constants as C
45
import qualified Ganeti.OpCodes as OpCodes
46

  
47
-- * Arbitrary instances
48

  
49
instance Arbitrary OpCodes.ReplaceDisksMode where
50
  arbitrary = elements [minBound..maxBound]
51

  
52
instance Arbitrary OpCodes.DiskIndex where
53
  arbitrary = choose (0, C.maxDisks - 1) >>= OpCodes.mkDiskIndex
54

  
55
instance Arbitrary OpCodes.OpCode where
56
  arbitrary = do
57
    op_id <- elements OpCodes.allOpIDs
58
    case op_id of
59
      "OP_TEST_DELAY" ->
60
        OpCodes.OpTestDelay <$> arbitrary <*> arbitrary
61
                 <*> resize maxNodes (listOf getFQDN)
62
      "OP_INSTANCE_REPLACE_DISKS" ->
63
        OpCodes.OpInstanceReplaceDisks <$> getFQDN <*> getMaybe getFQDN <*>
64
          arbitrary <*> resize C.maxDisks arbitrary <*> getMaybe getName
65
      "OP_INSTANCE_FAILOVER" ->
66
        OpCodes.OpInstanceFailover <$> getFQDN <*> arbitrary <*>
67
          getMaybe getFQDN
68
      "OP_INSTANCE_MIGRATE" ->
69
        OpCodes.OpInstanceMigrate <$> getFQDN <*> arbitrary <*>
70
          arbitrary <*> arbitrary <*> getMaybe getFQDN
71
      _ -> fail "Wrong opcode"
72

  
73
-- * Test cases
74

  
75
-- | Check that opcode serialization is idempotent.
76
prop_OpCodes_serialization :: OpCodes.OpCode -> Property
77
prop_OpCodes_serialization op =
78
  case J.readJSON (J.showJSON op) of
79
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
80
    J.Ok op' -> op ==? op'
81

  
82
-- | Check that Python and Haskell defined the same opcode list.
83
case_OpCodes_AllDefined :: HUnit.Assertion
84
case_OpCodes_AllDefined = do
85
  py_stdout <- runPython "from ganeti import opcodes\n\
86
                         \print '\\n'.join(opcodes.OP_MAPPING.keys())" "" >>=
87
               checkPythonResult
88
  let py_ops = sort $ lines py_stdout
89
      hs_ops = OpCodes.allOpIDs
90
      -- extra_py = py_ops \\ hs_ops
91
      extra_hs = hs_ops \\ py_ops
92
  -- FIXME: uncomment when we have parity
93
  -- HUnit.assertBool ("OpCodes missing from Haskell code:\n" ++
94
  --                  unlines extra_py) (null extra_py)
95
  HUnit.assertBool ("Extra OpCodes in the Haskell code code:\n" ++
96
                    unlines extra_hs) (null extra_hs)
97

  
98
-- | Custom HUnit test case that forks a Python process and checks
99
-- correspondence between Haskell-generated OpCodes and their Python
100
-- decoded, validated and re-encoded version.
101
--
102
-- Note that we have a strange beast here: since launching Python is
103
-- expensive, we don't do this via a usual QuickProperty, since that's
104
-- slow (I've tested it, and it's indeed quite slow). Rather, we use a
105
-- single HUnit assertion, and in it we manually use QuickCheck to
106
-- generate 500 opcodes times the number of defined opcodes, which
107
-- then we pass in bulk to Python. The drawbacks to this method are
108
-- two fold: we cannot control the number of generated opcodes, since
109
-- HUnit assertions don't get access to the test options, and for the
110
-- same reason we can't run a repeatable seed. We should probably find
111
-- a better way to do this, for example by having a
112
-- separately-launched Python process (if not running the tests would
113
-- be skipped).
114
case_OpCodes_py_compat :: HUnit.Assertion
115
case_OpCodes_py_compat = do
116
  let num_opcodes = length OpCodes.allOpIDs * 500
117
  sample_opcodes <- sample' (vectorOf num_opcodes
118
                             (arbitrary::Gen OpCodes.OpCode))
119
  let opcodes = head sample_opcodes
120
      serialized = J.encode opcodes
121
  py_stdout <-
122
     runPython "from ganeti import opcodes\n\
123
               \import sys\n\
124
               \from ganeti import serializer\n\
125
               \op_data = serializer.Load(sys.stdin.read())\n\
126
               \decoded = [opcodes.OpCode.LoadOpCode(o) for o in op_data]\n\
127
               \for op in decoded:\n\
128
               \  op.Validate(True)\n\
129
               \encoded = [op.__getstate__() for op in decoded]\n\
130
               \print serializer.Dump(encoded)" serialized
131
     >>= checkPythonResult
132
  let deserialised = (J.decode py_stdout::J.Result [OpCodes.OpCode])
133
  decoded <- case deserialised of
134
               J.Ok ops -> return ops
135
               J.Error msg ->
136
                 HUnit.assertFailure ("Unable to decode opcodes: " ++ msg)
137
                 -- this already raised an expection, but we need it
138
                 -- for proper types
139
                 >> fail "Unable to decode opcodes"
140
  HUnit.assertEqual "Mismatch in number of returned opcodes"
141
    (length opcodes) (length decoded)
142
  mapM_ (uncurry (HUnit.assertEqual "Different result after encoding/decoding")
143
        ) $ zip opcodes decoded
144

  
145
testSuite "OpCodes"
146
            [ 'prop_OpCodes_serialization
147
            , 'case_OpCodes_AllDefined
148
            , 'case_OpCodes_py_compat
149
            ]
b/htest/Test/Ganeti/Query/Language.hs
1
{-# LANGUAGE TemplateHaskell #-}
2
{-# OPTIONS_GHC -fno-warn-orphans #-}
3

  
4
{-| Unittests for ganeti-htools.
5

  
6
-}
7

  
8
{-
9

  
10
Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
11

  
12
This program is free software; you can redistribute it and/or modify
13
it under the terms of the GNU General Public License as published by
14
the Free Software Foundation; either version 2 of the License, or
15
(at your option) any later version.
16

  
17
This program is distributed in the hope that it will be useful, but
18
WITHOUT ANY WARRANTY; without even the implied warranty of
19
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
20
General Public License for more details.
21

  
22
You should have received a copy of the GNU General Public License
23
along with this program; if not, write to the Free Software
24
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
25
02110-1301, USA.
26

  
27
-}
28

  
29
module Test.Ganeti.Query.Language
30
  ( testQlang
31
  , genFilter
32
  ) where
33

  
34
import Test.QuickCheck
35

  
36
import Control.Applicative
37
import qualified Text.JSON as J
38

  
39
import Test.Ganeti.TestHelper
40
import Test.Ganeti.TestCommon
41

  
42
import qualified Ganeti.Query.Language as Qlang
43

  
44
-- | Custom 'Qlang.Filter' generator (top-level), which enforces a
45
-- (sane) limit on the depth of the generated filters.
46
genFilter :: Gen (Qlang.Filter Qlang.FilterField)
47
genFilter = choose (0, 10) >>= genFilter'
48

  
49
-- | Custom generator for filters that correctly halves the state of
50
-- the generators at each recursive step, per the QuickCheck
51
-- documentation, in order not to run out of memory.
52
genFilter' :: Int -> Gen (Qlang.Filter Qlang.FilterField)
53
genFilter' 0 =
54
  oneof [ return Qlang.EmptyFilter
55
        , Qlang.TrueFilter     <$> getName
56
        , Qlang.EQFilter       <$> getName <*> value
57
        , Qlang.LTFilter       <$> getName <*> value
58
        , Qlang.GTFilter       <$> getName <*> value
59
        , Qlang.LEFilter       <$> getName <*> value
60
        , Qlang.GEFilter       <$> getName <*> value
61
        , Qlang.RegexpFilter   <$> getName <*> arbitrary
62
        , Qlang.ContainsFilter <$> getName <*> value
63
        ]
64
    where value = oneof [ Qlang.QuotedString <$> getName
65
                        , Qlang.NumericValue <$> arbitrary
66
                        ]
67
genFilter' n = do
68
  oneof [ Qlang.AndFilter  <$> vectorOf n'' (genFilter' n')
69
        , Qlang.OrFilter   <$> vectorOf n'' (genFilter' n')
70
        , Qlang.NotFilter  <$> genFilter' n'
71
        ]
72
  where n' = n `div` 2 -- sub-filter generator size
73
        n'' = max n' 2 -- but we don't want empty or 1-element lists,
74
                       -- so use this for and/or filter list length
75

  
76
instance Arbitrary Qlang.ItemType where
77
  arbitrary = elements [minBound..maxBound]
78

  
79
instance Arbitrary Qlang.FilterRegex where
80
  arbitrary = getName >>= Qlang.mkRegex -- a name should be a good regex
81

  
82
-- | Tests that serialisation/deserialisation of filters is
83
-- idempotent.
84
prop_Qlang_Serialisation :: Property
85
prop_Qlang_Serialisation =
86
  forAll genFilter $ \flt ->
87
  J.readJSON (J.showJSON flt) ==? J.Ok flt
88

  
89
prop_Qlang_FilterRegex_instances :: Qlang.FilterRegex -> Property
90
prop_Qlang_FilterRegex_instances rex =
91
  printTestCase "failed JSON encoding"
92
    (J.readJSON (J.showJSON rex) ==? J.Ok rex) .&&.
93
  printTestCase "failed read/show instances" (read (show rex) ==? rex)
94

  
95
testSuite "Qlang"
96
  [ 'prop_Qlang_Serialisation
97
  , 'prop_Qlang_FilterRegex_instances
98
  ]
b/htest/Test/Ganeti/Ssconf.hs
1
{-# LANGUAGE TemplateHaskell #-}
2
{-# OPTIONS_GHC -fno-warn-orphans #-}
3

  
4
{-| Unittests for ganeti-htools.
5

  
6
-}
7

  
8
{-
9

  
10
Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
11

  
12
This program is free software; you can redistribute it and/or modify
13
it under the terms of the GNU General Public License as published by
14
the Free Software Foundation; either version 2 of the License, or
15
(at your option) any later version.
16

  
17
This program is distributed in the hope that it will be useful, but
18
WITHOUT ANY WARRANTY; without even the implied warranty of
19
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
20
General Public License for more details.
21

  
22
You should have received a copy of the GNU General Public License
23
along with this program; if not, write to the Free Software
24
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
25
02110-1301, USA.
26

  
27
-}
28

  
29
module Test.Ganeti.Ssconf (testSsconf) where
30

  
31
import Test.QuickCheck
32

  
33
import Data.List
34

  
35
import Test.Ganeti.TestHelper
36

  
37
import qualified Ganeti.Ssconf as Ssconf
38

  
39
-- * Ssconf tests
40

  
41
instance Arbitrary Ssconf.SSKey where
42
  arbitrary = elements [minBound..maxBound]
43

  
44
prop_Ssconf_filename :: Ssconf.SSKey -> Property
45
prop_Ssconf_filename key =
46
  printTestCase "Key doesn't start with correct prefix" $
47
    Ssconf.sSFilePrefix `isPrefixOf` Ssconf.keyToFilename (Just "") key
48

  
49
testSuite "Ssconf"
50
  [ 'prop_Ssconf_filename
51
  ]
b/htest/Test/Ganeti/TestCommon.hs
26 26
module Test.Ganeti.TestCommon where
27 27

  
28 28
import Control.Applicative
29
import Control.Exception (catchJust)
30
import Control.Monad
29 31
import Data.List
32
import qualified Test.HUnit as HUnit
30 33
import Test.QuickCheck
34
import System.Environment (getEnv)
35
import System.Exit (ExitCode(..))
36
import System.IO.Error (isDoesNotExistError)
37
import System.Process (readProcessWithExitCode)
31 38

  
32 39
-- * Constants
33 40

  
......
73 80
failTest :: String -> Property
74 81
failTest msg = printTestCase msg False
75 82

  
83
-- | Return the python binary to use. If the PYTHON environment
84
-- variable is defined, use its value, otherwise use just \"python\".
85
pythonCmd :: IO String
86
pythonCmd = catchJust (guard . isDoesNotExistError)
87
            (getEnv "PYTHON") (const (return "python"))
88

  
89
-- | Run Python with an expression, returning the exit code, standard
90
-- output and error.
91
runPython :: String -> String -> IO (ExitCode, String, String)
92
runPython expr stdin = do
93
  py_binary <- pythonCmd
94
  readProcessWithExitCode py_binary ["-c", expr] stdin
95

  
96
-- | Check python exit code, and fail via HUnit assertions if
97
-- non-zero. Otherwise, return the standard output.
98
checkPythonResult :: (ExitCode, String, String) -> IO String
99
checkPythonResult (py_code, py_stdout, py_stderr) = do
100
  HUnit.assertEqual ("python exited with error: " ++ py_stderr)
101
       ExitSuccess py_code
102
  return py_stdout
76 103

  
77 104
-- * Arbitrary instances
78 105

  
......
140 167
  n <- choose (0, 10::Int)
141 168
  tags <- mapM (const genTag) [1..n]
142 169
  return $ map (map tagGetChar) tags
170

  
171
-- | Generates a fields list. This uses the same character set as a
172
-- DNS name (just for simplicity).
173
getFields :: Gen [String]
174
getFields = do
175
  n <- choose (1, 32)
176
  vectorOf n getName
b/htest/test.hs
31 31

  
32 32
import Ganeti.HTools.QC
33 33
import Test.Ganeti.Confd.Utils
34
import Test.Ganeti.Luxi
34 35
import Test.Ganeti.Objects
36
import Test.Ganeti.OpCodes
37
import Test.Ganeti.Query.Language
35 38
import Test.Ganeti.Rpc
39
import Test.Ganeti.Ssconf
36 40

  
37 41
-- | Our default test options, overring the built-in test-framework
38 42
-- ones.
b/htools/Ganeti/HTools/QC.hs
39 39
  , testNode
40 40
  , testText
41 41
  , testSimu
42
  , testOpCodes
43 42
  , testJobs
44 43
  , testCluster
45 44
  , testLoader
46 45
  , testTypes
47 46
  , testCLI
48 47
  , testJSON
49
  , testLuxi
50
  , testSsconf
51
  , testQlang
52 48
  ) where
53 49

  
54 50
import qualified Test.HUnit as HUnit
......
87 83
import qualified Ganeti.OpCodes as OpCodes
88 84
import qualified Ganeti.Query.Language as Qlang
89 85
import qualified Ganeti.Runtime as Runtime
90
import qualified Ganeti.Ssconf as Ssconf
91 86
import qualified Ganeti.HTools.CLI as CLI
92 87
import qualified Ganeti.HTools.Cluster as Cluster
93 88
import qualified Ganeti.HTools.Container as Container
......
170 165
isFailure (Types.OpFail _) = True
171 166
isFailure _ = False
172 167

  
173
-- | Return the python binary to use. If the PYTHON environment
174
-- variable is defined, use its value, otherwise use just \"python\".
175
pythonCmd :: IO String
176
pythonCmd = catchJust (guard . isDoesNotExistError)
177
            (getEnv "PYTHON") (const (return "python"))
178

  
179
-- | Run Python with an expression, returning the exit code, standard
180
-- output and error.
181
runPython :: String -> String -> IO (ExitCode, String, String)
182
runPython expr stdin = do
183
  py_binary <- pythonCmd
184
  readProcessWithExitCode py_binary ["-c", expr] stdin
185

  
186
-- | Check python exit code, and fail via HUnit assertions if
187
-- non-zero. Otherwise, return the standard output.
188
checkPythonResult :: (ExitCode, String, String) -> IO String
189
checkPythonResult (py_code, py_stdout, py_stderr) = do
190
  HUnit.assertEqual ("python exited with error: " ++ py_stderr)
191
       ExitSuccess py_code
192
  return py_stdout
193

  
194 168
-- | Update an instance to be smaller than a node.
195 169
setInstanceSmallerThanNode :: Node.Node
196 170
                           -> Instance.Instance -> Instance.Instance
......
271 245
evacModeOptions Types.MirrorInternal = [minBound..maxBound] -- DRBD can do all
272 246
evacModeOptions Types.MirrorExternal = [Types.ChangePrimary, Types.ChangeAll]
273 247

  
274
-- | Generates a fields list. This uses the same character set as a
275
-- DNS name (just for simplicity).
276
getFields :: Gen [String]
277
getFields = do
278
  n <- choose (1, 32)
279
  vectorOf n getName
280

  
281 248
instance Arbitrary Types.InstanceStatus where
282 249
    arbitrary = elements [minBound..maxBound]
283 250

  
......
349 316
instance Arbitrary Node.Node where
350 317
  arbitrary = genNode Nothing Nothing
351 318

  
352
-- replace disks
353
instance Arbitrary OpCodes.ReplaceDisksMode where
354
  arbitrary = elements [minBound..maxBound]
355

  
356
instance Arbitrary OpCodes.DiskIndex where
357
  arbitrary = choose (0, C.maxDisks - 1) >>= OpCodes.mkDiskIndex
358

  
359
instance Arbitrary OpCodes.OpCode where
360
  arbitrary = do
361
    op_id <- elements OpCodes.allOpIDs
362
    case op_id of
363
      "OP_TEST_DELAY" ->
364
        OpCodes.OpTestDelay <$> arbitrary <*> arbitrary
365
                 <*> resize maxNodes (listOf getFQDN)
366
      "OP_INSTANCE_REPLACE_DISKS" ->
367
        OpCodes.OpInstanceReplaceDisks <$> getFQDN <*> getMaybe getFQDN <*>
368
          arbitrary <*> resize C.maxDisks arbitrary <*> getMaybe getName
369
      "OP_INSTANCE_FAILOVER" ->
370
        OpCodes.OpInstanceFailover <$> getFQDN <*> arbitrary <*>
371
          getMaybe getFQDN
372
      "OP_INSTANCE_MIGRATE" ->
373
        OpCodes.OpInstanceMigrate <$> getFQDN <*> arbitrary <*>
374
          arbitrary <*> arbitrary <*> getMaybe getFQDN
375
      _ -> fail "Wrong opcode"
376

  
377 319
instance Arbitrary Jobs.OpStatus where
378 320
  arbitrary = elements [minBound..maxBound]
379 321

  
......
454 396
                         , Types.iPolicySpindleRatio = spindle_ratio
455 397
                         }
456 398

  
457
-- | Custom 'Qlang.Filter' generator (top-level), which enforces a
458
-- (sane) limit on the depth of the generated filters.
459
genFilter :: Gen (Qlang.Filter Qlang.FilterField)
460
genFilter = choose (0, 10) >>= genFilter'
461

  
462
-- | Custom generator for filters that correctly halves the state of
463
-- the generators at each recursive step, per the QuickCheck
464
-- documentation, in order not to run out of memory.
465
genFilter' :: Int -> Gen (Qlang.Filter Qlang.FilterField)
466
genFilter' 0 =
467
  oneof [ return Qlang.EmptyFilter
468
        , Qlang.TrueFilter     <$> getName
469
        , Qlang.EQFilter       <$> getName <*> value
470
        , Qlang.LTFilter       <$> getName <*> value
471
        , Qlang.GTFilter       <$> getName <*> value
472
        , Qlang.LEFilter       <$> getName <*> value
473
        , Qlang.GEFilter       <$> getName <*> value
474
        , Qlang.RegexpFilter   <$> getName <*> arbitrary
475
        , Qlang.ContainsFilter <$> getName <*> value
476
        ]
477
    where value = oneof [ Qlang.QuotedString <$> getName
478
                        , Qlang.NumericValue <$> arbitrary
479
                        ]
480
genFilter' n = do
481
  oneof [ Qlang.AndFilter  <$> vectorOf n'' (genFilter' n')
482
        , Qlang.OrFilter   <$> vectorOf n'' (genFilter' n')
483
        , Qlang.NotFilter  <$> genFilter' n'
484
        ]
485
  where n' = n `div` 2 -- sub-filter generator size
486
        n'' = max n' 2 -- but we don't want empty or 1-element lists,
487
                       -- so use this for and/or filter list length
488

  
489
instance Arbitrary Qlang.ItemType where
490
  arbitrary = elements [minBound..maxBound]
491

  
492
instance Arbitrary Qlang.FilterRegex where
493
  arbitrary = getName >>= Qlang.mkRegex -- a name should be a good regex
494

  
495 399
-- * Actual tests
496 400

  
497 401
-- ** Utils tests
......
1484 1388
            , 'prop_Cluster_AllocPolicy
1485 1389
            ]
1486 1390

  
1487
-- ** OpCodes tests
1488

  
1489
-- | Check that opcode serialization is idempotent.
1490
prop_OpCodes_serialization :: OpCodes.OpCode -> Property
1491
prop_OpCodes_serialization op =
1492
  case J.readJSON (J.showJSON op) of
1493
    J.Error e -> failTest $ "Cannot deserialise: " ++ e
1494
    J.Ok op' -> op ==? op'
1495

  
1496
-- | Check that Python and Haskell defined the same opcode list.
1497
case_OpCodes_AllDefined :: HUnit.Assertion
1498
case_OpCodes_AllDefined = do
1499
  py_stdout <- runPython "from ganeti import opcodes\n\
1500
                         \print '\\n'.join(opcodes.OP_MAPPING.keys())" "" >>=
1501
               checkPythonResult
1502
  let py_ops = sort $ lines py_stdout
1503
      hs_ops = OpCodes.allOpIDs
1504
      -- extra_py = py_ops \\ hs_ops
1505
      extra_hs = hs_ops \\ py_ops
1506
  -- FIXME: uncomment when we have parity
1507
  -- HUnit.assertBool ("OpCodes missing from Haskell code:\n" ++
1508
  --                  unlines extra_py) (null extra_py)
1509
  HUnit.assertBool ("Extra OpCodes in the Haskell code code:\n" ++
1510
                    unlines extra_hs) (null extra_hs)
1511

  
1512
-- | Custom HUnit test case that forks a Python process and checks
1513
-- correspondence between Haskell-generated OpCodes and their Python
1514
-- decoded, validated and re-encoded version.
1515
--
1516
-- Note that we have a strange beast here: since launching Python is
1517
-- expensive, we don't do this via a usual QuickProperty, since that's
1518
-- slow (I've tested it, and it's indeed quite slow). Rather, we use a
1519
-- single HUnit assertion, and in it we manually use QuickCheck to
1520
-- generate 500 opcodes times the number of defined opcodes, which
1521
-- then we pass in bulk to Python. The drawbacks to this method are
1522
-- two fold: we cannot control the number of generated opcodes, since
1523
-- HUnit assertions don't get access to the test options, and for the
1524
-- same reason we can't run a repeatable seed. We should probably find
1525
-- a better way to do this, for example by having a
1526
-- separately-launched Python process (if not running the tests would
1527
-- be skipped).
1528
case_OpCodes_py_compat :: HUnit.Assertion
1529
case_OpCodes_py_compat = do
1530
  let num_opcodes = length OpCodes.allOpIDs * 500
1531
  sample_opcodes <- sample' (vectorOf num_opcodes
1532
                             (arbitrary::Gen OpCodes.OpCode))
1533
  let opcodes = head sample_opcodes
1534
      serialized = J.encode opcodes
1535
  py_stdout <-
1536
     runPython "from ganeti import opcodes\n\
1537
               \import sys\n\
1538
               \from ganeti import serializer\n\
1539
               \op_data = serializer.Load(sys.stdin.read())\n\
1540
               \decoded = [opcodes.OpCode.LoadOpCode(o) for o in op_data]\n\
1541
               \for op in decoded:\n\
1542
               \  op.Validate(True)\n\
1543
               \encoded = [op.__getstate__() for op in decoded]\n\
1544
               \print serializer.Dump(encoded)" serialized
1545
     >>= checkPythonResult
1546
  let deserialised = (J.decode py_stdout::J.Result [OpCodes.OpCode])
1547
  decoded <- case deserialised of
1548
               J.Ok ops -> return ops
1549
               J.Error msg ->
1550
                 HUnit.assertFailure ("Unable to decode opcodes: " ++ msg)
1551
                 -- this already raised an expection, but we need it
1552
                 -- for proper types
1553
                 >> fail "Unable to decode opcodes"
1554
  HUnit.assertEqual "Mismatch in number of returned opcodes"
1555
    (length opcodes) (length decoded)
1556
  mapM_ (uncurry (HUnit.assertEqual "Different result after encoding/decoding")
1557
        ) $ zip opcodes decoded
1558

  
1559
testSuite "OpCodes"
1560
            [ 'prop_OpCodes_serialization
1561
            , 'case_OpCodes_AllDefined
1562
            , 'case_OpCodes_py_compat
1563
            ]
1564

  
1565 1391
-- ** Jobs tests
1566 1392

  
1567 1393
-- | Check that (queued) job\/opcode status serialization is idempotent.
......
1809 1635
          [ 'prop_JSON_toArray
1810 1636
          , 'prop_JSON_toArrayFail
1811 1637
          ]
1812

  
1813
-- * Luxi tests
1814

  
1815
instance Arbitrary Luxi.TagObject where
1816
  arbitrary = elements [minBound..maxBound]
1817

  
1818
instance Arbitrary Luxi.LuxiReq where
1819
  arbitrary = elements [minBound..maxBound]
1820

  
1821
instance Arbitrary Luxi.LuxiOp where
1822
  arbitrary = do
1823
    lreq <- arbitrary
1824
    case lreq of
1825
      Luxi.ReqQuery -> Luxi.Query <$> arbitrary <*> getFields <*> genFilter
1826
      Luxi.ReqQueryFields -> Luxi.QueryFields <$> arbitrary <*> getFields
1827
      Luxi.ReqQueryNodes -> Luxi.QueryNodes <$> (listOf getFQDN) <*>
1828
                            getFields <*> arbitrary
1829
      Luxi.ReqQueryGroups -> Luxi.QueryGroups <$> arbitrary <*>
1830
                             arbitrary <*> arbitrary
1831
      Luxi.ReqQueryInstances -> Luxi.QueryInstances <$> (listOf getFQDN) <*>
1832
                                getFields <*> arbitrary
1833
      Luxi.ReqQueryJobs -> Luxi.QueryJobs <$> arbitrary <*> getFields
1834
      Luxi.ReqQueryExports -> Luxi.QueryExports <$>
1835
                              (listOf getFQDN) <*> arbitrary
1836
      Luxi.ReqQueryConfigValues -> Luxi.QueryConfigValues <$> getFields
1837
      Luxi.ReqQueryClusterInfo -> pure Luxi.QueryClusterInfo
1838
      Luxi.ReqQueryTags -> Luxi.QueryTags <$> arbitrary <*> getFQDN
1839
      Luxi.ReqSubmitJob -> Luxi.SubmitJob <$> (resize maxOpCodes arbitrary)
1840
      Luxi.ReqSubmitManyJobs -> Luxi.SubmitManyJobs <$>
1841
                                (resize maxOpCodes arbitrary)
1842
      Luxi.ReqWaitForJobChange -> Luxi.WaitForJobChange <$> arbitrary <*>
1843
                                  getFields <*> pure J.JSNull <*>
1844
                                  pure J.JSNull <*> arbitrary
1845
      Luxi.ReqArchiveJob -> Luxi.ArchiveJob <$> arbitrary
1846
      Luxi.ReqAutoArchiveJobs -> Luxi.AutoArchiveJobs <$> arbitrary <*>
1847
                                 arbitrary
1848
      Luxi.ReqCancelJob -> Luxi.CancelJob <$> arbitrary
1849
      Luxi.ReqSetDrainFlag -> Luxi.SetDrainFlag <$> arbitrary
1850
      Luxi.ReqSetWatcherPause -> Luxi.SetWatcherPause <$> arbitrary
1851

  
1852
-- | Simple check that encoding/decoding of LuxiOp works.
1853
prop_Luxi_CallEncoding :: Luxi.LuxiOp -> Property
1854
prop_Luxi_CallEncoding op =
1855
  (Luxi.validateCall (Luxi.buildCall op) >>= Luxi.decodeCall) ==? Types.Ok op
1856

  
1857
-- | Helper to a get a temporary file name.
1858
getTempFileName :: IO FilePath
1859
getTempFileName = do
1860
  tempdir <- getTemporaryDirectory
1861
  (fpath, handle) <- openTempFile tempdir "luxitest"
1862
  _ <- hClose handle
1863
  removeFile fpath
1864
  return fpath
1865

  
1866
-- | Server ping-pong helper.
1867
luxiServerPong :: Luxi.Client -> IO ()
1868
luxiServerPong c = do
1869
  msg <- Luxi.recvMsgExt c
1870
  case msg of
1871
    Luxi.RecvOk m -> Luxi.sendMsg c m >> luxiServerPong c
1872
    _ -> return ()
1873

  
1874
-- | Client ping-pong helper.
1875
luxiClientPong :: Luxi.Client -> [String] -> IO [String]
1876
luxiClientPong c =
1877
  mapM (\m -> Luxi.sendMsg c m >> Luxi.recvMsg c)
1878

  
1879
-- | Monadic check that, given a server socket, we can connect via a
1880
-- client to it, and that we can send a list of arbitrary messages and
1881
-- get back what we sent.
1882
prop_Luxi_ClientServer :: [[DNSChar]] -> Property
1883
prop_Luxi_ClientServer dnschars = monadicIO $ do
1884
  let msgs = map (map dnsGetChar) dnschars
1885
  fpath <- run $ getTempFileName
1886
  -- we need to create the server first, otherwise (if we do it in the
1887
  -- forked thread) the client could try to connect to it before it's
1888
  -- ready
1889
  server <- run $ Luxi.getServer fpath
1890
  -- fork the server responder
1891
  _ <- run . forkIO $
1892
    bracket
1893
      (Luxi.acceptClient server)
1894
      (\c -> Luxi.closeClient c >> Luxi.closeServer fpath server)
1895
      luxiServerPong
1896
  replies <- run $
1897
    bracket
1898
      (Luxi.getClient fpath)
1899
      Luxi.closeClient
1900
      (\c -> luxiClientPong c msgs)
1901
  assert $ replies == msgs
1902

  
1903
testSuite "Luxi"
1904
          [ 'prop_Luxi_CallEncoding
1905
          , 'prop_Luxi_ClientServer
1906
          ]
1907

  
1908
-- * Ssconf tests
1909

  
1910
instance Arbitrary Ssconf.SSKey where
1911
  arbitrary = elements [minBound..maxBound]
1912

  
1913
prop_Ssconf_filename :: Ssconf.SSKey -> Property
1914
prop_Ssconf_filename key =
1915
  printTestCase "Key doesn't start with correct prefix" $
1916
    Ssconf.sSFilePrefix `isPrefixOf` Ssconf.keyToFilename (Just "") key
1917

  
1918
testSuite "Ssconf"
1919
  [ 'prop_Ssconf_filename
1920
  ]
1921

  
1922
-- * Qlang tests
1923

  
1924
-- | Tests that serialisation/deserialisation of filters is
1925
-- idempotent.
1926
prop_Qlang_Serialisation :: Property
1927
prop_Qlang_Serialisation =
1928
  forAll genFilter $ \flt ->
1929
  J.readJSON (J.showJSON flt) ==? J.Ok flt
1930

  
1931
prop_Qlang_FilterRegex_instances :: Qlang.FilterRegex -> Property
1932
prop_Qlang_FilterRegex_instances rex =
1933
  printTestCase "failed JSON encoding"
1934
    (J.readJSON (J.showJSON rex) ==? J.Ok rex) .&&.
1935
  printTestCase "failed read/show instances" (read (show rex) ==? rex)
1936

  
1937
testSuite "Qlang"
1938
  [ 'prop_Qlang_Serialisation
1939
  , 'prop_Qlang_FilterRegex_instances
1940
  ]

Also available in: Unified diff