ghcid 0.6.4 → 0.6.5
raw patch · 15 files changed
+465/−364 lines, 15 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGES.txt +8/−0
- README.md +4/−1
- ghcid.cabal +11/−11
- src/Ghcid.hs +54/−36
- src/Language/Haskell/Ghcid.hs +7/−7
- src/Language/Haskell/Ghcid/Parser.hs +8/−6
- src/Language/Haskell/Ghcid/Util.hs +30/−1
- src/Session.hs +5/−3
- src/Test.hs +9/−6
- src/Test/API.hs +36/−0
- src/Test/Ghcid.hs +208/−0
- src/Test/HighLevel.hs +0/−106
- src/Test/Parser.hs +72/−35
- src/Test/Polling.hs +0/−137
- src/Test/Util.hs +13/−15
CHANGES.txt view
@@ -1,5 +1,13 @@ Changelog for ghcid +0.6.5+ #82, properly deal with warning messages including spans+ #78, support boot files better+ Better support for dealing with GHC 8.0 call stack messages+ #79, support multiple --test flags+ #71, improve multi-project stack support+ #74, make sure Cygwin terminals flush output properly+ If the test exits then exit ghcid 0.6.4 #69, fix up for stack project with file arguments 0.6.3
README.md view
@@ -35,4 +35,7 @@ * _This isn't as good as full IDE._ I've gone for simplicity over features. It's a point in the design space, but not necessarily the best point in the design space for you. For "real" IDEs see [the Haskell wiki](http://www.haskell.org/haskellwiki/IDEs). * _If I delete a file and put it back it gets stuck._ Yes, that's a [bug in GHCi](https://ghc.haskell.org/trac/ghc/ticket/9648). If you see GHCi getting confused just kill `ghcid` and start it again.-* _I want to run arbitrary commands when arbitrary files change._ This project reloads `ghci` when files loaded by `ghci` change. If you want a more general mechanism something like [Steel Overseer](https://github.com/steeloverseer/steeloverseer) will probably work better.+* _I want to run my tests when files change._ You can pass any `ghci` expression with the `--test` flag, e.g. `--test=:main`, which will be run whenever the code is warning free (or pass `--warnings` for when the code is merely error free).+* _I want to run arbitrary commands when arbitrary files change._ This project reloads `ghci` when files loaded by `ghci` change. If you want a more general mechanism something like [Steel Overseer](https://github.com/schell/steeloverseer) will probably work better.+* _I want syntax highlighting in the error messages._ One option is to use Neovim or Emacs and run the terminal in a buffer whose file type is set to Haskell.+* _I'm not seeing pattern matching warnings._ Ghcid automatically appends `-fno-code` to the command line, which makes the reload cycle about twice as fast. Unfortunately GHC 8.0 suffers from [bug 10600](https://ghc.haskell.org/trac/ghc/ticket/10600) which means `-fno-code` also disables pattern matching warnings. Until that GHC bug is fixed either accept no pattern match warnings or use `-c` to specify a command line to start `ghci` that doesn't include `-fno-code`.
ghcid.cabal view
@@ -1,7 +1,7 @@ cabal-version: >= 1.10 build-type: Simple name: ghcid-version: 0.6.4+version: 0.6.5 license: BSD3 license-file: LICENSE category: Development@@ -70,12 +70,12 @@ else build-depends: unix other-modules:- Language.Haskell.Ghcid.Types,- Language.Haskell.Ghcid.Parser,- Language.Haskell.Ghcid.Terminal,- Language.Haskell.Ghcid.Util,- Language.Haskell.Ghcid,- Session,+ Language.Haskell.Ghcid.Types+ Language.Haskell.Ghcid.Parser+ Language.Haskell.Ghcid.Terminal+ Language.Haskell.Ghcid.Util+ Language.Haskell.Ghcid+ Session Wait test-suite ghcid_test@@ -103,7 +103,7 @@ else build-depends: unix other-modules:- Test.Parser,- Test.HighLevel,- Test.Util,- Test.Polling+ Test.Parser+ Test.API+ Test.Util+ Test.Ghcid
src/Ghcid.hs view
@@ -2,7 +2,7 @@ {-# OPTIONS_GHC -fno-cse #-} -- | The application entry point-module Ghcid(main, runGhcid) where+module Ghcid(main, mainWithTerminal) where import Control.Exception import System.IO.Error@@ -19,21 +19,22 @@ import System.Exit import System.FilePath import System.IO-import System.IO.Unsafe import Paths_ghcid-import Language.Haskell.Ghcid import Language.Haskell.Ghcid.Terminal import Language.Haskell.Ghcid.Util import Language.Haskell.Ghcid.Types import Wait +import Data.Functor+import Prelude + -- | Command line options data Options = Options {command :: String ,arguments :: [String]- ,test :: Maybe String+ ,test :: [String] ,warnings :: Bool ,nostatus :: Bool ,height :: Maybe Int@@ -51,7 +52,7 @@ options = cmdArgsMode $ Options {command = "" &= typ "COMMAND" &= help "Command to run (defaults to ghci or cabal repl)" ,arguments = [] &= args &= typ "MODULE"- ,test = Nothing &= name "T" &= typ "EXPR" &= help "Command to run after successful loading"+ ,test = [] &= name "T" &= typ "EXPR" &= help "Command to run after successful loading" ,warnings = False &= name "W" &= help "Allow tests to run even with warnings" ,nostatus = False &= name "S" &= explicit &= name "no-status" &= help "Suppress status messages" ,height = Nothing &= help "Number of lines to use (defaults to console height)"@@ -96,13 +97,14 @@ files <- getDirectoryContents "." -- use unsafePerformIO to get nicer pattern matching for logic (read-only operations)- let isStack dir = unsafePerformIO $ flip catchIOError (const $ return False) $+ let isStack dir = flip catchIOError (const $ return False) $ doesFileExist (dir </> "stack.yaml") &&^ doesDirectoryExist (dir </> ".stack-work")+ stack <- isStack "." ||^ isStack ".." -- stack file might be parent, see #62 let cabal = filter ((==) ".cabal" . takeExtension) files- let opts = ["-fno-code" | isNothing test]+ let opts = ["-fno-code" | null test] return $ case () of- _ | cabal /= [], isStack "." || isStack ".." -> -- stack file might be parent, see #62+ _ | stack -> let flags = if null arguments then "stack ghci --test" : ["--no-load" | ".ghci" `elem` files] ++@@ -121,45 +123,58 @@ | otherwise = x -main :: IO ()-main = withWindowIcon $ withSession $ \session -> do+-- | Like 'main', but run with a fake terminal for testing+mainWithTerminal :: IO (Int,Int) -> ([(Style,String)] -> IO ()) -> IO ()+mainWithTerminal termSize termOutput = withWindowIcon $ withSession $ \session -> do+ -- On certain Cygwin terminals stdout defaults to BlockBuffering+ hSetBuffering stdout LineBuffering+ hSetBuffering stderr NoBuffering opts <- cmdArgsRun options withCurrentDirectory (directory opts) $ do- opts@Options{..} <- autoOptions opts- when topmost terminalTopmost- height <- return $ case (width, height) of+ opts <- autoOptions opts+ opts <- return $ opts{restart = nubOrd $ restart opts, reload = nubOrd $ reload opts}+ when (topmost opts) terminalTopmost++ termSize <- return $ case (width opts, height opts) of (Just w, Just h) -> return (w,h)- _ -> do- term <- fmap (fmap $ Term.width &&& Term.height) Term.size- whenLoud $ do- outStrLn $ "%CONSOLE: width = " ++ maybe "?" (show . fst) term ++ ", height = " ++ maybe "?" (show . snd) term- let f user def sel = fromMaybe (maybe def sel term) user- -- if we write to the end of the window then it wraps automatically+ (w, h) -> do+ term <- termSize+ -- if we write to the final column of the window then it wraps automatically -- so putStrLn width 'x' uses up two lines- return (f width 80 (pred . fst), f height 8 snd)+ return (fromMaybe (pred $ fst term) w, fromMaybe (snd term) h)+ withWaiterNotify $ \waiter -> handle (\(UnexpectedExit cmd _) -> putStrLn $ "Command \"" ++ cmd ++ "\" exited unexpectedly") $- runGhcid session waiter (nubOrd restart) (nubOrd reload) command outputfile test warnings nostatus height (not notitle) $ \xs -> do- outWith $ forM_ (groupOn fst xs) $ \x@((s,_):_) -> do- when (s == Bold) $ setSGR [SetConsoleIntensity BoldIntensity]- putStr $ concatMap ((:) '\n' . snd) x- when (s == Bold) $ setSGR []- hFlush stdout -- must flush, since we don't finish with a newline+ runGhcid session waiter termSize termOutput opts ++main :: IO ()+main = mainWithTerminal termSize termOutput+ where+ termSize = maybe (80, 8) (Term.width &&& Term.height) <$> Term.size++ termOutput xs = do+ outWith $ forM_ (groupOn fst xs) $ \x@((s,_):_) -> do+ when (s == Bold) $ setSGR [SetConsoleIntensity BoldIntensity]+ putStr $ concatMap ((:) '\n' . snd) x+ when (s == Bold) $ setSGR []+ hFlush stdout -- must flush, since we don't finish with a newline++ data Style = Plain | Bold deriving Eq -runGhcid :: Session -> Waiter -> [FilePath] -> [FilePath] -> String -> [FilePath] -> Maybe String -> Bool -> Bool -> IO (Int,Int) -> Bool -> ([(Style,String)] -> IO ()) -> IO ()-runGhcid session waiter restart reload command outputfiles test warnings nostatus size titles output = do+runGhcid :: Session -> Waiter -> IO (Int,Int) -> ([(Style,String)] -> IO ()) -> Options -> IO ()+runGhcid session waiter termSize termOutput opts@Options{..} = do let outputFill :: Maybe (Int, [Load]) -> [String] -> IO () outputFill load msg = do- (width, height) <- size+ (width, height) <- termSize let n = height - length msg load <- return $ take (if isJust load then n else 0) $ prettyOutput (maybe 0 fst load) [ m{loadMessage = concatMap (chunksOfWord width (width `div` 5)) $ loadMessage m} | m@Message{} <- maybe [] snd load]- output $ load ++ map (Plain,) msg ++ replicate (height - (length load + length msg)) (Plain,"")+ termOutput $ load ++ map (Plain,) msg ++ replicate (height - (length load + length msg)) (Plain,"") restartTimes <- mapM getModTime restart curdir <- getCurrentDirectory@@ -173,20 +188,22 @@ let (countErrors, countWarnings) = both sum $ unzip [if loadSeverity == Error then (1,0) else (0,1) | m@Message{..} <- messages, loadMessage /= []]- test <- return $ if countErrors == 0 && (warnings || countWarnings == 0) then test else Nothing+ test <- return $+ if null test || countErrors /= 0 || (countWarnings /= 0 && not warnings) then Nothing+ else Just $ intercalate "\n" test - when titles $ setWindowIcon $+ unless notitle $ setWindowIcon $ if countErrors > 0 then IconError else if countWarnings > 0 then IconWarning else IconOK - let updateTitle extra = when titles $ setTitle $+ let updateTitle extra = unless notitle $ setTitle $ let f n msg = if n == 0 then "" else show n ++ " " ++ msg ++ ['s' | n > 1] in (if countErrors == 0 && countWarnings == 0 then allGoodMessage else f countErrors "error" ++ (if countErrors > 0 && countWarnings > 0 then ", " else "") ++ f countWarnings "warning") ++ " " ++ extra ++ [' ' | extra /= ""] ++ "- " ++ takeFileName curdir - updateTitle $ if isJust test then "(running test) " else ""+ updateTitle $ if isJust test then "(running test)" else "" outputFill (Just (loadedCount, messages)) ["Running test..." | isJust test]- forM_ outputfiles $ \file ->+ forM_ outputfile $ \file -> writeFile file $ unlines $ map snd $ prettyOutput loadedCount $ filter isMessage messages when (null loaded) $ do putStrLn $ "No files loaded, nothing to wait for. Fix the last error and restart."@@ -195,6 +212,7 @@ whenLoud $ outStrLn $ "%TESTING: " ++ t sessionExecAsync session t $ \stderr -> do whenLoud $ outStrLn "%TESTING: Completed"+ hFlush stdout -- may not have been a terminating newline from test output if "*** Exception: " `isPrefixOf` stderr then do updateTitle "(test failed)" setWindowIcon IconError@@ -208,7 +226,7 @@ nextWait <- waitFiles waiter fire nextWait =<< sessionReload session else do- runGhcid session waiter restart reload command outputfiles test warnings nostatus size titles output+ runGhcid session waiter termSize termOutput opts nextWait <- waitFiles waiter (messages, loaded) <- sessionStart session command
src/Language/Haskell/Ghcid.hs view
@@ -71,7 +71,8 @@ let syncReplay = do i <- readVar syncCount let msg = "#~GHCID-FINISH-" ++ show i ++ "~#"- writeInp $ "Prelude.putStrLn " ++ show msg ++ "\nPrelude.error " ++ show msg+ writeInp $ "INTERNAL_GHCID.putStrLn " ++ show msg ++ "\n" +++ "INTERNAL_GHCID.hPutStrLn INTERNAL_GHCID.stderr " ++ show msg return $ isInfixOf msg let syncFresh = do modifyVar_ syncCount $ return . succ@@ -150,9 +151,10 @@ -- the thing before me may have done its own Haskell compiling writeIORef stdout [] writeIORef stderr []- writeIORef sync =<< syncFresh+ writeInp "import qualified System.IO as INTERNAL_GHCID" writeInp $ ":set prompt " ++ ghcid_prefix writeInp ":set -fno-break-on-exception -fno-break-on-error" -- see #43+ writeIORef sync =<< syncFresh echo0 strm s return Nothing r <- parseLoad . reverse <$> ((++) <$> readIORef stderr <*> readIORef stdout)@@ -206,9 +208,8 @@ quit ghci = do interrupt ghci handle (\UnexpectedExit{} -> return ()) $ void $ exec ghci ":quit"- -- Add ignore because sometimes I get a message about an invalid handle,- -- and I've got no idea why - but exceptions here probably mean the thing already died.- -- Windows only, and Appveyor only.+ -- Be aware that waitForProcess has a race condition, see https://github.com/haskell/process/issues/46.+ -- Therefore just ignore the exception anyway, its probably already terminated. ignore $ void $ waitForProcess $ process ghci @@ -217,9 +218,8 @@ -- within 5 seconds it just terminates the process. stopGhci :: Ghci -> IO () stopGhci ghci = do- forkIO $ quit ghci forkIO $ do -- if nicely doesn't work, kill ghci as the process level sleep 5 terminateProcess $ process ghci- void $ waitForProcess $ process ghci+ quit ghci
src/Language/Haskell/Ghcid/Parser.hs view
@@ -35,12 +35,14 @@ f (x:xs) | not $ " " `isPrefixOf` x , Just (file,rest) <- breakFileColon x- , takeExtension file `elem` [".hs",".lhs"]- , (pos,rest2) <- span (\c -> c == ':' || isDigit c) rest- , [p1,p2] <- map read $ words $ map (\c -> if c == ':' then ' ' else c) pos+ , takeExtension file `elem` [".hs",".lhs",".hs-boot",".lhs-boot"]+ -- take position, including span if present+ , (pos,rest) <- span (\c -> c == ':' || c == '-' || isSpan c || isDigit c) rest+ -- separate line and column, ignoring span (we want the start point only)+ , [p1,p2] <- map read $ wordsBy (\c -> c == ':' || isSpan c) $ takeWhile (\c->c /= '-') pos , (msg,las) <- span (isPrefixOf " ") xs- , rest3 <- trimStart rest2- , sev <- if "warning:" `isPrefixOf` lower rest3 then Warning else Error+ , rest <- trimStart $ unwords $ rest : xs+ , sev <- if "warning:" `isPrefixOf` lower rest then Warning else Error = Message sev file (p1,p2) (x:msg) : f las f (x:xs) | Just file <- stripPrefix "<no location info>: can't find file: " x@@ -54,7 +56,7 @@ [Message Error m (0,0) [] | m <- nubOrd ms] ++ f rest f (_:xs) = f xs f [] = []-+ isSpan c = c== ',' || c == '(' || c == ')' -- A filename, followed by a colon - be careful to handle Windows drive letters, see #61 breakFileColon :: String -> Maybe (FilePath, String)
src/Language/Haskell/Ghcid/Util.hs view
@@ -5,17 +5,21 @@ chunksOfWord, outWith, outStrLn, outStr, allGoodMessage,- getModTime+ getModTime, getModTimeResolution ) where import Control.Concurrent.Extra+import System.Time.Extra import System.IO.Unsafe+import System.IO.Extra+import System.FilePath import Data.List.Extra import Data.Char import Data.Time.Clock import System.IO.Error import System.Directory import Control.Exception+import Control.Monad.Extra import Control.Applicative import Prelude @@ -59,3 +63,28 @@ (\e -> if isDoesNotExistError e then Just () else Nothing) (\_ -> return Nothing) (Just <$> getModificationTime file)++++-- | Get the smallest difference that can be reported by two modification times+getModTimeResolution :: IO Seconds+getModTimeResolution = return getModTimeResolutionCache++{-# NOINLINE getModTimeResolutionCache #-}+-- Cache the result so only computed once per run+getModTimeResolutionCache :: Seconds+getModTimeResolutionCache = unsafePerformIO $ withTempDir $ \dir -> do+ let file = dir </> "calibrate.txt"++ -- with 10 measurements can get a bit slow, see Shake issue tracker #451+ -- if it rounds to a second then 1st will be a fraction, but 2nd will be full second+ mtime <- fmap maximum $ forM [1..3] $ \i -> fmap fst $ duration $ do+ writeFile file $ show i+ t1 <- getModificationTime file+ flip loopM 0 $ \j -> do+ writeFile file $ show (i,j)+ t2 <- getModificationTime file+ return $ if t1 == t2 then Left $ j+1 else Right ()+ putStrLn $ "Longest file modification time lag was " ++ show (ceiling (mtime * 1000)) ++ "ms"+ -- add a little bit of safety, but if it's really quick, don't make it that much slower+ return $ mtime + min 0.1 mtime
src/Session.hs view
@@ -31,8 +31,9 @@ -- | Ensure an action runs off the main thread, so can't get hit with Ctrl-C exceptions.+-- Disabled because it plays havoc with tests and cleaning up quickly enough. ctrlC :: IO a -> IO a-ctrlC = join . onceFork+ctrlC = id -- join . onceFork -- | The function 'withSession' expects to be run on the main thread,@@ -44,7 +45,7 @@ command <- newIORef Nothing warnings <- newIORef [] running <- newVar False- ctrlC (f $ Session{..}) `finally` do+ ctrlC (f Session{..}) `finally` do modifyVar_ running $ const $ return False whenJustM (readIORef ghci) $ \v -> do writeIORef ghci Nothing@@ -118,7 +119,8 @@ Just ghci <- readIORef ghci stderr <- newIORef "" modifyVar_ running $ const $ return True- void $ forkIO $ do+ caller <- myThreadId+ void $ flip forkFinally (either (throwTo caller) (const $ return ())) $ do execStream ghci cmd $ \strm msg -> when (msg /= "*** Exception: ExitSuccess") $ do when (strm == Stderr) $ writeIORef stderr msg
src/Test.hs view
@@ -2,21 +2,24 @@ module Test(main) where import Test.Tasty-import Test.Parser-import Test.HighLevel-import Test.Util-import Test.Polling import System.IO+import System.Console.CmdArgs +import Test.Util+import Test.Parser+import Test.API+import Test.Ghcid+ main :: IO () main = do hSetBuffering stdout NoBuffering+ setVerbosity Loud defaultMain tests tests :: TestTree tests = testGroup "Tests" [utilsTests ,parserTests- ,pollingTest- ,highLevelTests+ ,apiTests+ ,ghcidTest ]
+ src/Test/API.hs view
@@ -0,0 +1,36 @@+-- | Test the high level library API+module Test.API(apiTests) where++import Test.Tasty+import Test.Tasty.HUnit+import System.FilePath+import System.IO.Extra+import System.Time.Extra+import Language.Haskell.Ghcid+import Language.Haskell.Ghcid.Util+++apiTests :: TestTree+apiTests = testGroup "API test"+ [testCase "No files" $ withTempDir $ \dir -> do+ (ghci,load) <- startGhci "ghci" (Just dir) $ const putStrLn+ load @?= []+ showModules ghci >>= (@?= [])+ exec ghci "import Data.List"+ exec ghci "nub \"test\"" >>= (@?= ["\"tes\""])+ stopGhci ghci++ ,testCase "Load file" $ withTempDir $ \dir -> do+ writeFile (dir </> "File.hs") "module A where\na = 123"+ (ghci, load) <- startGhci "ghci File.hs" (Just dir) $ const putStrLn+ load @?= [Loading "A" "File.hs"]+ exec ghci "a + 1" >>= (@?= ["124"])+ reload ghci >>= (@?= [])++ sleep =<< getModTimeResolution+ writeFile (dir </> "File.hs") "module A where\na = 456"+ exec ghci "a + 1" >>= (@?= ["124"])+ reload ghci >>= (@?= [Loading "A" "File.hs"])+ exec ghci "a + 1" >>= (@?= ["457"])+ stopGhci ghci+ ]
+ src/Test/Ghcid.hs view
@@ -0,0 +1,208 @@++-- | Test behavior of the executable, polling files for changes+module Test.Ghcid(ghcidTest) where++import Control.Concurrent.Extra+import Control.Exception.Extra+import Control.Monad.Extra+import Data.Char+import Data.List.Extra+import System.Directory.Extra+import System.IO.Extra+import System.Time.Extra+import Data.Version.Extra+import System.Environment+import System.Process.Extra+import System.FilePath+import System.Exit++import Test.Tasty+import Test.Tasty.HUnit++import Ghcid+import Language.Haskell.Ghcid.Util+import Data.Functor+import Prelude+++ghcidTest :: TestTree+ghcidTest = testGroup "Ghcid test"+ [basicTest+ ,dotGhciTest+ ,cabalTest+ ,stackTest+ ]+++freshDir :: IO a -> IO a+freshDir act = withTempDir $ \tdir -> withCurrentDirectory tdir act++copyDir :: FilePath -> IO a -> IO ()+copyDir dir act = do+ b <- doesDirectoryExist dir+ if not b then putStrLn $ "Couldn't run test because test source is missing, " ++ dir else void $ do+ withTempDir $ \tdir -> do+ xs <- withCurrentDirectory dir $ listFilesRecursive "."+ forM_ xs $ \x -> do+ createDirectoryIfMissing True $ takeDirectory $ tdir </> x+ copyFile (dir </> x) (tdir </> x)+ withCurrentDirectory tdir act+++whenStack :: IO a -> IO ()+whenStack act = do+ v <- findExecutable "stack"+ case v of+ Nothing -> putStrLn "Couldn't run test because stack is missing"+ Just _ -> void act+++withGhcid :: [String] -> (([String] -> IO ()) -> IO a) -> IO a+withGhcid args script = do+ chan <- newChan+ let require want = do+ t <- timeout 30 $ readChan chan+ case t of+ Nothing -> fail $ "Require failed to produce results in time, expected: " ++ show want+ Just got -> assertApproxInfix want got+ sleep =<< getModTimeResolution++ let output = writeChan chan . filter (/= "") . map snd+ done <- newBarrier+ res <- bracket+ (flip forkFinally (const $ signalBarrier done ()) $+ withArgs (["--notitle","--no-status"]++args) $+ mainWithTerminal (return (100, 50)) output)+ killThread $ \_ -> script require+ waitBarrier done+ return res+++-- | Since different versions of GHCi give different messages, we only try to find what+-- we require anywhere in the obtained messages, ignoring weird characters.+assertApproxInfix :: [String] -> [String] -> IO ()+assertApproxInfix want got = do+ -- Spacing and quotes tend to be different on different GHCi versions+ let simple = lower . filter (\x -> isLetter x || isDigit x || x == ':')+ got2 = simple $ unwords got+ all (`isInfixOf` got2) (map simple want) @?+ "Expected " ++ show want ++ ", got " ++ show got+++write :: FilePath -> String -> IO ()+write file x = do+ print ("writeFile",file,x)+ writeFile file x++append :: FilePath -> String -> IO ()+append file x = do+ print ("appendFile",file,x)+ appendFile file x++rename :: FilePath -> FilePath -> IO ()+rename from to = do+ print ("renameFile",from,to)+ renameFile from to++++---------------------------------------------------------------------+-- ACTUAL TEST SUITE++basicTest :: TestTree+basicTest = testCase "Ghcid basic" $ freshDir $ do+ write "Main.hs" "main = print 1"+ withGhcid ["-cghci -fwarn-unused-binds Main.hs"] $ \require -> do+ require [allGoodMessage]+ write "Main.hs" "x"+ require ["Main.hs:1:1"," Parse error:"]+ write "Util.hs" "module Util where"+ write "Main.hs" "import Util\nmain = print 1"+ require [allGoodMessage]+ write "Util.hs" "module Util where\nx"+ require ["Util.hs:2:1","Parse error:"]+ write "Util.hs" "module Util() where\nx = 1"+ require ["Util.hs:2:1","Warning:","Defined but not used: `x'"]++ -- check warnings persist properly+ write "Main.hs" "import Util\nx"+ require ["Main.hs:2:1","Parse error:"+ ,"Util.hs:2:1","Warning:","Defined but not used: `x'"]+ write "Main.hs" "import Util\nmain = print 2"+ require ["Util.hs:2:1","Warning:","Defined but not used: `x'"]+ write "Main.hs" "main = print 3"+ require [allGoodMessage]+ write "Main.hs" "import Util\nmain = print 4"+ require ["Util.hs:2:1","Warning:","Defined but not used: `x'"]+ write "Util.hs" "module Util where"+ require [allGoodMessage]++ -- check recursive modules work+ write "Util.hs" "module Util where\nimport Main"+ require ["imports form a cycle","Main.hs","Util.hs"]+ write "Util.hs" "module Util where"+ require [allGoodMessage]++ ghcVer <- readVersion <$> systemOutput_ "ghc --numeric-version"++ -- check renaming files works+ when (ghcVer < makeVersion [8]) $ do+ -- note that due to GHC bug #9648 and #11596 this doesn't work with newer GHC+ -- see https://ghc.haskell.org/trac/ghc/ticket/11596+ rename "Util.hs" "Util2.hs"+ require ["Main.hs:1:8:","Could not find module `Util'"]+ rename "Util2.hs" "Util.hs"+ require [allGoodMessage]++ -- after this point GHC bugs mean nothing really works too much+++dotGhciTest :: TestTree+dotGhciTest = testCase "Ghcid .ghci" $ copyDir "test/foo" $ do+ write "test.txt" ""+ ignore $ void $ system "chmod go-w .ghci"+ withGhcid ["--test=:test"] $ \require -> do+ require [allGoodMessage]+ sleep 1 -- time to write out the test+ readFile "test.txt" >>= (@?= "X") -- the test writes out X+ append "Test.hs" "\n"+ require [allGoodMessage]+ sleep 1 -- time to write out the test+ readFile "test.txt" >>= (@?= "XX")+ print =<< readFile ".ghci"+ write ".ghci" ":set -fwarn-unused-imports\n:load Root Paths.hs Test"+ require ["The import of Paths_foo is redundant"]+ sleep 1 -- time to write out the test+ readFile "test.txt" >>= (@?= "XX") -- but shouldn't run on warning+++cabalTest :: TestTree+cabalTest = testCase "Ghcid Cabal" $ copyDir "test/bar" $ do+ env <- getEnvironment+ let db = ["--package-db=" ++ x | x <- maybe [] splitSearchPath $ lookup "GHC_PACKAGE_PATH" env]+ (_, _, _, pid) <- createProcess $+ (proc "cabal" $ "configure":db){env = Just $ filter ((/=) "GHC_PACKAGE_PATH" . fst) env}+ ExitSuccess <- waitForProcess pid++ withGhcid [] $ \require -> do+ require [allGoodMessage]+ orig <- readFile' "src/Literate.lhs"+ append "src/Literate.lhs" "> x"+ require ["src/Literate.lhs:5:3","Parse error:"]+ write "src/Literate.lhs" orig+ require [allGoodMessage]++stackTest :: TestTree+stackTest = testCase "Ghcid Stack" $ copyDir "test/bar" $ whenStack $ do+ system_ "stack init"+ createDirectoryIfMissing True ".stack-work"++ withGhcid [] $ \require -> do+ require [allGoodMessage]+ append "src/Literate.lhs" "> x"+ require ["src/Literate.lhs:5:3","Parse error:"]++ withGhcid ["src/Boot.hs"] $ \require -> do+ require [allGoodMessage]+ writeFile "src/Boot.hs" "X"+ require ["src/Boot.hs:1:1","Parse error:"]
− src/Test/HighLevel.hs
@@ -1,106 +0,0 @@--- | Test the high level library API-module Test.HighLevel(highLevelTests) where--import System.Directory-import System.IO.Extra-import System.Exit-import System.FilePath-import System.Environment-import Control.Monad-import System.Process--import Test.Tasty-import Test.Tasty.HUnit--import Language.Haskell.Ghcid---highLevelTests :: TestTree-highLevelTests = testGroup "High Level tests"- [testStartRepl- ,testShowModules- ]--testStartRepl :: TestTree-testStartRepl = testCase "Start cabal repl" $- withTestProject $ \root -> do- (ghci,load) <- startGhci "cabal repl" (Just root) $ const putStrLn- stopGhci ghci- load @?= [ Loading "B.C" (normalise "src/B/C.hs")- , Loading "A" (normalise "src/A.hs")- ]--testShowModules :: TestTree-testShowModules = testCase "Show Modules" $- withTestProject $ \root -> do- (ghci,_) <- startGhci "cabal repl" (Just root) $ const putStrLn- mods <- showModules ghci- stopGhci ghci- mods @?= [("A",normalise "src/A.hs"),("B.C",normalise "src/B/C.hs")]---testFiles :: [(FilePath, [String])]-testFiles =- [(projectName <.> "cabal",- ["name: " ++ projectName- ,"version:0.1"- ,"cabal-version: >= 1.8"- ,"build-type: Simple"- ,""- ,"library"- ," hs-source-dirs: src"- ," exposed-modules: A"- ," other-modules: B.C"- ," build-depends: base"- ,""- ,"executable BWTest"- ," hs-source-dirs: src"- ," main-is: Main.hs"- ," other-modules: B.D"- ," build-depends: base"- ," ghc-options: -dynamic"- ,""- ,"test-suite BWTest-test"- ," type: exitcode-stdio-1.0"- ," hs-source-dirs: test"- ," main-is: Main.hs"- ," other-modules: TestA"- ," build-depends: base"])- ,("Setup.hs",- ["#!/usr/bin/env runhaskell"- ,"import Distribution.Simple"- ,"main :: IO ()"- ,"main = defaultMain"])- ,("src/A.hs",- ["module A where"- ,"fA = undefined"])- ,("src/B/C.hs",- ["module B.C where"- ,"fC = undefined"])- ,("src/B/D.hs",- ["module B.D where"- ,"fD = undefined"])- ,("src/Main.hs",- ["module Main where"- ,"main = undefined"])- ,("test/Main.hs",- ["module Main where"- ,"main = undefined"])- ,("test/TestA.hs",- ["module TestA where"- ,"fTA = undefined"])- ]--projectName = "BWTest"--withTestProject :: (FilePath -> IO a) -> IO a-withTestProject act = withTempDir $ \dir -> do- forM_ testFiles $ \(name,contents) -> do- createDirectoryIfMissing True $ takeDirectory $ dir </> name- writeFile (dir </> name) $ unlines contents- env <- getEnvironment- let db = ["--package-db=" ++ x | x <- maybe [] splitSearchPath $ lookup "GHC_PACKAGE_PATH" env]- (_, _, _, pid) <- createProcess $- (proc "cabal" $ "configure":db){env = Just $ filter ((/=) "GHC_PACKAGE_PATH" . fst) env, cwd = Just dir}- ExitSuccess <- waitForProcess pid- act dir
src/Test/Parser.hs view
@@ -1,7 +1,5 @@ -- | Test the message parser-module Test.Parser- ( parserTests- )where+module Test.Parser(parserTests) where import Test.Tasty import Test.Tasty.HUnit@@ -11,40 +9,79 @@ parserTests :: TestTree-parserTests=testGroup "Parser tests"- [ testShowModules- , testParseLoad- ]--testShowModules :: TestTree-testShowModules = testCase "Show Modules" $ parseShowModules [- "Main ( src/Main.hs, interpreted )",- "AI.Neural.WiscDigit ( src/AI/Neural/WiscDigit.hs, interpreted )"- ] @?= [("Main","src/Main.hs"),("AI.Neural.WiscDigit","src/AI/Neural/WiscDigit.hs")]+parserTests = testGroup "Parser tests"+ [testParseShowModules+ ,testParseLoad+ ,testParseLoadSpans+ ] +testParseShowModules :: TestTree+testParseShowModules = testCase "Show Modules" $ parseShowModules+ ["Main ( src/Main.hs, interpreted )"+ ,"AI.Neural.WiscDigit ( src/AI/Neural/WiscDigit.hs, interpreted )"+ ] @?=+ [("Main","src/Main.hs")+ ,("AI.Neural.WiscDigit","src/AI/Neural/WiscDigit.hs")+ ] testParseLoad :: TestTree-testParseLoad = testGroup "Load Parsing"- [ testCase "output1" $ parseLoad output1 @?=- [ Loading "GHCi" "GHCi.hs"- , Message {loadSeverity = Error, loadFile = "GHCi.hs", loadFilePos = (70,1), loadMessage = ["GHCi.hs:70:1: Parse error: naked expression at top level"]}- , Message {loadSeverity = Error, loadFile = "GHCi.hs", loadFilePos = (72,13), loadMessage = ["GHCi.hs:72:13:"," No instance for (Num ([String] -> [String]))"," arising from the literal `1'"," Possible fix:"," add an instance declaration for (Num ([String] -> [String]))"," In the expression: 1"," In an equation for `parseLoad': parseLoad = 1"]}- , Message {loadSeverity = Warning, loadFile = "GHCi.hs", loadFilePos = (81,1), loadMessage = ["GHCi.hs:81:1: Warning: Defined but not used: `foo'"]}- , Message {loadSeverity = Warning, loadFile = "C:\\GHCi.hs", loadFilePos = (82,1), loadMessage = ["C:\\GHCi.hs:82:1: warning: Defined but not used: \8216foo\8217"]}+testParseLoad = testCase "Load Parsing" $ parseLoad+ ["[1 of 2] Compiling GHCi ( GHCi.hs, interpreted )"+ ,"GHCi.hs:70:1: Parse error: naked expression at top level"+ ,"GHCi.hs:72:13:"+ ," No instance for (Num ([String] -> [String]))"+ ," arising from the literal `1'"+ ," Possible fix:"+ ," add an instance declaration for (Num ([String] -> [String]))"+ ," In the expression: 1"+ ," In an equation for `parseLoad': parseLoad = 1"+ ,"GHCi.hs:81:1: Warning: Defined but not used: `foo'"+ ,"C:\\GHCi.hs:82:1: warning: Defined but not used: \8216foo\8217" -- GHC 7.12 uses lowercase+ ,"src\\Haskell.hs:4:23:"+ ," Warning: {-# SOURCE #-} unnecessary in import of `Boot'"+ ,"src\\Boot.hs-boot:2:8:"+ ," File name does not match module name:"+ ," Saw: `BootX'"+ ," Expected: `Boot'"+ ] @?=+ [Loading "GHCi" "GHCi.hs"+ ,Message {loadSeverity = Error, loadFile = "GHCi.hs", loadFilePos = (70,1), loadMessage = ["GHCi.hs:70:1: Parse error: naked expression at top level"]}+ ,Message {loadSeverity = Error, loadFile = "GHCi.hs", loadFilePos = (72,13), loadMessage = ["GHCi.hs:72:13:"," No instance for (Num ([String] -> [String]))"," arising from the literal `1'"," Possible fix:"," add an instance declaration for (Num ([String] -> [String]))"," In the expression: 1"," In an equation for `parseLoad': parseLoad = 1"]}+ ,Message {loadSeverity = Warning, loadFile = "GHCi.hs", loadFilePos = (81,1), loadMessage = ["GHCi.hs:81:1: Warning: Defined but not used: `foo'"]}+ ,Message {loadSeverity = Warning, loadFile = "C:\\GHCi.hs", loadFilePos = (82,1), loadMessage = ["C:\\GHCi.hs:82:1: warning: Defined but not used: \8216foo\8217"]}+ ,Message {loadSeverity = Warning, loadFile = "src\\Haskell.hs", loadFilePos = (4,23), loadMessage = ["src\\Haskell.hs:4:23:"," Warning: {-# SOURCE #-} unnecessary in import of `Boot'"]}+ ,Message {loadSeverity = Error, loadFile = "src\\Boot.hs-boot", loadFilePos = (2,8), loadMessage = ["src\\Boot.hs-boot:2:8:"," File name does not match module name:"," Saw: `BootX'"," Expected: `Boot'"]} ]- ] -output1 :: [String]-output1=- [ "[1 of 2] Compiling GHCi ( GHCi.hs, interpreted )"- , "GHCi.hs:70:1: Parse error: naked expression at top level"- , "GHCi.hs:72:13:"- , " No instance for (Num ([String] -> [String]))"- , " arising from the literal `1'"- , " Possible fix:"- , " add an instance declaration for (Num ([String] -> [String]))"- , " In the expression: 1"- , " In an equation for `parseLoad': parseLoad = 1"- , "GHCi.hs:81:1: Warning: Defined but not used: `foo'"- , "C:\\GHCi.hs:82:1: warning: Defined but not used: \8216foo\8217" -- GHC 7.12 uses lowercase- ]+-- | Test when error messages include spans (-ferror-spans)+testParseLoadSpans :: TestTree+testParseLoadSpans = testCase "Load Parsing when -ferror-spans is enabled" $ parseLoad+ ["[1 of 2] Compiling GHCi ( GHCi.hs, interpreted )"+ ,"GHCi.hs:70:1-2: Parse error: naked expression at top level"+ ,"GHCi.hs:72:13-14:"+ ," No instance for (Num ([String] -> [String]))"+ ," arising from the literal `1'"+ ," Possible fix:"+ ," add an instance declaration for (Num ([String] -> [String]))"+ ," In the expression: 1"+ ," In an equation for `parseLoad': parseLoad = 1"+ ,"GHCi.hs:81:1-15: Warning: Defined but not used: `foo'"+ ,"C:\\GHCi.hs:82:1-17: warning: Defined but not used: \8216foo\8217" -- GHC 7.12 uses lowercase+ ,"src\\Haskell.hs:4:23-24:"+ ," Warning: {-# SOURCE #-} unnecessary in import of `Boot'"+ ,"src\\Boot.hs-boot:2:8-5:"+ ," File name does not match module name:"+ ," Saw: `BootX'"+ ," Expected: `Boot'"+ ,"/src/TrieSpec.hs:(192,7)-(193,76): Warning:"+ ," A do-notation statement discarded a result of type ‘[()]’"+ ] @?=+ [Loading "GHCi" "GHCi.hs"+ ,Message {loadSeverity = Error, loadFile = "GHCi.hs", loadFilePos = (70,1), loadMessage = ["GHCi.hs:70:1-2: Parse error: naked expression at top level"]}+ ,Message {loadSeverity = Error, loadFile = "GHCi.hs", loadFilePos = (72,13), loadMessage = ["GHCi.hs:72:13-14:"," No instance for (Num ([String] -> [String]))"," arising from the literal `1'"," Possible fix:"," add an instance declaration for (Num ([String] -> [String]))"," In the expression: 1"," In an equation for `parseLoad': parseLoad = 1"]}+ ,Message {loadSeverity = Warning, loadFile = "GHCi.hs", loadFilePos = (81,1), loadMessage = ["GHCi.hs:81:1-15: Warning: Defined but not used: `foo'"]}+ ,Message {loadSeverity = Warning, loadFile = "C:\\GHCi.hs", loadFilePos = (82,1), loadMessage = ["C:\\GHCi.hs:82:1-17: warning: Defined but not used: \8216foo\8217"]}+ ,Message {loadSeverity = Warning, loadFile = "src\\Haskell.hs", loadFilePos = (4,23), loadMessage = ["src\\Haskell.hs:4:23-24:"," Warning: {-# SOURCE #-} unnecessary in import of `Boot'"]}+ ,Message {loadSeverity = Error, loadFile = "src\\Boot.hs-boot", loadFilePos = (2,8), loadMessage = ["src\\Boot.hs-boot:2:8-5:"," File name does not match module name:"," Saw: `BootX'"," Expected: `Boot'"]}+ ,Message {loadSeverity = Warning, loadFile = "/src/TrieSpec.hs", loadFilePos = (192,7), loadMessage = ["/src/TrieSpec.hs:(192,7)-(193,76): Warning:"," A do-notation statement discarded a result of type ‘[()]’"]}+ ]
− src/Test/Polling.hs
@@ -1,137 +0,0 @@-{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}---- | Test behavior of the executable, polling files for changes-module Test.Polling(pollingTest) where--import Control.Concurrent-import Control.Exception.Extra-import Control.Monad-import Data.Char-import Data.List.Extra-import System.FilePath-import System.Directory.Extra-import System.Process-import System.Console.CmdArgs-import System.Time.Extra--import Test.Tasty-import Test.Tasty.HUnit--import Ghcid-import Session-import Wait-import Language.Haskell.Ghcid.Util--pollingTest :: TestTree-pollingTest = testCase "Scripted Test" $ do- setVerbosity Loud- tdir <- fmap (</> ".ghcid") getTemporaryDirectory- try_ $ removeDirectoryRecursive tdir- createDirectoryIfMissing True tdir- withCurrentDirectory tdir $ do- ref <- newEmptyMVar- let require predi = do- res <- takeMVarDelay ref 5- predi res- outStr "."- sleep 1- -- t <- myThreadId- writeFile "Main.hs" "main = print 1"- writeFile ".ghci" ":set -fwarn-unused-binds \n:load Main"- -- otherwise GHC warns about .ghci being accessible by others- try_ $ system "chmod og-w . .ghci"-- withSession $ \session -> withWaiterPoll $ \waiter -> bracket (- forkIO $ runGhcid session waiter [] [] "ghci" [] Nothing False False (return (100, 50)) False $ \msg ->- unless (isLoading $ map snd msg) $ putMVarNow ref $ map snd msg- ) killThread $ \_ -> do- require requireAllGood- testScript require- outStrLn "\nSuccess"--isLoading :: [String] -> Bool-isLoading ("Reloading...":_) = True-isLoading (x:_) | "Loading" `isPrefixOf` x && "..." `isSuffixOf` x = True-isLoading _ = False--putMVarNow :: MVar a -> a -> IO ()-putMVarNow ref x = do- b <- tryPutMVar ref x- unless b $ error "Had a message and a new one arrived"--takeMVarDelay :: MVar a -> Double -> IO a-takeMVarDelay _ i | i <= 0 = error "timed out"-takeMVarDelay x i = do- b <- tryTakeMVar x- case b of- Nothing -> sleep 0.1 >> takeMVarDelay x (i-0.1)- Just v -> return v----(===) :: (Eq a, Show a) => a -> a -> IO ()---(===) a b = unless (a == b) $ error $ "Mismatch\nLHS: " ++ show a ++ "\nRHS: " ++ show b---- | The all good message-requireAllGood :: [String] -> IO ()-requireAllGood got = map (take (length allGoodMessage)) (filter (not . null) got) @?= [allGoodMessage]----requireNonIndents :: [String] -> [String] -> IO ()---requireNonIndents want got = [x | x@(c:_) <- got, not $ isSpace c] @?= want------requirePrefix :: [String] -> [String] -> IO ()---requirePrefix want got = take (length want) got @?= want---- | Since different versions of GHCi give different messages, we only try to find what we require anywhere in the obtained messages-requireSimilar :: [String] -> [String] -> IO ()-requireSimilar want got = let- allGot = ignoreSpacesAndWeird $ concat got- in all (`isInfixOf` allGot) (map ignoreSpacesAndWeird want) @? (show (filter (not . null) got) ++ " does not contain " ++ show want)---- | Spacing and quotes tend to be different on different GHCi versions-ignoreSpacesAndWeird :: String -> String-ignoreSpacesAndWeird = lower . filter (\x -> isLetter x || isDigit x || x == ':')-------------------------------------------------------------------------- ACTUAL TEST SUITE--testScript :: (([String] -> IO ()) -> IO ()) -> IO ()-testScript require = do- writeFile <- return $ \name x -> do print ("writeFile",name,x); writeFile name x- renameFile <- return $ \from to -> do print ("renameFile",from,to); renameFile from to-- writeFile "Main.hs" "x"- require $ requireSimilar ["Main.hs:1:1"," Parse error: naked expression at top level"]- writeFile "Util.hs" "module Util where"- writeFile "Main.hs" "import Util\nmain = print 1"- require requireAllGood- writeFile "Util.hs" "module Util where\nx"- require $ requireSimilar ["Util.hs:2:1","Parse error: naked expression at top level"]- writeFile "Util.hs" "module Util() where\nx = 1"- require $ requireSimilar ["Util.hs:2:1","Warning: Defined but not used: `x'"]-- -- check warnings persist properly- writeFile "Main.hs" "import Util\nx"- require $ requireSimilar ["Main.hs:2:1","Parse error: naked expression at top level"- ,"Util.hs:2:1","Warning: Defined but not used: `x'"]- writeFile "Main.hs" "import Util\nmain = print 2"- require $ requireSimilar ["Util.hs:2:1","Warning: Defined but not used: `x'"]- writeFile "Main.hs" "main = print 3"- require requireAllGood- writeFile "Main.hs" "import Util\nmain = print 4"- require $ requireSimilar ["Util.hs:2:1","Warning: Defined but not used: `x'"]- writeFile "Util.hs" "module Util where"- require requireAllGood-- -- check recursive modules work- writeFile "Util.hs" "module Util where\nimport Main"- require $ requireSimilar ["imports form a cycle","Main.hs","Util.hs"]- writeFile "Util.hs" "module Util where"- require requireAllGood-- -- check renaming files works- -- note that due to GHC bug #9648 we can't save down a new file- renameFile "Util.hs" "Util2.hs"- require $ requireSimilar ["Main.hs:1:8:","Could not find module `Util'"]- renameFile "Util2.hs" "Util.hs"- require requireAllGood- -- after this point GHC bugs mean nothing really works too much-
src/Test/Util.hs view
@@ -1,7 +1,5 @@ -- | Test utility functions-module Test.Util- ( utilsTests- ) where+module Test.Util(utilsTests) where import Test.Tasty import Test.Tasty.HUnit@@ -9,21 +7,21 @@ import Language.Haskell.Ghcid.Util utilsTests :: TestTree-utilsTests=testGroup "Utility tests"- [ dropPrefixTests- , chunksOfWordTests- ]+utilsTests = testGroup "Utility tests"+ [dropPrefixTests+ ,chunksOfWordTests+ ] dropPrefixTests :: TestTree dropPrefixTests = testGroup "dropPrefix"- [ testCase "Prefix not found" $ dropPrefixRepeatedly "prefix" "string" @?= "string"- , testCase "Empty prefix" $ dropPrefixRepeatedly "" "string" @?= "string"- , testCase "Prefix found once" $ dropPrefixRepeatedly "str" "string" @?= "ing"- , testCase "Prefix found twice" $ dropPrefixRepeatedly "str" "strstring" @?= "ing"- ]+ [testCase "Prefix not found" $ dropPrefixRepeatedly "prefix" "string" @?= "string"+ ,testCase "Empty prefix" $ dropPrefixRepeatedly "" "string" @?= "string"+ ,testCase "Prefix found once" $ dropPrefixRepeatedly "str" "string" @?= "ing"+ ,testCase "Prefix found twice" $ dropPrefixRepeatedly "str" "strstring" @?= "ing"+ ] chunksOfWordTests :: TestTree chunksOfWordTests = testGroup "chunksOfWord"- [ testCase "Max 0" $ chunksOfWord 4 0 "ab cd efgh" @?= ["ab c","d ef","gh"]- , testCase "Max 2" $ chunksOfWord 4 2 "ab cd efgh" @?= ["ab ","cd ","efgh"]- ]+ [testCase "Max 0" $ chunksOfWord 4 0 "ab cd efgh" @?= ["ab c","d ef","gh"]+ ,testCase "Max 2" $ chunksOfWord 4 2 "ab cd efgh" @?= ["ab ","cd ","efgh"]+ ]