ghcid 0.3.6 → 0.4
raw patch · 18 files changed
+592/−447 lines, 18 filesdep +containersdep +fsnotifydep ~terminal-sizePVP ok
version bump matches the API change (PVP)
Dependencies added: containers, fsnotify
Dependency ranges changed: terminal-size
API changes (from Hackage documentation)
Files
- CHANGES.txt +9/−0
- ghcid.cabal +18/−15
- src/Ghcid.hs +113/−83
- src/Language/Haskell/Ghcid.hs +12/−9
- src/Language/Haskell/Ghcid/Parser.hs +1/−1
- src/Language/Haskell/Ghcid/Types.hs +1/−1
- src/Language/Haskell/Ghcid/Util.hs +15/−1
- src/Test.hs +19/−0
- src/Test/HighLevel.hs +116/−0
- src/Test/Parser.hs +50/−0
- src/Test/Polling.hs +126/−0
- src/Test/Util.hs +29/−0
- src/Wait.hs +83/−0
- test/Language/Haskell/Ghcid/HighLevelTests.hs +0/−116
- test/Language/Haskell/Ghcid/ParserTest.hs +0/−48
- test/Language/Haskell/Ghcid/PollingTest.hs +0/−125
- test/Language/Haskell/Ghcid/UtilTest.hs +0/−29
- test/Test.hs +0/−19
CHANGES.txt view
@@ -1,5 +1,14 @@ Changelog for GHCiD +0.4+ #33, make Ctrl-C more robust+ #31, add an outputfile feature+ #32, make newer warnings first (save a file, view its warnings)+ #28, fix issues on VIM file saves+ #29, support running a quick test on each save+ Add a --directory flag to change directory first+ #26, use fs-notify to avoid excessive wakeups+ #25, detect console size just before using it 0.3.6 #24, don't error out if error/putStrLn are not imported 0.3.5
ghcid.cabal view
@@ -1,7 +1,7 @@ cabal-version: >= 1.10 build-type: Simple name: ghcid-version: 0.3.6+version: 0.4 license: BSD3 license-file: LICENSE category: Development@@ -13,7 +13,7 @@ Either \"GHCi as a daemon\" or \"GHC + a bit of an IDE\". A very simple Haskell development tool which shows you the errors in your project and updates them whenever you save. Run @ghcid --topmost --command=ghci@, where @--topmost@ makes the window on top of all others (Windows only) and @--command@ is the command to start GHCi on your project (defaults to @ghci@ if you have a @.ghci@ file, or else to @cabal repl@). homepage: https://github.com/ndmitchell/ghcid#readme bug-reports: https://github.com/ndmitchell/ghcid/issues-tested-with: GHC==7.10.1, GHC==7.8.3, GHC==7.6.3+tested-with: GHC==7.10.1, GHC==7.8.4, GHC==7.6.3 extra-source-files: CHANGES.txt README.md@@ -32,9 +32,8 @@ directory, extra >= 1.1, process >= 1.1,- cmdargs >= 0.10- if !os(windows)- build-depends: terminal-size >= 0.2.1+ cmdargs >= 0.10,+ terminal-size >= 0.3 other-modules: Paths_ghcid, Language.Haskell.Ghcid.Types,@@ -46,13 +45,15 @@ executable ghcid hs-source-dirs: src default-language: Haskell2010- ghc-options: -main-is Ghcid.main+ ghc-options: -main-is Ghcid.main -threaded main-is: Ghcid.hs build-depends: base == 4.*, filepath, time, directory,+ containers,+ fsnotify, extra >= 1.1, process >= 1.1, cmdargs >= 0.10,@@ -63,11 +64,12 @@ Language.Haskell.Ghcid.Parser, Language.Haskell.Ghcid.Terminal, Language.Haskell.Ghcid.Util,- Language.Haskell.Ghcid+ Language.Haskell.Ghcid,+ Wait test-suite ghcid_test type: exitcode-stdio-1.0- ghc-options: -rtsopts -main-is Test.main+ ghc-options: -rtsopts -main-is Test.main -threaded default-language: Haskell2010 build-depends: base >= 4,@@ -75,17 +77,18 @@ time, directory, process,+ containers,+ fsnotify, extra >= 1.1, ansi-terminal,+ terminal-size >= 0.3, cmdargs, tasty, tasty-hunit- if !os(windows)- build-depends: terminal-size >= 0.2.1- hs-source-dirs: test, src+ hs-source-dirs: src main-is: Test.hs other-modules: - Language.Haskell.Ghcid.ParserTest,- Language.Haskell.Ghcid.HighLevelTests,- Language.Haskell.Ghcid.UtilTest,- Language.Haskell.Ghcid.PollingTest+ Test.Parser,+ Test.HighLevel,+ Test.Util,+ Test.Polling
src/Ghcid.hs view
@@ -1,49 +1,57 @@-{-# LANGUAGE RecordWildCards, DeriveDataTypeable, CPP, ScopedTypeVariables, TupleSections #-}-{-# OPTIONS_GHC -O2 #-} -- only here to test ticket #11+{-# LANGUAGE RecordWildCards, DeriveDataTypeable, CPP, TupleSections #-}+{-# OPTIONS_GHC -O2 -fno-cse #-} -- only here to test ticket #11 -- | The application entry point module Ghcid(main, runGhcid) where -import Control.Exception+import Control.Applicative import Control.Monad.Extra+import Control.Concurrent.Extra+import Control.Exception import Data.List.Extra import Data.Maybe-import Data.Time.Clock import Data.Tuple.Extra import Data.Version import qualified System.Console.Terminal.Size as Term import System.Console.CmdArgs import System.Console.ANSI-import System.Directory+import System.Directory.Extra import System.Exit import System.FilePath import System.IO-import System.IO.Error-import System.Time.Extra 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 Prelude -- | Command line options data Options = Options {command :: String+ ,test :: Maybe String ,height :: Maybe Int ,width :: Maybe Int ,topmost :: Bool ,restart :: [FilePath]+ ,directory :: FilePath+ ,outputfile :: [FilePath] } deriving (Data,Typeable,Show) options :: Mode (CmdArgs Options) options = cmdArgsMode $ Options {command = "" &= typ "COMMAND" &= help "Command to run (defaults to ghci or cabal repl)"+ ,test = Nothing &= name "T" &= typ "EXPR" &= help "Command to run after successful loading" ,height = Nothing &= help "Number of lines to show (defaults to console height)" ,width = Nothing &= help "Number of columns to show (defaults to console width)" ,topmost = False &= name "t" &= help "Set window topmost (Windows only)"- ,restart = [] &= help "Restart the command if any of these files change (defaults to .ghci or .cabal)"+ ,restart = [] &= typFile &= help "Restart the command if any of these files change (defaults to .ghci or .cabal)"+ ,directory = "." &= typDir &= name "C" &= help "Set the current directory"+ ,outputfile = [] &= typFile &= name "o" &= help "File to write the full output to" } &= verbosity &= program "ghcid" &= summary ("Auto reloading GHCi daemon v" ++ showVersion version) @@ -59,98 +67,120 @@ else return o{command="cabal repl", restart=cabal} +ctrlC :: IO () -> IO ()+ctrlC act = do+ bar <- newBarrier+ forkFinally act $ signalBarrier bar+ either throwIO return =<< waitBarrier bar++ main :: IO ()-main = do- opts@Options{..} <- autoOptions =<< cmdArgsRun options- when topmost terminalTopmost- height <- return $ case (width, height) of- (Just w, Just h) -> return (w,h)- _ -> do- term <- fmap (fmap $ Term.width &&& Term.height) Term.size- let f user def sel = fromMaybe (maybe def sel term) user- -- if we write to the end 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)- runGhcid restart command height $ \xs -> do- outWith $ forM_ (groupOn fst xs) $ \x@((b,_):_) -> do- when b $ setSGR [SetConsoleIntensity BoldIntensity]- putStr $ concatMap ((:) '\n' . snd) x- when b $ setSGR []- hFlush stdout -- must flush, since we don't finish with a newline+main = ctrlC $ do+ opts <- cmdArgsRun options+ withCurrentDirectory (directory opts) $ do+ opts@Options{..} <- autoOptions opts+ when topmost terminalTopmost+ height <- return $ case (width, height) 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+ -- so putStrLn width 'x' uses up two lines+ return (f width 80 (pred . fst), f height 8 snd)+ withWaiterNotify $ \waiter ->+ runGhcid waiter restart command outputfile test height $ \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 :: [FilePath] -> String -> IO (Int,Int) -> ([(Bool,String)] -> IO ()) -> IO ()-runGhcid restart command size output = do- restartTimes <- mapM mtime restart- do (_,height) <- size; output $ map (False,) $ "Loading..." : replicate (height - 1) ""- (ghci,initLoad) <- startGhci command Nothing- curdir <- getCurrentDirectory- let fire load warnings = do- load <- return $ filter (not . whitelist) load+data Style = Plain | Bold deriving Eq+++runGhcid :: Waiter -> [FilePath] -> String -> [FilePath] -> Maybe String -> IO (Int,Int) -> ([(Style,String)] -> IO ()) -> IO ()+runGhcid waiter restart command outputfiles test size output = do+ let outputFill :: Maybe [Load] -> [String] -> IO ()+ outputFill load msg = do (width, height) <- size- start <- getCurrentTime- modsActive <- fmap (map snd) $ showModules ghci- let modsLoad = nubOrd $ map loadFile load+ let n = height - length msg+ load <- return $ take (if isJust load then n else 0) $ prettyOutput n+ [ m{loadMessage = concatMap (chunksOfWord width (width `div` 5)) $ loadMessage m}+ | m@Message{} <- fromMaybe [] load]+ output $ load ++ map (Plain,) msg ++ replicate (height - (length load + length msg)) (Plain,"")++ restartTimes <- mapM getModTime restart+ outputFill Nothing ["Loading..."]+ nextWait <- waitFiles waiter+ (ghci,messages) <- startGhci command Nothing+ curdir <- getCurrentDirectory++ -- fire, given a waiter, the messages, and the warnings from last time+ let fire nextWait messages warnings = do+ messages <- return $ filter (not . whitelist) messages++ loaded <- map snd <$> showModules ghci+ let reloaded = nubOrd $ map loadFile messages+ -- some may have reloaded, but caused an error, and thus not be in the loaded set whenLoud $ do- outStrLn $ "%ACTIVE: " ++ show modsActive- outStrLn $ "%LOAD: " ++ show load- let warn = [w | w <- warnings, loadFile w `elem` modsActive, loadFile w `notElem` modsLoad]- let outFill msg = output $ take height $ msg ++ map (False,) (replicate height "")- outFill $ prettyOutput height- [m{loadMessage = concatMap (chunksOfWord width (width `div` 5)) $ loadMessage m} | m@Message{} <- load ++ warn]- setTitle $- let (errs, warns) = both sum $ unzip [if loadSeverity m == Error then (1,0) else (0,1) | m@Message{} <- load ++ warn]- f n msg = if n == 0 then "" else show n ++ " " ++ msg ++ ['s' | n > 1]- in (if errs == 0 && warns == 0 then "All good" else f errs "error" ++- (if errs > 0 && warns > 0 then ", " else "") ++ f warns "warning") ++- " - " ++ takeFileName curdir- let wait = nubOrd $ modsLoad ++ modsActive+ outStrLn $ "%MESSAGES: " ++ show messages+ outStrLn $ "%LOADED: " ++ show loaded++ -- only keep old warnings from files that are still loaded, but did not reload+ let validWarn w = loadFile w `elem` loaded && loadFile w `notElem` reloaded+ -- newest warnings always go first, so the file you hit save on most recently has warnings first+ messages <- return $ messages ++ filter validWarn warnings+ let (countErrors, countWarnings) = both sum $ unzip [if loadSeverity m == Error then (1,0) else (0,1) | m@Message{} <- messages]+ test <- return $ if countErrors == 0 then test else Nothing++ let updateTitle extra = 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 ++ "- " ++ takeFileName curdir++ updateTitle $ if isJust test then "(running test) " else ""+ outputFill (Just messages) ["Running test..." | isJust test]+ forM_ outputfiles $ \file ->+ writeFile file $ unlines $ map snd $ prettyOutput 1000000 $ filter isMessage messages+ whenJust test $ \test -> do+ res <- exec ghci test+ outputFill (Just messages) $ fromMaybe res $ stripSuffix ["*** Exception: ExitSuccess"] res+ updateTitle ""++ let wait = nubOrd $ loaded ++ reloaded when (null wait) $ do putStrLn $ "No files loaded, probably did not start GHCi.\nCommand: " ++ command exitFailure- reason <- awaitFiles start $ restart ++ wait- outFill $ map (False,) $ "Reloading..." : map (" " ++) reason- restartTimes2 <- mapM mtime restart+ reason <- nextWait $ restart ++ wait+ outputFill Nothing $ "Reloading..." : map (" " ++) reason+ restartTimes2 <- mapM getModTime restart if restartTimes == restartTimes2 then do- load2 <- reload ghci- fire load2 [m | m@Message{..} <- warn ++ load, loadSeverity == Warning]- else do+ nextWait <- waitFiles waiter+ let warnings = [m | m@Message{..} <- messages, loadSeverity == Warning]+ messages <- reload ghci+ fire nextWait messages warnings+ else do stopGhci ghci- runGhcid restart command size output- fire initLoad []+ runGhcid waiter restart command outputfiles test size output + fire nextWait messages [] ++-- | Ignore messages that GHC shouldn't really generate. whitelist :: Load -> Bool whitelist Message{loadSeverity=Warning, loadMessage=[_,x]} = x `elem` [" -O conflicts with --interactive; -O ignored."] whitelist _ = False -prettyOutput :: Int -> [Load] -> [(Bool,String)]-prettyOutput _ [] = [(False,allGoodMessage)]+-- | Given an available height, and a set of messages to display, show them as best you can.+prettyOutput :: Int -> [Load] -> [(Style,String)]+prettyOutput height [] = [(Plain,allGoodMessage)] prettyOutput height xs = take (max 3 $ height - (length msgs * 2)) msg1 ++ concatMap (take 2) msgs where (err, warn) = partition ((==) Error . loadSeverity) xs- msg1:msgs = map (map (True,) . loadMessage) err ++ map (map (False,) . loadMessage) warn----- | return a message about why you are continuing (usually a file name)-awaitFiles :: UTCTime -> [FilePath] -> IO [String]-awaitFiles base files = handle (\(e :: IOError) -> do sleep 0.1; return [show e]) $ do- whenLoud $ outStrLn $ "%WAITING: " ++ unwords files- new <- mapM mtime files- case [x | (x,Just t) <- zip files new, t > base] of- [] -> recheck files new- xs -> return xs- where- recheck files' old = do- sleep 0.1- new <- mapM mtime files'- case [x | (x,t1,t2) <- zip3 files' old new, t1 /= t2] of- [] -> recheck files' new- xs -> return xs--mtime :: FilePath -> IO (Maybe UTCTime)-mtime file = handleJust- (\e -> if isDoesNotExistError e then Just () else Nothing)- (\_ -> return Nothing)- (fmap Just $ getModificationTime file)+ msg1:msgs = map (map (Bold,) . loadMessage) err ++ map (map (Plain,) . loadMessage) warn
src/Language/Haskell/Ghcid.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE ScopedTypeVariables #-}+ -- | The entry point of the library module Language.Haskell.Ghcid ( T.Ghci@@ -14,18 +14,21 @@ where import System.IO+import System.IO.Error import System.Process import Control.Concurrent-import Control.Exception+import Control.Exception.Extra import Control.Monad import Data.Function import Data.List+import Control.Applicative import System.Console.CmdArgs.Verbosity import Language.Haskell.Ghcid.Parser import Language.Haskell.Ghcid.Types as T import Language.Haskell.Ghcid.Util+import Prelude -- | Start GHCi, returning a function to perform further operation, as well as the result of the initial loading startGhci :: String -> Maybe FilePath -> IO (Ghci, [Load])@@ -46,9 +49,9 @@ result <- newEmptyMVar -- the end result buffer <- newMVar [] -- the things to go in result forkIO $ fix $ \rec -> do- el <- try $ hGetLine h+ el <- tryBool isEOFError $ hGetLine h case el of- Left (_ :: SomeException) -> putMVar result Nothing+ Left _ -> putMVar result Nothing Right l -> do whenLoud $ outStrLn $ "%" ++ name ++ ": " ++ l if finish `isInfixOf` l@@ -69,23 +72,23 @@ outC <- takeMVar outs errC <- takeMVar errs case liftM2 (++) outC errC of- Nothing -> throwIO $ UnexpectedExit cmd s + Nothing -> throwIO $ UnexpectedExit cmd s Just msg -> return msg- r <- fmap parseLoad $ f ""+ r <- parseLoad <$> f "" return (Ghci f,r) -- | Show modules showModules :: Ghci -> IO [(String,FilePath)]-showModules ghci = fmap parseShowModules $ exec ghci ":show modules"+showModules ghci = parseShowModules <$> exec ghci ":show modules" -- | reload modules reload :: Ghci -> IO [Load]-reload ghci = fmap parseLoad $ exec ghci ":reload"+reload ghci = parseLoad <$> exec ghci ":reload" -- | Stop GHCi stopGhci :: Ghci -> IO ()-stopGhci ghci = handle (\UnexpectedExit {} -> return ()) $ void $ exec ghci ":quit"+stopGhci ghci = handle (\UnexpectedExit{} -> return ()) $ void $ exec ghci ":quit" -- | Send a command, get lines of result exec :: Ghci -> String -> IO [String]
src/Language/Haskell/Ghcid/Parser.hs view
@@ -38,7 +38,7 @@ , [p1,p2] <- map read $ words $ map (\c -> if c == ':' then ' ' else c) pos , (msg,las) <- span (isPrefixOf " ") xs , rest3 <- trimStart rest2- , sev <- if "Warning:" `isPrefixOf` rest3 then Warning else Error+ , sev <- if "warning:" `isPrefixOf` lower rest3 then Warning else Error = Message sev file (p1,p2) (x:msg) : parseLoad las parseLoad' (x:xs) | Just file <- stripPrefix "<no location info>: can't find file: " x
src/Language/Haskell/Ghcid/Types.hs view
@@ -9,7 +9,7 @@ -- | A GHCi session. Created with 'startGhci'. newtype Ghci = Ghci (String -> IO [String]) --- | GHCi shut down+-- | GHCi shut down data GhciError = UnexpectedExit String String deriving (Show,Eq,Ord,Typeable)
src/Language/Haskell/Ghcid/Util.hs view
@@ -7,12 +7,19 @@ , outStrLn , outStr , allGoodMessage+ , getModTime ) where import Control.Concurrent.Extra import System.IO.Unsafe import Data.List.Extra import Data.Char+import Data.Time.Clock+import System.IO.Error+import System.Directory+import Control.Exception+import Control.Applicative+import Prelude -- | Drop a prefix from a list, no matter how many times that prefix is present@@ -36,7 +43,7 @@ -- | The message to show when no errors have been reported-allGoodMessage :: String +allGoodMessage :: String allGoodMessage = "All good" -- | Like chunksOf, but deal with words up to some gap.@@ -47,3 +54,10 @@ if null b then (a, []) else let (a1,a2) = breakEnd isSpace a in if length a2 <= gap then (a1, a2 ++ b) else (a, dropWhile isSpace b)++-- | Given a 'FilePath' return either 'Nothing' (file does not exist) or 'Just' (the modification time)+getModTime :: FilePath -> IO (Maybe UTCTime)+getModTime file = handleJust+ (\e -> if isDoesNotExistError e then Just () else Nothing)+ (\_ -> return Nothing)+ (Just <$> getModificationTime file)
+ src/Test.hs view
@@ -0,0 +1,19 @@++module Test(main) where++import Test.Tasty+import Test.Parser (parserTests)+import Test.HighLevel (highLevelTests)+import Test.Util (utilsTests)+import Test.Polling (pollingTest)++main :: IO()+main = defaultMain tests++tests :: TestTree+tests = testGroup "Tests"+ [ utilsTests+ , parserTests+ , highLevelTests+ , pollingTest+ ]
+ src/Test/HighLevel.hs view
@@ -0,0 +1,116 @@+-- | Test the high level library API+module Test.HighLevel + (+ highLevelTests+ ) where++import System.Directory+import System.FilePath+import Control.Monad (when)++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" $+ do+ root <- createTestProject+ (ghci,load) <- startGhci "cabal repl" (Just root)+ stopGhci ghci+ load @?= [ Loading "B.C" (normalise "src/B/C.hs")+ , Loading "A" (normalise "src/A.hs")+ ]++testShowModules :: TestTree+testShowModules = testCase "Show Modules" $+ do+ root <- createTestProject+ (ghci,_) <- startGhci "cabal repl" (Just root)+ mods <- showModules ghci+ stopGhci ghci+ mods @?= [("A",normalise "src/A.hs"),("B.C",normalise "src/B/C.hs")] ++testProjectName :: String+testProjectName="BWTest" ++testCabalContents :: String+testCabalContents = unlines ["name: "++testProjectName,+ "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",+ ""+ ] + +testCabalFile :: FilePath -> FilePath+testCabalFile root =root </> (last (splitDirectories root) <.> ".cabal") + +testAContents :: String +testAContents=unlines ["module A where","fA=undefined"]+testCContents :: String+testCContents=unlines ["module B.C where","fC=undefined"] +testDContents :: String+testDContents=unlines ["module B.D where","fD=undefined"] +testMainContents :: String+testMainContents=unlines ["module Main where","main=undefined"] +testMainTestContents :: String+testMainTestContents=unlines ["module Main where","main=undefined"] +testTestAContents :: String+testTestAContents=unlines ["module TestA where","fTA=undefined"] + +testSetupContents ::String+testSetupContents = unlines ["#!/usr/bin/env runhaskell",+ "import Distribution.Simple",+ "main :: IO ()",+ "main = defaultMain"] + +createTestProject :: IO FilePath+createTestProject = do+ temp<-getTemporaryDirectory+ let root=temp </> testProjectName+ ex<-doesDirectoryExist root+ when ex (removeDirectoryRecursive root)+ createDirectory root+ writeFile (testCabalFile root) testCabalContents+ writeFile (root </> "Setup.hs") testSetupContents+ let srcF=root </> "src"+ createDirectory srcF+ writeFile (srcF </> "A.hs") testAContents+ let b=srcF </> "B"+ createDirectory b+ writeFile (b </> "C.hs") testCContents+ writeFile (b </> "D.hs") testDContents+ writeFile (srcF </> "Main.hs") testMainContents+ let testF=root </> "test"+ createDirectory testF+ writeFile (testF </> "Main.hs") testMainTestContents+ writeFile (testF </> "TestA.hs") testTestAContents+ return root
+ src/Test/Parser.hs view
@@ -0,0 +1,50 @@+-- | Test the message parser+module Test.Parser + ( parserTests+ )where++import Test.Tasty+import Test.Tasty.HUnit++import Language.Haskell.Ghcid.Parser+import Language.Haskell.Ghcid.Types+++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")]+ + +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 = "GHCi.hs", loadFilePos = (82,1), loadMessage = ["GHCi.hs:82:1: warning: Defined but not used: \8216foo\8217"]}+ ]+ ]++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'"+ , "GHCi.hs:82:1: warning: Defined but not used: \8216foo\8217" -- GHC 7.12 uses lowercase+ ]
+ src/Test/Polling.hs view
@@ -0,0 +1,126 @@+{-# 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 Data.Maybe+import System.FilePath+import System.Directory.Extra+import System.IO+import System.Process+import System.Console.CmdArgs+import System.Time.Extra++import Test.Tasty+import Test.Tasty.HUnit++import Ghcid+import Wait+import Language.Haskell.Ghcid.Util++pollingTest :: TestTree+pollingTest = testCase "Scripted Test" $ do+ setVerbosity Loud+ hSetBuffering stdout NoBuffering+ 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"++ withWaiterPoll $ \waiter -> bracket (+ forkIO $ runGhcid waiter [] "ghci" [] Nothing (return (100, 50)) $ \msg ->+ unless (isLoading $ map snd msg) $ putMVarNow ref $ map snd msg+ ) killThread $ \_ -> do+ require requireAllGood+ testScript require+ outStrLn "\nSuccess"++isLoading :: [String] -> Bool+isLoading xs = listToMaybe xs `elem` [Just "Reloading...",Just "Loading..."]++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 = 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 "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 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
+ src/Test/Util.hs view
@@ -0,0 +1,29 @@+-- | Test utility functions+module Test.Util+ ( utilsTests+ ) where++import Test.Tasty+import Test.Tasty.HUnit++import Language.Haskell.Ghcid.Util++utilsTests :: TestTree+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"+ ]++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"]+ ]
+ src/Wait.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE ScopedTypeVariables #-}++module Wait(Waiter, withWaiterPoll, withWaiterNotify, waitFiles) where++import Control.Concurrent.Extra+import qualified Data.Map as Map+import qualified Data.Set as Set+import Control.Monad+import Data.List.Extra+import System.FilePath+import Control.Exception.Extra+import System.Directory+import Data.Time.Clock+import Data.String+import Data.Maybe+import System.Console.CmdArgs+import System.Time.Extra+import System.FSNotify+import Language.Haskell.Ghcid.Util+++data Waiter = WaiterPoll+ | WaiterNotify WatchManager (MVar ()) (Var (Map.Map FilePath StopListening))++withWaiterPoll :: (Waiter -> IO a) -> IO a+withWaiterPoll f = f WaiterPoll++withWaiterNotify :: (Waiter -> IO a) -> IO a+withWaiterNotify f = withManagerConf defaultConfig{confDebounce=NoDebounce} $ \manager -> do+ mvar <- newEmptyMVar+ var <- newVar Map.empty+ f $ WaiterNotify manager mvar var+++-- | Return a message about why you are continuing (usually a file name).+-- Reports any changes between the first+waitFiles :: Waiter -> IO ([FilePath] -> IO [String])+waitFiles waiter = do+ base <- getCurrentTime+ return $ \files -> handle (\(e :: IOError) -> do sleep 0.1; return [show e]) $ do+ whenLoud $ outStrLn $ "%WAITING: " ++ unwords files+ case waiter of+ WaiterPoll -> return ()+ WaiterNotify manager kick mp -> do+ dirs <- fmap Set.fromList $ mapM canonicalizePathSafe $ nubOrd $ map takeDirectory files+ modifyVar_ mp $ \mp -> do+ let (keep,del) = Map.partitionWithKey (\k v -> k `Set.member` dirs) mp+ sequence_ $ Map.elems del+ new <- forM (Set.toList $ dirs `Set.difference` Map.keysSet keep) $ \dir -> do+ can <- watchDir manager (fromString dir) (const True) $ \event -> do+ whenLoud $ outStrLn $ "%NOTIFY: " ++ show event+ void $ tryPutMVar kick ()+ return (dir, can)+ let mp2 = keep `Map.union` Map.fromList new+ whenLoud $ outStrLn $ "%WAITING: " ++ unwords (Map.keys mp2)+ return mp2+ void $ tryTakeMVar kick+ new <- mapM getModTime files+ case [x | (x,Just t) <- zip files new, t > base] of+ [] -> recheck files new+ xs -> return xs+ where+ recheck files old = do+ sleep 0.1+ case waiter of+ WaiterPoll -> return ()+ WaiterNotify _ kick _ -> do+ takeMVar kick+ whenLoud $ outStrLn "%WAITING: Notify signaled"+ new <- mapM getModTime files+ case [x | (x,t1,t2) <- zip3 files old new, t1 /= t2] of+ [] -> recheck files new+ xs -> do+ when (or $ zipWith (\o n -> isJust o && isNothing n) old new) $ do+ -- if someone is deleting a needed file, give them some space to put the file back+ -- typically caused by VIM+ whenLoud $ outStrLn "%WAITING: Extra wait due to file removal"+ sleep 1+ return xs+++canonicalizePathSafe :: FilePath -> IO FilePath+canonicalizePathSafe x = canonicalizePath x `catch_` const (return x)
− test/Language/Haskell/Ghcid/HighLevelTests.hs
@@ -1,116 +0,0 @@--- | Test the high level library API-module Language.Haskell.Ghcid.HighLevelTests - (- highLevelTests- ) where--import System.Directory-import System.FilePath-import Control.Monad (when)--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" $- do- root <- createTestProject- (ghci,load) <- startGhci "cabal repl" (Just root)- stopGhci ghci- load @?= [ Loading "B.C" (normalise "src/B/C.hs")- , Loading "A" (normalise "src/A.hs")- ]--testShowModules :: TestTree-testShowModules = testCase "Show Modules" $- do- root <- createTestProject- (ghci,_) <- startGhci "cabal repl" (Just root)- mods <- showModules ghci- stopGhci ghci- mods @?= [("A",normalise "src/A.hs"),("B.C",normalise "src/B/C.hs")] --testProjectName :: String-testProjectName="BWTest" --testCabalContents :: String-testCabalContents = unlines ["name: "++testProjectName,- "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",- ""- ] - -testCabalFile :: FilePath -> FilePath-testCabalFile root =root </> (last (splitDirectories root) <.> ".cabal") - -testAContents :: String -testAContents=unlines ["module A where","fA=undefined"]-testCContents :: String-testCContents=unlines ["module B.C where","fC=undefined"] -testDContents :: String-testDContents=unlines ["module B.D where","fD=undefined"] -testMainContents :: String-testMainContents=unlines ["module Main where","main=undefined"] -testMainTestContents :: String-testMainTestContents=unlines ["module Main where","main=undefined"] -testTestAContents :: String-testTestAContents=unlines ["module TestA where","fTA=undefined"] - -testSetupContents ::String-testSetupContents = unlines ["#!/usr/bin/env runhaskell",- "import Distribution.Simple",- "main :: IO ()",- "main = defaultMain"] - -createTestProject :: IO FilePath-createTestProject = do- temp<-getTemporaryDirectory- let root=temp </> testProjectName- ex<-doesDirectoryExist root- when ex (removeDirectoryRecursive root)- createDirectory root- writeFile (testCabalFile root) testCabalContents- writeFile (root </> "Setup.hs") testSetupContents- let srcF=root </> "src"- createDirectory srcF- writeFile (srcF </> "A.hs") testAContents- let b=srcF </> "B"- createDirectory b- writeFile (b </> "C.hs") testCContents- writeFile (b </> "D.hs") testDContents- writeFile (srcF </> "Main.hs") testMainContents- let testF=root </> "test"- createDirectory testF- writeFile (testF </> "Main.hs") testMainTestContents- writeFile (testF </> "TestA.hs") testTestAContents- return root
− test/Language/Haskell/Ghcid/ParserTest.hs
@@ -1,48 +0,0 @@--- | Test the message parser-module Language.Haskell.Ghcid.ParserTest - ( parserTests- )where--import Test.Tasty-import Test.Tasty.HUnit--import Language.Haskell.Ghcid.Parser-import Language.Haskell.Ghcid.Types---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")]- - -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'"]}- ]- ]--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'"- ]
− test/Language/Haskell/Ghcid/PollingTest.hs
@@ -1,125 +0,0 @@-{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}---- | Test behavior of the executable, polling files for changes-module Language.Haskell.Ghcid.PollingTest (pollingTest) where--import Control.Concurrent-import Control.Exception.Extra-import Control.Monad-import Data.Char-import Data.List-import Data.Maybe-import System.FilePath-import System.Directory.Extra-import System.IO-import System.Process-import System.Console.CmdArgs-import System.Time.Extra--import Test.Tasty-import Test.Tasty.HUnit--import Ghcid-import Language.Haskell.Ghcid.Util--pollingTest :: TestTree-pollingTest = testCase "Scripted Test" $ do- setVerbosity Loud- hSetBuffering stdout NoBuffering- 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"-- bracket (- forkIO $ runGhcid [] "ghci" (return (100, 50)) $ \msg ->- unless (isLoading $ map snd msg) $ putMVarNow ref $ map snd msg- ) killThread $ \_ -> do - require requireAllGood- testScript require- outStrLn "\nSuccess"- -isLoading :: [String] -> Bool-isLoading xs = listToMaybe xs `elem` [Just "Reloading...",Just "Loading..."]--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 = 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 = filter (\x->isLetter x || isDigit x || x==':')-------------------------------------------------------------------------- ACTUAL TEST SUITE--testScript :: (([String] -> IO ()) -> IO ()) -> IO ()-testScript require = do- 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 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
− test/Language/Haskell/Ghcid/UtilTest.hs
@@ -1,29 +0,0 @@--- | Test utility functions-module Language.Haskell.Ghcid.UtilTest - ( utilsTests- ) where--import Test.Tasty-import Test.Tasty.HUnit--import Language.Haskell.Ghcid.Util--utilsTests :: TestTree-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"- ]--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"]- ]
− test/Test.hs
@@ -1,19 +0,0 @@--module Test(main) where--import Test.Tasty-import Language.Haskell.Ghcid.ParserTest (parserTests)-import Language.Haskell.Ghcid.HighLevelTests (highLevelTests)-import Language.Haskell.Ghcid.UtilTest (utilsTests)-import Language.Haskell.Ghcid.PollingTest (pollingTest)--main :: IO()-main = defaultMain tests--tests :: TestTree-tests = testGroup "Tests" - [ utilsTests- , parserTests- , highLevelTests- , pollingTest- ]