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