packages feed

ghcid-1.0.0: test/Test/Ghcid.hs

-- | 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 Data.Maybe
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 (TermSize(..), mainWithTerminal, shouldWatchLoadConfig)
import Language.Haskell.Ghcid.Escape
import Language.Haskell.Ghcid.Util
import Test.Common
import Test.Util
import Data.Functor
import Prelude


ghcidTest :: TestTree
ghcidTest = localOption (mkTimeout 30000000) $ testGroup "Ghcid test"
    [basicTest
    ,cdTest
    ,dotGhciTest
    ,loadConfigRestartFilterTest
    ,cabalTest
    ]


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 $
        withTempDir $ \tdir -> do
            xs <- withCurrentDirectory dir $ listFilesRecursive "."
            forM_ xs $ \x -> do
                createDirectoryIfMissing True $ takeDirectory $ tdir </> x
                copyFile (dir </> x) (tdir </> x)
            withCurrentDirectory tdir act


whenExecutable :: String -> IO a -> IO ()
whenExecutable exe act = do
    v <- findExecutable exe
    case v of
        Nothing -> putStrLn $ "Couldn't run test because " ++ exe ++ " is missing"
        Just _ -> void act


withGhcid :: [String] -> (([String] -> IO ()) -> IO a) -> IO a
withGhcid args script = do
    chan <- newChan
    verbose <- isJust <$> lookupEnv "GHCID_TEST_VERBOSE"
    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
            -- Ensure the next write gets a distinguishable modification time. Sleeping
            -- for exactly the measured resolution can still leave both writes in the
            -- same timestamp bucket when the sleep straddles its boundary imprecisely.
            resolution <- getModTimeResolution
            sleep $ max 0.05 $ resolution * 2

    let output msg = do
            let msg2 = filter (/= "") msg
            when verbose $
                putStr $ unlines $ map ("%PRINT: "++) msg2
            writeChan chan msg2
    done <- newBarrier
    res <- bracket
        (flip forkFinally (const $ signalBarrier done ()) $
            withArgs (["--quiet","--no-title","--no-status"]++args) $
                mainWithTerminal (pure $ TermSize 100 (Just 50) WrapHard) output)
        killThread $ \_ -> script require
    waitBarrier done
    pure 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 == ':') . unescape
        got2 = simple $ unwords got
    all ((`isInfixOf` got2) . simple) want @?
        "Expected " ++ show want ++ ", got " ++ show got

write :: FilePath -> String -> IO ()
write file x = do
    whenVerbose $ print ("writeFile",file,x)
    createDirectoryIfMissing True $ takeDirectory file
    writeFile file x

append :: FilePath -> String -> IO ()
append file x = do
    whenVerbose $ print ("appendFile",file,x)
    appendFile file x

rename :: FilePath -> FilePath -> IO ()
rename from to = do
    whenVerbose $ print ("renameFile",from,to)
    renameFile from to



---------------------------------------------------------------------
-- ACTUAL TEST SUITE

basicTest :: TestTree
basicTest = disable19650 $ 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:"]

        -- Github issue 275
        write "Main.hs" "{-# LINE 42 \"foo.bar\" #-}\nx"
        require ["foo.bar:42: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 recursive modules work
        write "Util.hs" "module Util where\nimport Main"
        require ["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


cdTest :: TestTree
cdTest = disable19650 $ testCase "Cd basic" $ freshDir $ do
    write "foo/Main.hs" "main = print 1"
    write "foo/Util.hs" "import Bob"
    write "foo/.ghci" ":load Main"
    ignore $ void $ system "chmod go-w foo foo/.ghci"
    ghcVer <- readVersion <$> systemOutput_ "ghc --numeric-version"
    -- GHC 8.0 and lower don't emit the LoadConfig messages
    withGhcid ("-ccd foo && ghci" : ["--restart=foo/.ghci" | ghcVer < makeVersion [8,2]]) $ \require -> do
        require [allGoodMessage]
        write "foo/Main.hs" "x"
        require ["Main.hs:1:1"," Parse error:"]
        write "foo/.ghci" ":load Util"
        require ["Util.hs:1:","`Bob'"]


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


loadConfigRestartFilterTest :: TestTree
loadConfigRestartFilterTest = testCase "Ignore generated cabal script setcwd.ghci" $ do
    shouldWatchLoadConfig "/Users/test/.cabal/script-builds/hash/setcwd.ghci" @?= False
    shouldWatchLoadConfig "/Users/test/project/.ghci" @?= True
    shouldWatchLoadConfig "foo/.ghci" @?= True


cabalTest :: TestTree
cabalTest = testCase "Ghcid Cabal" $ copyDir "test/bar" $ whenExecutable "cabal" $ 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]