packages feed

ini-0.5.1: test/Main.hs

{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-orphans #-}

-- | Simple test suite.

module Main where

import           Data.Char (isControl, isSpace)
import qualified Data.HashMap.Strict as HM
import           Data.Ini (Ini (..), KeySeparator (..), WriteIniSettings (..), parseIni, printIniWith)
import           Data.Text (Text)
import qualified Data.Text as T
import           Test.Hspec (describe, hspec, it, shouldBe)
import           Test.QuickCheck

main :: IO ()
main =
    hspec $ do
        describe "parseIni" $ do
            it "parses multi-section file with comments" $
                parseIni
                    "# Some comment.\n\
                    \[SERVER]\n\
                    \port=6667\n\
                    \hostname=localhost\n\
                    \[AUTH]\n\
                    \user=hello\n\
                    \pass=world\n\
                    \# Salt can be an empty string.\n\
                    \salt="
                    `shouldBe` Right
                        ( Ini
                            { iniSections =
                                HM.fromList
                                    [
                                        ( "AUTH"
                                        ,
                                            [ ("user", "hello")
                                            , ("pass", "world")
                                            , ("salt", "")
                                            ]
                                        )
                                    ,
                                        ( "SERVER"
                                        , [("port", "6667"), ("hostname", "localhost")]
                                        )
                                    ]
                            , iniGlobals = []
                            }
                        )

            it "parses file with globals" $
                parseIni
                    "# Some comment.\n\
                    \port=6667\n\
                    \hostname=localhost\n\
                    \[AUTH]\n\
                    \user=hello\n\
                    \pass=world\n\
                    \# Salt can be an empty string.\n\
                    \salt="
                    `shouldBe` Right
                        ( Ini
                            { iniSections =
                                HM.fromList
                                    [
                                        ( "AUTH"
                                        ,
                                            [ ("user", "hello")
                                            , ("pass", "world")
                                            , ("salt", "")
                                            ]
                                        )
                                    ]
                            , iniGlobals =
                                [("port", "6667"), ("hostname", "localhost")]
                            }
                        )

            it "fails to parse file with invalid keys" $
                parseIni
                    "Name=Foo\n\
                    \Name[en_GB]=Fubar"
                    `shouldBe` Left "Failed reading: Name[en_GB]=Fubar"

            it "parses file ending with comments" $
                parseIni
                    "[default]\n\
                    \a = 1\n\
                    \\n\
                    \#[staging-PI]\n\
                    \#a = 2\n"
                    `shouldBe` Right
                        ( Ini
                            { iniSections = HM.fromList [("default", [("a", "1")])]
                            , iniGlobals = []
                            }
                        )

            it "parses file with globals only" $
                parseIni
                    "global1 = hello\n\
                    \global2 = 123\n\
                    \# An end of file comment here"
                    `shouldBe` Right
                        ( Ini
                            { iniSections = HM.empty
                            , iniGlobals = [("global1", "hello"), ("global2", "123")]
                            }
                        )

            it "parses empty file" $
                parseIni "" `shouldBe` Right mempty

            it "roundtrips with printIniWith" $
                property $ \ini settings ->
                    let printed = printIniWith settings ini
                        parsed = parseIni printed
                     in counterexample (T.unpack printed) $
                            parsed === Right ini

genKey :: Gen Text
genKey = do
    firstChar <- arbitrary `suchThat` isValidFirstKeyChar
    restChars <- listOf $ arbitrary `suchThat` isValidKeyChar
    pure $ T.pack (firstChar : restChars)
  where
    isValidKeyChar c = not (c == '=' || c == ':' || c == '[' || c == ']' || isControl c || isSpace c)
    isValidFirstKeyChar c = isValidKeyChar c && c /= ';' && c /= '#'

genSectionName :: Gen Text
genSectionName = do
    chars <- listOf1 $ arbitrary `suchThat` isValidSectionChar
    let name = T.strip $ T.pack chars
    if T.null name
        then genSectionName -- retry if stripping results in empty
        else pure name
  where
    isValidSectionChar c = c /= '[' && c /= ']' && not (isControl c)

genValue :: Gen Text
genValue = do
    chars <- listOf $ arbitrary `suchThat` (not . isControl)
    pure $ T.strip $ T.pack chars

genKeyValue :: Gen (Text, Text)
genKeyValue = (,) <$> genKey <*> genValue

instance Arbitrary KeySeparator where
    arbitrary = arbitraryBoundedEnum
    shrink ColonKeySeparator = []
    shrink EqualsKeySeparator = [ColonKeySeparator]

instance Arbitrary Ini where
    arbitrary = do
        numSections <- choose (0, 2)
        sections <- vectorOf numSections $ do
            name <- genSectionName
            numPairs <- choose (0, 2)
            pairs <- vectorOf numPairs genKeyValue
            pure (name, pairs)
        numGlobals <- choose (0, 3)
        globals <- vectorOf numGlobals genKeyValue
        pure $
            Ini
                { iniSections = HM.fromList sections
                , iniGlobals = globals
                }
    shrink ini =
        -- Shrink by removing sections
        [ ini{iniSections = HM.fromList secs'}
        | secs' <- shrinkList (const []) (HM.toList (iniSections ini))
        ]
            ++
            -- Shrink by removing key-value pairs from sections
            [ ini{iniSections = HM.fromList secs'}
            | secs' <- shrinkOne shrinkSection (HM.toList (iniSections ini))
            ]
            ++
            -- Shrink by removing globals
            [ ini{iniGlobals = globals'}
            | globals' <- shrinkList (const []) (iniGlobals ini)
            ]
      where
        shrinkSection (name, pairs) = [(name, pairs') | pairs' <- shrinkList (const []) pairs]
        shrinkOne _ [] = []
        shrinkOne f (x : xs) = [x' : xs | x' <- f x] ++ [x : xs' | xs' <- shrinkOne f xs]

instance Arbitrary WriteIniSettings where
    arbitrary = WriteIniSettings <$> arbitrary
    shrink (WriteIniSettings sep) = WriteIniSettings <$> shrink sep