doctest 0.21.0 → 0.21.1
raw patch · 8 files changed
+300/−172 lines, 8 filesdep ~ghcPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: ghc
API changes (from Hackage documentation)
Files
- CHANGES.markdown +3/−0
- doctest.cabal +8/−9
- ghci-wrapper/src/Language/Haskell/GhciWrapper.hs +0/−159
- src/Extract.hs +4/−2
- src/GhcUtil.hs +3/−1
- src/Language/Haskell/GhciWrapper.hs +159/−0
- test/Language/Haskell/GhciWrapperSpec.hs +114/−0
- test/RunSpec.hs +9/−1
CHANGES.markdown view
@@ -1,3 +1,6 @@+Changes in 0.21.1+ - GHC 9.6 compatibility.+ Changes in 0.21.0 - Accept `--fast`, `--preserve-it` and `--verbose` via `--repl-options`
doctest.cabal view
@@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.35.1.+-- This file has been generated from package.yaml by hpack version 0.35.2. -- -- see: https://github.com/sol/hpack name: doctest-version: 0.21.0+version: 0.21.1 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)@@ -111,7 +111,6 @@ ghc-options: -Wall hs-source-dirs: src- ghci-wrapper/src exposed-modules: Test.DocTest other-modules:@@ -119,6 +118,7 @@ GhcUtil Info Interpreter+ Language.Haskell.GhciWrapper Location Options PackageDBs@@ -128,7 +128,6 @@ Runner Runner.Example Util- Language.Haskell.GhciWrapper Paths_doctest build-depends: base >=4.5 && <5@@ -138,7 +137,7 @@ , directory , exceptions , filepath- , ghc >=8.0 && <9.5+ , ghc >=8.0 && <9.7 , ghc-paths >=0.1.0.9 , process , syb >=0.3@@ -161,7 +160,7 @@ , doctest , exceptions , filepath- , ghc >=8.0 && <9.5+ , ghc >=8.0 && <9.7 , ghc-paths >=0.1.0.9 , process , syb >=0.3@@ -174,6 +173,7 @@ ExtractSpec InfoSpec InterpreterSpec+ Language.Haskell.GhciWrapperSpec LocationSpec MainSpec OptionsSpec@@ -189,6 +189,7 @@ GhcUtil Info Interpreter+ Language.Haskell.GhciWrapper Location Options PackageDBs@@ -199,7 +200,6 @@ Runner.Example Test.DocTest Util- Language.Haskell.GhciWrapper Paths_doctest type: exitcode-stdio-1.0 ghc-options: -Wall -threaded@@ -207,7 +207,6 @@ hs-source-dirs: test src- ghci-wrapper/src c-sources: test/integration/with-cbits/foo.c build-tool-depends:@@ -222,7 +221,7 @@ , directory , exceptions , filepath- , ghc >=8.0 && <9.5+ , ghc >=8.0 && <9.7 , ghc-paths >=0.1.0.9 , hspec >=2.3.0 , hspec-core >=2.3.0
− ghci-wrapper/src/Language/Haskell/GhciWrapper.hs
@@ -1,159 +0,0 @@-{-# LANGUAGE RecordWildCards #-}-module Language.Haskell.GhciWrapper (- Interpreter-, Config(..)-, defaultConfig-, new-, close-, eval-, evalIt-, evalEcho-) where--import System.IO hiding (stdin, stdout, stderr)-import System.Process-import System.Exit-import Control.Monad-import Control.Exception-import Data.List (isSuffixOf)-import Data.Maybe--data Config = Config {- configGhci :: String-, configVerbose :: Bool-, configIgnoreDotGhci :: Bool-} deriving (Eq, Show)--defaultConfig :: Config-defaultConfig = Config {- configGhci = "ghci"-, configVerbose = False-, configIgnoreDotGhci = True-}---- | Truly random marker, used to separate expressions.------ IMPORTANT: This module relies upon the fact that this marker is unique. It--- has been obtained from random.org. Do not expect this module to work--- properly, if you reuse it for any purpose!-marker :: String-marker = show "dcbd2a1e20ae519a1c7714df2859f1890581d57fac96ba3f499412b2f5c928a1"--itMarker :: String-itMarker = "d42472243a0e6fc481e7514cbc9eb08812ed48daa29ca815844d86010b1d113a"--data Interpreter = Interpreter {- hIn :: Handle-, hOut :: Handle-, process :: ProcessHandle-}--new :: Config -> [String] -> IO Interpreter-new Config{..} args_ = do- (Just stdin_, Just stdout_, Nothing, processHandle ) <- createProcess (proc configGhci args) {- std_in = CreatePipe- , std_out = CreatePipe- , std_err = Inherit- }- setMode stdin_- setMode stdout_- let interpreter = Interpreter {hIn = stdin_, hOut = stdout_, process = processHandle}- evalThrow interpreter "import qualified System.IO"- evalThrow interpreter "import qualified GHC.IO.Encoding"- evalThrow interpreter "import qualified GHC.IO.Handle"- -- The buffering of stdout and stderr is NoBuffering- evalThrow interpreter "GHC.IO.Handle.hDuplicateTo System.IO.stdout System.IO.stderr"- -- Now the buffering of stderr is BlockBuffering Nothing- -- In this situation, GHC 7.7 does not flush the buffer even when- -- error happens.- evalThrow interpreter "GHC.IO.Handle.hSetBuffering System.IO.stdout GHC.IO.Handle.LineBuffering"- evalThrow interpreter "GHC.IO.Handle.hSetBuffering System.IO.stderr GHC.IO.Handle.LineBuffering"-- -- this is required on systems that don't use utf8 as default encoding (e.g.- -- Windows)- evalThrow interpreter "GHC.IO.Handle.hSetEncoding System.IO.stdout GHC.IO.Encoding.utf8"- evalThrow interpreter "GHC.IO.Handle.hSetEncoding System.IO.stderr GHC.IO.Encoding.utf8"-- evalThrow interpreter ":m - System.IO"- evalThrow interpreter ":m - GHC.IO.Encoding"- evalThrow interpreter ":m - GHC.IO.Handle"-- return interpreter- where- args = args_ ++ catMaybes [- if configIgnoreDotGhci then Just "-ignore-dot-ghci" else Nothing- , if configVerbose then Nothing else Just "-v0"- ]- setMode h = do- hSetBinaryMode h False- hSetBuffering h LineBuffering- hSetEncoding h utf8-- evalThrow :: Interpreter -> String -> IO ()- evalThrow interpreter expr = do- output <- eval interpreter expr- unless (null output || configVerbose) $ do- close interpreter- throwIO (ErrorCall output)--close :: Interpreter -> IO ()-close repl = do- hClose $ hIn repl-- -- It is crucial not to close `hOut` before calling `waitForProcess`,- -- otherwise ghci may not cleanly terminate on SIGINT (ctrl-c) and hang- -- around consuming 100% CPU. This happens when ghci tries to print- -- something to stdout in its signal handler (e.g. when it is blocked in- -- threadDelay it writes "Interrupted." on SIGINT).- e <- waitForProcess $ process repl- hClose $ hOut repl-- 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- hPutStrLn stdin e- when preserveIt $ hPutStrLn stdin $ "let " ++ itMarker ++ " = it"- hPutStrLn stdin (marker ++ " :: Data.String.String")- when preserveIt $ hPutStrLn stdin $ "let it = " ++ itMarker- hFlush stdin--getResult :: Bool -> Interpreter -> IO String-getResult echoMode Interpreter{hOut = stdout} = go- where- go = do- line <- hGetLine stdout- if marker `isSuffixOf` line- then do- let xs = stripMarker line- echo xs- return xs- else do- echo (line ++ "\n")- result <- go- return (line ++ "\n" ++ result)- stripMarker l = take (length l - length marker) l-- echo :: String -> IO ()- echo- | echoMode = putStr- | otherwise = \ _ -> return ()---- | Evaluate an expression-eval :: Interpreter -> String -> IO String-eval repl expr = do- putExpression repl False expr- getResult False repl---- | Like 'eval', but try to preserve the @it@ variable-evalIt :: Interpreter -> String -> IO String-evalIt repl expr = do- putExpression repl True expr- getResult False repl---- | Evaluate an expression-evalEcho :: Interpreter -> String -> IO String-evalEcho repl expr = do- putExpression repl False expr- getResult True repl
src/Extract.hs view
@@ -221,8 +221,10 @@ -- We process header, exports and declarations separately instead of -- traversing the whole source in a generic way, to ensure that we get -- everything in source order.-#if __GLASGOW_HASKELL__ >= 904- header = [(Nothing, hsDocString <$> x) | Just x <- [hsmodHaddockModHeader source]]+#if __GLASGOW_HASKELL__ >= 906+ header = [(Nothing, hsDocString <$> x) | Just x <- [hsmodHaddockModHeader (hsmodExt source)]]+#elif __GLASGOW_HASKELL__ >= 904+ header = [(Nothing, hsDocString <$> x) | Just x <- [hsmodHaddockModHeader (source)]] #else header = [(Nothing, x) | Just x <- [hsmodHaddockModHeader source]] #endif
src/GhcUtil.hs view
@@ -69,7 +69,9 @@ setHaddockMode :: DynFlags -> DynFlags setHaddockMode dynflags = (gopt_set dynflags Opt_Haddock) {-#if __GLASGOW_HASKELL__ >= 901+#if __GLASGOW_HASKELL__ >= 906+ backend = noBackend+#elif __GLASGOW_HASKELL__ >= 901 backend = NoBackend #else hscTarget = HscNothing
+ src/Language/Haskell/GhciWrapper.hs view
@@ -0,0 +1,159 @@+{-# LANGUAGE RecordWildCards #-}+module Language.Haskell.GhciWrapper (+ Interpreter+, Config(..)+, defaultConfig+, new+, close+, eval+, evalIt+, evalEcho+) where++import System.IO hiding (stdin, stdout, stderr)+import System.Process+import System.Exit+import Control.Monad+import Control.Exception+import Data.List (isSuffixOf)+import Data.Maybe++data Config = Config {+ configGhci :: String+, configVerbose :: Bool+, configIgnoreDotGhci :: Bool+} deriving (Eq, Show)++defaultConfig :: Config+defaultConfig = Config {+ configGhci = "ghci"+, configVerbose = False+, configIgnoreDotGhci = True+}++-- | Truly random marker, used to separate expressions.+--+-- IMPORTANT: This module relies upon the fact that this marker is unique. It+-- has been obtained from random.org. Do not expect this module to work+-- properly, if you reuse it for any purpose!+marker :: String+marker = show "dcbd2a1e20ae519a1c7714df2859f1890581d57fac96ba3f499412b2f5c928a1"++itMarker :: String+itMarker = "d42472243a0e6fc481e7514cbc9eb08812ed48daa29ca815844d86010b1d113a"++data Interpreter = Interpreter {+ hIn :: Handle+, hOut :: Handle+, process :: ProcessHandle+}++new :: Config -> [String] -> IO Interpreter+new Config{..} args_ = do+ (Just stdin_, Just stdout_, Nothing, processHandle ) <- createProcess (proc configGhci args) {+ std_in = CreatePipe+ , std_out = CreatePipe+ , std_err = Inherit+ }+ setMode stdin_+ setMode stdout_+ let interpreter = Interpreter {hIn = stdin_, hOut = stdout_, process = processHandle}+ evalThrow interpreter "import qualified System.IO"+ evalThrow interpreter "import qualified GHC.IO.Encoding"+ evalThrow interpreter "import qualified GHC.IO.Handle"+ -- The buffering of stdout and stderr is NoBuffering+ evalThrow interpreter "GHC.IO.Handle.hDuplicateTo System.IO.stdout System.IO.stderr"+ -- Now the buffering of stderr is BlockBuffering Nothing+ -- In this situation, GHC 7.7 does not flush the buffer even when+ -- error happens.+ evalThrow interpreter "GHC.IO.Handle.hSetBuffering System.IO.stdout GHC.IO.Handle.LineBuffering"+ evalThrow interpreter "GHC.IO.Handle.hSetBuffering System.IO.stderr GHC.IO.Handle.LineBuffering"++ -- this is required on systems that don't use utf8 as default encoding (e.g.+ -- Windows)+ evalThrow interpreter "GHC.IO.Handle.hSetEncoding System.IO.stdout GHC.IO.Encoding.utf8"+ evalThrow interpreter "GHC.IO.Handle.hSetEncoding System.IO.stderr GHC.IO.Encoding.utf8"++ evalThrow interpreter ":m - System.IO"+ evalThrow interpreter ":m - GHC.IO.Encoding"+ evalThrow interpreter ":m - GHC.IO.Handle"++ return interpreter+ where+ args = args_ ++ catMaybes [+ if configIgnoreDotGhci then Just "-ignore-dot-ghci" else Nothing+ , if configVerbose then Nothing else Just "-v0"+ ]+ setMode h = do+ hSetBinaryMode h False+ hSetBuffering h LineBuffering+ hSetEncoding h utf8++ evalThrow :: Interpreter -> String -> IO ()+ evalThrow interpreter expr = do+ output <- eval interpreter expr+ unless (null output || configVerbose) $ do+ close interpreter+ throwIO (ErrorCall output)++close :: Interpreter -> IO ()+close repl = do+ hClose $ hIn repl++ -- It is crucial not to close `hOut` before calling `waitForProcess`,+ -- otherwise ghci may not cleanly terminate on SIGINT (ctrl-c) and hang+ -- around consuming 100% CPU. This happens when ghci tries to print+ -- something to stdout in its signal handler (e.g. when it is blocked in+ -- threadDelay it writes "Interrupted." on SIGINT).+ e <- waitForProcess $ process repl+ hClose $ hOut repl++ 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+ hPutStrLn stdin e+ when preserveIt $ hPutStrLn stdin $ "let " ++ itMarker ++ " = it"+ hPutStrLn stdin (marker ++ " :: Data.String.String")+ when preserveIt $ hPutStrLn stdin $ "let it = " ++ itMarker+ hFlush stdin++getResult :: Bool -> Interpreter -> IO String+getResult echoMode Interpreter{hOut = stdout} = go+ where+ go = do+ line <- hGetLine stdout+ if marker `isSuffixOf` line+ then do+ let xs = stripMarker line+ echo xs+ return xs+ else do+ echo (line ++ "\n")+ result <- go+ return (line ++ "\n" ++ result)+ stripMarker l = take (length l - length marker) l++ echo :: String -> IO ()+ echo+ | echoMode = putStr+ | otherwise = \ _ -> return ()++-- | Evaluate an expression+eval :: Interpreter -> String -> IO String+eval repl expr = do+ putExpression repl False expr+ getResult False repl++-- | Like 'eval', but try to preserve the @it@ variable+evalIt :: Interpreter -> String -> IO String+evalIt repl expr = do+ putExpression repl True expr+ getResult False repl++-- | Evaluate an expression+evalEcho :: Interpreter -> String -> IO String+evalEcho repl expr = do+ putExpression repl False expr+ getResult True repl
+ test/Language/Haskell/GhciWrapperSpec.hs view
@@ -0,0 +1,114 @@+{-# LANGUAGE CPP #-}+module Language.Haskell.GhciWrapperSpec (main, spec) where++import Test.Hspec+import System.IO.Silently++import Control.Exception+import Data.List++import Language.Haskell.GhciWrapper (Interpreter, Config(..), defaultConfig)+import qualified Language.Haskell.GhciWrapper as Interpreter++main :: IO ()+main = hspec spec++withInterpreterConfig :: Config -> [String] -> (Interpreter -> IO a) -> IO a+withInterpreterConfig config args = bracket (Interpreter.new config args) Interpreter.close++withInterpreterArgs :: [String] -> ((String -> IO String) -> IO a) -> IO a+withInterpreterArgs args action = withInterpreterConfig defaultConfig args $ action . Interpreter.eval++withInterpreter :: ((String -> IO String) -> IO a) -> IO a+withInterpreter = withInterpreterArgs []++spec :: Spec+spec = do+ describe "evalEcho" $ do+ it "prints result to stdout" $ do+ 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 "eval" $ do+ it "shows literals" $ withInterpreter $ \ghci -> do+ ghci "23" `shouldReturn` "23\n"++ it "shows string literals containing Unicode" $ withInterpreter $ \ghci -> do+ ghci "\"λ\"" `shouldReturn` "\"\\955\"\n"++ it "evaluates simple expressions" $ withInterpreter $ \ghci -> do+ ghci "23 + 42" `shouldReturn` "65\n"++ it "supports let bindings" $ withInterpreter $ \ghci -> do+ ghci "let x = 10" `shouldReturn` ""+ ghci "x" `shouldReturn` "10\n"++ it "allows import statements" $ withInterpreter $ \ghci -> do+ ghci "import Data.Maybe" `shouldReturn` ""+ ghci "fromJust (Just 20)" `shouldReturn` "20\n"++ it "captures stdout" $ withInterpreter $ \ghci -> do+ ghci "putStr \"foo\"" `shouldReturn` "foo"++ it "captures stdout (Unicode)" $ withInterpreter $ \ghci -> do+ ghci "putStrLn \"λ\"" `shouldReturn` "λ\n"++ it "captures stdout (empty line)" $ withInterpreter $ \ghci -> do+ ghci "putStrLn \"\"" `shouldReturn` "\n"++ it "captures stdout (multiple lines)" $ withInterpreter $ \ghci -> do+ ghci "putStrLn \"foo\" >> putStrLn \"bar\" >> putStrLn \"baz\""+ `shouldReturn` "foo\nbar\nbaz\n"++ it "captures stderr" $ withInterpreter $ \ghci -> do+ ghci "import System.IO" `shouldReturn` ""+ ghci "hPutStrLn stderr \"foo\"" `shouldReturn` "foo\n"++ it "captures stderr (Unicode)" $ withInterpreter $ \ghci -> do+ ghci "import System.IO" `shouldReturn` ""+ ghci "hPutStrLn stderr \"λ\"" `shouldReturn` "λ\n"++ it "shows exceptions" $ withInterpreter $ \ghci -> do+ ghci "import Control.Exception" `shouldReturn` ""+ ghci "throwIO DivideByZero" `shouldReturn` "*** Exception: divide by zero\n"++ 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+ Interpreter.eval ghci "print 23" >>= (`shouldSatisfy`+ (`elem` [ "Prelude> 23\nPrelude> "+ , "ghci> 23\nghci> "+ ]))++ 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 "putStrLn \"foo\"" `shouldReturn` "foo\n"++ context "with NoImplicitPrelude" $ do+ it "works" $ withInterpreterArgs ["-XNoImplicitPrelude"] $ \ghci -> do+ ghci "putStrLn \"foo\"" >>= (`shouldContain` "Variable not in scope: putStrLn")+ ghci "23" `shouldReturn` "23\n"++ context "with a strange String type" $ do+ it "works" $ withInterpreter $ \ghci -> do+ ghci "type String = Int" `shouldReturn` ""+ ghci "putStrLn \"foo\"" `shouldReturn` "foo\n"
test/RunSpec.hs view
@@ -128,7 +128,15 @@ it "prints a useful error message" $ do (r, _) <- hCapture [stderr] (E.try action :: IO (Either ExitCode Summary))- stripAnsiColors (removeLoadedPackageEnvironment r) `shouldBe` "\nFoo.hs:6:1: error:\n parse error (possibly incorrect indentation or mismatched brackets)\n"+ stripAnsiColors (removeLoadedPackageEnvironment r) `shouldBe` unlines [+ ""+#if __GLASGOW_HASKELL__ >= 906+ , "Foo.hs:6:1: error: [GHC-58481]"+#else+ , "Foo.hs:6:1: error:"+#endif+ , " parse error (possibly incorrect indentation or mismatched brackets)"+ ] describe "expandDirs" $ do it "expands a directory" $ do