diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,27 @@
+## 0.5.1
+
+_2025-12-20, Jan Hrček_
+
+- Fixed a parser bug where parsing would fail in presence of comments at the end of file
+[#12](https://github.com/jhrcek/ini/pull/12)
+
+## 0.5.0
+
+_2023-02-08, Chris Martin_
+
+The behavior of `(<>)` for the `Ini` type has changed
+[#2](https://github.com/andreasabel/ini/issues/2)
+
+- `<>` previously discarded all `iniGlobals`. Now it concatenates
+  the globals from the two `Ini` values.
+
+- When two `Ini` values contained `iniSections` with the same name,
+  `<>` previously returned the section from the left value and
+  discarded the section of the same name from the right value.
+  Now it concatenates the sections of the same name.
+
+Tested with GHC 7.0 - ghc-9.6.0.20230128.
+
 ## 0.4.2
 
 _2022-07-26, Andreas Abel_
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 [![Hackage](https://img.shields.io/hackage/v/ini.svg?color=informational)](https://hackage.haskell.org/package/ini)
 [![ini on Stackage Nightly](https://stackage.org/package/ini/badge/nightly)](https://stackage.org/nightly/package/ini)
 [![Stackage LTS version](https://www.stackage.org/package/ini/badge/lts?label=Stackage)](https://www.stackage.org/package/ini)
-[![Haskell CI](https://github.com/andreasabel/ini/actions/workflows/haskell.yml/badge.svg)](https://github.com/andreasabel/ini/actions/workflows/haskell.yml)
+[![Haskell CI](https://github.com/jhrcek/ini/actions/workflows/haskell-ci.yml/badge.svg)](https://github.com/jhrcek/ini/actions/workflows/haskell-ci.yml)
 
 ini
 ===
diff --git a/ini.cabal b/ini.cabal
--- a/ini.cabal
+++ b/ini.cabal
@@ -1,34 +1,32 @@
 cabal-version:       >= 1.10
 name:                ini
-version:             0.4.2
+version:             0.5.1
 synopsis:            Configuration files in the INI format.
 description:         Quick and easy configuration files in the INI format.
 license:             BSD3
 license-file:        LICENSE
 author:              Chris Done
-maintainer:          Andreas Abel
-homepage:            https://github.com/andreasabel/ini
-bug-reports:         https://github.com/andreasabel/ini/issues
+maintainer:          Jan Hrček
+homepage:            https://github.com/jhrcek/ini
+bug-reports:         https://github.com/jhrcek/ini/issues
 copyright:           2013 Chris Done
 category:            Data, Configuration
 build-type:          Simple
 
 tested-with:
-  GHC == 9.4.1
-  GHC == 9.2.3
-  GHC == 9.0.2
-  GHC == 8.10.7
-  GHC == 8.8.4
-  GHC == 8.6.5
-  GHC == 8.4.4
-  GHC == 8.2.2
-  GHC == 8.0.2
-  GHC == 7.10.3
-  GHC == 7.8.4
-  GHC == 7.6.3
-  GHC == 7.4.2
-  GHC == 7.2.2
-  GHC == 7.0.4
+  GHC == 9.12.2
+   || == 9.10.3
+   || == 9.8.4
+   || == 9.6.7
+   || == 9.4.8
+   || == 9.2.8
+   || == 9.0.2
+   || == 8.10.7
+   || == 8.8.4
+   || == 8.6.5
+   || == 8.4.4
+   || == 8.2.2
+   || == 8.0.2
 
 extra-source-files:
   CHANGELOG.md
@@ -56,13 +54,16 @@
   type: exitcode-stdio-1.0
   hs-source-dirs: test
   main-is: Main.hs
+  ghc-options:     -Wall
   build-depends:     base >= 4 && <5
+                   , QuickCheck
                    , ini
                    , hspec
+                   , text
                    , unordered-containers
 
   default-language:  Haskell98
 
 source-repository head
   type:     git
-  location: https://github.com/andreasabel/ini.git
+  location: https://github.com/jhrcek/ini.git
diff --git a/src/Data/Ini.hs b/src/Data/Ini.hs
--- a/src/Data/Ini.hs
+++ b/src/Data/Ini.hs
@@ -1,5 +1,4 @@
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE OverloadedStrings #-}
 
 -- | Clean configuration files in the INI format.
 --
@@ -89,8 +88,13 @@
     }
   deriving (Show, Eq)
 
+-- | '<>' concatenates the lists of entries within each section (since @ini-0.5.0@)
 instance Semigroup Ini where
-  x <> y = Ini {iniGlobals = mempty, iniSections = iniSections x <> iniSections y}
+  x <> y =
+    Ini
+      { iniGlobals = iniGlobals x ++ iniGlobals y
+      , iniSections = M.unionWith (++) (iniSections x) (iniSections y)
+      }
 
 instance Monoid Ini where
   mempty = Ini {iniGlobals = mempty, iniSections = mempty}
@@ -209,7 +213,7 @@
 data KeySeparator
   = ColonKeySeparator
   | EqualsKeySeparator
-  deriving (Eq, Show)
+  deriving (Bounded, Enum, Eq, Show)
 
 -- | Settings determining how an INI file is written.
 data WriteIniSettings = WriteIniSettings
@@ -229,8 +233,8 @@
 -- | Print an INI config.
 printIniWith :: WriteIniSettings -> Ini -> Text
 printIniWith wis i =
-  T.concat $ (map buildPair (iniGlobals i)) ++
-             (map buildSection (M.toList (iniSections i)))
+  T.concat $ map buildPair (iniGlobals i) ++
+             map buildSection (M.toList (iniSections i))
   where buildSection (name,pairs) =
           "[" <> name <> "]\n" <>
           T.concat (map buildPair pairs)
@@ -246,6 +250,7 @@
   (\kv secs -> Ini {iniSections = M.fromList secs, iniGlobals = kv}) <$>
   many keyValueParser <*>
   many sectionParser <*
+  skipComments <*
   (endOfInput <|> (fail . T.unpack =<< takeWhile (not . isControl)))
 
 -- | A section. Format: @[foo]@. Conventionally, @[FOO]@.
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,74 +1,191 @@
 {-# 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
-import           Test.Hspec
+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
-          "Regular files"
-          (do it
-                "Multi-section file with comments"
-                (shouldBe
-                   (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=")
-                   (Right
-                      (Ini
-                         { iniSections =
-                             HM.fromList
-                               [ ( "AUTH"
-                                 , [ ("user", "hello")
-                                   , ("pass", "world")
-                                   , ("salt", "")
-                                   ])
-                               , ( "SERVER"
-                                 , [("port", "6667"), ("hostname", "localhost")])
-                               ]
-                         , iniGlobals = []
-                         })))
-              it
-                "File with globals"
-                (shouldBe
-                   (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=")
-                   (Right
-                      (Ini
-                         { iniSections =
-                             HM.fromList
-                               [ ( "AUTH"
-                                 , [ ("user", "hello")
-                                   , ("pass", "world")
-                                   , ("salt", "")
-                                   ])
-                               ]
-                         , iniGlobals =
-                             [("port", "6667"), ("hostname", "localhost")]
-                         })))
-              it
-                "File with invalid keys"
-                (shouldBe
-                   (parseIni
-                      "Name=Foo\n\
-                      \Name[en_GB]=Fubar")
-                   (Left "Failed reading: Name[en_GB]=Fubar"))))
+    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
