1{-# LANGUAGE OverloadedStrings #-}23module Main (main) where45import Data.Either (isLeft)6import Data.Scfg7import System.IO (hClose, hPutStr)8import System.IO.Temp (withSystemTempFile)9import Test.Hspec1011main :: IO ()12main = hspec $ do13 describe "parse" $ do14 describe "directives" $ do15 it "parses a bare directive with no params" $16 parse "foo\n" `shouldBe` Right [Directive "foo" [] []]1718 it "parses a directive with params" $19 parse "foo bar baz\n" `shouldBe` Right [Directive "foo" ["bar", "baz"] []]2021 it "parses multiple directives" $22 parse "foo\nbar\n" `shouldBe` Right [Directive "foo" [] [], Directive "bar" [] []]2324 it "parses an empty config" $25 parse "" `shouldBe` Right []2627 it "accepts missing final newline" $28 parse "foo" `shouldBe` Right [Directive "foo" [] []]2930 it "ignores blank lines" $31 parse "foo\n\nbar\n" `shouldBe` Right [Directive "foo" [] [], Directive "bar" [] []]3233 describe "blocks" $ do34 it "parses a directive with a block" $35 parse "foo {\nbar\n}\n" `shouldBe` Right [Directive "foo" [] [Directive "bar" [] []]]3637 it "allows whitespace after opening brace" $38 parse "foo { \nbar\n}\n" `shouldBe` Right [Directive "foo" [] [Directive "bar" [] []]]3940 describe "trailing whitespace" $ do41 it "allows trailing whitespace after params" $42 parse "foo bar \n" `shouldBe` Right [Directive "foo" ["bar"] []]4344 it "allows trailing whitespace with no params" $45 parse "foo \n" `shouldBe` Right [Directive "foo" [] []]4647 describe "comments" $ do48 it "ignores comments" $49 parse "# a comment\nfoo\n" `shouldBe` Right [Directive "foo" [] []]5051 it "rejects CTL characters in comments" $52 parse "# bad\1&comment\nfoo\n" `shouldSatisfy` isLeft5354 describe "ParseError" $ do55 it "includes line and column of the error" $ do56 let result = parse "foo\nbar 'unterminated\n"57 case result of58 Right _ -> expectationFailure "expected parse error"59 Left err -> do60 errorLine err `shouldBe` 261 errorColumn err `shouldBe` 56263 describe "atoms" $ do64 it "rejects single quote in atom" $65 parse "foo bar'baz\n" `shouldSatisfy` isLeft6667 it "rejects CTL characters" $68 parse "foo bar\1&baz\n" `shouldSatisfy` isLeft6970 it "allows C1 control characters" $71 parse "foo bar\x80baz\n" `shouldBe` Right [Directive "foo" ["bar\x80baz"] []]7273 describe "double-quoted words" $ do74 it "parses double-quoted params" $75 parse "foo \"hello world\"\n" `shouldBe` Right [Directive "foo" ["hello world"] []]7677 it "rejects CTL characters" $78 parse "foo \"bar\1&baz\"\n" `shouldSatisfy` isLeft7980 it "allows tab" $81 parse "foo \"bar\tbaz\"\n" `shouldBe` Right [Directive "foo" ["bar\tbaz"] []]8283 it "allows C1 control characters" $84 parse "foo \"bar\x80baz\"\n" `shouldBe` Right [Directive "foo" ["bar\x80baz"] []]8586 describe "single-quoted words" $ do87 it "parses single-quoted params" $88 parse "foo 'hello world'\n" `shouldBe` Right [Directive "foo" ["hello world"] []]8990 it "rejects newline" $91 parse "foo 'bar\nbaz'\n" `shouldSatisfy` isLeft9293 it "allows tab" $94 parse "foo 'bar\tbaz'\n" `shouldBe` Right [Directive "foo" ["bar\tbaz"] []]9596 it "allows C1 control characters" $97 parse "foo 'bar\x80baz'\n" `shouldBe` Right [Directive "foo" ["bar\x80baz"] []]9899 describe "escape sequences" $ do100 it "parses escape sequences in atoms" $101 parse "foo bar\\=baz\n" `shouldBe` Right [Directive "foo" ["bar=baz"] []]102103 it "parses escape sequences in double-quoted words" $104 parse "foo \"bar\\\"baz\"\n" `shouldBe` Right [Directive "foo" ["bar\"baz"] []]105106 it "rejects CTL character" $107 parse "foo bar\\\1&baz\n" `shouldSatisfy` isLeft108109 it "allows tab" $110 parse "foo bar\\\tbaz\n" `shouldBe` Right [Directive "foo" ["bar\tbaz"] []]111112 describe "parseFile" $ do113 it "parses a file" $114 withSystemTempFile "scfg" $ \path h -> do115 hPutStr h "foo bar\n"116 hClose h117 result <- parseFile path118 result `shouldBe` Right [Directive "foo" ["bar"] []]119120 it "returns Left on parse error" $121 withSystemTempFile "scfg" $ \path h -> do122 hPutStr h "foo 'unterminated\n"123 hClose h124 result <- parseFile path125 result `shouldSatisfy` isLeft126127 describe "format" $ do128 it "formats a bare directive" $129 format [Directive "foo" [] []] `shouldBe` "\"foo\"\n"130131 it "formats a directive with params" $132 format [Directive "foo" ["bar", "baz"] []] `shouldBe` "\"foo\" \"bar\" \"baz\"\n"133134 it "formats a directive with a block" $135 format [Directive "foo" [] [Directive "bar" [] []]]136 `shouldBe` "\"foo\" {\n\t\"bar\"\n}\n"137138 it "formats nested blocks with correct indentation" $139 format [Directive "foo" [] [Directive "bar" [] [Directive "baz" [] []]]]140 `shouldBe` "\"foo\" {\n\t\"bar\" {\n\t\t\"baz\"\n\t}\n}\n"141142 it "escapes double quotes in names and params" $143 format [Directive "fo\"o" ["ba\"r"] []] `shouldBe` "\"fo\\\"o\" \"ba\\\"r\"\n"144145 describe "integration" $ do146 it "parses the spec example" $147 parse148 "train \"Shinkansen\" {\n\149 \ model \"E5\" {\n\150 \ max-speed 320km/h\n\151 \ weight 453.5t\n\152 \ }\n\153 \}\n"154 `shouldBe` Right155 [ Directive156 "train"157 ["Shinkansen"]158 [ Directive159 "model"160 ["E5"]161 [ Directive "max-speed" ["320km/h"] []162 , Directive "weight" ["453.5t"] []163 ]164 ]165 ]