diff --git a/CHANGES.markdown b/CHANGES.markdown
--- a/CHANGES.markdown
+++ b/CHANGES.markdown
@@ -1,3 +1,51 @@
+Changes in 0.25.0
+  - Full GHC 9.14 compatibility / `-unit`-support
+
+Changes in 0.24.3
+  - Allow building with GHC 9.14 (#478)
+
+Changes in 0.24.2
+  - Use `GHC.ResponseFile.expandResponse`
+
+Changes in 0.24.1
+  - Interpret GHC response files
+
+Changes in 0.24.0
+  - cabal-doctest: Add support for cabal-install 3.14.*
+
+Changes in 0.23.0
+  - Add `--fail-fast`
+
+Changes in 0.22.10
+  - Make progress reporting more robust
+
+Changes in 0.22.9
+  - Use `-fprint-error-index-links=never` for GHC `>=9.10`
+
+Changes in 0.22.8
+  - cabal-doctest: Fix handling of options with optional arguments
+
+Changes in 0.22.7
+  - cabal-doctest: Accept component
+  - cabal-doctest: Get rid of separate `cabal build` step
+  - cabal-doctest: Add support for `--list-options`
+
+Changes in 0.22.6
+  - cabal-doctest: Take `with-compiler:` from `cabal-project` into account
+  - cabal-doctest: Add support for `--with-compiler`
+  - cabal-doctest: Fix `ghc-pkg` discovery logic
+  - cabal-doctest: Cache `doctest` executables
+
+Changes in 0.22.5
+  - Add (experimental) `cabal-doctest` executable.  This is guarded behind a
+    flag for now, use `cabal install doctest -f cabal-doctest` to install it.
+
+Changes in 0.22.4
+  - Use `-Wno-unused-packages` for GHC `8.10` / `9.0` / `9.2`
+
+Changes in 0.22.3
+  - Use `-Wno-unused-packages` when extracting comments
+
 Changes in 0.22.2
   - GHC 9.8 compatibility
 
@@ -17,7 +65,7 @@
   - GHC 9.4 compatibility. (#382)
 
 Changes in 0.20.0
-  - Allow doctest to be invoked via `cabal repl --with-ghc=doctest`
+  - Allow doctest to be invoked via `cabal repl --with-compiler=doctest`
   - Include `ghc --info` output in `--info`
   - Make `--info` output formatting consistent with GHC
 
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2009-2023 Simon Hengel <sol@typeful.net>
+Copyright (c) 2009-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/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
 # Doctest: Test interactive Haskell examples
 
 `doctest` is a tool that checks
-[examples](https://www.haskell.org/haddock/doc/html/ch03s08.html#idm140354810775744)
+[examples](https://haskell-haddock.readthedocs.io/latest/markup.html#examples)
 and
-[properties](https://www.haskell.org/haddock/doc/html/ch03s08.html#idm140354810771856)
+[properties](https://haskell-haddock.readthedocs.io/latest/markup.html#properties)
 in Haddock comments.
 It is similar in spirit to the [popular Python module with the same name](https://docs.python.org/3/library/doctest.html).
 
@@ -12,6 +12,7 @@
    * [A basic example](#a-basic-example)
 * [Running doctest for a Cabal package](#running-doctest-for-a-cabal-package)
    * [Passing doctest options to cabal repl](#passing-doctest-options-to-cabal-repl)
+   * [Cabal integration](#cabal-integration)
 * [Writing examples and properties](#writing-examples-and-properties)
    * [Example groups](#example-groups)
       * [A note on performance](#a-note-on-performance)
@@ -33,24 +34,30 @@
 ## Installation
 
 `doctest` is available from
-[Hackage](http://hackage.haskell.org/cgi-bin/hackage-scripts/package/doctest).
+[Hackage](https://hackage.haskell.org/package/doctest).
 Install it with:
 
-    cabal update && cabal install doctest
-
-Make sure that Cabal's `bindir` is on your `PATH`.
-
-On Linux:
+    cabal update && cabal install --ignore-project doctest
 
-    export PATH="$HOME/.cabal/bin:$PATH"
+Make sure that Cabal's `installdir` is on your `PATH`.
 
-On Mac OS X:
+On Linux / macOS / BSD:
 
-    export PATH="$HOME/Library/Haskell/bin:$PATH"
+```bash
+ # requires cabal-install version 3.12, or later
+export PATH="$(cabal -v0 path --installdir):$PATH"
+```
+or
+```bash
+export PATH="$HOME/.local/bin:$PATH"
+```
 
-On Windows:
+On Windows with PowerShell:
 
-    set PATH="%AppData%\cabal\bin\;%PATH%"
+```pwsh
+# requires cabal-install version 3.12, or later
+$Env:PATH = "$(cabal -v0 path --installdir)" + ";" + $Env:PATH
+```
 
 ## A basic example
 
@@ -81,7 +88,7 @@
 (A comment line starting with `>>>` denotes an _expression_.
 All comment lines following an expression denote the _result_ of that expression.
 Result is defined by what a
-[REPL](http://en.wikipedia.org/wiki/Read-eval-print_loop) (e.g. ghci)
+[REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) (e.g. ghci)
 prints to `stdout` and `stderr` when evaluating that expression.)
 
 With `doctest` you can check whether the implementation satisfies the given
@@ -94,7 +101,7 @@
 
 # Running `doctest` for a Cabal package
 
-The easiest way to run `doctest` for a Cabal package is via `cabal repl --with-ghc=doctest`.
+The easiest way to run `doctest` for a Cabal package is via `cabal repl --with-compiler=doctest`.
 
 This doesn't make a big difference for a simple package, but in more involved
 situations `cabal` will make sure that all dependencies are available and it
@@ -120,8 +127,8 @@
 
 With a `.cabal` file in place, it is possible to run `doctest` via `cabal repl`:
 
-```
-$ cabal repl --with-ghc=doctest
+```bash
+$ cabal repl --with-compiler=doctest
 ...
 Examples: 2  Tried: 2  Errors: 0  Failures: 0
 ```
@@ -132,19 +139,22 @@
 - If you use properties you need to pass `--build-depends=QuickCheck` and
   `--build-depends=template-haskell` to `cabal repl`.
 
+- You likely want to reset the warning strategy for `cabal repl` with
+  `--repl-options='-w -Wdefault'`.
+
 - `doctest` always uses the version of GHC it was compiled with.  Reinstalling
   `doctest` with `cabal install doctest --overwrite-policy=always` before each
   invocation ensures that it uses the same version of GHC as is on the `PATH`.
 
-- Technically, `cabal build` is not necessary. `cabal repl --with-ghc=doctest`
+- Technically, `cabal build` is not necessary. `cabal repl --with-compiler=doctest`
   will build any dependencies as needed.  However, it's more robust to run
   `cabal build` first (specifically it is not a good idea to build
-  `ghc-paths` with `--with-ghc=doctest`).
+  `ghc-paths` with `--with-compiler=doctest`).
 
 So a more robust way to call `doctest` is as follows:
 
 ```
-cabal install doctest --overwrite-policy=always && cabal build && cabal repl --build-depends=QuickCheck --build-depends=template-haskell --with-ghc=doctest
+cabal install doctest --ignore-project --overwrite-policy=always && cabal build && cabal repl --build-depends=QuickCheck --build-depends=template-haskell --with-compiler=doctest --repl-options='-w -Wdefault'
 ```
 
 (This is what you want to use on CI.)
@@ -156,8 +166,45 @@
 
 Example:
 
+```bash
+$ cabal repl --with-compiler=doctest --repl-options=--verbose
+### Started execution at src/Fib.hs:7.
+### example:
+fib 10
+### Successful!
+
+### Started execution at src/Fib.hs:10.
+### example:
+fib 5
+### Successful!
+
+# Final summary:
+Examples: 2  Tried: 2  Errors: 0  Failures: 0
 ```
-$ cabal repl --with-ghc=doctest --repl-options=--verbose
+
+## Cabal integration
+
+***NOTE:*** This feature is experimental.
+
+***NOTE:*** This feature requires `cabal-install` version 3.12 or later.
+
+
+```bash
+$ cabal install --ignore-project doctest --flag cabal-doctest
+```
+
+```bash
+$ cabal doctest
+Examples: 2  Tried: 2  Errors: 0  Failures: 0
+```
+
+```bash
+$ cabal doctest -w ghc-8.6.5
+Examples: 2  Tried: 2  Errors: 0  Failures: 0
+```
+
+```bash
+$ cabal doctest --repl-options=--verbose
 ### Started execution at src/Fib.hs:7.
 ### example:
 fib 10
@@ -172,6 +219,11 @@
 Examples: 2  Tried: 2  Errors: 0  Failures: 0
 ```
 
+```bash
+$ cabal doctest --build-depends transformers
+Examples: 2  Tried: 2  Errors: 0  Failures: 0
+```
+
 # Writing examples and properties
 
 ## Example groups
@@ -384,7 +436,7 @@
 ```
 
 If you see an error like the following, ensure that
-[QuickCheck](http://hackage.haskell.org/package/QuickCheck) is visible to
+[QuickCheck](https://hackage.haskell.org/package/QuickCheck) is visible to
 `doctest` (e.g. by passing `--build-depends=QuickCheck` to `cabal repl`).
 
 ```haskell
@@ -412,7 +464,7 @@
 -- 2
 ```
 
-[named-chunks]: http://www.haskell.org/haddock/doc/html/ch03s05.html
+[named-chunks]: https://haskell-haddock.readthedocs.io/latest/markup.html#named-chunks
 
 ## Using GHC extensions
 
@@ -430,7 +482,7 @@
 
 ```haskell
 -- |
--- >>> :set -XTupleSections
+-- >>> :seti -XTupleSections
 -- >>> fst' $ (1,) 2
 -- 1
 fst' :: (a, b) -> a
@@ -450,10 +502,10 @@
 
 ```haskell
 -- $
--- >>> :set -XTupleSections
+-- >>> :seti -XTupleSections
 ```
 
-[language-pragma]: https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/exts/pragmas.html#language-pragma
+[language-pragma]: https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/pragmas.html#language-pragma
 
 # Limitations
 
@@ -510,45 +562,48 @@
 Add tests for new features, and make sure that the test suite passes with your
 changes.
 
-    cabal build --enable-tests && cabal exec -- cabal test --test-show-details=direct
+    cabal build && cabal exec $(cabal list-bin spec)
 
 
 # Contributors
 
+ * Simon Hengel
+ * quasicomputational
+ * Kazu Yamamoto
+ * Andreas Abel
+ * Michael Snoyman
+ * Michael Orlitzky
+ * Sakari Jokinen
  * Adam Vogt
+ * Ryan Scott
+ * Oleg Grenrus
+ * Sönke Hahn
+ * Edward Kmett
+ * Elliot Marsden
+ * Greg Pfeil
+ * Ignat Insarov
+ * Julian K. Arni
+ * Takano Akio
+ * Joachim Breitner
  * Alan Zimmerman
  * Alexander Bernauer
  * Alexandre Esteves
  * Anders Persson
- * Andreas Abel
  * Ankit Ahuja
  * Artyom Kazak
- * Edward Kmett
  * Gabor Greif
+ * Guillaume Bouchard
  * Hiroki Hattori
- * Ignat Insarov
  * Jens Petersen
- * Joachim Breitner
  * John Chee
  * João Cristóvão
- * Julian Arni
- * Kazu Yamamoto
  * Leon Schoorl
  * Levent Erkok
  * Luke Murphy
  * Matvey Aksenov
- * Michael Orlitzky
- * Michael Snoyman
  * Mitchell Rosen
  * Nick Smallbone
  * Nikos Baxevanis
- * Oleg Grenrus
- * quasicomputational
- * Ryan Scott
- * Sakari Jokinen
- * Simon Hengel
- * Sönke Hahn
- * Takano Akio
  * Tamar Christina
  * Veronika Romashkina
 
diff --git a/doctest.cabal b/doctest.cabal
--- a/doctest.cabal
+++ b/doctest.cabal
@@ -1,11 +1,11 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.36.0.
+-- This file has been generated from package.yaml by hpack version 0.39.1.
 --
 -- see: https://github.com/sol/hpack
 
 name:           doctest
-version:        0.22.2
+version:        0.25.0
 synopsis:       Test interactive Haskell examples
 description:    `doctest` is a tool that checks [examples](https://www.haskell.org/haddock/doc/html/ch03s08.html#idm140354810775744)
                 and [properties](https://www.haskell.org/haddock/doc/html/ch03s08.html#idm140354810771856)
@@ -18,7 +18,7 @@
 homepage:       https://github.com/sol/doctest#readme
 license:        MIT
 license-file:   LICENSE
-copyright:      (c) 2009-2023 Simon Hengel
+copyright:      (c) 2009-2026 Simon Hengel
 author:         Simon Hengel <sol@typeful.net>
 maintainer:     Simon Hengel <sol@typeful.net>
 build-type:     Simple
@@ -67,6 +67,10 @@
     test/integration/custom-package-conf/foo/doctest-foo.cabal
     test/integration/custom-package-conf/foo/Foo.hs
     test/integration/dos-line-endings/Fib.hs
+    test/integration/fail-fast/Bar.hs
+    test/integration/fail-fast/Foo.hs
+    test/integration/fail-fast/SetupBar.hs
+    test/integration/fail-fast/SetupFoo.hs
     test/integration/failing-multiple/Foo.hs
     test/integration/failing/Foo.hs
     test/integration/it/Foo.hs
@@ -107,6 +111,11 @@
   type: git
   location: https://github.com/sol/doctest
 
+flag cabal-doctest
+  description: Install (experimental) cabal-doctest executable
+  manual: True
+  default: False
+
 library
   ghc-options: -Wall
   hs-source-dirs:
@@ -122,7 +131,12 @@
       Test.DocTest.Internal.Location
       Test.DocTest.Internal.Parse
       Test.DocTest.Internal.Run
+      Test.DocTest.Internal.Cabal
   other-modules:
+      Cabal
+      Cabal.Options
+      Cabal.Paths
+      Cabal.ReplOptions
       Extract
       GhcUtil
       Imports
@@ -140,53 +154,73 @@
       Util
       Paths_doctest
   build-depends:
-      base >=4.5 && <5
+      base >=4.12 && <5
     , code-page >=0.1
+    , containers
     , deepseq
     , directory
     , exceptions
     , filepath
-    , ghc >=8.0 && <9.10
+    , ghc >=8.6 && <9.16
     , ghc-paths >=0.1.0.9
     , process
     , syb >=0.3
+    , temporary
     , transformers
   default-language: Haskell2010
+  if impl(ghc >= 9.0)
+    ghc-options: -fwarn-unused-packages
   if impl(ghc >= 9.8)
     ghc-options: -fno-warn-x-partial
 
+executable cabal-doctest
+  main-is: driver/cabal-doctest.hs
+  other-modules:
+      Paths_doctest
+  default-extensions:
+      NamedFieldPuns
+      RecordWildCards
+      DeriveFunctor
+      NoImplicitPrelude
+  ghc-options: -Wall -threaded
+  build-depends:
+      base >=4.12 && <5
+    , doctest
+  default-language: Haskell2010
+  if impl(ghc >= 9.0)
+    ghc-options: -fwarn-unused-packages
+  if impl(ghc >= 9.8)
+    ghc-options: -fno-warn-x-partial
+  if flag(cabal-doctest)
+    buildable: True
+  else
+    buildable: False
+
 executable doctest
-  main-is: Main.hs
+  main-is: driver/doctest.hs
   other-modules:
       Paths_doctest
   ghc-options: -Wall -threaded
-  hs-source-dirs:
-      driver
   default-extensions:
       NamedFieldPuns
       RecordWildCards
       DeriveFunctor
       NoImplicitPrelude
   build-depends:
-      base >=4.5 && <5
-    , code-page >=0.1
-    , deepseq
-    , directory
+      base >=4.12 && <5
     , doctest
-    , exceptions
-    , filepath
-    , ghc >=8.0 && <9.10
-    , ghc-paths >=0.1.0.9
-    , process
-    , syb >=0.3
-    , transformers
   default-language: Haskell2010
+  if impl(ghc >= 9.0)
+    ghc-options: -fwarn-unused-packages
   if impl(ghc >= 9.8)
     ghc-options: -fno-warn-x-partial
 
 test-suite spec
   main-is: Spec.hs
   other-modules:
+      Cabal.OptionsSpec
+      Cabal.PathsSpec
+      Cabal.ReplOptionsSpec
       ExtractSpec
       InfoSpec
       InterpreterSpec
@@ -201,6 +235,10 @@
       RunnerSpec
       RunSpec
       UtilSpec
+      Cabal
+      Cabal.Options
+      Cabal.Paths
+      Cabal.ReplOptions
       Extract
       GhcUtil
       Imports
@@ -216,6 +254,7 @@
       Runner
       Runner.Example
       Test.DocTest
+      Test.DocTest.Internal.Cabal
       Test.DocTest.Internal.Extract
       Test.DocTest.Internal.Location
       Test.DocTest.Internal.Parse
@@ -240,23 +279,26 @@
   build-depends:
       HUnit
     , QuickCheck >=2.13.1
-    , base >=4.5 && <5
+    , base >=4.12 && <5
     , code-page >=0.1
+    , containers
     , deepseq
     , directory
     , exceptions
     , filepath
-    , ghc >=8.0 && <9.10
+    , ghc >=8.6 && <9.16
     , ghc-paths >=0.1.0.9
     , hspec >=2.3.0
     , hspec-core >=2.3.0
     , mockery
     , process
-    , setenv
     , silently >=1.2.4
     , stringbuilder >=0.4
     , syb >=0.3
+    , temporary
     , transformers
   default-language: Haskell2010
+  if impl(ghc >= 9.0)
+    ghc-options: -fwarn-unused-packages
   if impl(ghc >= 9.8)
     ghc-options: -fno-warn-x-partial
diff --git a/driver/Main.hs b/driver/Main.hs
deleted file mode 100644
--- a/driver/Main.hs
+++ /dev/null
@@ -1,8 +0,0 @@
-module Main (main) where
-
-import           Prelude
-import           Test.DocTest
-import           System.Environment (getArgs)
-
-main :: IO ()
-main = getArgs >>= doctest
diff --git a/driver/cabal-doctest.hs b/driver/cabal-doctest.hs
new file mode 100644
--- /dev/null
+++ b/driver/cabal-doctest.hs
@@ -0,0 +1,8 @@
+module Main (main) where
+
+import           Prelude
+import qualified Test.DocTest.Internal.Cabal as Cabal
+import           System.Environment (getArgs)
+
+main :: IO ()
+main = getArgs >>= Cabal.doctest
diff --git a/driver/doctest.hs b/driver/doctest.hs
new file mode 100644
--- /dev/null
+++ b/driver/doctest.hs
@@ -0,0 +1,8 @@
+module Main (main) where
+
+import           Prelude
+import           Test.DocTest
+import           System.Environment (getArgs)
+
+main :: IO ()
+main = getArgs >>= doctest
diff --git a/src/Cabal.hs b/src/Cabal.hs
new file mode 100644
--- /dev/null
+++ b/src/Cabal.hs
@@ -0,0 +1,77 @@
+{-# LANGUAGE LambdaCase #-}
+module Cabal (externalCommand) where
+
+import           Imports
+
+import           Data.List
+import           Data.Version (makeVersion)
+import           System.IO
+import           System.IO.Temp
+import           System.Environment
+import           System.Directory
+import           System.FilePath
+import           System.Process
+
+import qualified Info
+import           Cabal.Paths
+import           Cabal.Options
+
+externalCommand :: [String] -> IO ()
+externalCommand args = do
+  lookupEnv "CABAL" >>= \ case
+    Nothing -> run "cabal" args
+    Just cabal -> run cabal (drop 1 args)
+
+run :: String -> [String] -> IO ()
+run cabal args = do
+  rejectUnsupportedOptions args
+
+  Paths{..} <- paths cabal (discardReplOptions args)
+
+  let
+    doctest = cache </> "doctest" <> "-" <> Info.version
+    script = cache </> "init-ghci-" <> Info.version
+
+  doesFileExist doctest >>= \ case
+    True -> pass
+    False -> callProcess cabal [
+        "install" , "doctest-" <> Info.version
+      , "--flag", "-cabal-doctest"
+      , "--ignore-project"
+      , "--installdir", cache
+      , "--program-suffix", "-" <> Info.version
+      , "--install-method=copy"
+      , "--with-compiler", ghc
+      ]
+
+  doesFileExist script >>= \ case
+    True -> pass
+    False -> writeFileAtomically script ":seti -w -Wdefault"
+
+  callProcess doctest ["--version"]
+
+  let
+    repl extraArgs = call cabal ("repl"
+      : "--build-depends=QuickCheck"
+      : "--build-depends=template-haskell"
+      : ("--repl-options=-ghci-script=" <> script)
+      : args ++ extraArgs)
+
+  case ghcVersion < makeVersion [9,4] of
+    True -> do
+      callProcess cabal ("build" : "--only-dependencies" : discardReplOptions args)
+      repl ["--with-compiler", doctest, "--with-hc-pkg", ghcPkg]
+
+    False -> do
+      withSystemTempDirectory "cabal-doctest" $ \ dir -> do
+        repl ["--keep-temp-files", "--repl-multi-file", dir]
+        files <- filter (isSuffixOf "-inplace") <$> listDirectory dir
+        let options = concatMap (\ file -> ["-unit", '@' : dir </> file]) files
+        call doctest ("--no-magic" : options)
+
+writeFileAtomically :: FilePath -> String -> IO ()
+writeFileAtomically name contents = do
+  (tmp, h) <- openTempFile (takeDirectory name) (takeFileName name)
+  hPutStr h contents
+  hClose h
+  renameFile tmp name
diff --git a/src/Cabal/Options.hs b/src/Cabal/Options.hs
new file mode 100644
--- /dev/null
+++ b/src/Cabal/Options.hs
@@ -0,0 +1,78 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE LambdaCase #-}
+module Cabal.Options (
+  rejectUnsupportedOptions
+, discardReplOptions
+
+#ifdef TEST
+, replOnlyOptions
+#endif
+) where
+
+import           Imports
+
+import           System.Exit
+import           System.Console.GetOpt
+
+import           Data.Set (Set)
+import qualified Data.Set as Set
+
+import qualified Cabal.ReplOptions as Repl
+
+replOnlyOptions :: Set String
+replOnlyOptions = Set.fromList [
+    "-z"
+  , "--ignore-project"
+  , "--repl-no-load"
+  , "--repl-options"
+  , "--repl-multi-file"
+  , "-b"
+  , "--build-depends"
+  , "--no-transitive-deps"
+  , "--enable-multi-repl"
+  , "--disable-multi-repl"
+  , "--with-repl"
+  ]
+
+rejectUnsupportedOptions :: [String] -> IO ()
+rejectUnsupportedOptions args = case getOpt' Permute options args of
+  (xs, _, _, _) | ListOptions `elem` xs -> do
+    let
+      names :: [String]
+      names = concat [map (\ c -> ['-', c]) short ++ map ("--" <> ) long | Option short long _ _ <- documentedOptions]
+    putStr (unlines names)
+    exitSuccess
+  (_, _, unsupported : _, _) -> do
+    die $ "Error: cabal: unrecognized 'doctest' option `" <> unsupported <> "'"
+  _ -> pass
+
+data Argument = Argument String (Maybe String) | ListOptions
+  deriving (Eq, Show)
+
+options :: [OptDescr Argument]
+options =
+    Option [] ["list-options"] (NoArg ListOptions) ""
+  : documentedOptions
+
+documentedOptions :: [OptDescr Argument]
+documentedOptions = map toOptDescr Repl.options
+  where
+    toOptDescr :: Repl.Option -> OptDescr Argument
+    toOptDescr (Repl.Option long short arg help) = Option (maybeToList short) [long] (toArgDescr long arg) help
+
+    toArgDescr :: String -> Repl.Argument -> ArgDescr Argument
+    toArgDescr long = \ case
+      Repl.Argument name -> ReqArg (argument . Just) name
+      Repl.NoArgument -> NoArg (argument Nothing)
+      Repl.OptionalArgument name -> OptArg argument name
+      where
+        argument :: Maybe String -> Argument
+        argument value = Argument ("--" <> long) value
+
+discardReplOptions :: [String] -> [String]
+discardReplOptions args = case getOpt Permute options args of
+  (xs, _, _) -> [renderArgument name value | Argument name value <- xs, Set.notMember name replOnlyOptions]
+  where
+    renderArgument name = \ case
+      Nothing -> name
+      Just value -> name <> "=" <> value
diff --git a/src/Cabal/Paths.hs b/src/Cabal/Paths.hs
new file mode 100644
--- /dev/null
+++ b/src/Cabal/Paths.hs
@@ -0,0 +1,96 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE StrictData #-}
+module Cabal.Paths (
+  Paths(..)
+, paths
+) where
+
+import           Imports
+
+import           Data.Char
+import           Data.Tuple
+import           Data.Version hiding (parseVersion)
+import qualified Data.Version as Version
+import           System.Exit hiding (die)
+import           System.Directory
+import           System.FilePath
+import           System.IO
+import           System.Process
+import           Text.ParserCombinators.ReadP
+
+data Paths = Paths {
+  ghcVersion :: Version
+, ghc  :: FilePath
+, ghcPkg :: FilePath
+, cache :: FilePath
+} deriving (Eq, Show)
+
+paths :: FilePath -> [String] -> IO Paths
+paths cabal args = do
+  cabalVersion <- strip <$> readProcess cabal ["--numeric-version"] ""
+
+  let
+    required :: Version
+    required = makeVersion [3, 12]
+
+  when (parseVersion cabalVersion < Just required) $ do
+    die $ "'cabal-install' version " <> showVersion required <> " or later is required, but 'cabal --numeric-version' returned " <> cabalVersion <> "."
+
+  values <- parseFields <$> readProcess cabal ("path" : args ++ ["-v0"]) ""
+
+  let
+    getPath :: String -> String -> IO FilePath
+    getPath subject key = case lookup key values of
+      Nothing -> die $ "Cannot determine the path to " <> subject <> ". Running 'cabal path' did not return a value for '" <> key <> "'."
+      Just path -> canonicalizePath path
+
+  ghc <- getPath "'ghc'" "compiler-path"
+
+  ghcVersionString <- strip <$> readProcess ghc ["--numeric-version"] ""
+
+  ghcVersion <- case parseVersion ghcVersionString of
+    Nothing -> die $ "Cannot determine GHC version from '" <> ghcVersionString <> "'."
+    Just version -> return version
+
+  let
+    ghcPkg :: FilePath
+    ghcPkg = takeDirectory ghc </> "ghc-pkg-" <> ghcVersionString
+#ifdef mingw32_HOST_OS
+      <.> "exe"
+#endif
+
+  doesFileExist ghcPkg >>= \ case
+    True -> pass
+    False -> die $ "Cannot determine the path to 'ghc-pkg' from '" <> ghc <> "'. File '" <> ghcPkg <> "' does not exist."
+
+  abi <- strip <$> readProcess ghcPkg ["--no-user-package-db", "field", "base", "abi", "--simple-output"] ""
+
+  cache_home <- getPath "Cabal's cache directory" "cache-home"
+  let cache = cache_home </> "doctest" </> "ghc-" <> ghcVersionString <> "-" <> abi
+
+  createDirectoryIfMissing True cache
+
+  return Paths {
+    ghcVersion
+  , ghc
+  , ghcPkg
+  , cache
+  }
+  where
+    parseFields :: String -> [(String, FilePath)]
+    parseFields = map parseField . lines
+
+    parseField :: String -> (String, FilePath)
+    parseField input = case break (== ':') input of
+      (key, ':' : value) -> (key, dropWhile isSpace value)
+      (key, _) -> (key, "")
+
+die :: String -> IO a
+die message = do
+  hPutStrLn stderr "Error: [cabal-doctest]"
+  hPutStrLn stderr message
+  exitFailure
+
+parseVersion :: String -> Maybe Version
+parseVersion = lookup "" . map swap . readP_to_S Version.parseVersion
diff --git a/src/Cabal/ReplOptions.hs b/src/Cabal/ReplOptions.hs
new file mode 100644
--- /dev/null
+++ b/src/Cabal/ReplOptions.hs
@@ -0,0 +1,195 @@
+module Cabal.ReplOptions (
+  Option(..)
+, Argument(..)
+, options
+) where
+
+import           Imports
+
+data Option = Option {
+  optionName :: String
+, optionShortName :: Maybe Char
+, optionArgument :: Argument
+, optionHelp :: String
+} deriving (Eq, Show)
+
+data Argument = Argument String | NoArgument | OptionalArgument String
+  deriving (Eq, Show)
+
+options :: [Option]
+options = [
+    Option "help" (Just 'h') NoArgument "Show this help text"
+  , Option "verbose" (Just 'v') (OptionalArgument "n") "Control verbosity (n is 0--3, default verbosity level is 1)"
+  , Option "builddir" Nothing (Argument "DIR") "The directory where Cabal puts generated build files (default dist)"
+  , Option "keep-temp-files" Nothing NoArgument "Keep temporary files."
+  , Option "ghc" (Just 'g') NoArgument "compile with GHC"
+  , Option "ghcjs" Nothing NoArgument "compile with GHCJS"
+  , Option "uhc" Nothing NoArgument "compile with UHC"
+  , Option "with-compiler" (Just 'w') (Argument "PATH") "give the path to a particular compiler"
+  , Option "with-hc-pkg" Nothing (Argument "PATH") "give the path to the package tool"
+  , Option "prefix" Nothing (Argument "DIR") "bake this prefix in preparation of installation"
+  , Option "bindir" Nothing (Argument "DIR") "installation directory for executables"
+  , Option "libdir" Nothing (Argument "DIR") "installation directory for libraries"
+  , Option "libsubdir" Nothing (Argument "DIR") "subdirectory of libdir in which libs are installed"
+  , Option "dynlibdir" Nothing (Argument "DIR") "installation directory for dynamic libraries"
+  , Option "libexecdir" Nothing (Argument "DIR") "installation directory for program executables"
+  , Option "libexecsubdir" Nothing (Argument "DIR") "subdirectory of libexecdir in which private executables are installed"
+  , Option "datadir" Nothing (Argument "DIR") "installation directory for read-only data"
+  , Option "datasubdir" Nothing (Argument "DIR") "subdirectory of datadir in which data files are installed"
+  , Option "docdir" Nothing (Argument "DIR") "installation directory for documentation"
+  , Option "htmldir" Nothing (Argument "DIR") "installation directory for HTML documentation"
+  , Option "haddockdir" Nothing (Argument "DIR") "installation directory for haddock interfaces"
+  , Option "sysconfdir" Nothing (Argument "DIR") "installation directory for configuration files"
+  , Option "program-prefix" Nothing (Argument "PREFIX") "prefix to be applied to installed executables"
+  , Option "program-suffix" Nothing (Argument "SUFFIX") "suffix to be applied to installed executables"
+  , Option "enable-library-vanilla" Nothing NoArgument "Enable Vanilla libraries"
+  , Option "disable-library-vanilla" Nothing NoArgument "Disable Vanilla libraries"
+  , Option "enable-library-profiling" (Just 'p') NoArgument "Enable Library profiling"
+  , Option "disable-library-profiling" Nothing NoArgument "Disable Library profiling"
+  , Option "enable-shared" Nothing NoArgument "Enable Shared library"
+  , Option "disable-shared" Nothing NoArgument "Disable Shared library"
+  , Option "enable-static" Nothing NoArgument "Enable Static library"
+  , Option "disable-static" Nothing NoArgument "Disable Static library"
+  , Option "enable-executable-dynamic" Nothing NoArgument "Enable Executable dynamic linking"
+  , Option "disable-executable-dynamic" Nothing NoArgument "Disable Executable dynamic linking"
+  , Option "enable-executable-static" Nothing NoArgument "Enable Executable fully static linking"
+  , Option "disable-executable-static" Nothing NoArgument "Disable Executable fully static linking"
+  , Option "enable-profiling" Nothing NoArgument "Enable Executable and library profiling"
+  , Option "disable-profiling" Nothing NoArgument "Disable Executable and library profiling"
+  , Option "enable-profiling-shared" Nothing NoArgument "Enable Build profiling shared libraries"
+  , Option "disable-profiling-shared" Nothing NoArgument "Disable Build profiling shared libraries"
+  , Option "enable-executable-profiling" Nothing NoArgument "Enable Executable profiling (DEPRECATED)"
+  , Option "disable-executable-profiling" Nothing NoArgument "Disable Executable profiling (DEPRECATED)"
+  , Option "profiling-detail" Nothing (Argument "level") "Profiling detail level for executable and library (default, none, exported-functions, toplevel-functions, all-functions, late)."
+  , Option "library-profiling-detail" Nothing (Argument "level") "Profiling detail level for libraries only."
+  , Option "enable-optimization" (Just 'O') (OptionalArgument "n") "Build with optimization (n is 0--2, default is 1)"
+  , Option "disable-optimization" Nothing NoArgument "Build without optimization"
+  , Option "enable-debug-info" Nothing (OptionalArgument "n") "Emit debug info (n is 0--3, default is 0)"
+  , Option "disable-debug-info" Nothing NoArgument "Don't emit debug info"
+  , Option "enable-build-info" Nothing NoArgument "Enable build information generation during project building"
+  , Option "disable-build-info" Nothing NoArgument "Disable build information generation during project building"
+  , Option "enable-library-for-ghci" Nothing NoArgument "Enable compile library for use with GHCi"
+  , Option "disable-library-for-ghci" Nothing NoArgument "Disable compile library for use with GHCi"
+  , Option "enable-split-sections" Nothing NoArgument "Enable compile library code such that unneeded definitions can be dropped from the final executable (GHC 7.8+)"
+  , Option "disable-split-sections" Nothing NoArgument "Disable compile library code such that unneeded definitions can be dropped from the final executable (GHC 7.8+)"
+  , Option "enable-split-objs" Nothing NoArgument "Enable split library into smaller objects to reduce binary sizes (GHC 6.6+)"
+  , Option "disable-split-objs" Nothing NoArgument "Disable split library into smaller objects to reduce binary sizes (GHC 6.6+)"
+  , Option "enable-executable-stripping" Nothing NoArgument "Enable strip executables upon installation to reduce binary sizes"
+  , Option "disable-executable-stripping" Nothing NoArgument "Disable strip executables upon installation to reduce binary sizes"
+  , Option "enable-library-stripping" Nothing NoArgument "Enable strip libraries upon installation to reduce binary sizes"
+  , Option "disable-library-stripping" Nothing NoArgument "Disable strip libraries upon installation to reduce binary sizes"
+  , Option "configure-option" Nothing (Argument "OPT") "Extra option for configure"
+  , Option "user" Nothing NoArgument "Enable doing a per-user installation"
+  , Option "global" Nothing NoArgument "Disable doing a per-user installation"
+  , Option "package-db" Nothing (Argument "DB") "Append the given package database to the list of package databases used (to satisfy dependencies and register into). May be a specific file, 'global' or 'user'. The initial list is ['global'], ['global', 'user'], or ['global', $sandbox], depending on context. Use 'clear' to reset the list to empty. See the user guide for details."
+  , Option "flags" (Just 'f') (Argument "FLAGS") "Force values for the given flags in Cabal conditionals in the .cabal file. E.g., --flags=\"debug -usebytestrings\" forces the flag \"debug\" to true and \"usebytestrings\" to false."
+  , Option "extra-include-dirs" Nothing (Argument "PATH") "A list of directories to search for header files"
+  , Option "enable-deterministic" Nothing NoArgument "Enable Try to be as deterministic as possible (used by the test suite)"
+  , Option "disable-deterministic" Nothing NoArgument "Disable Try to be as deterministic as possible (used by the test suite)"
+  , Option "ipid" Nothing (Argument "IPID") "Installed package ID to compile this package as"
+  , Option "cid" Nothing (Argument "CID") "Installed component ID to compile this component as"
+  , Option "extra-lib-dirs" Nothing (Argument "PATH") "A list of directories to search for external libraries"
+  , Option "extra-lib-dirs-static" Nothing (Argument "PATH") "A list of directories to search for external libraries when linking fully static executables"
+  , Option "extra-framework-dirs" Nothing (Argument "PATH") "A list of directories to search for external frameworks (OS X only)"
+  , Option "extra-prog-path" Nothing (Argument "PATH") "A list of directories to search for required programs (in addition to the normal search locations)"
+  , Option "instantiate-with" Nothing (Argument "NAME=MOD") "A mapping of signature names to concrete module instantiations."
+  , Option "enable-tests" Nothing NoArgument "Enable dependency checking and compilation for test suites listed in the package description file."
+  , Option "disable-tests" Nothing NoArgument "Disable dependency checking and compilation for test suites listed in the package description file."
+  , Option "enable-coverage" Nothing NoArgument "Enable build package with Haskell Program Coverage. (GHC only)"
+  , Option "disable-coverage" Nothing NoArgument "Disable build package with Haskell Program Coverage. (GHC only)"
+  , Option "enable-library-coverage" Nothing NoArgument "Enable build package with Haskell Program Coverage. (GHC only) (DEPRECATED)"
+  , Option "disable-library-coverage" Nothing NoArgument "Disable build package with Haskell Program Coverage. (GHC only) (DEPRECATED)"
+  , Option "enable-benchmarks" Nothing NoArgument "Enable dependency checking and compilation for benchmarks listed in the package description file."
+  , Option "disable-benchmarks" Nothing NoArgument "Disable dependency checking and compilation for benchmarks listed in the package description file."
+  , Option "enable-relocatable" Nothing NoArgument "Enable building a package that is relocatable. (GHC only)"
+  , Option "disable-relocatable" Nothing NoArgument "Disable building a package that is relocatable. (GHC only)"
+  , Option "disable-response-files" Nothing NoArgument "enable workaround for old versions of programs like \"ar\" that do not support @file arguments"
+  , Option "allow-depending-on-private-libs" Nothing NoArgument "Allow depending on private libraries. If set, the library visibility check MUST be done externally."
+  , Option "coverage-for" Nothing (Argument "UNITID") "A list of unit-ids of libraries to include in the Haskell Program Coverage report."
+  , Option "ignore-build-tools" Nothing NoArgument "Ignore build tool dependencies. If set, declared build tools needn't be found for compilation to proceed."
+  , Option "cabal-lib-version" Nothing (Argument "VERSION") "Select which version of the Cabal lib to use to build packages (useful for testing)."
+  , Option "enable-append" Nothing NoArgument "Enable appending the new config to the old config file"
+  , Option "disable-append" Nothing NoArgument "Disable appending the new config to the old config file"
+  , Option "enable-backup" Nothing NoArgument "Enable the backup of the config file before any alterations"
+  , Option "disable-backup" Nothing NoArgument "Disable the backup of the config file before any alterations"
+  , Option "constraint" (Just 'c') (Argument "CONSTRAINT") "Specify constraints on a package (version, installed/source, flags)"
+  , Option "preference" Nothing (Argument "CONSTRAINT") "Specify preferences (soft constraints) on the version of a package"
+  , Option "solver" Nothing (Argument "SOLVER") "Select dependency solver to use (default: modular). Choices: modular."
+  , Option "allow-older" Nothing (OptionalArgument "DEPS") "Ignore lower bounds in all dependencies or DEPS"
+  , Option "allow-newer" Nothing (OptionalArgument "DEPS") "Ignore upper bounds in all dependencies or DEPS"
+  , Option "write-ghc-environment-files" Nothing (Argument "always|never|ghc8.4.4+") "Whether to create a .ghc.environment file after a successful build (v2-build only)"
+  , Option "enable-documentation" Nothing NoArgument "Enable building of documentation"
+  , Option "disable-documentation" Nothing NoArgument "Disable building of documentation"
+  , Option "doc-index-file" Nothing (Argument "TEMPLATE") "A central index of haddock API documentation (template cannot use $pkgid)"
+  , Option "dry-run" Nothing NoArgument "Do not install anything, only print what would be installed."
+  , Option "only-download" Nothing NoArgument "Do not build anything, only fetch the packages."
+  , Option "max-backjumps" Nothing (Argument "NUM") "Maximum number of backjumps allowed while solving (default: 4000). Use a negative number to enable unlimited backtracking. Use 0 to disable backtracking completely."
+  , Option "reorder-goals" Nothing NoArgument "Try to reorder goals according to certain heuristics. Slows things down on average, but may make backtracking faster for some packages."
+  , Option "count-conflicts" Nothing NoArgument "Try to speed up solving by preferring goals that are involved in a lot of conflicts (default)."
+  , Option "fine-grained-conflicts" Nothing NoArgument "Skip a version of a package if it does not resolve the conflicts encountered in the last version, as a solver optimization (default)."
+  , Option "minimize-conflict-set" Nothing NoArgument "When there is no solution, try to improve the error message by finding a minimal conflict set (default: false). May increase run time significantly."
+  , Option "independent-goals" Nothing NoArgument "Treat several goals on the command line as independent. If several goals depend on the same package, different versions can be chosen."
+  , Option "prefer-oldest" Nothing NoArgument "Prefer the oldest (instead of the latest) versions of packages available. Useful to determine lower bounds in the build-depends section."
+  , Option "shadow-installed-packages" Nothing NoArgument "If multiple package instances of the same version are installed, treat all but one as shadowed."
+  , Option "strong-flags" Nothing NoArgument "Do not defer flag choices (this used to be the default in cabal-install <= 1.20)."
+  , Option "allow-boot-library-installs" Nothing NoArgument "Allow cabal to install base, ghc-prim, integer-simple, integer-gmp, and template-haskell."
+  , Option "reject-unconstrained-dependencies" Nothing (Argument "none|all") "Require these packages to have constraints on them if they are to be selected (default: none)."
+  , Option "reinstall" Nothing NoArgument "Install even if it means installing the same version again."
+  , Option "avoid-reinstalls" Nothing NoArgument "Do not select versions that would destructively overwrite installed packages."
+  , Option "force-reinstalls" Nothing NoArgument "Reinstall packages even if they will most likely break other installed packages."
+  , Option "upgrade-dependencies" Nothing NoArgument "Pick the latest version for all dependencies, rather than trying to pick an installed version."
+  , Option "only-dependencies" Nothing NoArgument "Install only the dependencies necessary to build the given packages"
+  , Option "dependencies-only" Nothing NoArgument "A synonym for --only-dependencies"
+  , Option "index-state" Nothing (Argument "STATE") "Use source package index state as it existed at a previous time. Accepts unix-timestamps (e.g. '@1474732068'), ISO8601 UTC timestamps (e.g. '2016-09-24T17:47:48Z'), or 'HEAD' (default: 'HEAD')."
+  , Option "root-cmd" Nothing (Argument "COMMAND") "(No longer supported, do not use.)"
+  , Option "build-summary" Nothing (Argument "TEMPLATE") "Save build summaries to file (name template can use $pkgid, $compiler, $os, $arch)"
+  , Option "build-log" Nothing (Argument "TEMPLATE") "Log all builds to file (name template can use $pkgid, $compiler, $os, $arch)"
+  , Option "remote-build-reporting" Nothing (Argument "LEVEL") "Generate build reports to send to a remote server (none, anonymous or detailed)."
+  , Option "report-planning-failure" Nothing NoArgument "Generate build reports when the dependency solver fails. This is used by the Hackage build bot."
+  , Option "enable-per-component" Nothing NoArgument "Enable Per-component builds when possible"
+  , Option "disable-per-component" Nothing NoArgument "Disable Per-component builds when possible"
+  , Option "run-tests" Nothing NoArgument "Run package test suites during installation."
+  , Option "semaphore" Nothing NoArgument "Use a semaphore so GHC can compile components in parallel"
+  , Option "jobs" (Just 'j') (OptionalArgument "NUM") "Run NUM jobs simultaneously (or '$ncpus' if no NUM is given)."
+  , Option "keep-going" Nothing NoArgument "After a build failure, continue to build other unaffected packages."
+  , Option "offline" Nothing NoArgument "Don't download packages from the Internet."
+  , Option "haddock-hoogle" Nothing NoArgument "Generate a hoogle database"
+  , Option "haddock-html" Nothing NoArgument "Generate HTML documentation (the default)"
+  , Option "haddock-html-location" Nothing (Argument "URL") "Location of HTML documentation for pre-requisite packages"
+  , Option "haddock-for-hackage" Nothing NoArgument "Collection of flags to generate documentation suitable for upload to hackage"
+  , Option "haddock-executables" Nothing NoArgument "Run haddock for Executables targets"
+  , Option "haddock-tests" Nothing NoArgument "Run haddock for Test Suite targets"
+  , Option "haddock-benchmarks" Nothing NoArgument "Run haddock for Benchmark targets"
+  , Option "haddock-all" Nothing NoArgument "Run haddock for all targets"
+  , Option "haddock-internal" Nothing NoArgument "Run haddock for internal modules and include all symbols"
+  , Option "haddock-css" Nothing (Argument "PATH") "Use PATH as the haddock stylesheet"
+  , Option "haddock-hyperlink-source" Nothing NoArgument "Hyperlink the documentation to the source code"
+  , Option "haddock-quickjump" Nothing NoArgument "Generate an index for interactive documentation navigation"
+  , Option "haddock-hscolour-css" Nothing (Argument "PATH") "Use PATH as the HsColour stylesheet"
+  , Option "haddock-contents-location" Nothing (Argument "URL") "Bake URL in as the location for the contents page"
+  , Option "haddock-base-url" Nothing (Argument "URL") "Base URL for static files."
+  , Option "haddock-resources-dir" Nothing (Argument "DIR") "location of Haddocks static / auxiliary files"
+  , Option "haddock-output-dir" Nothing (Argument "DIR") "Generate haddock documentation into this directory. This flag is provided as a technology preview and is subject to change in the next releases."
+  , Option "haddock-use-unicode" Nothing NoArgument "Pass --use-unicode option to haddock"
+  , Option "test-log" Nothing (Argument "TEMPLATE") "Log all test suite results to file (name template can use $pkgid, $compiler, $os, $arch, $test-suite, $result)"
+  , Option "test-machine-log" Nothing (Argument "TEMPLATE") "Produce a machine-readable log file (name template can use $pkgid, $compiler, $os, $arch, $result)"
+  , Option "test-show-details" Nothing (Argument "FILTER") "'always': always show results of individual test cases. 'never': never show results of individual test cases. 'failures': show results of failing test cases. 'streaming': show results of test cases in real time.'direct': send results of test cases in real time; no log file."
+  , Option "test-keep-tix-files" Nothing NoArgument "keep .tix files for HPC between test runs"
+  , Option "test-wrapper" Nothing (Argument "FILE") "Run test through a wrapper."
+  , Option "test-fail-when-no-test-suites" Nothing NoArgument "Exit with failure when no test suites are found."
+  , Option "test-options" Nothing (Argument "TEMPLATES") "give extra options to test executables (split on spaces, use \"\" to prevent splitting; name templates can use $pkgid, $compiler, $os, $arch, $test-suite)"
+  , Option "test-option" Nothing (Argument "TEMPLATE") "give extra option to test executables (passed directly as a single argument; name template can use $pkgid, $compiler, $os, $arch, $test-suite)"
+  , Option "benchmark-options" Nothing (Argument "TEMPLATES") "give extra options to benchmark executables (split on spaces, use \"\" to prevent splitting; name templates can use $pkgid, $compiler, $os, $arch, $benchmark)"
+  , Option "benchmark-option" Nothing (Argument "TEMPLATE") "give extra option to benchmark executables (passed directly as a single argument; name template can use $pkgid, $compiler, $os, $arch, $benchmark)"
+  , Option "project-dir" Nothing (Argument "DIR") "Set the path of the project directory"
+  , Option "project-file" Nothing (Argument "FILE") "Set the path of the cabal.project file (relative to the project directory when relative)"
+  , Option "ignore-project" (Just 'z') NoArgument "Ignore local project configuration (unless --project-dir or --project-file is also set)"
+  , Option "repl-no-load" Nothing NoArgument "Disable loading of project modules at REPL startup."
+  , Option "repl-options" Nothing (Argument "FLAG") "Use the option(s) for the repl"
+  , Option "repl-multi-file" Nothing (Argument "DIR") "Write repl options to this directory rather than starting repl mode"
+  , Option "with-repl" Nothing (Argument "PATH") "Give the path to a program to use for REPL"
+  , Option "build-depends" (Just 'b') (Argument "DEPENDENCIES") "Include additional packages in the environment presented to GHCi."
+  , Option "no-transitive-deps" Nothing NoArgument "Don't automatically include transitive dependencies of requested packages."
+  , Option "enable-multi-repl" Nothing NoArgument "Enable multi-component repl sessions"
+  , Option "disable-multi-repl" Nothing NoArgument "Disable multi-component repl sessions"
+  ]
diff --git a/src/Extract.hs b/src/Extract.hs
--- a/src/Extract.hs
+++ b/src/Extract.hs
@@ -1,11 +1,8 @@
 {-# LANGUAGE CPP #-}
 module Extract (Module(..), extract) where
 
-import           Prelude hiding (mod, concat)
-import           Control.Monad
-import           Control.Exception
+import           Imports hiding (mod, concat)
 import           Data.List (partition, isSuffixOf)
-import           Data.Maybe
 
 import           Control.DeepSeq (deepseq, NFData(rnf))
 import           Data.Generics
@@ -32,10 +29,6 @@
 import           System.Directory
 import           System.FilePath
 
-#if __GLASGOW_HASKELL__ < 805
-import           FastString (unpackFS)
-#endif
-
 import           System.Posix.Internals (c_getpid)
 
 import           GhcUtil (withGhc)
@@ -44,13 +37,11 @@
 import           Util (convertDosLineEndings)
 import           PackageDBs (getPackageDBArgs)
 
-#if __GLASGOW_HASKELL__ >= 806
 #if __GLASGOW_HASKELL__ < 900
 import           DynamicLoading (initializePlugins)
 #else
 import           GHC.Runtime.Loader (initializePlugins)
 #endif
-#endif
 
 #if __GLASGOW_HASKELL__ >= 901
 import           GHC.Unit.Module.Graph
@@ -58,7 +49,9 @@
 
 -- | A wrapper around `SomeException`, to allow for a custom `Show` instance.
 newtype ExtractError = ExtractError SomeException
+#if __GLASGOW_HASKELL__ < 912
   deriving Typeable
+#endif
 
 instance Show ExtractError where
   show (ExtractError e) =
@@ -88,15 +81,6 @@
 instance NFData a => NFData (Module a) where
   rnf (Module name setup content) = name `deepseq` setup `deepseq` content `deepseq` ()
 
-#if __GLASGOW_HASKELL__ < 803
-type GhcPs = RdrName
-#endif
-
-#if __GLASGOW_HASKELL__ < 805
-addQuoteInclude :: [String] -> [String] -> [String]
-addQuoteInclude includes new = new ++ includes
-#endif
-
 -- | Parse a list of modules.
 parse :: [String] -> IO [ParsedModule]
 parse args = withGhc args $ \modules_ -> withTempOutputDir $ do
@@ -111,7 +95,11 @@
                 Nothing)
   mods <- depanal [] False
 
-  let sortedMods = flattenSCCs
+  let sortedMods =
+#if __GLASGOW_HASKELL__ >= 914
+                   mapMaybe maybeModSummaryFromModuleNodeInfo $
+#endif
+                   flattenSCCs
 #if __GLASGOW_HASKELL__ >= 901
                      $ filterToposortToModules
 #endif
@@ -157,7 +145,6 @@
       , includePaths = addQuoteInclude (includePaths d) [f]
       }
 
-#if __GLASGOW_HASKELL__ >= 806
     -- Since GHC 8.6, plugins are initialized on a per module basis
     loadModPlugins modsum = do
       _ <- setSessionDynFlags (GHC.ms_hspp_opts modsum)
@@ -166,14 +153,11 @@
 # if __GLASGOW_HASKELL__ >= 902
       hsc_env' <- liftIO (initializePlugins hsc_env)
       setSession hsc_env'
-      return $ modsum
+      return modsum
 # else
       dynflags' <- liftIO (initializePlugins hsc_env (GHC.ms_hspp_opts modsum))
       return $ modsum { ms_hspp_opts = dynflags' }
 # endif
-#else
-    loadModPlugins = return
-#endif
 
 -- | Extract all docstrings from given list of files/modules.
 --
@@ -182,7 +166,15 @@
 extract :: [String] -> IO [Module (Located String)]
 extract args = do
   packageDBArgs <- getPackageDBArgs
-  let args'  = args ++ packageDBArgs
+  let
+    args' = args ++
+#if __GLASGOW_HASKELL__ >= 810
+      -- `ghci` ignores unused packages in certain situation.  This ensures
+      -- that we don't fail in situations where `ghci` would not.
+      "-Wno-unused-packages" :
+#endif
+      packageDBArgs
+
   mods <- parse args'
   let docs = map (fmap (fmap convertDosLineEndings) . extractFromModule) mods
 
@@ -203,6 +195,13 @@
     (setup, docs) = partition isSetup (docStringsFromModule m)
     name = (moduleNameString . GHC.moduleName . ms_mod . pm_mod_summary) m
 
+#if __GLASGOW_HASKELL__ >= 914
+maybeModSummaryFromModuleNodeInfo :: ModuleNodeInfo -> Maybe ModSummary
+maybeModSummaryFromModuleNodeInfo i = case i of
+  ModuleNodeCompile summary -> Just summary
+  ModuleNodeFixed _ _ -> Nothing
+#endif
+
 #if __GLASGOW_HASKELL__ >= 904
 unpackHDS :: HsDocString -> String
 unpackHDS = renderHsDocString
@@ -234,11 +233,7 @@
 #else
     exports = [ (Nothing, L (locA loc) doc)
 #endif
-#if __GLASGOW_HASKELL__ < 805
-              | L loc (IEDoc doc) <- maybe [] unLoc (hsmodExports source)
-#else
               | L loc (IEDoc _ doc) <- maybe [] unLoc (hsmodExports source)
-#endif
               ]
     decls :: [(Maybe String, LHsDocString)]
     decls   = extractDocStrings (hsmodDecls source)
@@ -253,7 +248,7 @@
   in
     flip fmap docStrs $ \docStr -> (lookup (getLoc docStr) docStrNames, docStr)
   where
-    extractAll z = everything (++) ((mkQ [] ((:[]) . z)))
+    extractAll z = everything (++) (mkQ [] ((:[]) . z))
 
     extractDocDocString :: LHsDoc GhcPs -> LHsDocString
     extractDocDocString = fmap hsDocString
@@ -276,11 +271,7 @@
       -- Top-level documentation has to be treated separately, because it has
       -- no location information attached.  The location information is
       -- attached to HsDecl instead.
-#if __GLASGOW_HASKELL__ < 805
-      DocD x
-#else
       DocD _ x
-#endif
                -> select (fromDocDecl (locA loc) x)
 
       _ -> (extractDocStrings decl, True)
@@ -306,12 +297,6 @@
 -- | Collect given value and descend into subtree.
 select :: a -> ([a], Bool)
 select x = ([x], False)
-#endif
-
-#if __GLASGOW_HASKELL__ < 805
--- | Convert a docstring to a plain string.
-unpackHDS :: HsDocString -> String
-unpackHDS (HsDocString s) = unpackFS s
 #endif
 
 #if __GLASGOW_HASKELL__ < 901
diff --git a/src/GhcUtil.hs b/src/GhcUtil.hs
--- a/src/GhcUtil.hs
+++ b/src/GhcUtil.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE CPP #-}
-module GhcUtil (withGhc) where
+{-# LANGUAGE LambdaCase #-}
+module GhcUtil (withGhc, expandUnits) where
 
 import           Imports
 
@@ -23,12 +24,9 @@
 import           GHC.Utils.Monad (liftIO)
 #endif
 
+import           GHC.ResponseFile (expandResponse)
 import           System.Exit (exitFailure)
 
-#if __GLASGOW_HASKELL__ < 801
-import           StaticFlags (discardStaticFlags)
-#endif
-
 -- Catch GHC source errors, print them and exit.
 handleSrcErrors :: Ghc a -> Ghc a
 handleSrcErrors action' = flip handleSourceError action' $ \err -> do
@@ -37,20 +35,17 @@
 
 -- | Run a GHC action in Haddock mode
 withGhc :: [String] -> ([String] -> Ghc a) -> IO a
-withGhc flags action = do
-  flags_ <- handleStaticFlags flags
-
-  runGhc (Just libdir) $ do
-    handleDynamicFlags flags_ >>= handleSrcErrors . action
+withGhc flags action = runGhc (Just libdir) $ do
+  liftIO (expandUnits flags) >>= handleDynamicFlags >>= handleSrcErrors . action
 
-handleStaticFlags :: [String] -> IO [Located String]
-#if __GLASGOW_HASKELL__ < 801
-handleStaticFlags flags = return $ map noLoc $ discardStaticFlags flags
-#else
-handleStaticFlags flags = return $ map noLoc $ flags
-#endif
+expandUnits :: [String] -> IO [String]
+expandUnits = \ case
+  [] -> return []
+  "-unit" : file@('@' : _) : args -> (++) <$> expandResponse [file] <*> expandUnits args
+  file@('@' : _) : args -> (++) <$> expandResponse [file] <*> expandUnits args
+  x : xs -> (:) x <$> expandUnits xs
 
-handleDynamicFlags :: GhcMonad m => [Located String] -> m [String]
+handleDynamicFlags :: GhcMonad m => [String] -> m [String]
 handleDynamicFlags flags = do
 #if __GLASGOW_HASKELL__ >= 901
   logger <- getLogger
@@ -58,7 +53,7 @@
 #else
   let parseDynamicFlags' = parseDynamicFlags
 #endif
-  (dynflags, locSrcs, _) <- (setHaddockMode `fmap` getSessionDynFlags) >>= (`parseDynamicFlags'` flags)
+  (dynflags, locSrcs, _) <- (setHaddockMode `fmap` getSessionDynFlags) >>= (`parseDynamicFlags'` map noLoc flags)
   _ <- setSessionDynFlags dynflags
 
   -- We basically do the same thing as `ghc/Main.hs` to distinguish
diff --git a/src/Imports.hs b/src/Imports.hs
--- a/src/Imports.hs
+++ b/src/Imports.hs
@@ -1,5 +1,34 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE LambdaCase #-}
 module Imports (module Imports) where
 
 import           Prelude as Imports
 import           Data.Monoid as Imports
-import           Control.Monad as Imports
+import           Data.Maybe as Imports
+import           Control.Monad as Imports hiding (forM_)
+import           Control.Exception as Imports
+import           Data.Foldable as Imports (forM_)
+import           Control.Arrow as Imports
+
+import           Data.Char
+import           System.Exit
+import           System.Process
+
+import           Data.Functor as Imports ((<&>))
+
+pass :: Monad m => m ()
+pass = return ()
+
+equals :: Eq a => a -> a -> Bool
+equals = (==)
+
+strip :: String -> String
+strip = reverse . dropWhile isSpace . reverse . dropWhile isSpace
+
+call :: FilePath -> [FilePath] -> IO ()
+call name args = rawSystem name args >>= \ case
+  ExitSuccess -> pass
+  err -> exitWith err
+
+exec :: FilePath -> [FilePath] -> IO ()
+exec name args = rawSystem name args >>= exitWith
diff --git a/src/Info.hs b/src/Info.hs
--- a/src/Info.hs
+++ b/src/Info.hs
@@ -2,6 +2,7 @@
 module Info (
   versionInfo
 , info
+, version
 #ifdef TEST
 , formatInfo
 #endif
@@ -19,13 +20,22 @@
 import           GHC.Settings.Config as GHC
 #endif
 
+import           Interpreter (ghc)
+
+#ifdef TEST
+
+version :: String
+version = "0.0.0"
+
+#else
+
 import           Data.Version (showVersion)
 import qualified Paths_doctest
 
-import           Interpreter (ghc)
-
 version :: String
 version = showVersion Paths_doctest.version
+
+#endif
 
 ghcVersion :: String
 ghcVersion = GHC.cProjectVersion
diff --git a/src/Interpreter.hs b/src/Interpreter.hs
--- a/src/Interpreter.hs
+++ b/src/Interpreter.hs
@@ -1,8 +1,9 @@
 {-# LANGUAGE CPP #-}
 module Interpreter (
   Interpreter
+, PreserveIt(..)
 , safeEval
-, safeEvalIt
+, safeEvalWith
 , withInterpreter
 , ghc
 , interpreterSupported
@@ -17,8 +18,6 @@
 
 import           System.Process
 import           System.Directory (getPermissions, executable)
-import           Control.Exception hiding (handle)
-import           Data.Char
 import           GHC.Paths (ghc)
 
 import           Language.Haskell.GhciWrapper
@@ -37,7 +36,7 @@
   unless (executable x) $ do
     fail $ ghc ++ " is not executable!"
 
-  maybe False (== "YES") . lookup haveInterpreterKey <$> ghcInfo
+  (== Just "YES") . lookup haveInterpreterKey <$> ghcInfo
 
 withInterpreter
   :: (String, [String])
@@ -47,10 +46,14 @@
   let
     args = flags ++ [
         xTemplateHaskell
-#if __GLASGOW_HASKELL__ >= 802
       , "-fdiagnostics-color=never"
       , "-fno-diagnostics-show-caret"
+#if __GLASGOW_HASKELL__ >= 810 && __GLASGOW_HASKELL__ < 904
+      , "-Wno-unused-packages"
 #endif
+#if __GLASGOW_HASKELL__ >= 910
+      , "-fprint-error-index-links=never"
+#endif
       ]
   bracket (new defaultConfig{configGhci = command} args) close action
 
@@ -61,10 +64,10 @@
 --
 -- An exception may e.g. be caused on unterminated multiline expressions.
 safeEval :: Interpreter -> String -> IO (Either String String)
-safeEval repl = either (return . Left) (fmap Right . eval repl) . filterExpression
+safeEval = safeEvalWith NoPreserveIt
 
-safeEvalIt :: Interpreter -> String -> IO (Either String String)
-safeEvalIt repl = either (return . Left) (fmap Right . evalIt repl) . filterExpression
+safeEvalWith :: PreserveIt -> Interpreter -> String -> IO (Either String String)
+safeEvalWith preserveIt repl = either (return . Left) (fmap Right . evalWith preserveIt repl) . filterExpression
 
 filterExpression :: String -> Either String String
 filterExpression e =
@@ -75,9 +78,6 @@
         firstLine = strip $ head l
         lastLine  = strip $ last l
         err = Left "unterminated multi-line command"
-  where
-    strip :: String -> String
-    strip = dropWhile isSpace . reverse . dropWhile isSpace . reverse
 
 filterXTemplateHaskell :: String -> String
 filterXTemplateHaskell input = case words input of
diff --git a/src/Language/Haskell/GhciWrapper.hs b/src/Language/Haskell/GhciWrapper.hs
--- a/src/Language/Haskell/GhciWrapper.hs
+++ b/src/Language/Haskell/GhciWrapper.hs
@@ -1,11 +1,13 @@
+{-# LANGUAGE ViewPatterns #-}
 module Language.Haskell.GhciWrapper (
   Interpreter
 , Config(..)
 , defaultConfig
+, PreserveIt(..)
 , new
 , close
 , eval
-, evalIt
+, evalWith
 , evalEcho
 ) where
 
@@ -14,9 +16,7 @@
 import           System.IO hiding (stdin, stdout, stderr)
 import           System.Process
 import           System.Exit
-import           Control.Exception
 import           Data.List (isSuffixOf)
-import           Data.Maybe
 
 data Config = Config {
   configGhci :: String
@@ -31,6 +31,9 @@
 , configIgnoreDotGhci = True
 }
 
+data PreserveIt = NoPreserveIt | PreserveIt
+  deriving Eq
+
 -- | Truly random marker, used to separate expressions.
 --
 -- IMPORTANT: This module relies upon the fact that this marker is unique.  It
@@ -111,8 +114,8 @@
   when (e /= ExitSuccess) $ do
     throwIO (userError $ "Language.Haskell.GhciWrapper.close: Interpreter exited with an error (" ++ show e ++ ")")
 
-putExpression :: Interpreter -> Bool -> String -> IO ()
-putExpression Interpreter{hIn = stdin} preserveIt e = do
+putExpression :: Interpreter -> PreserveIt -> String -> IO ()
+putExpression Interpreter{hIn = stdin} (equals PreserveIt -> preserveIt) e = do
   hPutStrLn stdin e
   when preserveIt $ hPutStrLn stdin $ "let " ++ itMarker ++ " = it"
   hPutStrLn stdin (marker ++ " :: Data.String.String")
@@ -142,18 +145,16 @@
 
 -- | Evaluate an expression
 eval :: Interpreter -> String -> IO String
-eval repl expr = do
-  putExpression repl False expr
-  getResult False repl
+eval = evalWith NoPreserveIt
 
 -- | Like 'eval', but try to preserve the @it@ variable
-evalIt :: Interpreter -> String -> IO String
-evalIt repl expr = do
-  putExpression repl True expr
+evalWith :: PreserveIt -> Interpreter -> String -> IO String
+evalWith preserveIt repl expr = do
+  putExpression repl preserveIt expr
   getResult False repl
 
 -- | Evaluate an expression
 evalEcho :: Interpreter -> String -> IO String
 evalEcho repl expr = do
-  putExpression repl False expr
+  putExpression repl NoPreserveIt expr
   getResult True repl
diff --git a/src/Options.hs b/src/Options.hs
--- a/src/Options.hs
+++ b/src/Options.hs
@@ -28,7 +28,7 @@
 usage :: String
 usage = unlines [
     "Usage:"
-  , "  doctest [ --fast | --preserve-it | --no-magic | --verbose | GHC OPTION | MODULE ]..."
+  , "  doctest [ --fast | --preserve-it | --fail-fast | --no-magic | --verbose | GHC OPTION | MODULE ]..."
   , "  doctest --help"
   , "  doctest --version"
   , "  doctest --info"
@@ -36,6 +36,8 @@
   , "Options:"
   , "  --fast         disable :reload between example groups"
   , "  --preserve-it  preserve the `it` variable between examples"
+  , "  --fail-fast    abort on first failure"
+  , "  --no-magic     disable magic mode"
   , "  --verbose      print each test as it is run"
   , "  --help         display this help and exit"
   , "  --version      output version information and exit"
@@ -57,6 +59,7 @@
   ghcOptions :: [String]
 , fastMode :: Bool
 , preserveIt :: Bool
+, failFast :: Bool
 , verbose :: Bool
 , repl :: (String, [String])
 } deriving (Eq, Show)
@@ -66,6 +69,7 @@
   ghcOptions = []
 , fastMode = False
 , preserveIt = False
+, failFast = False
 , verbose = False
 , repl = (ghc, ["--interactive"])
 }
@@ -105,6 +109,9 @@
 setPreserveIt :: Bool -> Run -> Run
 setPreserveIt preserveIt run@Run{..} = run { runConfig = runConfig { preserveIt } }
 
+setFailFastMode :: Bool -> Run -> Run
+setFailFastMode failFast run@Run{..} = run { runConfig = runConfig { failFast } }
+
 setVerbose :: Bool -> Run -> Run
 setVerbose verbose run@Run{..} = run { runConfig = runConfig { verbose } }
 
@@ -134,6 +141,7 @@
 commonRunOptions = do
   parseFlag "--fast" (setFastMode True)
   parseFlag "--preserve-it" (setPreserveIt True)
+  parseFlag "--fail-fast" (setFailFastMode True)
   parseFlag "--verbose" (setVerbose True)
 
 parseFlag :: String -> (Run -> Run) -> RunOptionsParser
diff --git a/src/Parse.hs b/src/Parse.hs
--- a/src/Parse.hs
+++ b/src/Parse.hs
@@ -21,7 +21,6 @@
 
 import           Data.Char (isSpace)
 import           Data.List (isPrefixOf, stripPrefix)
-import           Data.Maybe
 import           Data.String
 import           Extract
 import           Location
@@ -175,8 +174,3 @@
           then (0, c : replicate count '.' ++ acc, res)
           else (0, c : acc, res)
     finish (count, acc, res) = mkChunk (replicate count '.' ++ acc) ++ res
-
-
--- | Remove leading and trailing whitespace.
-strip :: String -> String
-strip = dropWhile isSpace . reverse . dropWhile isSpace . reverse
diff --git a/src/Property.hs b/src/Property.hs
--- a/src/Property.hs
+++ b/src/Property.hs
@@ -11,7 +11,6 @@
 import           Imports
 
 import           Data.List
-import           Data.Maybe
 import           Data.Foldable
 
 import           Util
diff --git a/src/Run.hs b/src/Run.hs
--- a/src/Run.hs
+++ b/src/Run.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE CPP #-}
+{-# LANGUAGE LambdaCase #-}
 module Run (
   doctest
 , doctestWithRepl
@@ -9,6 +10,7 @@
 
 , Result
 , Summary(..)
+, formatSummary
 , isSuccess
 , evaluateResult
 , doctestWithResult
@@ -27,7 +29,6 @@
 import           System.FilePath ((</>), takeExtension)
 import           System.IO
 import           System.IO.CodePage (withCP65001)
-import           System.Process (rawSystem)
 
 import qualified Control.Exception as E
 
@@ -37,6 +38,10 @@
 import           GHC.Utils.Panic
 #endif
 
+#if __GLASGOW_HASKELL__ < 904
+import           GhcUtil (expandUnits)
+#endif
+
 import           PackageDBs
 import           Parse
 import           Options hiding (Result(..))
@@ -62,8 +67,14 @@
 doctest = doctestWithRepl (repl defaultConfig)
 
 doctestWithRepl :: (String, [String]) -> [String] -> IO ()
-doctestWithRepl repl args0 = case parseOptions args0 of
-  Options.ProxyToGhc args -> rawSystem Interpreter.ghc args >>= E.throwIO
+#if __GLASGOW_HASKELL__ < 904
+-- GHC versions prior to 9.4.1 don't support response files.  For that reason
+-- we want to expand early, so that GHCi gets expanded args.
+doctestWithRepl repl = expandUnits >=> parseOptions >>> \ case
+#else
+doctestWithRepl repl = parseOptions >>> \ case
+#endif
+  Options.ProxyToGhc args -> exec Interpreter.ghc args
   Options.Output s -> putStr s
   Options.Result (Run warnings magicMode config) -> do
     mapM_ (hPutStrLn stderr) warnings
@@ -115,10 +126,7 @@
 getAddDistArgs :: IO ([String] -> [String])
 getAddDistArgs = do
     env <- getEnvironment
-    let dist =
-            case lookup "HASKELL_DIST_DIR" env of
-                Nothing -> "dist"
-                Just x -> x
+    let dist = fromMaybe "dist" $ lookup "HASKELL_DIST_DIR" env
         autogen = dist ++ "/build/autogen/"
         cabalMacros = autogen ++ "cabal_macros.h"
 
@@ -139,11 +147,8 @@
 
 type Result = Summary
 
-isSuccess :: Result -> Bool
-isSuccess s = sErrors s == 0 && sFailures s == 0
-
 evaluateResult :: Result -> IO ()
-evaluateResult r = when (not $ isSuccess r) exitFailure
+evaluateResult r = unless (isSuccess r) exitFailure
 
 doctestWithResult :: Config -> IO Result
 doctestWithResult config = do
@@ -158,4 +163,9 @@
 runDocTests :: Config -> [Module [Located DocTest]] -> IO Result
 runDocTests Config{..} modules = do
   Interpreter.withInterpreter ((<> ghcOptions) <$> repl) $ \ interpreter -> withCP65001 $ do
-    runModules fastMode preserveIt verbose interpreter modules
+    runModules
+      (if fastMode then FastMode else NoFastMode)
+      (if preserveIt then PreserveIt else NoPreserveIt)
+      (if failFast then FailFast else NoFailFast)
+      (if verbose then Verbose else NonVerbose)
+      interpreter modules
diff --git a/src/Runner.hs b/src/Runner.hs
--- a/src/Runner.hs
+++ b/src/Runner.hs
@@ -1,27 +1,39 @@
 {-# LANGUAGE CPP #-}
+{-# LANGUAGE LambdaCase #-}
 module Runner (
   runModules
+, FastMode(..)
+, PreserveIt(..)
+, FailFast(..)
+, Verbose(..)
 , Summary(..)
+, isSuccess
+, formatSummary
 
 #ifdef TEST
 , Report
-, ReportState (..)
+, ReportState(..)
+, runReport
+, Interactive(..)
 , report
-, report_
+, reportTransient
 #endif
 ) where
 
-import           Prelude hiding (putStr, putStrLn, error)
+import           Prelude ()
+import           Imports hiding (putStr, putStrLn, error)
 
-import           Control.Monad hiding (forM_)
 import           Text.Printf (printf)
-import           System.IO (hPutStrLn, hPutStr, stderr, hIsTerminalDevice)
-import           Data.Foldable (forM_)
+import           System.IO hiding (putStr, putStrLn)
 
-import           Control.Monad.Trans.State
+import           Control.Monad.Trans.Class
+import           Control.Monad.Trans.Maybe
+import           Control.Monad.Trans.State (StateT, evalStateT)
+import qualified Control.Monad.Trans.State as State
 import           Control.Monad.IO.Class
+import           Data.IORef
 
-import           Interpreter (Interpreter)
+import           Interpreter (Interpreter, PreserveIt(..), safeEvalWith)
 import qualified Interpreter
 import           Parse
 import           Location
@@ -30,115 +42,137 @@
 
 -- | Summary of a test run.
 data Summary = Summary {
-  sExamples :: Int
-, sTried    :: Int
-, sErrors   :: Int
-, sFailures :: Int
+  sExamples :: !Int
+, sTried    :: !Int
+, sErrors   :: !Int
+, sFailures :: !Int
 } deriving Eq
 
--- | Format a summary.
 instance Show Summary where
-  show (Summary examples tried errors failures) =
-    printf "Examples: %d  Tried: %d  Errors: %d  Failures: %d" examples tried errors failures
+  show = formatSummary
 
+isSuccess :: Summary -> Bool
+isSuccess s = sErrors s == 0 && sFailures s == 0
 
+formatSummary :: Summary -> String
+formatSummary (Summary examples tried errors failures) =
+  printf "Examples: %d  Tried: %d  Errors: %d  Failures: %d" examples tried errors failures
+
 -- | Sum up summaries.
 instance Monoid Summary where
   mempty = Summary 0 0 0 0
-#if __GLASGOW_HASKELL__ < 804
-  mappend
-#else
 instance Semigroup Summary where
-  (<>)
-#endif
-    (Summary x1 x2 x3 x4) (Summary y1 y2 y3 y4) = Summary (x1 + y1) (x2 + y2) (x3 + y3) (x4 + y4)
+  Summary x1 x2 x3 x4 <> Summary y1 y2 y3 y4 = Summary (x1 + y1) (x2 + y2) (x3 + y3) (x4 + y4)
 
+withLineBuffering :: Handle -> IO c -> IO c
+withLineBuffering h action = bracket (hGetBuffering h) (hSetBuffering h) $ \ _ -> do
+  hSetBuffering h LineBuffering
+  action
+
 -- | Run all examples from a list of modules.
-runModules :: Bool -> Bool -> Bool -> Interpreter -> [Module [Located DocTest]] -> IO Summary
-runModules fastMode preserveIt verbose repl modules = do
-  isInteractive <- hIsTerminalDevice stderr
-  ReportState _ _ _ s <- (`execStateT` ReportState 0 isInteractive verbose mempty {sExamples = c}) $ do
-    forM_ modules $ runModule fastMode preserveIt repl
+runModules :: FastMode -> PreserveIt -> FailFast -> Verbose -> Interpreter -> [Module [Located DocTest]] -> IO Summary
+runModules fastMode preserveIt failFast verbose repl modules = withLineBuffering stderr $ do
 
-    verboseReport "# Final summary:"
-    gets (show . reportStateSummary) >>= report
+  interactive <- hIsTerminalDevice stderr <&> \ case
+    False -> NonInteractive
+    True -> Interactive
 
-  return s
+  summary <- newIORef mempty {sExamples = n}
+
+  let
+    reportFinalResult :: IO ()
+    reportFinalResult = do
+      final <- readIORef summary
+      hPutStrLn stderr (formatSummary final)
+
+    run :: IO ()
+    run = runReport (ReportState interactive failFast verbose summary) $ do
+      reportProgress
+      forM_ modules $ runModule fastMode preserveIt repl
+      verboseReport "# Final summary:"
+
+  run `finally` reportFinalResult
+
+  readIORef summary
   where
-    c = (sum . map count) modules
+    n :: Int
+    n = sum (map countExpressions modules)
 
--- | Count number of expressions in given module.
-count :: Module [Located DocTest] -> Int
-count (Module _ setup tests) = sum (map length tests) + maybe 0 length setup
+countExpressions :: Module [Located DocTest] -> Int
+countExpressions (Module _ setup tests) = sum (map length tests) + maybe 0 length setup
 
--- | A monad for generating test reports.
-type Report = StateT ReportState IO
+type Report = MaybeT (StateT ReportState IO)
 
+data Interactive = NonInteractive | Interactive
+
+data FastMode = NoFastMode | FastMode
+
+data FailFast = NoFailFast | FailFast
+
+data Verbose = NonVerbose | Verbose
+
 data ReportState = ReportState {
-  reportStateCount        :: Int     -- ^ characters on the current line
-, reportStateInteractive  :: Bool    -- ^ should intermediate results be printed?
-, reportStateVerbose      :: Bool
-, reportStateSummary      :: Summary -- ^ test summary
+  reportStateInteractive :: Interactive
+, reportStateFailFast :: FailFast
+, reportStateVerbose :: Verbose
+, reportStateSummary :: IORef Summary
 }
 
+runReport :: ReportState -> Report () -> IO ()
+runReport st = void . flip evalStateT st . runMaybeT
+
+getSummary :: Report Summary
+getSummary = gets reportStateSummary >>= liftIO . readIORef
+
+gets :: (ReportState -> a) -> Report a
+gets = lift . State.gets
+
 -- | Add output to the report.
 report :: String -> Report ()
-report msg = do
-  overwrite msg
-
-  -- add a newline, this makes the output permanent
-  liftIO $ hPutStrLn stderr ""
-  modify (\st -> st {reportStateCount = 0})
+report = liftIO . hPutStrLn stderr
 
 -- | Add intermediate output to the report.
 --
 -- This will be overwritten by subsequent calls to `report`/`report_`.
 -- Intermediate out may not contain any newlines.
-report_ :: String -> Report ()
-report_ msg = do
-  f <- gets reportStateInteractive
-  when f $ do
-    overwrite msg
-    modify (\st -> st {reportStateCount = length msg})
-
--- | Add output to the report, overwrite any intermediate out.
-overwrite :: String -> Report ()
-overwrite msg = do
-  n <- gets reportStateCount
-  let str | 0 < n     = "\r" ++ msg ++ replicate (n - length msg) ' '
-          | otherwise = msg
-  liftIO (hPutStr stderr str)
+reportTransient :: String -> Report ()
+reportTransient msg = gets reportStateInteractive >>= \ case
+  NonInteractive -> pass
+  Interactive -> liftIO $ do
+    hPutStr stderr msg
+    hFlush stderr
+    hPutStr stderr $ '\r' : (replicate (length msg) ' ') ++ "\r"
 
 -- | Run all examples from given module.
-runModule :: Bool -> Bool -> Interpreter -> Module [Located DocTest] -> Report ()
+runModule :: FastMode -> PreserveIt -> Interpreter -> Module [Located DocTest] -> Report ()
 runModule fastMode preserveIt repl (Module module_ setup examples) = do
 
-  Summary _ _ e0 f0 <- gets reportStateSummary
+  Summary _ _ e0 f0 <- getSummary
 
   forM_ setup $
     runTestGroup preserveIt repl reload
 
-  Summary _ _ e1 f1 <- gets reportStateSummary
+  Summary _ _ e1 f1 <- getSummary
 
   -- only run tests, if setup does not produce any errors/failures
   when (e0 == e1 && f0 == f1) $
-    forM_ examples $
-      runTestGroup preserveIt repl setup_
+    forM_ examples $ runTestGroup preserveIt repl setup_
   where
     reload :: IO ()
     reload = do
-      unless fastMode $
-        -- NOTE: It is important to do the :reload first! See
-        -- https://gitlab.haskell.org/ghc/ghc/-/issues/5904, which results in a
-        -- panic on GHC 7.4.1 if you do the :reload second.
-        void $ Interpreter.safeEval repl ":reload"
+      case fastMode of
+        NoFastMode -> void $ Interpreter.safeEval repl ":reload"
+        FastMode -> pass
       void $ Interpreter.safeEval repl $ ":m *" ++ module_
 
-      when preserveIt $
-        -- Evaluate a dumb expression to populate the 'it' variable NOTE: This is
-        -- one reason why we cannot have safeEval = safeEvalIt: 'it' isn't set in
-        -- a fresh GHCi session.
-        void $ Interpreter.safeEval repl $ "()"
+      case preserveIt of
+        NoPreserveIt -> pass
+        PreserveIt -> do
+          -- Evaluate a dumb expression to populate the 'it' variable.
+          --
+          -- NOTE: This is one reason why we cannot just always use PreserveIt:
+          -- 'it' isn't set in a fresh GHCi session.
+          void $ Interpreter.safeEval repl $ "()"
 
     setup_ :: IO ()
     setup_ = do
@@ -171,29 +205,35 @@
   updateSummary (Summary 0 1 0 0)
 
 verboseReport :: String -> Report ()
-verboseReport xs = do
-  verbose <- gets reportStateVerbose
-  when verbose $ report xs
+verboseReport msg = gets reportStateVerbose >>= \ case
+  NonVerbose -> pass
+  Verbose -> report msg
 
 updateSummary :: Summary -> Report ()
 updateSummary summary = do
-  ReportState n f v s <- get
-  put (ReportState n f v $ s `mappend` summary)
+  ref <- gets reportStateSummary
+  liftIO $ modifyIORef' ref $ mappend summary
+  reportProgress
+  gets reportStateFailFast >>= \ case
+    NoFailFast -> pass
+    FailFast -> unless (isSuccess summary) abort
 
+abort :: Report ()
+abort = MaybeT $ return Nothing
+
 reportProgress :: Report ()
-reportProgress = do
-  verbose <- gets reportStateVerbose
-  when (not verbose) $ gets (show . reportStateSummary) >>= report_
+reportProgress = gets reportStateVerbose >>= \ case
+  NonVerbose -> do
+    summary <- getSummary
+    reportTransient (formatSummary summary)
+  Verbose -> pass
 
 -- | Run given test group.
 --
 -- The interpreter state is zeroed with @:reload@ first.  This means that you
 -- can reuse the same 'Interpreter' for several test groups.
-runTestGroup :: Bool -> Interpreter -> IO () -> [Located DocTest] -> Report ()
+runTestGroup :: PreserveIt -> Interpreter -> IO () -> [Located DocTest] -> Report ()
 runTestGroup preserveIt repl setup tests = do
-
-  reportProgress
-
   liftIO setup
   runExampleGroup preserveIt repl examples
 
@@ -220,7 +260,7 @@
 -- |
 -- Execute all expressions from given example in given 'Interpreter' and verify
 -- the output.
-runExampleGroup :: Bool -> Interpreter -> [Located Interaction] -> Report ()
+runExampleGroup :: PreserveIt -> Interpreter -> [Located Interaction] -> Report ()
 runExampleGroup preserveIt repl = go
   where
     go ((Located loc (expression, expected)) : xs) = do
@@ -236,8 +276,3 @@
             reportSuccess
             go xs
     go [] = return ()
-
-safeEvalWith :: Bool -> Interpreter -> String -> IO (Either String String)
-safeEvalWith preserveIt
-  | preserveIt = Interpreter.safeEvalIt
-  | otherwise  = Interpreter.safeEval
diff --git a/src/Test/DocTest/Internal/Cabal.hs b/src/Test/DocTest/Internal/Cabal.hs
new file mode 100644
--- /dev/null
+++ b/src/Test/DocTest/Internal/Cabal.hs
@@ -0,0 +1,10 @@
+module Test.DocTest.Internal.Cabal (
+  doctest
+) where
+
+import           Imports
+
+import qualified Cabal
+
+doctest :: [String] -> IO ()
+doctest = Cabal.externalCommand
diff --git a/test/Cabal/OptionsSpec.hs b/test/Cabal/OptionsSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Cabal/OptionsSpec.hs
@@ -0,0 +1,60 @@
+{-# LANGUAGE CPP #-}
+module Cabal.OptionsSpec (spec) where
+
+import           Imports
+
+import           Test.Hspec
+
+import           System.IO
+import           System.IO.Silently
+import           System.Exit
+import           System.Process
+import           Data.Set ((\\))
+import qualified Data.Set as Set
+
+import qualified Cabal.ReplOptionsSpec as Repl
+
+import           Cabal.Options
+
+spec :: Spec
+spec = do
+  describe "replOnlyOptions" $ do
+    it "is the set of options that are unique to 'cabal repl'" $ do
+      build <- Set.fromList . lines <$> readProcess "cabal" ["build", "--list-options"] ""
+      repl <- Set.fromList . lines <$> readProcess "cabal" ["repl", "--list-options"] ""
+      Set.toList replOnlyOptions `shouldMatchList` Set.toList (repl \\ build)
+
+  describe "rejectUnsupportedOptions" $ do
+    it "produces error messages that are consistent with 'cabal repl'" $ do
+      let
+        shouldFail :: HasCallStack => String -> IO a -> Expectation
+        shouldFail command action = do
+          hCapture_ [stderr] (action `shouldThrow` (== ExitFailure 1))
+            `shouldReturn` "Error: cabal: unrecognized '" <> command <> "' option `--installdir'\n"
+
+#ifndef mingw32_HOST_OS
+      shouldFail "repl" $ call "cabal" ["repl", "--installdir"]
+#endif
+      shouldFail "doctest" $ rejectUnsupportedOptions ["--installdir"]
+
+    context "with --list-options" $ do
+      it "lists supported command-line options" $ do
+        repl <- Set.fromList . lines <$> readProcess "cabal" ["repl", "--list-options"] ""
+        doctest <- Set.fromList . lines <$> capture_ (rejectUnsupportedOptions ["--list-options"] `shouldThrow` (== ExitSuccess))
+        Set.toList (doctest \\ repl) `shouldMatchList` []
+        Set.toList (repl \\ doctest) `shouldMatchList` Set.toList Repl.unsupported
+
+  describe "discardReplOptions" $ do
+    it "discards 'cabal repl'-only options" $ do
+      discardReplOptions [
+          "-w", "ghc-9.10"
+        , "--build-depends=foo"
+        , "--build-depends", "foo"
+        , "-bfoo"
+        , "-b", "foo"
+        , "--disable-optimization"
+        , "--enable-multi-repl"
+        , "--repl-options", "foo"
+        , "--repl-options=foo"
+        , "--allow-newer"
+        ] `shouldBe` ["--with-compiler=ghc-9.10", "--disable-optimization", "--allow-newer"]
diff --git a/test/Cabal/PathsSpec.hs b/test/Cabal/PathsSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Cabal/PathsSpec.hs
@@ -0,0 +1,22 @@
+module Cabal.PathsSpec (spec) where
+
+import           Imports
+
+import           Test.Hspec
+
+import           System.Directory
+
+import           Cabal ()
+import           Cabal.Paths
+
+spec :: Spec
+spec = do
+  describe "paths" $ do
+    it "returns the path to 'ghc'" $ do
+      (paths "cabal" [] >>= doesFileExist . ghc) `shouldReturn` True
+
+    it "returns the path to 'ghc-pkg'" $ do
+      (paths "cabal" [] >>= doesFileExist . ghcPkg) `shouldReturn` True
+
+    it "returns the path to Cabal's cache directory" $ do
+      (paths "cabal" [] >>= doesDirectoryExist . cache) `shouldReturn` True
diff --git a/test/Cabal/ReplOptionsSpec.hs b/test/Cabal/ReplOptionsSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Cabal/ReplOptionsSpec.hs
@@ -0,0 +1,99 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE ViewPatterns #-}
+module Cabal.ReplOptionsSpec (spec, unsupported) where
+
+import           Imports
+
+import           Test.Hspec
+
+import           Data.List
+import           System.Process
+import           Data.Set (Set)
+import qualified Data.Set as Set
+
+import           Cabal.ReplOptions
+
+phony :: [String]
+phony = [
+    "with-PROG"
+  , "PROG-option"
+  , "PROG-options"
+  ]
+
+undocumented :: Set String
+undocumented = Set.fromList [
+    "--enable-optimisation"
+  , "--disable-optimisation"
+  , "--haddock-hyperlink-sources"
+  , "--haddock-hyperlinked-source"
+  ]
+
+unsupported :: Set String
+unsupported = undocumented <> Set.fromList (map ("--" <>) phony)
+
+spec :: Spec
+spec = do
+  describe "options" $ do
+    it "is the list of documented 'repl' options" $ do
+      documentedOptions <- parseOptions <$> readProcess "cabal" ["help", "repl"] ""
+      options `shouldBe` filter (optionName >>> (`notElem` phony)) documentedOptions
+
+    it "is consistent with 'cabal repl --list-options'" $ do
+      let
+        optionNames :: Option -> [String]
+        optionNames option = reverse $ "--" <> optionName option : case optionShortName option of
+          Nothing -> []
+          Just c -> [['-', c]]
+  
+      repl <- filter (`Set.notMember` unsupported) . lines <$> readProcess "cabal" ["repl", "--list-options"] ""
+      concatMap optionNames options `shouldBe` repl
+
+parseOptions :: String -> [Option]
+parseOptions = map parseOption . takeOptions
+  where
+    parseOption :: String -> Option
+    parseOption input = case input of
+      longAndHelp@('-':'-':_) -> parseLongOption Nothing longAndHelp
+      '-':short:',':' ':longAndHelp -> parseLongOption (Just short) longAndHelp
+      '-':short:'[':(breakOn ']' ->
+        (_arg, ']':',':' ':longAndHelp)) -> parseLongOption (Just short) longAndHelp
+      '-':short:' ':(breakOn ' ' ->
+        (arg, ' ':'o':'r':' ':(stripPrefix ('-':short:arg) ->
+          Just (',':' ':longAndHelp)))) -> parseLongOption (Just short) longAndHelp
+      _ -> err
+      where
+        parseLongOption :: Maybe Char -> String -> Option
+        parseLongOption short longAndHelp = case breakOnAny " [=" longAndHelp of
+          ('-':'-':long, ' ':help) -> accept long NoArgument help
+          ('-':'-':long, '[':'=': (breakOn ']' ->
+            (arg, ']':help))) -> accept long (OptionalArgument arg) help
+          ('-':'-':long, '=':(breakOn ' ' ->
+            (arg, ' ':help))) -> accept long (Argument arg) help
+          _ -> err
+          where
+            accept :: String -> Argument -> String -> Option
+            accept long arg help = Option long short arg (strip help)
+
+        err :: HasCallStack => Option
+        err = error input
+
+        breakOn c = break (== c)
+        breakOnAny xs = break (`elem` xs)
+
+    takeOptions :: String -> [String]
+    takeOptions input = map strip . joinLines $ case break (== "Flags for repl:") (lines input) of
+      (_, "Flags for repl:" : xs) -> case break (== "") xs of
+        (ys, "" : _) -> ys
+        _ -> undefined
+      _ -> undefined
+
+    joinLines :: [String] -> [String]
+    joinLines = go
+      where
+        go = \ case
+          x : y : ys | isOption y  -> x : go (y : ys)
+          x : y : ys -> go $ (x ++ ' ' : strip y) : ys
+          x : xs -> x : xs
+          [] -> []
+
+        isOption = isPrefixOf " -"
diff --git a/test/Language/Haskell/GhciWrapperSpec.hs b/test/Language/Haskell/GhciWrapperSpec.hs
--- a/test/Language/Haskell/GhciWrapperSpec.hs
+++ b/test/Language/Haskell/GhciWrapperSpec.hs
@@ -6,10 +6,9 @@
 import           Test.Hspec
 import           System.IO.Silently
 
-import           Control.Exception
 import           Data.List
 
-import           Language.Haskell.GhciWrapper (Interpreter, Config(..), defaultConfig)
+import           Language.Haskell.GhciWrapper (Interpreter, Config(..), defaultConfig, PreserveIt(..))
 import qualified Language.Haskell.GhciWrapper as Interpreter
 
 main :: IO ()
@@ -31,11 +30,12 @@
       withInterpreterConfig defaultConfig [] $ \ghci -> do
         (capture $ Interpreter.evalEcho ghci ("putStr" ++ show "foo\nbar")) `shouldReturn` ("foo\nbar", "foo\nbar")
 
-  describe "evalIt" $ do
-    it "preserves it" $ do
-      withInterpreterConfig defaultConfig [] $ \ghci -> do
-        Interpreter.evalIt ghci "23" `shouldReturn` "23\n"
-        Interpreter.eval ghci "it" `shouldReturn` "23\n"
+  describe "evalWith" $ do
+    context "with PreserveIt" $ do
+      it "preserves it" $ do
+        withInterpreterConfig defaultConfig [] $ \ghci -> do
+          Interpreter.evalWith PreserveIt ghci "23" `shouldReturn` "23\n"
+          Interpreter.eval ghci "it" `shouldReturn` "23\n"
 
   describe "eval" $ do
     it "shows literals" $ withInterpreter $ \ghci -> do
@@ -78,20 +78,19 @@
 
     it "shows exceptions" $ withInterpreter $ \ghci -> do
       ghci "import Control.Exception" `shouldReturn` ""
+#if __GLASGOW_HASKELL__ >= 912
+      ghci "throwIO DivideByZero" `shouldReturn` "*** Exception: divide by zero\n\nHasCallStack backtrace:\n  throwIO, called at <interactive>:25:1 in interactive:Ghci22\n\n"
+#else
       ghci "throwIO DivideByZero" `shouldReturn` "*** Exception: divide by zero\n"
+#endif
 
     it "shows exceptions (ExitCode)" $ withInterpreter $ \ghci -> do
       ghci "import System.Exit" `shouldReturn` ""
       ghci "exitWith $ ExitFailure 10" `shouldReturn` "*** Exception: ExitFailure 10\n"
 
     it "gives an error message for identifiers that are not in scope" $ withInterpreter $ \ghci -> do
-#if __GLASGOW_HASKELL__ >= 800
       ghci "foo" >>= (`shouldSatisfy` isInfixOf "Variable not in scope: foo")
-#elif __GLASGOW_HASKELL__ >= 707
-      ghci "foo" >>= (`shouldSatisfy` isSuffixOf "Not in scope: \8216foo\8217\n")
-#else
-      ghci "foo" >>= (`shouldSatisfy` isSuffixOf "Not in scope: `foo'\n")
-#endif
+
     context "when configVerbose is True" $ do
       it "prints prompt" $ do
         withInterpreterConfig defaultConfig{configVerbose = True} [] $ \ghci -> do
@@ -102,7 +101,7 @@
 
     context "with -XOverloadedStrings, -Wall and -Werror" $ do
       it "does not fail on marker expression (bug fix)" $ withInterpreter $ \ghci -> do
-        ghci ":set -XOverloadedStrings -Wall -Werror" `shouldReturn` ""
+        ghci ":seti -XOverloadedStrings -Wall -Werror" `shouldReturn` ""
         ghci "putStrLn \"foo\"" `shouldReturn` "foo\n"
 
     context "with NoImplicitPrelude" $ do
diff --git a/test/MainSpec.hs b/test/MainSpec.hs
--- a/test/MainSpec.hs
+++ b/test/MainSpec.hs
@@ -7,10 +7,9 @@
 import           Test.Hspec
 import           Test.HUnit (assertEqual, Assertion)
 
-import           Control.Exception
 import           System.Directory (getCurrentDirectory, setCurrentDirectory)
 import           System.FilePath
-import           Run hiding (doctest)
+import           Run hiding (doctest, doctestWith)
 import           System.IO.Silently
 import           System.IO
 
@@ -20,14 +19,19 @@
     setCurrentDirectory workingDir
     action
 
--- | Construct a doctest specific 'Assertion'.
 doctest :: HasCallStack => FilePath -> [String] -> Summary -> Assertion
-doctest = doctestWithPreserveIt False
+doctest = doctestWith False False
 
-doctestWithPreserveIt :: HasCallStack => Bool -> FilePath -> [String] -> Summary -> Assertion
-doctestWithPreserveIt preserveIt workingDir ghcOptions expected = do
-  actual <- withCurrentDirectory ("test/integration" </> workingDir) (hSilence [stderr] $ doctestWithResult defaultConfig {ghcOptions, preserveIt})
-  assertEqual label expected actual
+doctestWithPreserveIt :: HasCallStack => FilePath -> [String] -> Summary -> Assertion
+doctestWithPreserveIt = doctestWith True False
+
+doctestWithFailFast :: HasCallStack => FilePath -> [String] -> Summary -> Assertion
+doctestWithFailFast = doctestWith False True
+
+doctestWith :: HasCallStack => Bool -> Bool -> FilePath -> [String] -> Summary -> Assertion
+doctestWith preserveIt failFast workingDir ghcOptions expected = do
+  actual <- withCurrentDirectory ("test/integration" </> workingDir) (hSilence [stderr] $ doctestWithResult defaultConfig {ghcOptions, preserveIt, failFast})
+  assertEqual label (formatSummary expected) (formatSummary actual)
   where
     label = workingDir ++ " " ++ show ghcOptions
 
@@ -45,20 +49,34 @@
         (cases 1)
 
     it "it-variable" $ do
-      doctestWithPreserveIt True "." ["it/Foo.hs"]
+      doctestWithPreserveIt "." ["it/Foo.hs"]
         (cases 5)
 
     it "it-variable in $setup" $ do
-      doctestWithPreserveIt True "." ["it/Setup.hs"]
+      doctestWithPreserveIt "." ["it/Setup.hs"]
         (cases 5)
 
     it "failing" $ do
       doctest "." ["failing/Foo.hs"]
         (cases 1) {sFailures = 1}
 
-    it "skips subsequent examples from the same group if an example fails" $
+    it "skips subsequent examples from the same group if an example fails" $ do
       doctest "." ["failing-multiple/Foo.hs"]
         (cases 4) {sTried = 2, sFailures = 1}
+
+    context "without --fail-fast" $ do
+      it "continuous even if some tests fail" $ do
+        doctest "fail-fast" ["Foo.hs"]
+          (cases 4) {sTried = 4, sFailures = 1}
+
+    context "with --fail-fast" $ do
+      it "stops after the first failure" $ do
+        doctestWithFailFast "fail-fast" ["Foo.hs"]
+          (cases 4) {sTried = 2, sFailures = 1}
+
+      it "stops after failures in $setup" $ do
+        doctestWithFailFast "fail-fast" ["SetupFoo.hs"]
+          (cases 6) {sTried = 1, sFailures = 1}
 
     it "testImport" $ do
       doctest "testImport" ["ModuleA.hs"]
diff --git a/test/OptionsSpec.hs b/test/OptionsSpec.hs
--- a/test/OptionsSpec.hs
+++ b/test/OptionsSpec.hs
@@ -65,7 +65,7 @@
           fastMode . runConfig <$> parseOptions [] `shouldBe` Result False
 
       context "with --fast" $ do
-        it "enabled fast mode" $ do
+        it "enables fast mode" $ do
           fastMode . runConfig <$> parseOptions ["--fast"] `shouldBe` Result True
 
     describe "--preserve-it" $ do
@@ -76,6 +76,15 @@
       context "with --preserve-it" $ do
         it "preserves the `it` variable" $ do
           preserveIt . runConfig <$> parseOptions ["--preserve-it"] `shouldBe` Result True
+
+    describe "--fail-fast" $ do
+      context "without --fail-fast" $ do
+        it "disables fail-fast mode" $ do
+          failFast . runConfig <$> parseOptions [] `shouldBe` Result False
+
+      context "with --fail-fast" $ do
+        it "enables fail-fast mode" $ do
+          failFast . runConfig <$> parseOptions ["--fail-fast"] `shouldBe` Result True
 
     context "with --help" $ do
       it "outputs usage information" $ do
diff --git a/test/RunSpec.hs b/test/RunSpec.hs
--- a/test/RunSpec.hs
+++ b/test/RunSpec.hs
@@ -9,8 +9,8 @@
 import qualified Control.Exception as E
 import           System.FilePath
 import           System.Directory (getCurrentDirectory, setCurrentDirectory)
+import           System.IO.Temp (withSystemTempDirectory)
 import           Data.List (isPrefixOf, sort)
-import           Data.Char
 
 import           System.IO.Silently
 import           System.IO (stderr)
@@ -35,6 +35,17 @@
 removeLoadedPackageEnvironment = id
 #endif
 
+verboseFibOutput :: String
+verboseFibOutput = unlines [
+    "### Started execution at test/integration/testSimple/Fib.hs:5."
+  , "### example:"
+  , "fib 10"
+  , "### Successful!"
+  , ""
+  , "# Final summary:"
+  , "Examples: 1  Tried: 1  Errors: 0  Failures: 0"
+  ]
+
 spec :: Spec
 spec = do
   describe "doctest" $ do
@@ -70,17 +81,18 @@
         , "Try `doctest --help' for more information."
         ]
 
+    it "interprets GHC response files" $ do
+      withSystemTempDirectory "hspec" $ \ dir -> do
+        let file = dir </> "response-file"
+        writeFile file $ unlines [
+            "test/integration/testSimple/Fib.hs"
+          ]
+        (r, ()) <- hCapture [stderr] $ doctest ["--verbose", '@':file]
+        removeLoadedPackageEnvironment r `shouldBe` verboseFibOutput
+
     it "prints verbose description of a specification" $ do
       (r, ()) <- hCapture [stderr] $ doctest ["--verbose", "test/integration/testSimple/Fib.hs"]
-      removeLoadedPackageEnvironment r `shouldBe` unlines [
-          "### Started execution at test/integration/testSimple/Fib.hs:5."
-        , "### example:"
-        , "fib 10"
-        , "### Successful!"
-        , ""
-        , "# Final summary:"
-        , "Examples: 1  Tried: 1  Errors: 0  Failures: 0"
-        ]
+      removeLoadedPackageEnvironment r `shouldBe` verboseFibOutput
 
     it "prints verbose description of a property" $ do
       (r, ()) <- hCapture [stderr] $ doctest ["--verbose", "test/integration/property-bool/Foo.hs"]
@@ -118,23 +130,46 @@
   describe "doctestWithResult" $ do
     context "on parse error" $ do
       let
+        action :: IO Result
         action = withCurrentDirectory "test/integration/parse-error" $ do
-          doctestWithResult defaultConfig { ghcOptions = ["Foo.hs"] }
+          doctestWithResult defaultConfig {
+              ghcOptions = [
+                  "Foo.hs"
 
+                -- This is necessary due to:
+                --
+                -- https://gitlab.haskell.org/ghc/ghc/-/commit/88f38b03025386f0f1e8f5861eed67d80495168a
+                --
+                -- It will be fixed by:
+                --
+                -- https://gitlab.haskell.org/ghc/ghc/-/merge_requests/15995
+                --
+                , "-fdiagnostics-color=never"
+#if __GLASGOW_HASKELL__ >= 910
+                , "-fprint-error-index-links=never"
+#endif
+                ]
+            }
+
       it "aborts with (ExitFailure 1)" $ do
         hSilence [stderr] action `shouldThrow` (== ExitFailure 1)
 
       it "prints a useful error message" $ do
         (r, _) <- hCapture [stderr] (E.try action :: IO (Either ExitCode Summary))
-        stripAnsiColors (removeLoadedPackageEnvironment r) `shouldBe` unlines [
-            ""
+        removeLoadedPackageEnvironment r `shouldBe` unlines (
+#if __GLASGOW_HASKELL__ < 910
+          "" :
+#endif
 #if __GLASGOW_HASKELL__ >= 906
-          , "Foo.hs:6:1: error: [GHC-58481]"
+          [ "Foo.hs:6:1: error: [GHC-58481]"
 #else
-          , "Foo.hs:6:1: error:"
+          [ "Foo.hs:6:1: error:"
 #endif
           , "    parse error (possibly incorrect indentation or mismatched brackets)"
-          ]
+#if __GLASGOW_HASKELL__ >= 910
+          , ""
+#endif
+          ])
 
   describe "expandDirs" $ do
     it "expands a directory" $ do
@@ -150,10 +185,3 @@
       let x = "foo bar baz bin"
       res <- expandDirs x
       res `shouldBe` [x]
-
-stripAnsiColors :: String -> String
-stripAnsiColors xs = case xs of
-  '\ESC' : '[' : ';' : ys | 'm' : zs <- dropWhile isNumber ys -> stripAnsiColors zs
-  '\ESC' : '[' : ys | 'm' : zs <- dropWhile isNumber ys -> stripAnsiColors zs
-  y : ys -> y : stripAnsiColors ys
-  [] -> []
diff --git a/test/RunnerSpec.hs b/test/RunnerSpec.hs
--- a/test/RunnerSpec.hs
+++ b/test/RunnerSpec.hs
@@ -1,89 +1,44 @@
 {-# LANGUAGE CPP, OverloadedStrings #-}
-module RunnerSpec (main, spec) where
+module RunnerSpec (spec) where
 
 import           Imports
 
 import           Test.Hspec
 
+import           Data.IORef
 import           System.IO
-import           System.IO.Silently (hCapture)
-import           Control.Monad.Trans.State
+import           System.IO.Silently (hCapture_)
 import           Runner
 
-main :: IO ()
-main = hspec spec
-
-capture :: Report a -> IO String
-capture = fmap fst . hCapture [stderr] . (`execStateT` ReportState 0 True False mempty)
-
--- like capture, but with interactivity set to False
-capture_ :: Report a -> IO String
-capture_ = fmap fst . hCapture [stderr] . (`execStateT` ReportState 0 False False mempty)
+capture :: Interactive -> Report () -> IO String
+capture interactive action = do
+  ref <- newIORef mempty
+  hCapture_ [stderr] (runReport (ReportState interactive NoFailFast NonVerbose ref) action)
 
 spec :: Spec
 spec = do
-
   describe "report" $ do
-
     context "when mode is interactive" $ do
-
       it "writes to stderr" $ do
-        capture $ do
+        capture Interactive $ do
           report "foobar"
         `shouldReturn` "foobar\n"
 
-      it "overwrites any intermediate output" $ do
-        capture $ do
-          report_ "foo"
-          report  "bar"
-        `shouldReturn` "foo\rbar\n"
-
-      it "blank out intermediate output if necessary" $ do
-        capture $ do
-          report_ "foobar"
-          report  "baz"
-        `shouldReturn` "foobar\rbaz   \n"
-
     context "when mode is non-interactive" $ do
       it "writes to stderr" $ do
-        capture_ $ do
+        capture NonInteractive $ do
           report "foobar"
         `shouldReturn` "foobar\n"
 
   describe "report_" $ do
-
     context "when mode is interactive" $ do
-      it "writes intermediate output to stderr" $ do
-        capture $ do
-          report_ "foobar"
-        `shouldReturn` "foobar"
-
-      it "overwrites any intermediate output" $ do
-        capture $ do
-          report_ "foo"
-          report_ "bar"
-        `shouldReturn` "foo\rbar"
-
-      it "blank out intermediate output if necessary" $ do
-        capture $ do
-          report_ "foobar"
-          report_  "baz"
-        `shouldReturn` "foobar\rbaz   "
+      it "writes transient output to stderr" $ do
+        capture Interactive $ do
+          reportTransient "foobar"
+        `shouldReturn` "foobar\r      \r"
 
     context "when mode is non-interactive" $ do
       it "is ignored" $ do
-        capture_ $ do
-          report_ "foobar"
+        capture NonInteractive $ do
+          reportTransient "foobar"
         `shouldReturn` ""
-
-      it "does not influence a subsequent call to `report`" $ do
-        capture_ $ do
-          report_ "foo"
-          report  "bar"
-        `shouldReturn` "bar\n"
-
-      it "does not require `report` to blank out any intermediate output" $ do
-        capture_ $ do
-          report_ "foobar"
-          report  "baz"
-        `shouldReturn` "baz\n"
diff --git a/test/integration/fail-fast/Bar.hs b/test/integration/fail-fast/Bar.hs
new file mode 100644
--- /dev/null
+++ b/test/integration/fail-fast/Bar.hs
@@ -0,0 +1,8 @@
+module Bar where
+
+-- | bar
+-- a passing test
+-- >>> bar
+-- 42
+bar :: Int
+bar = 42
diff --git a/test/integration/fail-fast/Foo.hs b/test/integration/fail-fast/Foo.hs
new file mode 100644
--- /dev/null
+++ b/test/integration/fail-fast/Foo.hs
@@ -0,0 +1,24 @@
+module Foo where
+
+import Bar
+
+-- | A passing example
+--
+-- >>> 23
+-- 23
+test1 :: a
+test1 = undefined
+
+-- | A failing example
+--
+-- >>> 23
+-- 42
+test2 :: a
+test2 = undefined
+
+-- | Another passing example
+--
+-- >>> 23
+-- 23
+test3 :: a
+test3 = undefined
diff --git a/test/integration/fail-fast/SetupBar.hs b/test/integration/fail-fast/SetupBar.hs
new file mode 100644
--- /dev/null
+++ b/test/integration/fail-fast/SetupBar.hs
@@ -0,0 +1,12 @@
+module SetupBar where
+
+-- $setup
+-- >>> 23
+-- 23
+
+-- | bar
+-- a passing test
+-- >>> bar
+-- 42
+bar :: Int
+bar = 42
diff --git a/test/integration/fail-fast/SetupFoo.hs b/test/integration/fail-fast/SetupFoo.hs
new file mode 100644
--- /dev/null
+++ b/test/integration/fail-fast/SetupFoo.hs
@@ -0,0 +1,29 @@
+module SetupFoo where
+
+import SetupBar
+
+
+-- $setup
+-- >>> 24
+-- 23
+
+-- | A passing example
+--
+-- >>> 23
+-- 23
+test1 :: a
+test1 = undefined
+
+-- | A failing example
+--
+-- >>> 23
+-- 42
+test2 :: a
+test2 = undefined
+
+-- | Another passing example
+--
+-- >>> 23
+-- 23
+test3 :: a
+test3 = undefined
