sensei 0.6.2 → 0.7.0
raw patch · 16 files changed
+646/−209 lines, 16 filesdep +QuickCheckdep +hspec-contribdep ~fsnotify
Dependencies added: QuickCheck, hspec-contrib
Dependency ranges changed: fsnotify
Files
- doctest/ghci-wrapper/src/Language/Haskell/GhciWrapper.hs +0/−146
- driver/sensei-web.hs +4/−1
- driver/sensei.hs +4/−1
- sensei.cabal +21/−15
- src/Imports.hs +15/−0
- src/Language/Haskell/GhciWrapper.hs +141/−0
- src/ReadHandle.hs +170/−0
- src/Run.hs +29/−17
- src/Session.hs +14/−21
- src/Trigger.hs +3/−0
- startup.ghci +4/−0
- test/Helper.hs +20/−3
- test/Language/Haskell/GhciWrapperSpec.hs +89/−0
- test/ReadHandleSpec.hs +111/−0
- test/SessionSpec.hs +16/−5
- test/TriggerSpec.hs +5/−0
− doctest/ghci-wrapper/src/Language/Haskell/GhciWrapper.hs
@@ -1,146 +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}- _ <- eval interpreter "import qualified System.IO"- _ <- eval interpreter "import qualified GHC.IO.Handle"- -- The buffering of stdout and stderr is NoBuffering- _ <- eval 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.- _ <- eval interpreter "GHC.IO.Handle.hSetBuffering System.IO.stdout GHC.IO.Handle.LineBuffering"- _ <- eval 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)- _ <- eval interpreter "GHC.IO.Handle.hSetEncoding System.IO.stdout GHC.IO.Handle.utf8"- _ <- eval interpreter "GHC.IO.Handle.hSetEncoding System.IO.stderr GHC.IO.Handle.utf8"-- _ <- eval interpreter ":m - System.IO"- _ <- eval 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--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 = (const $ 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
driver/sensei-web.hs view
@@ -2,7 +2,10 @@ import System.Environment +import Paths_sensei import Run main :: IO ()-main = getArgs >>= runWeb+main = do+ startupFile <- getDataFileName "startup.ghci"+ getArgs >>= runWeb startupFile
driver/sensei.hs view
@@ -2,7 +2,10 @@ import System.Environment +import Paths_sensei import Run main :: IO ()-main = getArgs >>= run+main = do+ startupFile <- getDataFileName "startup.ghci"+ getArgs >>= run startupFile
sensei.cabal view
@@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.34.7.+-- This file has been generated from package.yaml by hpack version 0.35.1. -- -- see: https://github.com/sol/hpack name: sensei-version: 0.6.2+version: 0.7.0 synopsis: Automatically run Hspec tests on file modifications category: Development homepage: https://github.com/hspec/sensei#readme@@ -14,6 +14,8 @@ license: MIT license-file: LICENSE build-type: Simple+data-files:+ startup.ghci source-repository head type: git@@ -23,7 +25,6 @@ main-is: seito.hs hs-source-dirs: src- doctest/ghci-wrapper/src/ driver ghc-options: -Wall build-depends:@@ -32,7 +33,7 @@ , bytestring , directory , filepath- , fsnotify+ , fsnotify ==0.4.* , http-client >=0.5.0 , http-types , network@@ -48,12 +49,13 @@ EventQueue HTTP Imports+ Language.Haskell.GhciWrapper Options+ ReadHandle Run Session Trigger Util- Language.Haskell.GhciWrapper Paths_sensei default-language: Haskell2010 @@ -61,7 +63,6 @@ main-is: sensei.hs hs-source-dirs: src- doctest/ghci-wrapper/src/ driver ghc-options: -Wall build-depends:@@ -70,7 +71,7 @@ , bytestring , directory , filepath- , fsnotify+ , fsnotify ==0.4.* , http-client >=0.5.0 , http-types , network@@ -86,12 +87,13 @@ EventQueue HTTP Imports+ Language.Haskell.GhciWrapper Options+ ReadHandle Run Session Trigger Util- Language.Haskell.GhciWrapper Paths_sensei default-language: Haskell2010 @@ -99,7 +101,6 @@ main-is: sensei-web.hs hs-source-dirs: src- doctest/ghci-wrapper/src/ driver ghc-options: -Wall build-depends:@@ -108,7 +109,7 @@ , bytestring , directory , filepath- , fsnotify+ , fsnotify ==0.4.* , http-client >=0.5.0 , http-types , network@@ -124,12 +125,13 @@ EventQueue HTTP Imports+ Language.Haskell.GhciWrapper Options+ ReadHandle Run Session Trigger Util- Language.Haskell.GhciWrapper Paths_sensei default-language: Haskell2010 @@ -138,20 +140,21 @@ main-is: Spec.hs hs-source-dirs: src- doctest/ghci-wrapper/src/ test ghc-options: -Wall cpp-options: -DTEST build-tool-depends: hspec-discover:hspec-discover build-depends:- ansi-terminal+ QuickCheck+ , ansi-terminal , base >=4.11 && <5 , bytestring , directory , filepath- , fsnotify+ , fsnotify ==0.4.* , hspec >=2.9.0+ , hspec-contrib >=0.5.2 , hspec-wai , http-client >=0.5.0 , http-types@@ -170,17 +173,20 @@ EventQueue HTTP Imports+ Language.Haskell.GhciWrapper Options+ ReadHandle Run Session Trigger Util- Language.Haskell.GhciWrapper ClientSpec EventQueueSpec Helper HTTPSpec+ Language.Haskell.GhciWrapperSpec OptionsSpec+ ReadHandleSpec SessionSpec TriggerSpec UtilSpec
src/Imports.hs view
@@ -1,14 +1,29 @@ module Imports (module Imports) where +import Control.Arrow as Imports ((>>>), (&&&)) import Control.Concurrent as Imports import Control.Exception as Imports import Control.Monad as Imports+import Data.Functor as Imports import Data.Bifunctor as Imports import Data.Char as Imports import Data.List as Imports import Data.Maybe as Imports import Data.String as Imports+import Data.ByteString.Char8 as Imports (ByteString, pack, unpack) import Data.Text.Lazy.Encoding as Imports (encodeUtf8) import Data.Tuple as Imports import System.IO.Error as Imports (isDoesNotExistError) import Text.Read as Imports (readMaybe)++pass :: Applicative m => m ()+pass = pure ()++while :: Monad m => m Bool -> m () -> m ()+while p action = go+ where+ go = do+ notDone <- p+ when notDone $ do+ action+ go
+ src/Language/Haskell/GhciWrapper.hs view
@@ -0,0 +1,141 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE NamedFieldPuns #-}+module Language.Haskell.GhciWrapper (+ Config(..)+, Interpreter+, withInterpreter+, eval+, evalVerbose+) where++import Imports++import qualified Data.ByteString as B+import Data.Text.Encoding (decodeUtf8)+import qualified Data.Text as T+import System.IO hiding (stdin, stdout, stderr)+import qualified System.IO as System+import System.Directory (doesFileExist)+import System.Process+import System.Exit (ExitCode(..))++import qualified ReadHandle+import ReadHandle (ReadHandle, toReadHandle)++data Config = Config {+ configIgnoreDotGhci :: Bool+, configVerbose :: Bool+, configStartupFile :: FilePath+} deriving (Eq, Show)++data Interpreter = Interpreter {+ hIn :: Handle+, hOut :: Handle+, readHandle :: ReadHandle+, process :: ProcessHandle+}++die :: String -> IO a+die = throwIO . ErrorCall++withInterpreter :: Config -> [String] -> (Interpreter -> IO r) -> IO r+withInterpreter config args = bracket (new config args) (close $ verbosity config)++new :: Config -> [String] -> IO Interpreter+new config@Config{..} args_ = do++ requireFile configStartupFile++ (Just stdin_, Just stdout_, Nothing, processHandle ) <- createProcess (proc "ghci" args) {+ std_in = CreatePipe+ , std_out = CreatePipe+ , std_err = Inherit+ }++ setMode stdin_+ readHandle <- toReadHandle stdout_ 1024++ let+ interpreter = Interpreter {+ hIn = stdin_+ , readHandle+ , hOut = stdout_+ , process = processHandle+ }++ _ <- printStartupMessages interpreter++ evalThrow interpreter "GHC.IO.Handle.hDuplicateTo System.IO.stdout System.IO.stderr"++ -- GHCi uses NoBuffering for stdout and stderr by default:+ -- https://downloads.haskell.org/ghc/9.4.4/docs/users_guide/ghci.html+ 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"++ 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"++ return interpreter+ where+ requireFile name = do+ exists <- doesFileExist name+ unless exists $ do+ die $ "Required file " <> show name <> " does not exist!"++ args = "-ghci-script" : configStartupFile : args_ ++ catMaybes [+ if configIgnoreDotGhci then Just "-ignore-dot-ghci" else Nothing+ ]++ setMode h = do+ hSetBinaryMode h False+ hSetBuffering h LineBuffering+ hSetEncoding h utf8++ printStartupMessages interpreter = evalWith (verbosity config) interpreter ""++ evalThrow :: Interpreter -> String -> IO ()+ evalThrow interpreter expr = do+ output <- eval interpreter expr+ unless (null output) $ do+ close (verbosity config) interpreter+ die output++close :: (ByteString -> IO ()) -> Interpreter -> IO ()+close echo Interpreter{..} = do+ hClose hIn+ ReadHandle.drain readHandle echo+ hClose hOut+ e <- waitForProcess process+ when (e /= ExitSuccess) $ do+ throwIO (userError $ "Language.Haskell.GhciWrapper.close: Interpreter exited with an error (" ++ show e ++ ")")++putExpression :: Interpreter -> String -> IO ()+putExpression Interpreter{hIn = stdin} e = do+ hPutStrLn stdin e+ B.hPut stdin ReadHandle.marker+ hFlush stdin++getResult :: Interpreter -> (ByteString -> IO ()) -> IO String+getResult Interpreter{readHandle = h} = fmap (T.unpack . decodeUtf8) . ReadHandle.getResult h++verbosity :: Config -> ByteString -> IO ()+verbosity config+ | configVerbose config = verbose+ | otherwise = silent++verbose :: ByteString -> IO ()+verbose string = B.putStr string >> hFlush System.stdout++silent :: ByteString -> IO ()+silent _ = pass++evalWith :: (ByteString -> IO ()) -> Interpreter -> String -> IO String+evalWith echo repl expr = do+ putExpression repl expr+ getResult repl echo++eval :: Interpreter -> String -> IO String+eval = evalWith silent++evalVerbose :: Interpreter -> String -> IO String+evalVerbose = evalWith verbose
+ src/ReadHandle.hs view
@@ -0,0 +1,170 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ViewPatterns #-}+module ReadHandle (+ ReadHandle(..)+, toReadHandle+, marker+, getResult+, drain+#ifdef TEST+, newEmptyBuffer+#endif+) where++import Imports++import qualified Data.ByteString.Char8 as B+import Data.IORef+import System.IO hiding (stdin, stdout, stderr, isEOF)++#if MIN_VERSION_bytestring(0,11,0)+import Data.ByteString (dropEnd)+#else+import qualified Data.ByteString.Internal as B+dropEnd :: Int -> ByteString -> ByteString+dropEnd n ps@(B.PS x offset len)+ | n <= 0 = ps+ | n >= len = B.empty+ | otherwise = B.PS x offset (len - n)+#endif++-- | 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 :: ByteString+marker = pack (show @String "be77d2c8427d29cd1d62b7612d8e98cc") <> "\n"++partialMarkers :: [ByteString]+partialMarkers = reverse . drop 1 . init $ B.inits marker++data ReadHandle = ReadHandle {+ getChunk :: IO ByteString+, buffer :: IORef Buffer+}++drain :: ReadHandle -> (ByteString -> IO ()) -> IO ()+drain h echo = while (not <$> isEOF h) $ do+ _ <- getResult h echo+ pass++isEOF :: ReadHandle -> IO Bool+isEOF ReadHandle{..} = do+ readIORef buffer <&> \ case+ BufferEOF -> True+ BufferEmpty -> False+ BufferPartialMarker {} -> False+ BufferChunk {} -> False++emptyBuffer :: Buffer -> Buffer+emptyBuffer old = case old of+ BufferEOF -> BufferEOF+ BufferEmpty -> BufferEmpty+ BufferPartialMarker {} -> BufferEmpty+ BufferChunk {} -> BufferEmpty++mkBufferChunk :: ByteString -> Buffer+mkBufferChunk chunk+ | B.null chunk = BufferEmpty+ | otherwise = BufferChunk chunk++data Buffer =+ BufferEOF+ | BufferEmpty+ | BufferPartialMarker !ByteString+ | BufferChunk !ByteString++toReadHandle :: Handle -> Int -> IO ReadHandle+toReadHandle h n = do+ hSetBinaryMode h True+ ReadHandle (B.hGetSome h n) <$> newEmptyBuffer++newEmptyBuffer :: IO (IORef Buffer)+newEmptyBuffer = newIORef BufferEmpty++getResult :: ReadHandle -> (ByteString -> IO ()) -> IO ByteString+getResult h echo = mconcat <$> go+ where+ go :: IO [ByteString]+ go = nextChunk h >>= \ case+ Chunk chunk -> echo chunk >> (chunk :) <$> go+ Marker -> return []+ EOF -> return []++data Chunk = Chunk ByteString | Marker | EOF++nextChunk :: ReadHandle -> IO Chunk+nextChunk ReadHandle {..} = go+ where+ takeBuffer :: IO Buffer+ takeBuffer = atomicModifyIORef' buffer (emptyBuffer &&& id)++ putBuffer :: Buffer -> IO ()+ putBuffer = writeIORef buffer++ putBuffer_ :: ByteString -> IO ()+ putBuffer_ = putBuffer . mkBufferChunk++ getSome :: IO (Maybe ByteString)+ getSome = do+ chunk <- getChunk+ if B.null chunk then do+ putBuffer BufferEOF+ return Nothing+ else do+ return (Just chunk)++ go :: IO Chunk+ go = takeBuffer >>= \ case+ BufferEOF -> return EOF+ BufferEmpty -> getSome >>= \ case+ Nothing -> return EOF+ Just chunk -> processChunk chunk+ BufferPartialMarker partialMarker -> getSome >>= \ case+ Nothing -> return (Chunk partialMarker)+ Just chunk -> processChunk (partialMarker <> chunk)+ BufferChunk chunk -> processChunk chunk++ processChunk :: ByteString -> IO Chunk+ processChunk chunk = case stripMarker chunk of+ StrippedMarker rest -> do+ putBuffer_ rest+ return Marker+ PrefixBeforeMarker prefix rest -> do+ putBuffer_ rest+ return (Chunk prefix)+ NoMarker -> case splitPartialMarker chunk of+ Just (prefix, partialMarker) -> do+ putBuffer (BufferPartialMarker partialMarker)+ if B.null prefix then do+ go+ else do+ return (Chunk prefix)+ Nothing -> return (Chunk chunk)++splitPartialMarker :: ByteString -> Maybe (ByteString, ByteString)+splitPartialMarker chunk = split <$> findPartialMarker chunk+ where+ split partialMarker = (dropEnd (B.length partialMarker) chunk, partialMarker)++findPartialMarker :: ByteString -> Maybe ByteString+findPartialMarker chunk = find (`B.isSuffixOf` chunk) partialMarkers++data StripMarker =+ NoMarker+ | PrefixBeforeMarker !ByteString !ByteString+ | StrippedMarker !ByteString++stripMarker :: ByteString -> StripMarker+stripMarker input = case brakeAtMarker input of+ (_, "") -> NoMarker+ ("", dropMarker -> ys) -> StrippedMarker ys+ (xs, ys) -> PrefixBeforeMarker xs ys+ where+ brakeAtMarker = B.breakSubstring marker+ dropMarker = B.drop (B.length marker)
src/Run.hs view
@@ -11,7 +11,7 @@ import qualified HTTP import qualified Session-import Session (Session)+import Session (Session, Config(..)) import EventQueue import Trigger@@ -22,19 +22,24 @@ watchFiles :: EventQueue -> IO () watchFiles queue = do- watch $ emitEvent queue . \ case- Added file _ _ -> FileEvent FileAdded file- Removed file _ _ -> FileEvent FileRemoved file- Modified file _ _ -> FileEvent FileModified file- Unknown file _ _ -> FileEvent FileModified file+ watch $ \ case+ Added file _ _ -> emit $ FileEvent FileAdded file+ Modified file _ _ -> emit $ FileEvent FileModified file+ ModifiedAttributes _file _ _ -> pass+ Removed file _ _ -> emit $ FileEvent FileRemoved file+ WatchedDirectoryRemoved _file _ _ -> pass+ CloseWrite file _ _ -> emit $ FileEvent FileModified file+ Unknown file _ _ _ -> emit $ FileEvent FileModified file where- isInteresting = (&&) <$> not . eventIsDirectory <*> not . isBoring . eventPath+ emit = emitEvent queue watch action = void . forkIO $ do withManager $ \ manager -> do _stopListening <- watchTree manager "." isInteresting action waitForever-+ where+ isInteresting = (&&) <$> isFile <*> not . isBoring . eventPath+ isFile = eventIsDirectory >>> (== IsFile) watchInput :: EventQueue -> IO () watchInput queue = void . forkIO $ do@@ -43,8 +48,8 @@ emitEvent queue TriggerAll emitEvent queue Done -run :: [String] -> IO ()-run args = do+run :: FilePath -> [String] -> IO ()+run startupFile args = do queue <- newQueue watchFiles queue watchInput queue@@ -55,7 +60,7 @@ saveOutput action = modifyMVar_ lastOutput $ \_ -> action go = do- status <- withSession args $ \ session -> do+ status <- withSession startupFile args $ \ session -> do let triggerAction = saveOutput (trigger session) triggerAllAction = saveOutput (triggerAll session)@@ -66,16 +71,16 @@ Terminate -> return () go -runWeb :: [String] -> IO ()-runWeb args = do- withSession args $ \session -> do+runWeb :: FilePath -> [String] -> IO ()+runWeb startupFile args = do+ withSession startupFile args $ \session -> do _ <- trigger session lock <- newMVar () HTTP.withServer (withMVar lock $ \() -> trigger session) $ do waitForever -withSession :: [String] -> (Session -> IO a) -> IO a-withSession args action = do+withSession :: FilePath -> [String] -> (Session -> IO a) -> IO a+withSession startupFile args action = do check <- dotGhciWritableByOthers when check $ do putStrLn ".ghci is writable by others, you can fix this with:"@@ -83,4 +88,11 @@ putStrLn " chmod go-w .ghci ." putStrLn "" exitFailure- bracket (Session.new args) Session.close action+ Session.withSession config args action+ where+ config :: Config+ config = Config {+ configIgnoreDotGhci = False+ , configVerbose = True+ , configStartupFile = startupFile+ }
src/Session.hs view
@@ -1,9 +1,9 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE RecordWildCards #-} module Session (- Session(..)-, new-, close+ Config(..)+, Session(..)+, withSession , reload , Summary(..)@@ -27,8 +27,7 @@ import Data.IORef -import qualified Language.Haskell.GhciWrapper as GhciWrapper-import Language.Haskell.GhciWrapper hiding (new, close)+import Language.Haskell.GhciWrapper import Util import Options@@ -48,23 +47,17 @@ hspecPreviousSummary :: Session -> IO (Maybe Summary) hspecPreviousSummary Session{..} = readIORef sessionHspecPreviousSummary -new :: [String] -> IO Session-new args = do- let (ghciArgs, hspecArgs) = splitArgs args- ghci <- GhciWrapper.new defaultConfig{configVerbose = True, configIgnoreDotGhci = False} ghciArgs- _ <- eval ghci (":set prompt " ++ show "")- _ <- eval ghci ("import qualified System.Environment")- _ <- eval ghci ("import qualified Test.Hspec.Runner")- _ <- eval ghci ("import qualified Test.Hspec.Meta")- _ <- eval ghci ("System.Environment.unsetEnv " ++ show hspecFailureEnvName)- ref <- newIORef (Just $ Summary 0 0)- return (Session ghci hspecArgs ref)--close :: Session -> IO ()-close = GhciWrapper.close . sessionInterpreter+withSession :: Config -> [String] -> (Session -> IO r) -> IO r+withSession config args action = do+ withInterpreter config ghciArgs $ \ ghci -> do+ _ <- eval ghci ("System.Environment.unsetEnv " ++ show hspecFailureEnvName)+ ref <- newIORef (Just $ Summary 0 0)+ action (Session ghci hspecArgs ref)+ where+ (ghciArgs, hspecArgs) = splitArgs args reload :: Session -> IO String-reload Session{..} = evalEcho sessionInterpreter ":reload"+reload Session{..} = evalVerbose sessionInterpreter ":reload" data Summary = Summary { summaryExamples :: Int@@ -103,7 +96,7 @@ runSpec command session@Session{..} = do failedPreviously <- isFailure <$> hspecPreviousSummary session let args = "--color" : (if failedPreviously then addRerun else id) sessionHspecArgs- r <- evalEcho sessionInterpreter $ "System.Environment.withArgs " ++ show args ++ " $ " ++ command+ r <- evalVerbose sessionInterpreter $ "System.Environment.withArgs " ++ show args ++ " $ " ++ command writeIORef sessionHspecPreviousSummary (parseSummary r) return r where
src/Trigger.hs view
@@ -41,12 +41,14 @@ withColor Red $ putStrLn "RELOADING FAILED" return (False, "") where+ hspec :: IO (Bool, String) hspec = do mRun <- Session.getRunSpec session case mRun of Just run -> runSpecs run Nothing -> return (True, "") + runSpecs :: IO String -> IO (Bool, String) runSpecs run = do failedPreviously <- isFailure <$> hspecPreviousSummary session (success, xs) <- runSpec run@@ -54,6 +56,7 @@ then runSpec run else return (success, "") + runSpec :: IO a -> IO (Bool, a) runSpec run = do xs <- run success <- isSuccess <$> hspecPreviousSummary session
+ startup.ghci view
@@ -0,0 +1,4 @@+:set prompt ""+:unset +m +r +s +t +c+:seti -XHaskell2010+:seti -XNoOverloadedStrings
test/Helper.hs view
@@ -1,6 +1,6 @@-{-# LANGUAGE QuasiQuotes #-} module Helper ( module Imports+, ghciConfig , withSession , withSomeSpec , passingSpec@@ -16,16 +16,33 @@ import System.Process as Imports (readProcess, callCommand) import Test.Hspec as Imports import Test.Mockery.Directory as Imports+import Test.Hspec.Contrib.Mocks.V1 as Imports import Run () import qualified Session import Session (Session)+import Language.Haskell.GhciWrapper (Config(..)) +startupFile :: FilePath+startupFile = "startup.ghci"++ghciConfig :: Config+ghciConfig = Config {+ configIgnoreDotGhci = True+, configVerbose = False+, configStartupFile = startupFile+}+ withSession :: [String] -> (Session -> IO a) -> IO a-withSession args action = bracket (Session.new $ "-ignore-dot-ghci" : args) Session.close action+withSession = Session.withSession ghciConfig withSomeSpec :: IO a -> IO a-withSomeSpec = (inTempDirectory . (writeFile "Spec.hs" passingSpec >>))+withSomeSpec action = do+ startup <- readFile startupFile+ inTempDirectory $ do+ writeFile startupFile startup+ writeFile "Spec.hs" passingSpec+ action passingSpec :: String passingSpec = unlines [
+ test/Language/Haskell/GhciWrapperSpec.hs view
@@ -0,0 +1,89 @@+{-# LANGUAGE CPP #-}+module Language.Haskell.GhciWrapperSpec (main, spec) where++import Helper++import Language.Haskell.GhciWrapper (Config(..), Interpreter)+import qualified Language.Haskell.GhciWrapper as Interpreter++main :: IO ()+main = hspec spec++withInterpreter :: [String] -> (Interpreter -> IO a) -> IO a+withInterpreter = Interpreter.withInterpreter ghciConfig++withGhci :: ((String -> IO String) -> IO a) -> IO a+withGhci action = withInterpreter [] $ action . Interpreter.eval++spec :: Spec+spec = do+ describe "withInterpreter" $ do+ context "on shutdown" $ do+ it "drains `stdout` of the `ghci` process" $ do+ result <- capture_ $ Interpreter.withInterpreter ghciConfig {configVerbose = True} [] $ \ _ghci -> do+ pass+ last (lines result) `shouldBe` "Leaving GHCi."++ describe "evalVerbose" $ do+ it "echos result to stdout" $ do+ withInterpreter [] $ \ ghci -> do+ capture (Interpreter.evalVerbose ghci $ "putStr" ++ show "foo\nbar") `shouldReturn` ("foo\nbar", "foo\nbar")++ describe "eval" $ do+ it "shows literals" $ withGhci $ \ ghci -> do+ ghci "23" `shouldReturn` "23\n"++ it "shows string literals containing Unicode" $ withGhci $ \ ghci -> do+ ghci "\"λ\"" `shouldReturn` "\"\\955\"\n"++ it "evaluates simple expressions" $ withGhci $ \ ghci -> do+ ghci "23 + 42" `shouldReturn` "65\n"++ it "uses LineBuffering for stdout and stderr" $ withGhci $ \ ghci -> do+ ghci "GHC.IO.Handle.hGetBuffering System.IO.stdout" `shouldReturn` "LineBuffering\n"+ ghci "GHC.IO.Handle.hGetBuffering System.IO.stderr" `shouldReturn` "LineBuffering\n"++ it "supports let bindings" $ withGhci $ \ ghci -> do+ ghci "let x = 10" `shouldReturn` ""+ ghci "x" `shouldReturn` "10\n"++ it "allows import statements" $ withGhci $ \ ghci -> do+ ghci "import Data.Maybe" `shouldReturn` ""+ ghci "fromJust (Just 20)" `shouldReturn` "20\n"++ it "captures stdout" $ withGhci $ \ ghci -> do+ ghci "putStr \"foo\"" `shouldReturn` "foo"++ it "captures stdout (Unicode)" $ withGhci $ \ ghci -> do+ ghci "putStrLn \"λ\"" `shouldReturn` "λ\n"++ it "captures stdout (empty line)" $ withGhci $ \ ghci -> do+ ghci "putStrLn \"\"" `shouldReturn` "\n"++ it "captures stdout (multiple lines)" $ withGhci $ \ ghci -> do+ ghci "putStrLn \"foo\" >> putStrLn \"bar\" >> putStrLn \"baz\""+ `shouldReturn` "foo\nbar\nbaz\n"++ it "captures stderr" $ withGhci $ \ ghci -> do+ ghci "import System.IO" `shouldReturn` ""+ ghci "hPutStrLn stderr \"foo\"" `shouldReturn` "foo\n"++ it "captures stderr (Unicode)" $ withGhci $ \ ghci -> do+ ghci "import System.IO" `shouldReturn` ""+ ghci "hPutStrLn stderr \"λ\"" `shouldReturn` "λ\n"++ it "shows exceptions" $ withGhci $ \ ghci -> do+ ghci "import Control.Exception" `shouldReturn` ""+ ghci "throwIO DivideByZero" `shouldReturn` "*** Exception: divide by zero\n"++ it "shows exceptions (ExitCode)" $ withGhci $ \ 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" $ withGhci $ \ ghci -> do+ ghci "foo" >>= (`shouldSatisfy` isInfixOf "Variable not in scope: foo")++ context "with -XNoImplicitPrelude" $ do+ it "works" $ withInterpreter ["-XNoImplicitPrelude"] $ \ ghci -> do+ Interpreter.eval ghci "putStrLn \"foo\"" >>= (`shouldContain` "Variable not in scope: putStrLn")+ Interpreter.eval ghci "23" `shouldReturn` "23\n"
+ test/ReadHandleSpec.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ViewPatterns #-}+module ReadHandleSpec (spec) where++import Helper+import Test.QuickCheck+import qualified Data.ByteString as B++import ReadHandle++chunkByteString :: (Int, Int) -> ByteString -> Gen [ByteString]+chunkByteString size = go+ where+ go "" = return []+ go xs = do+ n <- chooseInt size+ let (chunk, rest) = B.splitAt n xs+ (chunk :) <$> go rest++fakeHandle :: [ByteString] -> IO ReadHandle+fakeHandle chunks = ReadHandle <$> stubAction chunks <*> newEmptyBuffer++data ChunkSizes = SmallChunks | BigChunks++withRandomChunkSizes :: [ByteString] -> (ReadHandle -> Expectation) -> Property+withRandomChunkSizes (mconcat -> input) action = property $ do+ chunkSizes <- elements [SmallChunks, BigChunks]+ let+ maxChunkSize = case chunkSizes of+ SmallChunks -> 4+ BigChunks -> B.length input++ chunks <- chunkByteString (1, maxChunkSize) input+ return $ fakeHandle chunks >>= action++partialMarker :: ByteString+partialMarker = B.take 5 marker++spec :: Spec+spec = do+ describe "drain" $ do+ it "drains all remaining input" $ do+ h <- fakeHandle ["foo", marker, "bar", marker, "baz", marker, ""]+ withSpy (drain h) `shouldReturn` ["foo", "bar", "baz"]++ describe "getResult" $ do+ context "with a single result" $ do+ let input = ["foo", "bar", "baz", marker]++ it "returns result" $ do+ withSpy $ \ echo -> do+ h <- fakeHandle input+ getResult h echo `shouldReturn` "foobarbaz"+ `shouldReturn` ["foo", "bar", "baz"]++ context "with chunks of arbitrary size" $ do+ it "returns result" $ do+ withRandomChunkSizes input $ \ h -> do+ fmap mconcat . withSpy $ \ echo -> do+ getResult h echo `shouldReturn` "foobarbaz"+ `shouldReturn` "foobarbaz"++ context "with multiple results" $ do+ let input = ["foo", marker, "bar", marker, "baz", marker]++ it "returns one result at a time" $ do+ withSpy $ \ echo -> do+ h <- fakeHandle input+ getResult h echo `shouldReturn` "foo"+ getResult h echo `shouldReturn` "bar"+ getResult h echo `shouldReturn` "baz"+ `shouldReturn` ["foo", "bar", "baz"]++ context "with chunks of arbitrary size" $ do+ it "returns one result at a time" $ do+ withRandomChunkSizes input $ \ h -> do+ fmap mconcat . withSpy $ \ echo -> do+ getResult h echo `shouldReturn` "foo"+ getResult h echo `shouldReturn` "bar"+ getResult h echo `shouldReturn` "baz"+ `shouldReturn` "foobarbaz"++ context "when a chunk that contains a marker ends with a partial marker" $ do+ it "correctly gives the marker precedence over the partial marker" $ do+ withSpy $ \ echo -> do+ h <- fakeHandle ["foo" <> marker <> "bar" <> partialMarker, ""]+ getResult h echo `shouldReturn` "foo"+ getResult h echo `shouldReturn` ("bar" <> partialMarker)+ `shouldReturn` ["foo", "bar", partialMarker]++ context "on EOF" $ do+ it "returns all remaining input" $ do+ withSpy $ \ echo -> do+ h <- fakeHandle ["foo", "bar", "baz", ""]+ getResult h echo `shouldReturn` "foobarbaz"+ `shouldReturn` ["foo", "bar", "baz"]++ context "with a partialMarker at the end" $ do+ it "includes the partial marker in the output" $ do+ withSpy $ \ echo -> do+ h <- fakeHandle ["foo", "bar", "baz", partialMarker, ""]+ getResult h echo `shouldReturn` ("foobarbaz" <> partialMarker)+ `shouldReturn` ["foo", "bar", "baz", partialMarker]++ context "after a marker" $ do+ it "returns all remaining input" $ do+ withSpy $ \ echo -> do+ h <- fakeHandle ["foo", "bar", "baz", marker, "qux", ""]+ getResult h echo `shouldReturn` "foobarbaz"+ getResult h echo `shouldReturn` "qux"+ `shouldReturn` ["foo", "bar", "baz", "qux"]
test/SessionSpec.hs view
@@ -7,16 +7,27 @@ import Language.Haskell.GhciWrapper (eval) import qualified Session-import Session (Session(..), Summary(..), hspecFailureEnvName, hspecPreviousSummary, hspecCommand)+import Session (Config(..), Session(..), Summary(..), hspecFailureEnvName, hspecPreviousSummary, hspecCommand) spec :: Spec spec = do- describe "new" $ do+ describe "withSession" $ do it "unsets HSPEC_FAILURES" $ do setEnv hspecFailureEnvName "foo" True- withSession [] $ \Session{..} -> do- _ <- eval sessionInterpreter "import System.Environment"- eval sessionInterpreter ("lookupEnv " ++ show hspecFailureEnvName) `shouldReturn` "Nothing\n"+ withSession [] $ \ Session{..} -> do+ eval sessionInterpreter ("System.Environment.lookupEnv " ++ show hspecFailureEnvName) `shouldReturn` "Nothing\n"++ context "with `:set +t +s`" $ do+ it "works just fine" $ do+ withSomeSpec $ do+ writeFile ".ghci" ":set +t +s"+ Session.withSession ghciConfig {configIgnoreDotGhci = False} [] $ \ Session{..} -> do+ eval sessionInterpreter "23" `shouldReturn` "23\n"++ context "with -XOverloadedStrings" $ do+ it "works just fine" $ do+ withSession ["-XOverloadedStrings", "-Wall", "-Werror"] $ \ Session{..} -> do+ eval sessionInterpreter "23 :: Int" `shouldReturn` "23\n" describe "reload" $ do it "reloads" $ do
test/TriggerSpec.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} module TriggerSpec (spec) where import Helper@@ -148,7 +149,11 @@ writeFile "Spec.hs" passingSpec (True, xs) <- silence (trigger session) normalize xs `shouldBe` [+#if __GLASGOW_HASKELL__ < 904 "[1 of 1] Compiling Spec ( Spec.hs, interpreted )"+#else+ "[1 of 1] Compiling Spec ( Spec.hs, interpreted ) [Source file changed]"+#endif , modulesLoaded Ok ["Spec"] , "" , "bar [✔]"