diff --git a/Setup.hs b/Setup.hs
deleted file mode 100644
--- a/Setup.hs
+++ /dev/null
@@ -1,40 +0,0 @@
-#! /usr/bin/env runhaskell
-{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}
--- Copyright : 2011 Magnus Therning
--- License   : BSD3
-module Main where
-
-import Distribution.Simple
-import Distribution.PackageDescription
-import Distribution.Simple.Utils
-import Distribution.Simple.LocalBuildInfo
-import System.Cmd
-import Control.Monad
-import System.FilePath
-import System.Directory
-
-main = defaultMainWithHooks $ simpleUserHooks
-    { cleanHook = profileClean
-    , runTests = runTestsBuild
-    }
-
-profileClean pd v uh cf = let
-        _matchFileGlob g = catch (matchFileGlob g) (\ _ -> return [])
-    in do
-        (cleanHook simpleUserHooks) pd v uh cf
-        tixFiles <- _matchFileGlob "*.tix"
-        mapM_ removeFile tixFiles
-        doesDirectoryExist ".hpc" >>= \ d -> when d $ removeDirectoryRecursive ".hpc"
-
-runTestsBuild a b pd lbi = let
-        doWithExe bldDir e _ = let
-                _eN = exeName e
-                _exe = bldDir </> _eN </> _eN
-                _runTest = do
-                    putStrLn $ "** " ++ _eN ++ ":"
-                    system _exe >> return ()
-            in
-                when (_eN `elem` ["tests"]) _runTest
-    in do
-        (runTests simpleUserHooks) a b pd lbi
-        withExeLBI pd lbi (doWithExe $ buildDir lbi)
diff --git a/hsini.cabal b/hsini.cabal
--- a/hsini.cabal
+++ b/hsini.cabal
@@ -1,42 +1,54 @@
-name          : hsini
-version       : 0.1
-license       : BSD3
-license-file  : LICENSE
-author        : Magnus Therning
-maintainer    : magnus@therning.org
-copyright     : Magnus Therning, 2010
-synopsis      : Package for user configuration files (INI)
-description   : None yet
-build-type    : Custom
-category      : Network
-cabal-version : >= 1.6
+cabal-version: 3.0
+name:          hsini
+version:       0.5.2.2
+synopsis:      ini configuration files
+description:
+  Library for reading and writing configuration files in INI format (see <https://en.wikipedia.org/wiki/INI_file>).
 
+category:      Configuration, Data
+author:        Magnus Therning <magnus@therning.org>
+maintainer:    Magnus Therning <magnus@therning.org>
+license:       BSD-3-Clause
+license-file:  LICENSE
+build-type:    Simple
+
 source-repository head
-    type     : git
-    location : https : //github.com/magthe/hsini.git
+  type:     git
+  location: https://github.com/magthe/hsini.git
 
-source-repository this
-    type     : git
-    location : https   : //github.com/magthe/hsini.git
-    tag      : REL_0.1
+library
+  exposed-modules:
+    Data.Ini
+    Data.Ini.Reader
+    Data.Ini.Reader.Internals
+    Data.Ini.Types
 
-flag Test
-    description : Enable building of tests
-    default     : False
+  hs-source-dirs:   src
+  build-depends:
+    , base        <5
+    , containers
+    , mtl
+    , parsec
 
-library
-    hs-source-dirs  : src
-    build-depends   : base ==4.3.*, bytestring ==0.9.*, containers ==0.4.*, mtl ==2.0.*, parsec ==3.1.*
-    exposed-modules : Data.Ini Data.Ini.Types Data.Ini.Reader
-    other-modules   : Data.Ini.Reader.Internals
+  default-language: Haskell2010
+  ghc-options: -Wall -Wunused-packages
 
-executable tests
-    main-is        : Main.hs
-    hs-source-dirs : tst, src
-    other-modules  : Ini ReaderI
-    if flag(Test)
-        build-depends : HUnit, test-framework, test-framework-hunit, test-framework-quickcheck2, test-framework-th, QuickCheck
-        buildable     : True
-        ghc-options   : -fhpc
-    else
-        buildable     : False
+test-suite hsini-tests
+  type:             exitcode-stdio-1.0
+  main-is:          Main.hs
+  other-modules:
+    Ini
+    ReaderI
+
+  hs-source-dirs:   tst
+  build-depends:
+    , base              <5
+    , hsini
+    , parsec
+    , tasty
+    , tasty-hunit
+    , tasty-quickcheck
+    , tasty-th
+
+  default-language: Haskell2010
+  ghc-options: -Wall -Wunused-packages
diff --git a/src/Data/Ini.hs b/src/Data/Ini.hs
--- a/src/Data/Ini.hs
+++ b/src/Data/Ini.hs
@@ -1,28 +1,33 @@
--- |
--- Module    : Data.Ini
--- Copyright : 2011 Magnus Therning
--- License   : BSD3
---
--- A representation of configuration options.  It consists of /sections/,
--- each which can contain 0 or more /options/.  Each options is a /key/,
--- /value/ pair.
---
--- This module contains the API for constructing, manipulating, and querying
--- configurations.
+{-# LANGUAGE ImportQualifiedPost #-}
+
+{- |
+Module    : Data.Ini
+Copyright : 2011-2014 Magnus Therning
+License   : BSD3
+
+A representation of configuration options.  It consists of /sections/,
+each which can contain 0 or more /options/.  Each options is a /key/,
+/value/ pair.
+
+This module contains the API for constructing, manipulating, and querying
+configurations.
+-}
 module Data.Ini where
 
 -- {{{1 imports
-import qualified Data.Map as M
-import Data.Maybe
+import Data.Map qualified as M
+import Data.Maybe (isJust)
 
-import Data.Ini.Types
+import Data.Ini.Types (Config, OptionName, OptionValue, Section, SectionName)
 
 -- {{{1 configurations
+
 -- | Constructs an empty configuration.
 emptyConfig :: Config
 emptyConfig = M.empty
 
 -- {{{1 sections
+
 -- | Returns @True@ iff the configuration has a section with that name.
 hasSection :: SectionName -> Config -> Bool
 hasSection = M.member
@@ -40,37 +45,35 @@
 delSection = M.delete
 
 -- {{{1 options
+
 -- | Returns @True@ if the names section has the option.
 hasOption :: SectionName -> OptionName -> Config -> Bool
-hasOption sn on cfg = isJust $ do
-    s <- getSection sn cfg
-    M.lookup on s
+hasOption sn on cfg = isJust $ getSection sn cfg >>= M.lookup on
 
 -- | Returns the value of the option, if it exists.
 getOption :: SectionName -> OptionName -> Config -> Maybe OptionValue
-getOption sn on cfg = do
-    s <- getSection sn cfg
-    M.lookup on s
+getOption sn on cfg = getSection sn cfg >>= M.lookup on
 
 -- | Returns a list of all options in the section.
-options ::  SectionName -> Config -> [OptionName]
-options sn cfg = maybe [] (M.keys) (getSection sn cfg)
+options :: SectionName -> Config -> [OptionName]
+options sn cfg = maybe [] M.keys (getSection sn cfg)
 
 -- | Sets the value of the option, adding it if it doesn't exist.
 setOption :: SectionName -> OptionName -> OptionValue -> Config -> Config
-setOption sn on ov cfg = let
-        s = getSection sn cfg
-        new_s = M.insert on ov M.empty
-    in maybe (M.insert sn new_s cfg) (\ sec -> M.insert sn (M.insert on ov sec) cfg) s
+setOption sn on ov cfg = maybe (M.insert sn new_s cfg) (\sec -> M.insert sn (M.insert on ov sec) cfg) s
+  where
+    s = getSection sn cfg
+    new_s = M.insert on ov M.empty
 
 -- | Removes the option if it exists.  Empty sections are pruned.
 delOption :: SectionName -> OptionName -> Config -> Config
-delOption sn on cfg = let
-        s = getSection sn cfg
-        sEmptyAfterDelete = maybe True (\ sec -> M.empty == M.delete on sec) s
-    in if sEmptyAfterDelete
+delOption sn on cfg =
+    if sEmptyAfterDelete
         then M.delete sn cfg
-        else maybe cfg (\ sec -> M.insert sn (M.delete on sec) cfg) s
+        else maybe cfg (\sec -> M.insert sn (M.delete on sec) cfg) s
+  where
+    s = getSection sn cfg
+    sEmptyAfterDelete = maybe True (\sec -> M.empty == M.delete on sec) s
 
 -- | Returns all options and their values of a section.
 allItems :: SectionName -> Config -> [(OptionName, OptionValue)]
diff --git a/src/Data/Ini/Reader.hs b/src/Data/Ini/Reader.hs
--- a/src/Data/Ini/Reader.hs
+++ b/src/Data/Ini/Reader.hs
@@ -1,25 +1,26 @@
--- |
--- Module    : Data.Ini.Reader
--- Copyright : 2011 Magnus Therning
--- License   : BSD3
---
--- Parser for configurations.
-module Data.Ini.Reader
-    ( parse
-    , IniReaderError(..)
-    , IniParseResult
-    ) where
+{-# LANGUAGE ImportQualifiedPost #-}
 
-import Control.Monad.Error
-import qualified Text.ParserCombinators.Parsec as P
+{- |
+Module    : Data.Ini.Reader
+Copyright : 2011-2014 Magnus Therning
+License   : BSD3
 
-import Data.Ini.Types
-import Data.Ini.Reader.Internals
+Parser for configurations.
+-}
+module Data.Ini.Reader (
+    parse,
+    IniReaderError (..),
+    IniParseResult,
+) where
 
+import Control.Monad.Except (throwError)
+import Text.ParserCombinators.Parsec qualified as P
+
+import Data.Ini.Reader.Internals (IniParseResult, IniReaderError (..), buildConfig, iniParser)
+import Data.Ini.Types (Config)
+
 -- | Parser for a configuration contained in a 'String'.
 parse :: String -> IniParseResult Config
-parse s = let
-        pr = P.parse iniParser "ini" s
-    in case pr of
-        Left e -> throwError . IniParserError $ show e
-        Right is -> buildConfig is
+parse s = case P.parse iniParser "ini" s of
+    Left e -> throwError . IniParserError $ show e
+    Right is -> buildConfig is
diff --git a/src/Data/Ini/Reader/Internals.hs b/src/Data/Ini/Reader/Internals.hs
--- a/src/Data/Ini/Reader/Internals.hs
+++ b/src/Data/Ini/Reader/Internals.hs
@@ -1,20 +1,35 @@
--- |
--- Module    : Data.Ini.Reader.Internals
--- Copyright : 2011 Magnus Therning
--- License   : BSD3
---
--- Internal functions used in 'Data.Ini.Reader'.
+{-# LANGUAGE FlexibleContexts #-}
+
+{- |
+Module    : Data.Ini.Reader.Internals
+Copyright : 2011-2014 Magnus Therning
+License   : BSD3
+
+Internal functions used in 'Data.Ini.Reader'.
+-}
 module Data.Ini.Reader.Internals where
 
-import Control.Monad.Error
-import Control.Monad.State
--- import Text.ParserCombinators.Parsec as P
-import Text.Parsec as P
-import Text.Parsec.String
-import qualified Data.ByteString as BS
+import Control.Monad.Except (MonadError (throwError))
+import Control.Monad.State (evalState, get, put)
+import Data.Functor (($>))
+import Text.Parsec as P (
+    anyChar,
+    between,
+    char,
+    choice,
+    many,
+    many1,
+    manyTill,
+    newline,
+    noneOf,
+    oneOf,
+ )
+import Text.Parsec.String (Parser)
 
-import Data.Ini
-import Data.Ini.Types
+import Data.Char (isSpace)
+import Data.Ini (emptyConfig, setOption)
+import Data.Ini.Types (Config)
+import Data.List (dropWhileEnd)
 
 data IniReaderError
     = IniParserError String
@@ -22,10 +37,6 @@
     | IniOtherError String
     deriving (Eq, Show)
 
-instance Error IniReaderError where
-    noMsg = IniOtherError "Unknown error"
-    strMsg s = IniOtherError s
-
 type IniParseResult = Either IniReaderError
 
 -- | The type used to represent a line of a config file.
@@ -38,7 +49,8 @@
 
 -- | Build a configuration from a list of 'IniFile' items.
 buildConfig :: [IniFile] -> IniParseResult Config
-buildConfig ifs = let
+buildConfig ifs =
+    let
         isComment CommentL = True
         isComment _ = False
 
@@ -46,10 +58,10 @@
 
         -- merge together OptionL and subsequent OptionContL items
         mergeOptions [] = return []
-        mergeOptions (s@(SectionL _) : ifs) = (s :) `liftM` mergeOptions ifs
-        mergeOptions (CommentL : ifs ) = (CommentL :) `liftM` mergeOptions ifs
-        mergeOptions (OptionL on ov : OptionContL ov2 : ifs) = mergeOptions $ (OptionL on (ov ++ ov2)) : ifs
-        mergeOptions (o@(OptionL on ov) : ifs) = (o :) `liftM` mergeOptions ifs
+        mergeOptions (s@(SectionL _) : ifs') = (s :) `fmap` mergeOptions ifs'
+        mergeOptions (CommentL : ifs') = (CommentL :) `fmap` mergeOptions ifs'
+        mergeOptions (OptionL on ov : OptionContL ov2 : ifs') = mergeOptions $ OptionL on (ov ++ ov2) : ifs'
+        mergeOptions (o@(OptionL _ _) : ifs') = (o :) `fmap` mergeOptions ifs'
         mergeOptions _ = throwError $ IniSyntaxError "Syntax error in INI file."
 
         -- build the configuration from a [IniFile]
@@ -59,69 +71,61 @@
             sn <- get
             let na = setOption sn on ov a
             buildit na is
-
-    in mergeOptions fIfs >>= (\ is -> return . fst $ runState (buildit emptyConfig is) "default")
+        buildit _ _ = undefined
+     in
+        mergeOptions fIfs >>= \is -> return $ evalState (buildit emptyConfig is) "default"
 
 -- | Consumer of whitespace \"@ \t@\".
 eatWhiteSpace :: Parser String
 eatWhiteSpace = many $ oneOf " \t"
 
--- | Parser for the start-of-section line.  It expects the line to start with a
--- @[@ then find the section name, and finally a @]@.  The section name may be
--- surrounded by any number of white space characters (see 'eatWhiteSpace').
+{- | Parser for the start-of-section line.  It expects the line to start with a
+@[@ then find the section name, and finally a @]@.  The section name may be
+surrounded by any number of white space characters (see 'eatWhiteSpace').
+-}
 secParser :: Parser IniFile
-secParser = let
-        validSecNameChrs = ['a'..'z'] ++ ['A'..'Z'] ++ "-/@"
-    in do
-        char '['
-        eatWhiteSpace
-        sn <- many1 $ oneOf validSecNameChrs
-        eatWhiteSpace
-        char ']'
-        manyTill anyChar newline
-        return $ SectionL sn
+secParser = SectionL <$> between sectionNameOpen sectionNameClose sectionName
+  where
+    sectionNameOpen = char '[' *> eatWhiteSpace
+    sectionNameClose = eatWhiteSpace *> char ']' *> manyTill anyChar newline
+    sectionName = many1 $ oneOf validSecNameChrs
+    validSecNameChrs = ['a' .. 'z'] ++ ['A' .. 'Z'] ++ ['0' .. '9'] ++ "._-/@\" "
 
--- | Parser for a single line of an option.  The line must start with an option
--- name, then a @=@ must be found, and finally the rest of the line is taken as
--- the option value.  The equal sign may be surrounded by any number of white
--- space characters (see 'eatWhiteSpace').
+{- | Parser for a single line of an option.  The line must start with an option
+name, then a @=@ must be found, and finally the rest of the line is taken as
+the option value.  The equal sign may be surrounded by any number of white
+space characters (see 'eatWhiteSpace').
+-}
 optLineParser :: Parser IniFile
-optLineParser = let
-        validOptNameChrs = ['a'..'z'] ++ ['A'..'Z'] ++ "-/@"
-    in do
-        on <- many1 $ oneOf validOptNameChrs
-        eatWhiteSpace
-        char '='
-        eatWhiteSpace
-        ov <- manyTill anyChar newline
-        return $ OptionL on ov
+optLineParser = OptionL <$> optionName <*> (optionEqual *> optionValue)
+  where
+    optionName = dropWhileEnd isSpace <$> (eatWhiteSpace *> many1 (oneOf validOptNameChrs))
+    optionEqual = eatWhiteSpace *> char '=' *> eatWhiteSpace
+    optionValue = manyTill anyChar newline
+    validOptNameChrs = ['a' .. 'z'] ++ ['A' .. 'Z'] ++ ['0' .. '9'] ++ "_-/@ "
 
--- | Parser for an option-value continuation line.  The line must start with
--- either a space or a tab character (\"@ \t@\").  Everything else on the line,
--- until the newline character, is taken as the continuation of an option
--- value.
+{- | Parser for an option-value continuation line.  The line must start with
+either a space or a tab character (\"@ \t@\").  Everything else on the line,
+until the newline character, is taken as the continuation of an option
+value.
+-}
 optContParser :: Parser IniFile
-optContParser = do
-    oneOf " \t"
-    eatWhiteSpace
-    oc <- noneOf " \t"
-    ov <- manyTill anyChar newline
-    return $ OptionContL $ oc:ov
+optContParser = OptionContL <$> value
+  where
+    value = (:) <$> (oneOf " \t" *> eatWhiteSpace *> noneOf " \t") <*> manyTill anyChar newline
 
--- | Parser for "noise" in the configuration file, such as comments and empty
--- lines.  (Note that lines containing only space characters will be
--- successfully parsed by 'optContParser'.)
+{- | Parser for "noise" in the configuration file, such as comments and empty
+lines.  (Note that lines containing only space characters will be
+successfully parsed by 'optContParser'.)
+-}
 noiseParser :: Parser IniFile
-noiseParser = let
-        commentP = do
-            char '#'
-            manyTill anyChar newline
-        emptyL = newline >> return ""
-    in choice [commentP, emptyL] >> return CommentL
+noiseParser =
+    let
+        commentP = oneOf "#;" *> manyTill anyChar newline
+        emptyL = (newline $> "")
+     in
+        choice [commentP, emptyL] $> CommentL
 
 iniParser :: Parser [IniFile]
-iniParser = do
-    many noiseParser
-    s1 <- secParser
-    r <- many $ choice [secParser, optLineParser, optContParser, noiseParser]
-    return (s1:r)
+iniParser =
+    many $ choice [secParser, optLineParser, optContParser, noiseParser]
diff --git a/src/Data/Ini/Types.hs b/src/Data/Ini/Types.hs
--- a/src/Data/Ini/Types.hs
+++ b/src/Data/Ini/Types.hs
@@ -1,10 +1,14 @@
--- |
--- Module    : Data.Ini.Types
--- Copyright : 2011 Magnus Therning
--- License   : BSD3
+{-# LANGUAGE ImportQualifiedPost #-}
+
+{- |
+Module    : Data.Ini.Types
+Copyright : 2011-2014 Magnus Therning
+License   : BSD3
+-}
 module Data.Ini.Types where
 
-import qualified Data.Map as M
+import Control.Arrow (second)
+import Data.Map qualified as M
 
 type Config = M.Map SectionName Section
 
@@ -16,7 +20,7 @@
 
 -- useful since Map doesn't have any Serial instance
 cfgFromList :: [(SectionName, [(OptionName, OptionValue)])] -> Config
-cfgFromList = M.fromList . map (\ (sn, ol) -> (sn, M.fromList ol))
+cfgFromList = M.fromList . map (second M.fromList)
 
 cfgToList :: Config -> [(SectionName, [(OptionName, OptionValue)])]
 cfgToList = M.toList . M.map M.toList
diff --git a/tst/Ini.hs b/tst/Ini.hs
--- a/tst/Ini.hs
+++ b/tst/Ini.hs
@@ -1,71 +1,86 @@
-{-# OPTIONS_GHC -XTemplateHaskell #-}
--- Copyright : 2011 Magnus Therning
+{-# LANGUAGE TemplateHaskell #-}
+
+-- Copyright : 2011-2014 Magnus Therning
 -- License   : BSD3
-module Ini where
+module Ini (
+    allTests,
+) where
 
 -- {{{1 imports
-import Data.List
-import Data.Maybe
-import Test.Framework
-import Test.Framework.TH
-import Test.Framework.Providers.QuickCheck2
-import Test.QuickCheck
+import Data.Maybe (fromJust, isJust, isNothing)
+import Test.Tasty (TestTree)
+import Test.Tasty.QuickCheck (Property, testProperty, (==>))
+import Test.Tasty.TH (testGroupGenerator)
 
-import Data.Ini
-import Data.Ini.Types
+import Data.Ini (allItems, delOption, delSection, getOption, getSection, hasOption, hasSection, setOption)
+import Data.Ini.Types (OptionName, OptionValue, SectionName, cfgFromList, cfgToList)
 
 -- {{{1 section properties
 -- adding and then deleting a section is a no-op (if the section doesn't exist
 -- already)
+prop_secAddDel :: [Char] -> [(SectionName, [(OptionName, OptionValue)])] -> Bool
 prop_secAddDel sn cfglst = delSection sn (setOption sn "foo" "bar" cfg2) == cfg2
-    where
-        cfg = cfgFromList cfglst
-        cfg2 = delSection sn cfg -- must make sure the section doesn't exist before adding
+  where
+    cfg = cfgFromList cfglst
+    cfg2 = delSection sn cfg -- must make sure the section doesn't exist before adding
 
 -- after adding a section the config has such a section
+prop_secAddHas :: SectionName -> [(SectionName, [(OptionName, OptionValue)])] -> Bool
 prop_secAddHas sn cfglst = hasSection sn (setOption sn "foo" "bar" cfg)
-    where cfg = cfgFromList cfglst
+  where
+    cfg = cfgFromList cfglst
 
 -- after adding a section it's possible to get it
+prop_secAddGet :: SectionName -> [(SectionName, [(OptionName, OptionValue)])] -> Bool
 prop_secAddGet sn cfglst = isJust $ getSection sn (setOption sn "foo" "bar" cfg)
-    where cfg = cfgFromList cfglst
+  where
+    cfg = cfgFromList cfglst
 
 -- after deleting a section it's gone
+prop_secDelGet :: [Char] -> [(SectionName, [(OptionName, OptionValue)])] -> Bool
 prop_secDelGet sn cfglst = isNothing $ getSection sn $ delSection sn cfg2
-    where
-        cfg = cfgFromList cfglst
-        cfg2 = setOption sn "foo" "bar" cfg
+  where
+    cfg = cfgFromList cfglst
+    cfg2 = setOption sn "foo" "bar" cfg
 
 -- {{{1 option properties
 -- setting and then deleting an option is a no-op (if the option doesn't exist
 -- already)
+prop_optSetDel :: [Char] -> [Char] -> OptionValue -> [(SectionName, [(OptionName, OptionValue)])] -> Bool
 prop_optSetDel sn on ov cfglst = delOption sn on (setOption sn on ov cfg) == cfg2
-    where
-        cfg = cfgFromList cfglst
-        cfg2 = delOption sn on cfg
+  where
+    cfg = cfgFromList cfglst
+    cfg2 = delOption sn on cfg
 
 -- after setting an option it's there
+prop_optSetHas :: SectionName -> OptionName -> OptionValue -> [(SectionName, [(OptionName, OptionValue)])] -> Bool
 prop_optSetHas sn on ov cfglst = hasOption sn on (setOption sn on ov cfg)
-    where cfg = cfgFromList cfglst
+  where
+    cfg = cfgFromList cfglst
 
 -- after setting an option it's possible to get it
+prop_optSetGet :: SectionName -> OptionName -> OptionValue -> [(SectionName, [(OptionName, OptionValue)])] -> Bool
 prop_optSetGet sn on ov cfglst = isJust $ getOption sn on $ setOption sn on ov cfg
-    where cfg = cfgFromList cfglst
+  where
+    cfg = cfgFromList cfglst
 
 -- after deleting a section it's gone
+prop_optDelGet :: [Char] -> [Char] -> [Char] -> [(SectionName, [(OptionName, OptionValue)])] -> Bool
 prop_optDelGet sn on ov cfglst = isNothing $ getOption sn on $ delOption sn on cfg2
-    where
-        cfg = cfgFromList cfglst
-        cfg2 = setOption sn on ov cfg
+  where
+    cfg = cfgFromList cfglst
+    cfg2 = setOption sn on ov cfg
 
 -- getting all items
-prop_optAllItems cfglst = (length _cfglst > 0) ==> lstItems == (allItems sn cfg)
-    where
-        cfg = cfgFromList cfglst
-        _cfglst = cfgToList cfg
-        -- sn = head . sort $ map fst _cfglst
-        sn = head $ map fst _cfglst
-        lstItems = fromJust $ lookup sn _cfglst
+prop_optAllItems :: [(SectionName, [(OptionName, OptionValue)])] -> Property
+prop_optAllItems cfglst = not (null _cfglst) ==> lstItems == allItems sn cfg
+  where
+    cfg = cfgFromList cfglst
+    _cfglst = cfgToList cfg
+    -- sn = head . sort $ map fst _cfglst
+    sn = head $ map fst _cfglst
+    lstItems = fromJust $ lookup sn _cfglst
 
 -- {{{1 allTests
+allTests :: TestTree
 allTests = $(testGroupGenerator)
diff --git a/tst/Main.hs b/tst/Main.hs
--- a/tst/Main.hs
+++ b/tst/Main.hs
@@ -1,13 +1,14 @@
--- Copyright : 2011 Magnus Therning
+-- Copyright : 2011-2014 Magnus Therning
 -- License   : BSD3
 module Main where
 
-import Test.Framework
+import Test.Tasty
 
 import qualified Ini as I
 import qualified ReaderI as RI
 
-main = defaultMain
-    [ I.allTests
-    , RI.allTests
-    ]
+main :: IO ()
+main = defaultMain allTests
+
+allTests :: TestTree
+allTests = testGroup "All tests" [I.allTests, RI.allTests]
diff --git a/tst/ReaderI.hs b/tst/ReaderI.hs
--- a/tst/ReaderI.hs
+++ b/tst/ReaderI.hs
@@ -1,92 +1,258 @@
-{-# OPTIONS_GHC -XTemplateHaskell #-}
--- Copyright : 2011 Magnus Therning
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+-- Copyright : 2011-2014 Magnus Therning
 -- License   : BSD3
-module ReaderI where
+module ReaderI (
+    allTests,
+) where
 
-import Test.Framework
-import Test.Framework.Providers.HUnit
-import Test.Framework.TH
-import Test.HUnit.Base
-import Text.ParserCombinators.Parsec as P
+import Test.Tasty (TestTree)
+import Test.Tasty.HUnit (Assertion, testCase, (@=?))
+import Test.Tasty.TH (testGroupGenerator)
+import Text.ParserCombinators.Parsec as P (Parser, parse)
 
-import Data.Ini.Reader.Internals
+import Data.Ini.Reader.Internals (
+    IniFile (CommentL, OptionContL, OptionL, SectionL),
+    iniParser,
+    noiseParser,
+    optContParser,
+    optLineParser,
+    secParser,
+ )
 
 -- Convenience function that translates a parser result to something that's
 -- easier to check.
-p2E p s t = let
+p2E :: Parser a -> String -> String -> Either String a
+p2E p s t =
+    let
         res = P.parse p s t
-    in case res of
-        Left _ -> Left "bad"
-        Right e -> Right e
+     in
+        case res of
+            Left _ -> Left "bad"
+            Right e -> Right e
 
 -- {{{1 secParser
-case_secParserBasic = let
+case_secParserAllowedChars1 :: Assertion
+case_secParserAllowedChars1 =
+    let
         expected = Right $ SectionL "foo"
         actual = p2E secParser "sec" "[foo]\n"
-    in expected @=? actual
+     in
+        expected @=? actual
 
-case_secParserDropSpace = let
+case_secParserAllowedChars2 :: Assertion
+case_secParserAllowedChars2 =
+    let
+        expected = Right $ SectionL "FooBar"
+        actual = p2E secParser "sec" "[FooBar]\n"
+     in
+        expected @=? actual
+
+case_secParserAllowedChars3 :: Assertion
+case_secParserAllowedChars3 =
+    let
+        expected = Right $ SectionL "@Foo/Bar-"
+        actual = p2E secParser "sec" "[@Foo/Bar-]\n"
+     in
+        expected @=? actual
+
+case_secParserAllowedChars4 :: Assertion
+case_secParserAllowedChars4 =
+    let
+        expected = Right $ SectionL "foo123"
+        actual = p2E secParser "sec" "[foo123]\n"
+     in
+        expected @=? actual
+
+case_secParserAllowedChars5 :: Assertion
+case_secParserAllowedChars5 =
+    let
+        expected = Right $ SectionL "_foo"
+        actual = p2E secParser "sec" "[_foo]\n"
+     in
+        expected @=? actual
+
+case_secParserDropSpace :: Assertion
+case_secParserDropSpace =
+    let
         expected = Right $ SectionL "foo"
         actual = p2E secParser "sec" "[ \tfoo\t ]\n"
-    in expected @=? actual
+     in
+        expected @=? actual
 
-case_secParserDropTrailing = let
+case_secParserDropTrailing :: Assertion
+case_secParserDropTrailing =
+    let
         expected = Right $ SectionL "foo"
         actual = p2E secParser "sec" "[foo]  \t foobar\n"
-    in expected @=? actual
+     in
+        expected @=? actual
 
+case_secParserAllowGit1 :: Assertion
+case_secParserAllowGit1 =
+    let
+        expected = Right $ SectionL "branch \"master\""
+        actual = p2E secParser "sec" "[branch \"master\"]\n"
+     in
+        expected @=? actual
+
+case_secParserAllowGit2 :: Assertion
+case_secParserAllowGit2 =
+    let
+        expected = Right $ SectionL "foo \"bar.baz\""
+        actual = p2E secParser "sec" "[foo \"bar.baz\"]\n"
+     in
+        expected @=? actual
+
 -- {{{1 optLineParser
-case_optLineParserBasic = let
+case_optLineParserAllowedChars1 :: Assertion
+case_optLineParserAllowedChars1 =
+    let
         expected = Right $ OptionL "foo" "bar"
         actual = p2E optLineParser "optLine" "foo=bar\n"
-    in expected @=? actual
+     in
+        expected @=? actual
 
-case_optLineParserDropSpace = let
+case_optLineParserAllowedChars2 :: Assertion
+case_optLineParserAllowedChars2 =
+    let
+        expected = Right $ OptionL "Foo" "bAr"
+        actual = p2E optLineParser "optLine" "Foo=bAr\n"
+     in
+        expected @=? actual
+
+case_optLineParserAllowedChars3 :: Assertion
+case_optLineParserAllowedChars3 =
+    let
+        expected = Right $ OptionL "foo@/foo-" "bar"
+        actual = p2E optLineParser "optLine" "foo@/foo-=bar\n"
+     in
+        expected @=? actual
+
+case_optLineParserAllowedChars4 :: Assertion
+case_optLineParserAllowedChars4 =
+    let
+        expected = Right $ OptionL "foo123" "bar"
+        actual = p2E optLineParser "optLine" "foo123=bar\n"
+     in
+        expected @=? actual
+
+case_optLineParserAllowedChars5 :: Assertion
+case_optLineParserAllowedChars5 =
+    let
+        expected = Right $ OptionL "_foo" "bar"
+        actual = p2E optLineParser "optLine" "_foo=bar\n"
+     in
+        expected @=? actual
+
+case_optLineParserAllowedChars6 :: Assertion
+case_optLineParserAllowedChars6 =
+    let
+        expected = Right $ OptionL "foo bar" "baz"
+        actual = p2E optLineParser "optLine" "foo bar=baz\n"
+     in
+        expected @=? actual
+
+case_optLineParserDisallowedChars1 :: Assertion
+case_optLineParserDisallowedChars1 =
+    let
+        expected = Left "bad"
+        actual = p2E optLineParser "optLine" "foo.bar=baz\n"
+     in
+        expected @=? actual
+
+case_optLineParserDropSpace1 :: Assertion
+case_optLineParserDropSpace1 =
+    let
         expected = Right $ OptionL "foo" "bar"
-        actual = p2E optLineParser "optLine" "foo\t \t=\t \t bar\n"
-    in expected @=? actual
+        actual = p2E optLineParser "optLine" " foo =  bar\n"
+     in
+        expected @=? actual
 
-case_optLineParserKeepSpace = let
+case_optLineParserDropSpace2 :: Assertion
+case_optLineParserDropSpace2 =
+    let
+        expected = Right $ OptionL "foo" "bar"
+        actual = p2E optLineParser "optLine" " \tfoo\t \t=\t \t bar\n"
+     in
+        expected @=? actual
+
+case_optLineParserKeepSpace :: Assertion
+case_optLineParserKeepSpace =
+    let
         expected = Right $ OptionL "foo" "bar \t \t"
         actual = p2E optLineParser "optLine" "foo\t \t=\t \t bar \t \t\n"
-    in expected @=? actual
+     in
+        expected @=? actual
 
 -- {{{1 optContParser
-case_optContParserSpace = let
+case_optContParserSpace :: Assertion
+case_optContParserSpace =
+    let
         expected = Right $ OptionContL "foo"
         actual = p2E optContParser "optCont" " foo\n"
-    in expected @=? actual
+     in
+        expected @=? actual
 
-case_optContParserTab = let
-        expected = Right $OptionContL "foo"
+case_optContParserTab :: Assertion
+case_optContParserTab =
+    let
+        expected = Right $ OptionContL "foo"
         actual = p2E optContParser "optCont" "\tfoo\n"
-    in expected @=? actual
+     in
+        expected @=? actual
 
-case_optContParserKeepTrailing = let
+case_optContParserKeepTrailing :: Assertion
+case_optContParserKeepTrailing =
+    let
         expected = Right $ OptionContL "foo  \t\t"
         actual = p2E optContParser "optCont" "\tfoo  \t\t\n"
-    in expected @=? actual
+     in
+        expected @=? actual
 
 -- {{{1 noiseParser
-case_noiseParserEmptyLine = let
+case_noiseParserEmptyLine :: Assertion
+case_noiseParserEmptyLine =
+    let
         expected = Right CommentL
         actual = p2E noiseParser "noise" "\n"
-    in expected @=? actual
+     in
+        expected @=? actual
 
-case_noiseParserComment = let
+case_noiseParserComment1 :: Assertion
+case_noiseParserComment1 =
+    let
         expected = Right CommentL
         actual = p2E noiseParser "noise" "# a comment\n"
-    in expected @=? actual
+     in
+        expected @=? actual
 
-case_noiseParserNonEmpty = let
+case_noiseParserComment2 :: Assertion
+case_noiseParserComment2 =
+    let
+        expected = Right CommentL
+        actual = p2E noiseParser "noise" "; another comment\n"
+     in
+        expected @=? actual
+
+case_noiseParserNonEmpty :: Assertion
+case_noiseParserNonEmpty =
+    let
         expected = Left "bad"
         actual = p2E noiseParser "noise" " \n"
-    in expected @=? actual
+     in
+        expected @=? actual
 
 -- {{{1 iniParser
--- TBD
+case_iniParserEmpty :: Assertion
+case_iniParserEmpty = expected @=? actual
+  where
+    expected = Right []
+    actual = p2E iniParser "parsing empty file" ""
 
 -- {{{1 buildConfig
 -- TBD
 
+allTests :: TestTree
 allTests = $(testGroupGenerator)
