Skip to content

Commit 522984e

Browse files
committed
Add tests for per-node config parser
1 parent ef1bb99 commit 522984e

3 files changed

Lines changed: 184 additions & 1 deletion

File tree

cardano-testnet/cardano-testnet.cabal

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,8 @@ library
136136
Testnet.TestQueryCmds
137137
Testnet.Types
138138

139-
other-modules: Parsers.Cardano
139+
exposed-modules: Parsers.Cardano
140+
other-modules:
140141
Parsers.Help
141142
Parsers.Version
142143
Testnet.TestEnumGenerator
@@ -241,6 +242,7 @@ test-suite cardano-testnet-test
241242
Cardano.Testnet.Test.Rpc.Transaction
242243
Cardano.Testnet.Test.Misc
243244
Cardano.Testnet.Test.Node.Shutdown
245+
Cardano.Testnet.Test.Parser
244246
Cardano.Testnet.Test.MainnetParams
245247
Cardano.Testnet.Test.SanityCheck
246248
Cardano.Testnet.Test.RunTestnet
@@ -288,6 +290,7 @@ test-suite cardano-testnet-test
288290
, regex-compat
289291
, rio
290292
, tasty ^>= 1.5
293+
, tasty-hedgehog
291294
, text
292295
, time
293296
, transformers
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
module Cardano.Testnet.Test.Parser
2+
( prop_parseNodeSpecs_roundtrip
3+
, prop_parseNodeSpecs_counts
4+
, prop_relay_before_spo_rejected
5+
, unit_valid_mixed_specs
6+
, unit_quoted_paths
7+
) where
8+
9+
import Prelude
10+
11+
import Data.Either (isLeft)
12+
import Data.List (intercalate)
13+
import qualified Data.List.NonEmpty as NEL
14+
15+
import qualified Hedgehog as H
16+
import qualified Hedgehog.Extras as H
17+
import qualified Hedgehog.Gen as Gen
18+
import qualified Hedgehog.Range as Range
19+
20+
import Parsers.Cardano (parseNodeSpecs)
21+
import Testnet.Start.Types (NodeWithOptions (..), TestnetNodesWithOptions (..))
22+
23+
prop_parseNodeSpecs_roundtrip :: H.Property
24+
prop_parseNodeSpecs_roundtrip = H.property $ do
25+
specs <- H.forAll genValidSpecs
26+
let input = encodeSpecs specs
27+
(spoSpecs, relaySpecs) = span isSpo specs
28+
result <- H.leftFail $ parseNodeSpecs input
29+
H.footnote $ "Input: " ++ input
30+
map nodeBin (NEL.toList (optSpoNodes result)) H.=== map specPath spoSpecs
31+
map nodeBin (optRelayNodes result) H.=== map specPath relaySpecs
32+
33+
prop_parseNodeSpecs_counts :: H.Property
34+
prop_parseNodeSpecs_counts = H.property $ do
35+
specs <- H.forAll genValidSpecs
36+
let input = encodeSpecs specs
37+
nSpos = length $ filter isSpo specs
38+
nRelays = length $ filter (not . isSpo) specs
39+
result <- H.leftFail $ parseNodeSpecs input
40+
H.footnote $ "Input: " ++ input
41+
length (NEL.toList (optSpoNodes result)) H.=== nSpos
42+
length (optRelayNodes result) H.=== nRelays
43+
44+
prop_relay_before_spo_rejected :: H.Property
45+
prop_relay_before_spo_rejected = H.property $ do
46+
specs <- H.forAll genBadOrderSpecs
47+
let input = encodeSpecs specs
48+
H.footnote $ "Input: " ++ input
49+
H.assert $ isLeft $ parseNodeSpecs input
50+
51+
-- | Valid mixed specs parse correctly.
52+
unit_valid_mixed_specs :: H.Property
53+
unit_valid_mixed_specs = H.withTests 1 . H.property $ do
54+
result1 <- H.leftFail $ parseNodeSpecs "spo,spo,relay,relay"
55+
length (NEL.toList (optSpoNodes result1)) H.=== 2
56+
length (optRelayNodes result1) H.=== 2
57+
58+
result2 <- H.leftFail $ parseNodeSpecs "spo"
59+
length (NEL.toList (optSpoNodes result2)) H.=== 1
60+
length (optRelayNodes result2) H.=== 0
61+
62+
result3 <- H.leftFail $ parseNodeSpecs "spo:node-bin=/usr/bin/cardano-node,relay"
63+
nodeBin (NEL.head (optSpoNodes result3)) H.=== Just "/usr/bin/cardano-node"
64+
length (optRelayNodes result3) H.=== 1
65+
66+
-- | Quoted paths with commas, colons, backslashes, and quotes parse correctly.
67+
unit_quoted_paths :: H.Property
68+
unit_quoted_paths = H.withTests 1 . H.property $ do
69+
r1 <- H.leftFail $ parseNodeSpecs "spo:node-bin=\"/path,with,commas\""
70+
nodeBin (NEL.head (optSpoNodes r1)) H.=== Just "/path,with,commas"
71+
72+
r2 <- H.leftFail $ parseNodeSpecs "spo:node-bin=\"/path:with:colons\""
73+
nodeBin (NEL.head (optSpoNodes r2)) H.=== Just "/path:with:colons"
74+
75+
r3 <- H.leftFail $ parseNodeSpecs "spo:node-bin=\"/path\\\\with\\\\backslashes\""
76+
nodeBin (NEL.head (optSpoNodes r3)) H.=== Just "/path\\with\\backslashes"
77+
78+
r4 <- H.leftFail $ parseNodeSpecs "spo:node-bin=\"/path\\\"with\\\"quotes\""
79+
nodeBin (NEL.head (optSpoNodes r4)) H.=== Just "/path\"with\"quotes"
80+
81+
-- Misplaced quotes must fail
82+
H.assert $ isLeft $ parseNodeSpecs "spo:node-bin=\"/unclosed" -- opening quote, no close
83+
H.assert $ isLeft $ parseNodeSpecs "spo:node-bin=/closed\"" -- no opening, quote at end
84+
H.assert $ isLeft $ parseNodeSpecs "spo:node-bin=/mid\"dle" -- quote in the middle
85+
86+
-- Adversarial substrings that could confuse the parser
87+
adversarialFragments :: [String]
88+
adversarialFragments =
89+
[ ":"
90+
, "spo"
91+
, "relay"
92+
, "node-bin"
93+
, "\\\""
94+
, "\\\\"
95+
, "spo:node-bin=\"\""
96+
, "relay,spo,relay"
97+
, "\",spo:node-bin=evil\""
98+
, "node-bin=gotcha"
99+
, "spo:node-bin=\"foo,bar\",relay"
100+
, ","
101+
, "="
102+
, "\""
103+
, "\\"
104+
]
105+
106+
genPathSegment :: H.Gen String
107+
genPathSegment = Gen.choice
108+
[ Gen.element adversarialFragments
109+
, Gen.string (Range.linear 1 10) (Gen.frequency [(9, Gen.alphaNum), (1, pure '.')])
110+
]
111+
112+
genPath :: H.Gen FilePath
113+
genPath = do
114+
segments <- Gen.list (Range.linear 1 5) genPathSegment
115+
pure $ "/" ++ intercalate "/" segments
116+
117+
data Role = RSpo | RRelay deriving Show
118+
119+
data Spec = Spec Role (Maybe FilePath) deriving Show
120+
121+
genSpec :: Role -> H.Gen Spec
122+
genSpec role = Spec role <$> Gen.maybe genPath
123+
124+
genValidSpecs :: H.Gen [Spec]
125+
genValidSpecs = do
126+
spos <- Gen.list (Range.linear 1 4) (genSpec RSpo)
127+
relays <- Gen.list (Range.linear 0 4) (genSpec RRelay)
128+
pure $ spos ++ relays
129+
130+
encodePath :: FilePath -> String
131+
encodePath path
132+
| any (`elem` path) (",:\\\"" :: String) = "\"" ++ concatMap escapeChar path ++ "\""
133+
| otherwise = path
134+
where
135+
escapeChar '"' = "\\\""
136+
escapeChar '\\' = "\\\\"
137+
escapeChar c = [c]
138+
139+
encodeSpec :: Spec -> String
140+
encodeSpec (Spec role mpath) =
141+
roleStr ++ maybe "" (\p -> ":node-bin=" ++ encodePath p) mpath
142+
where
143+
roleStr = case role of
144+
RSpo -> "spo"
145+
RRelay -> "relay"
146+
147+
encodeSpecs :: [Spec] -> String
148+
encodeSpecs = intercalate "," . map encodeSpec
149+
150+
specPath :: Spec -> Maybe FilePath
151+
specPath (Spec _ mp) = mp
152+
153+
isSpo :: Spec -> Bool
154+
isSpo (Spec RSpo _) = True
155+
isSpo _ = False
156+
157+
-- | Generate specs matching: spo*, relay+, (relay|spo)*, spo+
158+
-- i.e. at least one relay appears before at least one spo.
159+
genBadOrderSpecs :: H.Gen [Spec]
160+
genBadOrderSpecs = do
161+
before <- Gen.list (Range.linear 0 10) (genSpec RSpo)
162+
relay <- genSpec RRelay
163+
middle <- Gen.list (Range.linear 0 10) (genSpec =<< Gen.element [RSpo, RRelay])
164+
after <- genSpec RSpo
165+
pure $ before ++ [relay] ++ middle ++ [after]

cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import qualified Cardano.Testnet.Test.Gov.TreasuryDonation as Gov
3434
import qualified Cardano.Testnet.Test.Gov.TreasuryWithdrawal as Gov
3535
import qualified Cardano.Testnet.Test.MainnetParams
3636
import qualified Cardano.Testnet.Test.Node.Shutdown
37+
import qualified Cardano.Testnet.Test.Parser
3738
import qualified Cardano.Testnet.Test.Rpc.Query
3839
import qualified Cardano.Testnet.Test.Rpc.Transaction
3940
import qualified Cardano.Testnet.Test.RunTestnet
@@ -44,13 +45,15 @@ import qualified Cardano.Testnet.Test.UpdateTimeStamps
4445

4546
import Prelude
4647

48+
import Data.String (fromString)
4749
import qualified System.Environment as E
4850
import System.IO (BufferMode (LineBuffering), hSetBuffering, hSetEncoding, stdout, utf8)
4951

5052
import Testnet.Property.Run (ignoreOnMacAndWindows, ignoreOnWindows)
5153

5254
import qualified Test.Tasty as T
5355
import Test.Tasty (TestTree)
56+
import qualified Test.Tasty.Hedgehog as H
5457

5558
-- import qualified Cardano.Testnet.Test.Cli.LeadershipSchedule
5659
-- import qualified Cardano.Testnet.Test.Gov.NoConfidence as Gov
@@ -146,6 +149,18 @@ tests = do
146149
[ ignoreOnWindows "RPC Query Protocol Params" Cardano.Testnet.Test.Rpc.Query.hprop_rpc_query_pparams
147150
, ignoreOnWindows "RPC Transaction Submit" Cardano.Testnet.Test.Rpc.Transaction.hprop_rpc_transaction
148151
]
152+
, T.testGroup "NodesWithOptions parser"
153+
[ H.testPropertyNamed "Roundtrip" (fromString "prop_parseNodeSpecs_roundtrip")
154+
Cardano.Testnet.Test.Parser.prop_parseNodeSpecs_roundtrip
155+
, H.testPropertyNamed "Counts" (fromString "prop_parseNodeSpecs_counts")
156+
Cardano.Testnet.Test.Parser.prop_parseNodeSpecs_counts
157+
, H.testPropertyNamed "Relay before SPO rejected" (fromString "prop_relay_before_spo_rejected")
158+
Cardano.Testnet.Test.Parser.prop_relay_before_spo_rejected
159+
, H.testPropertyNamed "Valid mixed specs" (fromString "unit_valid_mixed_specs")
160+
Cardano.Testnet.Test.Parser.unit_valid_mixed_specs
161+
, H.testPropertyNamed "Quoted paths" (fromString "unit_quoted_paths")
162+
Cardano.Testnet.Test.Parser.unit_quoted_paths
163+
]
149164
]
150165

151166
main :: IO ()

0 commit comments

Comments
 (0)