diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,4 @@
-Copyright (c) 2011-2015 Simon Hengel <sol@typeful.net>
-Copyright (c) 2011-2012 Trystan Spangler <trystan.s@comcast.net>
-Copyright (c) 2011-2011 Greg Weber <greg@gregweber.info>
+Copyright (c) 2012-2026 Simon Hengel <sol@typeful.net>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/driver/hspec-discover.hs b/driver/hspec-discover.hs
new file mode 100644
--- /dev/null
+++ b/driver/hspec-discover.hs
@@ -0,0 +1,8 @@
+module Main (main) where
+
+import           System.Environment
+
+import           Test.Hspec.Discover.Run (run)
+
+main :: IO ()
+main = getArgs >>= run
diff --git a/hspec-discover.cabal b/hspec-discover.cabal
--- a/hspec-discover.cabal
+++ b/hspec-discover.cabal
@@ -1,26 +1,27 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.39.1.
+--
+-- see: https://github.com/sol/hpack
+
 name:             hspec-discover
-version:          2.1.10
+version:          2.11.17
 license:          MIT
 license-file:     LICENSE
-copyright:        (c) 2012-2015 Simon Hengel
+copyright:        (c) 2012-2026 Simon Hengel
 author:           Simon Hengel <sol@typeful.net>
 maintainer:       Simon Hengel <sol@typeful.net>
 build-type:       Simple
-cabal-version:    >= 1.10
 category:         Testing
 stability:        experimental
 bug-reports:      https://github.com/hspec/hspec/issues
-homepage:         http://hspec.github.io/
+homepage:         https://hspec.github.io/
 synopsis:         Automatically discover and run Hspec tests
 description:      Automatically discover and run Hspec tests
                   .
-                  <http://hspec.github.io/hspec-discover.html>
-
+                  <https://hspec.github.io/hspec-discover.html>
 extra-source-files:
-  test-data/nested-spec/FooSpec.hs
-  test-data/nested-spec/Foo/Bar/BazSpec.hs
-  test-data/nested-spec/Foo/BarSpec.hs
-  test-data/empty-dir/Foo/Bar/Baz/.placeholder
+    version.yaml
 
 source-repository head
   type: git
@@ -28,41 +29,56 @@
   subdir: hspec-discover
 
 library
+  hs-source-dirs:
+      src
+  ghc-options: -Wall -fno-warn-incomplete-uni-patterns
+  build-depends:
+      base >=4.5.0.0 && <5
+    , directory
+    , filepath
+  exposed: False
+  exposed-modules:
+      Test.Hspec.Discover.Config
+      Test.Hspec.Discover.Run
+      Test.Hspec.Discover.Sort
+  other-modules:
+      Paths_hspec_discover
   default-language: Haskell2010
 
 executable hspec-discover
-  ghc-options:
-      -Wall
+  ghc-options: -Wall -fno-warn-incomplete-uni-patterns
   hs-source-dirs:
-      src
-  main-is:
-      Main.hs
-  other-modules:
-      Run
-      Config
+      driver
+  main-is: hspec-discover.hs
   build-depends:
-      base == 4.*
-    , filepath
+      base >=4.5.0.0 && <5
     , directory
+    , filepath
+    , hspec-discover
+  other-modules:
+      Paths_hspec_discover
   default-language: Haskell2010
 
 test-suite spec
-  type:
-      exitcode-stdio-1.0
-  ghc-options:
-      -Wall
+  type: exitcode-stdio-1.0
+  ghc-options: -Wall -fno-warn-incomplete-uni-patterns
   hs-source-dirs:
-      src
-    , test
-  main-is:
-      Spec.hs
+      test
+  main-is: Spec.hs
   other-modules:
       Helper
-      RunSpec
-      ConfigSpec
+      Test.Hspec.Discover.ConfigSpec
+      Test.Hspec.Discover.RunSpec
+      Test.Hspec.Discover.SortSpec
+      Paths_hspec_discover
   build-depends:
-      base == 4.*
-    , filepath
+      QuickCheck >=2.7
+    , base >=4.5.0.0 && <5
     , directory
-    , hspec-meta >= 2.1.5
+    , filepath
+    , hspec-discover
+    , hspec-meta ==2.11.17
+    , mockery >=0.3.5
+  build-tool-depends:
+      hspec-meta:hspec-meta-discover
   default-language: Haskell2010
diff --git a/src/Config.hs b/src/Config.hs
deleted file mode 100644
--- a/src/Config.hs
+++ /dev/null
@@ -1,45 +0,0 @@
-module Config (
-  Config (..)
-, defaultConfig
-, parseConfig
-, usage
-) where
-
-import           Data.Maybe
-import           System.Console.GetOpt
-
-data Config = Config {
-  configNested :: Bool
-, configFormatter :: Maybe String
-, configNoMain :: Bool
-, configModuleName :: Maybe String
-} deriving (Eq, Show)
-
-defaultConfig :: Config
-defaultConfig = Config False Nothing False Nothing
-
-options :: [OptDescr (Config -> Config)]
-options = [
-    Option [] ["nested"] (NoArg $ \c -> c {configNested = True}) ""
-  , Option [] ["formatter"] (ReqArg (\s c -> c {configFormatter = Just s}) "FORMATTER") ""
-  , Option [] ["module-name"] (ReqArg (\s c -> c {configModuleName = Just s}) "NAME") ""
-  , Option [] ["no-main"] (NoArg $ \c   -> c {configNoMain = True}) ""
-  ]
-
-usage :: String -> String
-usage prog = "\nUsage: " ++ prog ++ " SRC CUR DST [--module-name=NAME]\n"
-
-parseConfig :: String -> [String] -> Either String Config
-parseConfig prog args = case getOpt Permute options args of
-    (opts, [], []) -> let
-        c = (foldl (flip id) defaultConfig opts)
-      in
-        if (configNoMain c && isJust (configFormatter c))
-           then
-             formatError "option `--formatter=<fmt>' does not make sense with `--no-main'\n"
-           else
-             Right c
-    (_, _, err:_)  -> formatError err
-    (_, arg:_, _)  -> formatError ("unexpected argument `" ++ arg ++ "'\n")
-  where
-    formatError err = Left (prog ++ ": " ++ err ++ usage prog)
diff --git a/src/Main.hs b/src/Main.hs
deleted file mode 100644
--- a/src/Main.hs
+++ /dev/null
@@ -1,8 +0,0 @@
-module Main (main) where
-
-import           System.Environment
-
-import           Run (run)
-
-main :: IO ()
-main = getArgs >>= run
diff --git a/src/Run.hs b/src/Run.hs
deleted file mode 100644
--- a/src/Run.hs
+++ /dev/null
@@ -1,148 +0,0 @@
-{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, OverloadedStrings #-}
-{-# OPTIONS_GHC -fno-warn-orphans #-}
--- | A preprocessor that finds and combines specs.
-module Run (
-  run
-
--- exported for testing
-, Spec(..)
-, importList
-, fileToSpec
-, findSpecs
-, getFilesRecursive
-, driverWithFormatter
-, moduleNameFromId
-, pathToModule
-) where
-import           Control.Monad
-import           Control.Applicative
-import           Data.List
-import           Data.Char
-import           Data.Maybe
-import           Data.String
-import           System.Environment
-import           System.Exit
-import           System.IO
-import           System.Directory (doesDirectoryExist, getDirectoryContents, doesFileExist)
-import           System.FilePath hiding (combine)
-
-import           Config
-
-instance IsString ShowS where
-  fromString = showString
-
-data Spec = Spec {
-  specFile :: FilePath
-, specModule :: String
-} deriving (Eq, Show)
-
-run :: [String] -> IO ()
-run args_ = do
-  name <- getProgName
-  case args_ of
-    src : _ : dst : args -> case parseConfig name args of
-      Left err -> do
-        hPutStrLn stderr err
-        exitFailure
-      Right conf -> do
-        when (configNested conf) (hPutStrLn stderr "hspec-discover: WARNING - The `--nested' flag is deprecated and will be removed in a future release!")
-        specs <- findSpecs src
-        writeFile dst (mkSpecModule src conf specs)
-    _ -> do
-      hPutStrLn stderr (usage name)
-      exitFailure
-
-mkSpecModule :: FilePath -> Config -> [Spec] -> String
-mkSpecModule src conf nodes =
-  ( "{-# LINE 1 " . shows src . " #-}\n"
-  . showString "{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}\n"
-  . showString ("module " ++ moduleName src conf ++" where\n")
-  . importList nodes
-  . showString "import Test.Hspec.Discover\n"
-  . maybe driver driverWithFormatter (configFormatter conf)
-  . showString "spec :: Spec\n"
-  . showString "spec = "
-  . formatSpecs nodes
-  ) "\n"
-  where
-    driver =
-        case configNoMain conf of
-          False ->
-              showString "main :: IO ()\n"
-            . showString "main = hspec spec\n"
-          True -> ""
-
-moduleName :: FilePath -> Config -> String
-moduleName src conf = fromMaybe (if configNoMain conf then pathToModule src else "Main") (configModuleName conf)
-
--- | Derive module name from specified path.
-pathToModule :: FilePath -> String
-pathToModule f = toUpper m:ms
-  where
-    fileName = last $ splitDirectories f
-    m:ms = takeWhile (/='.') fileName
-
-driverWithFormatter :: String -> ShowS
-driverWithFormatter f =
-    showString "import qualified " . showString (moduleNameFromId f) . showString "\n"
-  . showString "main :: IO ()\n"
-  . showString "main = hspecWithFormatter " . showString f . showString " spec\n"
-
--- | Return module name of a fully qualified identifier.
-moduleNameFromId :: String -> String
-moduleNameFromId = reverse . dropWhile (== '.') . dropWhile (/= '.') . reverse
-
--- | Generate imports for a list of specs.
-importList :: [Spec] -> ShowS
-importList = foldr (.) "" . map f
-  where
-    f :: Spec -> ShowS
-    f spec = "import qualified " . showString (specModule spec) . "Spec\n"
-
--- | Combine a list of strings with (>>).
-sequenceS :: [ShowS] -> ShowS
-sequenceS = foldr (.) "" . intersperse " >> "
-
--- | Convert a list of specs to code.
-formatSpecs :: [Spec] -> ShowS
-formatSpecs xs
-  | null xs   = "return ()"
-  | otherwise = sequenceS (map formatSpec xs)
-
--- | Convert a spec to code.
-formatSpec :: Spec -> ShowS
-formatSpec (Spec file name) = "postProcessSpec " . shows file . " (describe " . shows name . " " . showString name . "Spec.spec)"
-
-findSpecs :: FilePath -> IO [Spec]
-findSpecs src = do
-  let (dir, file) = splitFileName src
-  mapMaybe (fileToSpec dir) . filter (/= file) <$> getFilesRecursive dir
-
-fileToSpec :: FilePath -> FilePath -> Maybe Spec
-fileToSpec dir file = case reverse $ splitDirectories file of
-  x:xs -> case stripSuffix "Spec.hs" x <|> stripSuffix "Spec.lhs" x of
-    Just name | isValidModuleName name && all isValidModuleName xs ->
-      Just . Spec (dir </> file) $ (intercalate "." . reverse) (name : xs)
-    _ -> Nothing
-  _ -> Nothing
-  where
-    stripSuffix :: Eq a => [a] -> [a] -> Maybe [a]
-    stripSuffix suffix str = reverse <$> stripPrefix (reverse suffix) (reverse str)
-
--- See `Cabal.Distribution.ModuleName` (http://git.io/bj34)
-isValidModuleName :: String -> Bool
-isValidModuleName [] = False
-isValidModuleName (c:cs) = isUpper c && all isValidModuleChar cs
-
-isValidModuleChar :: Char -> Bool
-isValidModuleChar c = isAlphaNum c || c == '_' || c == '\''
-
-getFilesRecursive :: FilePath -> IO [FilePath]
-getFilesRecursive baseDir = sort <$> go []
-  where
-    go :: FilePath -> IO [FilePath]
-    go dir = do
-      c <- map (dir </>) . filter (`notElem` [".", ".."]) <$> getDirectoryContents (baseDir </> dir)
-      dirs <- filterM (doesDirectoryExist . (baseDir </>)) c >>= mapM go
-      files <- filterM (doesFileExist . (baseDir </>)) c
-      return (files ++ concat dirs)
diff --git a/src/Test/Hspec/Discover/Config.hs b/src/Test/Hspec/Discover/Config.hs
new file mode 100644
--- /dev/null
+++ b/src/Test/Hspec/Discover/Config.hs
@@ -0,0 +1,48 @@
+-- |
+-- /NOTE:/ This module is not meant for public consumption.  For user
+-- documentation look at https://hspec.github.io/hspec-discover.html.
+module Test.Hspec.Discover.Config (
+  Config (..)
+, defaultConfig
+, parseConfig
+, usage
+) where
+
+import           Data.Maybe
+import           System.Console.GetOpt
+
+data Config = Config {
+  configNested :: Bool
+, configFormatter :: Maybe String
+, configNoMain :: Bool
+, configModuleName :: Maybe String
+} deriving (Eq, Show)
+
+defaultConfig :: Config
+defaultConfig = Config False Nothing False Nothing
+
+options :: [OptDescr (Config -> Config)]
+options = [
+    Option [] ["nested"] (NoArg $ \c -> c {configNested = True}) ""
+  , Option [] ["formatter"] (ReqArg (\s c -> c {configFormatter = Just s}) "FORMATTER") ""
+  , Option [] ["module-name"] (ReqArg (\s c -> c {configModuleName = Just s}) "NAME") ""
+  , Option [] ["no-main"] (NoArg $ \c   -> c {configNoMain = True}) ""
+  ]
+
+usage :: String -> String
+usage prog = "\nUsage: " ++ prog ++ " SRC CUR DST [--module-name=NAME]\n"
+
+parseConfig :: String -> [String] -> Either String Config
+parseConfig prog args = case getOpt Permute options args of
+    (opts, [], []) -> let
+        c = foldl (flip id) defaultConfig opts
+      in
+        if configNoMain c && isJust (configFormatter c)
+           then
+             formatError "option `--formatter=<fmt>' does not make sense with `--no-main'\n"
+           else
+             Right c
+    (_, _, err:_)  -> formatError err
+    (_, arg:_, _)  -> formatError ("unexpected argument `" ++ arg ++ "'\n")
+  where
+    formatError err = Left (prog ++ ": " ++ err ++ usage prog)
diff --git a/src/Test/Hspec/Discover/Run.hs b/src/Test/Hspec/Discover/Run.hs
new file mode 100644
--- /dev/null
+++ b/src/Test/Hspec/Discover/Run.hs
@@ -0,0 +1,227 @@
+{-# LANGUAGE FlexibleInstances, OverloadedStrings #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+-- | A preprocessor that finds and combines specs.
+--
+-- /NOTE:/ This module is not meant for public consumption.  For user
+-- documentation look at https://hspec.github.io/hspec-discover.html.
+module Test.Hspec.Discover.Run (
+  run
+
+-- exported for testing
+, Spec(..)
+, importList
+, driverWithFormatter
+, moduleNameFromId
+, pathToModule
+, Tree(..)
+, Forest(..)
+, Hook(..)
+, discover
+) where
+import           Control.Monad
+import           Control.Applicative
+import           Data.List
+import           Data.Char
+import           Data.Maybe
+import           Data.String
+import           System.Environment
+import           System.Exit
+import           System.IO
+import           System.Directory (doesDirectoryExist, getDirectoryContents, doesFileExist)
+import           System.FilePath hiding (combine)
+
+import           Test.Hspec.Discover.Config
+import           Test.Hspec.Discover.Sort
+
+instance IsString ShowS where
+  fromString = showString
+
+data Spec = Spec String | Hook String [Spec]
+  deriving (Eq, Show)
+
+run :: [String] -> IO ()
+run args_ = do
+  name <- getProgName
+  case args_ of
+    src : _ : dst : args -> case parseConfig name args of
+      Left err -> do
+        hPutStrLn stderr err
+        exitFailure
+      Right conf -> do
+        when (configNested conf)             (hPutStrLn stderr "hspec-discover: WARNING - The `--nested' option is deprecated and will be removed in a future release!")
+        when (configNoMain conf)             (hPutStrLn stderr "hspec-discover: WARNING - The `--no-main' option is deprecated and will be removed in a future release!")
+        when (isJust $ configFormatter conf) (hPutStrLn stderr "hspec-discover: WARNING - The `--formatter' option is deprecated and will be removed in a future release!")
+        specs <- findSpecs src
+        writeFile dst (mkSpecModule src conf specs)
+    _ -> do
+      hPutStrLn stderr (usage name)
+      exitFailure
+
+mkSpecModule :: FilePath -> Config -> Maybe [Spec] -> String
+mkSpecModule src conf nodes =
+  ( "{-# LINE 1 " . shows src . " #-}\n"
+  . showString "{-# LANGUAGE NoImplicitPrelude #-}\n"
+  . showString "{-# OPTIONS_GHC -w -Wall -fno-warn-warnings-deprecations #-}\n"
+  . showString ("module " ++ moduleName src conf ++ " where\n")
+  . importList nodes
+  . showString "import Test.Hspec.Discover\n"
+  . maybe driver driverWithFormatter (configFormatter conf)
+  . showString "spec :: Spec\n"
+  . showString "spec = "
+  . formatSpecs nodes
+  ) "\n"
+  where
+    driver =
+        case configNoMain conf of
+          False ->
+              showString "main :: IO ()\n"
+            . showString "main = hspec spec\n"
+          True -> ""
+
+moduleName :: FilePath -> Config -> String
+moduleName src conf = fromMaybe (if configNoMain conf then pathToModule src else "Main") (configModuleName conf)
+
+-- | Derive module name from specified path.
+pathToModule :: FilePath -> String
+pathToModule f = toUpper m:ms
+  where
+    fileName = last $ splitDirectories f
+    m:ms = takeWhile (/= '.') fileName
+
+driverWithFormatter :: String -> ShowS
+driverWithFormatter f =
+    showString "import qualified " . showString (moduleNameFromId f) . showString "\n"
+  . showString "main :: IO ()\n"
+  . showString "main = hspecWithFormatter " . showString f . showString " spec\n"
+
+-- | Return module name of a fully qualified identifier.
+moduleNameFromId :: String -> String
+moduleNameFromId = reverse . dropWhile (== '.') . dropWhile (/= '.') . reverse
+
+-- | Generate imports for a list of specs.
+importList :: Maybe [Spec] -> ShowS
+importList = foldr (.) "" . map f . maybe [] moduleNames
+  where
+    f :: String -> ShowS
+    f spec = "import qualified " . showString spec . "\n"
+
+moduleNames :: [Spec] -> [String]
+moduleNames = fromForest
+  where
+    fromForest :: [Spec] -> [String]
+    fromForest = concatMap fromTree
+
+    fromTree :: Spec -> [String]
+    fromTree tree = case tree of
+      Spec name -> [name ++ "Spec"]
+      Hook name forest -> name : fromForest forest
+
+-- | Combine a list of strings with (>>).
+sequenceS :: [ShowS] -> ShowS
+sequenceS = foldr (.) "" . intersperse " >> "
+
+formatSpecs :: Maybe [Spec] -> ShowS
+formatSpecs = maybe "return ()" fromForest
+  where
+    fromForest :: [Spec] -> ShowS
+    fromForest = sequenceS . map fromTree
+
+    fromTree :: Spec -> ShowS
+    fromTree tree = case tree of
+      Spec name -> "describe " . shows name . " " . showString name . "Spec.spec"
+      Hook name forest -> "(" . showString name . ".hook $ " . fromForest forest . ")"
+
+findSpecs :: FilePath -> IO (Maybe [Spec])
+findSpecs = fmap (fmap toSpecs) . discover
+
+toSpecs :: Forest -> [Spec]
+toSpecs = fromForest []
+  where
+    fromForest :: [String] -> Forest -> [Spec]
+    fromForest names (Forest WithHook xs) = [Hook (mkModule ("SpecHook" : names)) $ concatMap (fromTree names) xs]
+    fromForest names (Forest WithoutHook xs) = concatMap (fromTree names) xs
+
+    fromTree :: [String] -> Tree -> [Spec]
+    fromTree names spec = case spec of
+      Leaf name -> [Spec $ mkModule (name : names )]
+      Node name forest -> fromForest (name : names) forest
+
+    mkModule :: [String] -> String
+    mkModule = intercalate "." . reverse
+
+-- See `Cabal.Distribution.ModuleName` (https://git.io/bj34)
+isValidModuleName :: String -> Bool
+isValidModuleName [] = False
+isValidModuleName (c:cs) = isUpper c && all isValidModuleChar cs
+
+isValidModuleChar :: Char -> Bool
+isValidModuleChar c = isAlphaNum c || c == '_' || c == '\''
+
+data Tree = Leaf String | Node String Forest
+  deriving (Eq, Show)
+
+data Forest = Forest Hook [Tree]
+  deriving (Eq, Show)
+
+data Hook = WithHook | WithoutHook
+  deriving (Eq, Show)
+
+sortKey :: Tree -> (String, Int)
+sortKey tree = case tree of
+  Leaf name -> (name, 0)
+  Node name _ -> (name, 1)
+
+discover :: FilePath -> IO (Maybe Forest)
+discover src = (>>= filterSrc) <$> specForest dir
+  where
+    filterSrc :: Forest -> Maybe Forest
+    filterSrc (Forest hook xs) = ensureForest hook $ maybe id (filter . (/=)) (toSpec file) xs
+
+    (dir, file) = splitFileName src
+
+specForest :: FilePath -> IO (Maybe Forest)
+specForest dir = do
+  files <- listDirectory dir
+  hook <- mkHook dir files
+  ensureForest hook . sortNaturallyBy sortKey . catMaybes <$> mapM toSpecTree files
+  where
+    toSpecTree :: FilePath -> IO (Maybe Tree)
+    toSpecTree name
+      | isValidModuleName name = do
+          doesDirectoryExist (dir </> name) `fallback` Nothing $ do
+            xs <- specForest (dir </> name)
+            return $ Node name <$> xs
+      | otherwise = do
+          doesFileExist (dir </> name) `fallback` Nothing $ do
+            return $ toSpec name
+
+mkHook :: FilePath -> [FilePath] -> IO Hook
+mkHook dir files
+  | "SpecHook.hs" `elem` files = do
+    doesFileExist (dir </> "SpecHook.hs") `fallback` WithoutHook $ do
+      return WithHook
+  | otherwise = return WithoutHook
+
+fallback :: IO Bool -> a -> IO a -> IO a
+fallback p def action = do
+  bool <- p
+  if bool then action else return def
+
+toSpec :: FilePath -> Maybe Tree
+toSpec file = Leaf <$> (spec >>= ensure isValidModuleName)
+  where
+    spec :: Maybe String
+    spec = stripSuffix "Spec.hs" file <|> stripSuffix "Spec.lhs" file
+
+    stripSuffix :: Eq a => [a] -> [a] -> Maybe [a]
+    stripSuffix suffix str = reverse <$> stripPrefix (reverse suffix) (reverse str)
+
+ensure :: (a -> Bool) -> a -> Maybe a
+ensure p a = guard (p a) >> Just a
+
+ensureForest :: Hook -> [Tree] -> Maybe Forest
+ensureForest hook = fmap (Forest hook) . ensure (not . null)
+
+listDirectory :: FilePath -> IO [FilePath]
+listDirectory path = filter f <$> getDirectoryContents path
+  where f filename = filename /= "." && filename /= ".."
diff --git a/src/Test/Hspec/Discover/Sort.hs b/src/Test/Hspec/Discover/Sort.hs
new file mode 100644
--- /dev/null
+++ b/src/Test/Hspec/Discover/Sort.hs
@@ -0,0 +1,33 @@
+-- |
+-- /NOTE:/ This module is not meant for public consumption.  For user
+-- documentation look at https://hspec.github.io/hspec-discover.html.
+module Test.Hspec.Discover.Sort (
+  sortNaturallyBy
+, NaturalSortKey
+, naturalSortKey
+) where
+
+import           Control.Arrow
+import           Data.Char
+import           Data.List
+import           Data.Ord
+
+sortNaturallyBy :: (a -> (String, Int)) -> [a] -> [a]
+sortNaturallyBy f = sortBy (comparing ((\ (k, t) -> (naturalSortKey k, t)) . f))
+
+newtype NaturalSortKey = NaturalSortKey [Chunk]
+  deriving (Eq, Ord)
+
+data Chunk = Numeric Integer Int | Textual [(Char, Char)]
+  deriving (Eq, Ord)
+
+naturalSortKey :: String -> NaturalSortKey
+naturalSortKey = NaturalSortKey . chunks
+  where
+    chunks [] = []
+    chunks s@(c:_)
+      | isDigit c = Numeric (read num) (length num) : chunks afterNum
+      | otherwise = Textual (map (toLower &&& id) str) : chunks afterStr
+      where
+        (num, afterNum) = span  isDigit s
+        (str, afterStr) = break isDigit s
diff --git a/test-data/empty-dir/Foo/Bar/Baz/.placeholder b/test-data/empty-dir/Foo/Bar/Baz/.placeholder
deleted file mode 100644
--- a/test-data/empty-dir/Foo/Bar/Baz/.placeholder
+++ /dev/null
diff --git a/test-data/nested-spec/Foo/Bar/BazSpec.hs b/test-data/nested-spec/Foo/Bar/BazSpec.hs
deleted file mode 100644
--- a/test-data/nested-spec/Foo/Bar/BazSpec.hs
+++ /dev/null
diff --git a/test-data/nested-spec/Foo/BarSpec.hs b/test-data/nested-spec/Foo/BarSpec.hs
deleted file mode 100644
--- a/test-data/nested-spec/Foo/BarSpec.hs
+++ /dev/null
diff --git a/test-data/nested-spec/FooSpec.hs b/test-data/nested-spec/FooSpec.hs
deleted file mode 100644
--- a/test-data/nested-spec/FooSpec.hs
+++ /dev/null
diff --git a/test/ConfigSpec.hs b/test/ConfigSpec.hs
deleted file mode 100644
--- a/test/ConfigSpec.hs
+++ /dev/null
@@ -1,47 +0,0 @@
-module ConfigSpec (main, spec) where
-
-import           Helper
-
-import           Config
-
-main :: IO ()
-main = hspec spec
-
-spec :: Spec
-spec = do
-  describe "parseConfig" $ do
-    let parse = parseConfig "hspec-discover"
-
-    it "recognizes --nested" $ do
-      parse ["--nested"] `shouldBe` Right (defaultConfig {configNested = True})
-
-    it "recognizes --formatter" $ do
-      parse ["--formatter", "someFormatter"] `shouldBe` Right (defaultConfig {configFormatter = Just "someFormatter"})
-
-    it "recognizes --no-main" $ do
-      parse ["--no-main"] `shouldBe` Right (defaultConfig {configNoMain = True})
-
-    it "returns error message on unrecognized option" $ do
-      parse ["--foo"] `shouldBe` (Left . unlines) [
-          "hspec-discover: unrecognized option `--foo'"
-        , ""
-        , "Usage: hspec-discover SRC CUR DST [--module-name=NAME]"
-        ]
-
-    it "returns error message on unexpected argument" $ do
-      parse ["foo"]   `shouldBe` (Left . unlines) [
-          "hspec-discover: unexpected argument `foo'"
-        , ""
-        , "Usage: hspec-discover SRC CUR DST [--module-name=NAME]"
-        ]
-
-    it "returns error message on --formatter=<fmt> with --no-main" $ do
-      parse ["--no-main", "--formatter=foo"] `shouldBe` (Left . unlines) [
-          "hspec-discover: option `--formatter=<fmt>' does not make sense with `--no-main'"
-        , ""
-        , "Usage: hspec-discover SRC CUR DST [--module-name=NAME]"
-        ]
-
-    context "when option is given multiple times" $ do
-      it "gives the last occurrence precedence" $ do
-        parse ["--formatter", "foo", "--formatter", "bar"] `shouldBe` Right (defaultConfig {configFormatter = Just "bar"})
diff --git a/test/RunSpec.hs b/test/RunSpec.hs
deleted file mode 100644
--- a/test/RunSpec.hs
+++ /dev/null
@@ -1,126 +0,0 @@
-module RunSpec (main, spec) where
-
-import           Helper
-
-import           System.IO
-import           System.Directory
-import           System.FilePath
-import           Data.List (sort)
-
-import           Run hiding (Spec)
-import qualified Run
-
-main :: IO ()
-main = hspec spec
-
-withTempFile :: (FilePath -> IO a) -> IO a
-withTempFile action = do
-  dir <- getTemporaryDirectory
-  (file, h) <- openTempFile dir ""
-  hClose h
-  action file <* removeFile file
-
-
-spec :: Spec
-spec = do
-  describe "run" $ do
-    it "generates test driver" $ withTempFile $ \f -> do
-      run ["test-data/nested-spec/Spec.hs", "", f]
-      readFile f `shouldReturn` unlines [
-          "{-# LINE 1 \"test-data/nested-spec/Spec.hs\" #-}"
-        , "{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}"
-        , "module Main where"
-        , "import qualified Foo.Bar.BazSpec"
-        , "import qualified Foo.BarSpec"
-        , "import qualified FooSpec"
-        , "import Test.Hspec.Discover"
-        , "main :: IO ()"
-        , "main = hspec spec"
-        , "spec :: Spec"
-        , "spec = " ++ unwords [
-               "postProcessSpec \"test-data/nested-spec/Foo/Bar/BazSpec.hs\" (describe \"Foo.Bar.Baz\" Foo.Bar.BazSpec.spec)"
-          , ">> postProcessSpec \"test-data/nested-spec/Foo/BarSpec.hs\" (describe \"Foo.Bar\" Foo.BarSpec.spec)"
-          , ">> postProcessSpec \"test-data/nested-spec/FooSpec.hs\" (describe \"Foo\" FooSpec.spec)"
-          ]
-        ]
-
-    it "generates test driver for an empty directory" $ withTempFile $ \f -> do
-      run ["test-data/empty-dir/Spec.hs", "", f]
-      readFile f `shouldReturn` unlines [
-          "{-# LINE 1 \"test-data/empty-dir/Spec.hs\" #-}"
-        , "{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}"
-        , "module Main where"
-        , "import Test.Hspec.Discover"
-        , "main :: IO ()"
-        , "main = hspec spec"
-        , "spec :: Spec"
-        , "spec = return ()"
-        ]
-
-  describe "pathToModule" $ do
-    it "derives module name from a given path" $ do
-      pathToModule "test/Spec.hs" `shouldBe` "Spec"
-
-  describe "getFilesRecursive" $ do
-    it "recursively returns all file entries of a given directory" $ do
-      getFilesRecursive "test-data" `shouldReturn` sort [
-          "empty-dir/Foo/Bar/Baz/.placeholder"
-        , "nested-spec/Foo/Bar/BazSpec.hs"
-        , "nested-spec/Foo/BarSpec.hs"
-        , "nested-spec/FooSpec.hs"
-        ]
-
-  describe "fileToSpec" $ do
-    it "converts path to spec name" $ do
-      fileToSpec "" "FooSpec.hs" `shouldBe` Just (spec_ "FooSpec.hs" "Foo")
-
-    it "rejects spec with empty name" $ do
-      fileToSpec "" "Spec.hs" `shouldBe` Nothing
-
-    it "works for lhs files" $ do
-      fileToSpec "" "FooSpec.lhs" `shouldBe` Just (spec_ "FooSpec.lhs" "Foo")
-
-    it "returns Nothing for invalid spec name" $ do
-      fileToSpec "" "foo" `shouldBe` Nothing
-
-    context "when spec does not have a valid module name" $ do
-      it "returns Nothing" $ do
-        fileToSpec "" "flycheck_Spec.hs" `shouldBe` Nothing
-
-    context "when any component of a hierarchical module name is not valid"$ do
-      it "returns Nothing" $ do
-        fileToSpec "" ("Valid" </> "invalid"  </>"MiddleNamesSpec.hs") `shouldBe` Nothing
-
-    context "when path has directory component" $ do
-      it "converts path to spec name" $ do
-        let file = "Foo" </> "Bar" </> "BazSpec.hs"
-        fileToSpec "" file `shouldBe` Just (spec_ file "Foo.Bar.Baz")
-
-      it "rejects spec with empty name" $ do
-        fileToSpec "" ("Foo" </> "Bar" </> "Spec.hs") `shouldBe` Nothing
-
-  describe "findSpecs" $ do
-    it "finds specs" $ do
-      let dir = "test-data/nested-spec"
-      findSpecs (dir </> "Spec.hs") `shouldReturn` [spec_ (dir </> "Foo/Bar/BazSpec.hs") "Foo.Bar.Baz", spec_ (dir </> "Foo/BarSpec.hs") "Foo.Bar", spec_ (dir </> "FooSpec.hs") "Foo"]
-
-  describe "driverWithFormatter" $ do
-    it "generates a test driver that uses a custom formatter" $ do
-      driverWithFormatter "Some.Module.formatter" "" `shouldBe` unlines [
-          "import qualified Some.Module"
-        , "main :: IO ()"
-        , "main = hspecWithFormatter Some.Module.formatter spec"
-        ]
-
-  describe "moduleNameFromId" $ do
-    it "returns the module name of a fully qualified identifier" $ do
-      moduleNameFromId "Some.Module.someId" `shouldBe` "Some.Module"
-
-  describe "importList" $ do
-    it "generates imports for a list of specs" $ do
-      importList [spec_ "FooSpec.hs" "Foo", spec_ "BarSpec.hs" "Bar"] "" `shouldBe` unlines [
-          "import qualified FooSpec"
-        , "import qualified BarSpec"
-        ]
-  where
-    spec_ = Run.Spec
diff --git a/test/Test/Hspec/Discover/ConfigSpec.hs b/test/Test/Hspec/Discover/ConfigSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Hspec/Discover/ConfigSpec.hs
@@ -0,0 +1,47 @@
+module Test.Hspec.Discover.ConfigSpec (main, spec) where
+
+import           Helper
+
+import           Test.Hspec.Discover.Config
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec = do
+  describe "parseConfig" $ do
+    let parse = parseConfig "hspec-discover"
+
+    it "recognizes --nested" $ do
+      parse ["--nested"] `shouldBe` Right (defaultConfig {configNested = True})
+
+    it "recognizes --formatter" $ do
+      parse ["--formatter", "someFormatter"] `shouldBe` Right (defaultConfig {configFormatter = Just "someFormatter"})
+
+    it "recognizes --no-main" $ do
+      parse ["--no-main"] `shouldBe` Right (defaultConfig {configNoMain = True})
+
+    it "returns error message on unrecognized option" $ do
+      parse ["--foo"] `shouldBe` (Left . unlines) [
+          "hspec-discover: unrecognized option `--foo'"
+        , ""
+        , "Usage: hspec-discover SRC CUR DST [--module-name=NAME]"
+        ]
+
+    it "returns error message on unexpected argument" $ do
+      parse ["foo"]   `shouldBe` (Left . unlines) [
+          "hspec-discover: unexpected argument `foo'"
+        , ""
+        , "Usage: hspec-discover SRC CUR DST [--module-name=NAME]"
+        ]
+
+    it "returns error message on --formatter=<fmt> with --no-main" $ do
+      parse ["--no-main", "--formatter=foo"] `shouldBe` (Left . unlines) [
+          "hspec-discover: option `--formatter=<fmt>' does not make sense with `--no-main'"
+        , ""
+        , "Usage: hspec-discover SRC CUR DST [--module-name=NAME]"
+        ]
+
+    context "when option is given multiple times" $ do
+      it "gives the last occurrence precedence" $ do
+        parse ["--formatter", "foo", "--formatter", "bar"] `shouldBe` Right (defaultConfig {configFormatter = Just "bar"})
diff --git a/test/Test/Hspec/Discover/RunSpec.hs b/test/Test/Hspec/Discover/RunSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Hspec/Discover/RunSpec.hs
@@ -0,0 +1,171 @@
+module Test.Hspec.Discover.RunSpec (spec) where
+
+import           Helper
+import           Test.Mockery.Directory
+
+import           Test.Hspec.Discover.Run hiding (Spec)
+import qualified Test.Hspec.Discover.Run as Run
+
+spec :: Spec
+spec = do
+  describe "run" $ around_ inTempDirectory $ do
+    it "generates a test driver" $ do
+      touch "test/FooSpec.hs"
+      touch "test/Foo/Bar/BazSpec.hs"
+      touch "test/Foo/BarSpec.hs"
+      run ["test/Spec.hs", "", "out"]
+      readFile "out" `shouldReturn` unlines [
+          "{-# LINE 1 \"test/Spec.hs\" #-}"
+        , "{-# LANGUAGE NoImplicitPrelude #-}"
+        , "{-# OPTIONS_GHC -w -Wall -fno-warn-warnings-deprecations #-}"
+        , "module Main where"
+        , "import qualified FooSpec"
+        , "import qualified Foo.BarSpec"
+        , "import qualified Foo.Bar.BazSpec"
+        , "import Test.Hspec.Discover"
+        , "main :: IO ()"
+        , "main = hspec spec"
+        , "spec :: Spec"
+        , "spec = " ++ unwords [
+               "describe \"Foo\" FooSpec.spec"
+          , ">> describe \"Foo.Bar\" Foo.BarSpec.spec"
+          , ">> describe \"Foo.Bar.Baz\" Foo.Bar.BazSpec.spec"
+          ]
+        ]
+
+    it "generates a test driver with no Main/main" $ do
+      touch "test/FooSpec.hs"
+      touch "test/Foo/Bar/BazSpec.hs"
+      touch "test/Foo/BarSpec.hs"
+      run ["test/Spec.hs", "", "out", "--no-main"]
+      readFile "out" `shouldReturn` unlines [
+          "{-# LINE 1 \"test/Spec.hs\" #-}"
+        , "{-# LANGUAGE NoImplicitPrelude #-}"
+        , "{-# OPTIONS_GHC -w -Wall -fno-warn-warnings-deprecations #-}"
+        , "module Spec where"
+        , "import qualified FooSpec"
+        , "import qualified Foo.BarSpec"
+        , "import qualified Foo.Bar.BazSpec"
+        , "import Test.Hspec.Discover"
+        , "spec :: Spec"
+        , "spec = " ++ unwords [
+               "describe \"Foo\" FooSpec.spec"
+          , ">> describe \"Foo.Bar\" Foo.BarSpec.spec"
+          , ">> describe \"Foo.Bar.Baz\" Foo.Bar.BazSpec.spec"
+          ]
+        ]
+
+    it "generates a test driver with hooks" $ do
+      touch "test/FooSpec.hs"
+      touch "test/Foo/Bar/BazSpec.hs"
+      touch "test/Foo/BarSpec.hs"
+      touch "test/Foo/SpecHook.hs"
+      touch "test/SpecHook.hs"
+      run ["test/Spec.hs", "", "out"]
+      readFile "out" `shouldReturn` unlines [
+          "{-# LINE 1 \"test/Spec.hs\" #-}"
+        , "{-# LANGUAGE NoImplicitPrelude #-}"
+        , "{-# OPTIONS_GHC -w -Wall -fno-warn-warnings-deprecations #-}"
+        , "module Main where"
+        , "import qualified SpecHook"
+        , "import qualified FooSpec"
+        , "import qualified Foo.SpecHook"
+        , "import qualified Foo.BarSpec"
+        , "import qualified Foo.Bar.BazSpec"
+        , "import Test.Hspec.Discover"
+        , "main :: IO ()"
+        , "main = hspec spec"
+        , "spec :: Spec"
+        , "spec = " ++ unwords [
+               "(SpecHook.hook $ describe \"Foo\" FooSpec.spec"
+          , ">> (Foo.SpecHook.hook $ describe \"Foo.Bar\" Foo.BarSpec.spec"
+          , ">> describe \"Foo.Bar.Baz\" Foo.Bar.BazSpec.spec))"
+          ]
+        ]
+
+    it "generates a test driver for an empty directory" $ do
+      touch "test/Foo/Bar/Baz/.placeholder"
+      run ["test/Spec.hs", "", "out"]
+      readFile "out" `shouldReturn` unlines [
+          "{-# LINE 1 \"test/Spec.hs\" #-}"
+        , "{-# LANGUAGE NoImplicitPrelude #-}"
+        , "{-# OPTIONS_GHC -w -Wall -fno-warn-warnings-deprecations #-}"
+        , "module Main where"
+        , "import Test.Hspec.Discover"
+        , "main :: IO ()"
+        , "main = hspec spec"
+        , "spec :: Spec"
+        , "spec = return ()"
+        ]
+
+  describe "pathToModule" $ do
+    it "derives module name from a given path" $ do
+      pathToModule "test/Spec.hs" `shouldBe` "Spec"
+
+  describe "driverWithFormatter" $ do
+    it "generates a test driver that uses a custom formatter" $ do
+      driverWithFormatter "Some.Module.formatter" "" `shouldBe` unlines [
+          "import qualified Some.Module"
+        , "main :: IO ()"
+        , "main = hspecWithFormatter Some.Module.formatter spec"
+        ]
+
+  describe "moduleNameFromId" $ do
+    it "returns the module name of a fully qualified identifier" $ do
+      moduleNameFromId "Some.Module.someId" `shouldBe` "Some.Module"
+
+  describe "importList" $ do
+    it "generates imports for a list of specs" $ do
+      importList (Just [Run.Spec "Foo", Run.Spec "Bar"]) "" `shouldBe` unlines [
+          "import qualified FooSpec"
+        , "import qualified BarSpec"
+        ]
+
+  describe "discover" $ do
+    it "discovers spec files" $ do
+      inTempDirectory $ do
+        touch "test/Spec.hs"
+        touch "test/FooSpec.hs"
+        touch "test/BarSpec.hs"
+        discover "test/Spec.hs" `shouldReturn` Just (Forest WithoutHook [Leaf "Bar", Leaf "Foo"])
+
+    it "discovers nested spec files" $ do
+      inTempDirectory $ do
+        touch "test/Spec.hs"
+        touch "test/Foo/BarSpec.hs"
+        touch "test/Foo/BazSpec.hs"
+        discover "test/Spec.hs" `shouldReturn` Just (Forest WithoutHook [Node "Foo" (Forest WithoutHook [Leaf "Bar", Leaf "Baz"])])
+
+    it "discovers hooks" $ do
+      inTempDirectory $ do
+        touch "test/Spec.hs"
+        touch "test/FooSpec.hs"
+        touch "test/BarSpec.hs"
+        touch "test/SpecHook.hs"
+        discover "test/Spec.hs" `shouldReturn` Just (Forest WithHook [Leaf "Bar", Leaf "Foo"])
+
+    it "discovers nested hooks" $ do
+      inTempDirectory $ do
+        touch "test/Spec.hs"
+        touch "test/Foo/BarSpec.hs"
+        touch "test/Foo/BazSpec.hs"
+        touch "test/Foo/SpecHook.hs"
+        discover "test/Spec.hs" `shouldReturn` Just (Forest WithoutHook [Node "Foo" (Forest WithHook [Leaf "Bar", Leaf "Baz"])])
+
+    it "ignores invalid module names" $ do
+      inTempDirectory $ do
+        touch "test/Spec.hs"
+        touch "test/barSpec.hs"
+        discover "test/Spec.hs" `shouldReturn` Nothing
+
+    it "ignores empty directories" $ do
+      inTempDirectory $ do
+        touch "test/Spec.hs"
+        touch "test/Foo/.keep"
+        discover "test/Spec.hs" `shouldReturn` Nothing
+
+    it "ignores directories with extension" $ do
+      inTempDirectory $ do
+        touch "test/Spec.hs"
+        touch "test/Foo.hs/BarSpec.hs"
+        discover "test/Spec.hs" `shouldReturn` Nothing
diff --git a/test/Test/Hspec/Discover/SortSpec.hs b/test/Test/Hspec/Discover/SortSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Hspec/Discover/SortSpec.hs
@@ -0,0 +1,83 @@
+module Test.Hspec.Discover.SortSpec (spec) where
+
+import           Helper
+import           Test.QuickCheck
+
+import           Test.Hspec.Discover.Sort
+
+shuffleAndSort :: [String] -> IO [String]
+shuffleAndSort xs = sortNaturally <$> generate (shuffle xs)
+
+sortNaturally :: [String] -> [String]
+sortNaturally = sortNaturallyBy $ \ name -> (name, 0)
+
+spec :: Spec
+spec = do
+  describe "naturalSortKey" $ do
+    it "is injective" $ property $ \ a  b -> do
+      a /= b ==> naturalSortKey a /= naturalSortKey b
+
+  describe "sortNaturally" $ do
+    it "gives shorter strings precedence" $ do
+      let expected = [
+              ""
+            , "a"
+            , "aa"
+            ]
+      shuffleAndSort expected `shouldReturn` expected
+
+    it "gives numbers precedence" $ do
+      let expected = [
+              "Hello2World"
+            , "Hello World"
+            ]
+      shuffleAndSort expected `shouldReturn` expected
+
+    it "sorts numbers in ascending order" $ do
+      let expected = [
+              "Spec9.hs"
+            , "Spec10.hs"
+            ]
+      shuffleAndSort expected `shouldReturn` expected
+
+    it "breaks numeric ties by string length" $ do
+      let expected = [
+              "Hello 2 World"
+            , "Hello 02 World"
+            ]
+      shuffleAndSort expected `shouldReturn` expected
+
+    it "given upper-case letters precedence over lower-case letters" $ do
+      let
+        expected = [
+            "AA.hs"
+          , "Aa.hs"
+          , "aA.hs"
+          , "aa.hs"
+          , "B.hs"
+          , "b.hs"
+          ]
+      shuffleAndSort expected `shouldReturn` expected
+
+    it "sorts number separated strings" $ do
+      let expected = [
+              "Hello2World9"
+            , "Hello2World!0"
+            ]
+      shuffleAndSort expected `shouldReturn` expected
+
+    it "sorts string separated numbers" $ do
+      let expected = [
+              "3.1.415"
+            , "3.14.15"
+            ]
+      shuffleAndSort expected `shouldReturn` expected
+
+    it "groups common string prefixes together" $ do
+      let expected = [
+              "SpecFoo.hs"
+            , "SpecFoo.lhs"
+            , "Specfoo.hs"
+            , "Specfoo.lhs"
+            ]
+      shuffleAndSort expected `shouldReturn` expected
diff --git a/version.yaml b/version.yaml
new file mode 100644
--- /dev/null
+++ b/version.yaml
@@ -0,0 +1,7 @@
+version: &version 2.11.17
+synopsis: A Testing Framework for Haskell
+author: Simon Hengel <sol@typeful.net>
+maintainer: Simon Hengel <sol@typeful.net>
+category: Testing
+stability: experimental
+homepage: https://hspec.github.io/
