intero-0.1.40: src/test/Main.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Test that various commands work properly.
module Main where
import Control.Exception
import Control.Monad.IO.Class
import Control.Monad (when, forM_)
import Data.Char
import System.IO
import System.IO.Temp
import System.FilePath ((</>))
import System.Process
import System.Info (os)
import Test.Hspec
import Text.Regex
-- | Main entry point.
main :: IO ()
main = hspec spec
--------------------------------------------------------------------------------
-- Test suite specification
-- | Test suite.
spec :: Spec
spec = do
argsparser
basics
load
types
alltypes
use
definition
bytecode
completion
it "Support GHC 8.2 better" pending
-- | Argument parsing should be user-friendly.
argsparser :: Spec
argsparser =
describe
"Arguments parser"
(do issue
":type-at \"Foo Bar.hs\" 1 1 1 1"
"https://github.com/commercialhaskell/intero/issues/25"
(atFile
":type-at"
"Foo Bar.hs"
"x = 'a'"
(1, 1, 1, 1, "x")
id
"x :: Char\n")
issue
":type-at"
"https://github.com/commercialhaskell/intero/issues/28"
(eval
":type-at"
"<no location info>: Expected a span: \"<module-name/filepath>\" <start line> <start column> <end line> <end column> \"<sample string>\"\n"))
maybeModuleStr :: String
#if __GLASGOW_HASKELL__ >= 806
maybeModuleStr = "GHC.Maybe"
#else
maybeModuleStr = "GHC.Base"
#endif
-- | Basic commands that should work out of the box.
basics :: Spec
basics =
describe
"Basics"
(do it ":t 1" (eval ":t 1 :: Num a => a" "1 :: Num a => a :: Num a => a\n")
it
":i Nothing"
(do reply <- withIntero [] (\_ repl -> repl ":i Nothing")
shouldBe
(subRegex (mkRegex "Data.Maybe") reply "GHC.Base")
("data Maybe a = Nothing | ... \t-- Defined in " ++ (quote maybeModuleStr) ++ "\n"))
it ":k Just" (eval ":k Maybe" "Maybe :: * -> *\n"))
where
quote s = opQuote : s ++ [clQuote]
opQuote = case os of
"mingw32" -> '`'
_ -> '‘'
clQuote = case os of
"mingw32" -> '\''
_ -> '’'
-- | Loading files and seeing the results.
load :: Spec
load =
describe
"Load"
(do it
":l X.hs"
(do result <-
withIntero
[]
(\dir repl -> do
writeFile (dir ++ "/X.hs") "x = 'a'"
repl (":l X.hs"))
shouldBe
result
(unlines
[ "[1 of 1] Compiling Main ( X.hs, interpreted )"
, "Ok, modules loaded: Main."
, "Collecting type info for 1 module(s) ... "]))
it
":l X.hs; :extensions X"
(do result <-
withIntero
[]
(\dir repl -> do
writeFile (dir ++ "/X.hs") "{-# LANGUAGE ScopedTypeVariables #-}\nmodule X where\nx = 'a'"
_ <- repl (":l X.hs")
repl (":extensions X"))
shouldBe
(filter (== "ScopedTypeVariables") (words result))
["ScopedTypeVariables"])
it
":l NonExistent.hs"
(do result <- withIntero [] (\_ repl -> repl (":l NonExistent.hs"))
shouldBe
(stripError result)
(unlines
[ "Failed, modules loaded: none."
, ""
, "<no location info>: can't find file: NonExistent.hs"])))
where
stripError = \i -> subRegex (mkRegex "error: ") i ""
-- | Check things when in -fbyte-code mode.
bytecode :: Spec
bytecode =
describe
"Bytecode"
(do it
":set -fobject-code ; :l X.hs; :set -byte-code; :l X.hs"
(do result <-
withIntero
[]
(\dir repl -> do
_ <- repl (":set -fobject-code")
writeFile (dir ++ "/X.hs") "x = 'a'"
_ <- repl (":l X.hs")
_ <- repl (":set -fbyte-code")
writeFile (dir ++ "/X.hs") "x = 123"
repl (":l X.hs"))
shouldBe
(unlines (reverse (take 2 (reverse (lines result)))))
(unlines
[ "Ok, modules loaded: Main."
, "Collecting type info for 1 module(s) ... "])))
-- | Get type information of file contents.
types :: Spec
types =
describe
"Types"
(do it
":type-at X.hs 1 1 1 1 x -- Char"
(typeAt "x = 'a'" (1, 1, 1, 1, "x") "x :: Char\n")
it
":type-at X.hs 1 1 1 1 -- Char (string omitted)"
(typeAt "x = 'a'" (1, 1, 1, 1, "") " :: Char\n")
it
":type-at X.hs 1 1 1 1 x -- [Char]"
(typeAt "x = 'a' : x" (1, 1, 1, 1, "x") "x :: [Char]\n")
it
":type-at X.hs 1 11 1 12 x -- [Char]"
(typeAt "x = 'a' : x" (1, 11, 1, 12, "x") "x :: [Char]\n")
it
":type-at X.hs 1 11 1 12 y -- [Char] (internal variable)"
(typeAt "x = 'a' : y where y = x" (1, 11, 1, 12, "y") "y :: [Char]\n")
issue
":type-at X.hs 1 1 1 1 f -- Num a => a"
"https://github.com/commercialhaskell/intero/issues/14"
(typeAt "f x = x * 2" (1, 1, 1, 2, "f") "f :: Num a => a -> a\n")
issue
":type-at X.hs 1 1 1 1 x -- Char (oddly bounded selection)"
"https://github.com/commercialhaskell/intero/issues/29"
(typeAt "foo = 'a'" (1, 1, 1, 1, "f") "f :: Char\n")
issue
":type-at half of 2 arguments within function call"
"https://github.com/commercialhaskell/intero/issues/29"
(typeAt testFile (1, 29, 1, 32, "\" \"") "\" \" :: [Char] -> [Char]\n")
issue
":type-at funtion + half of its first argument"
"https://github.com/commercialhaskell/intero/issues/29"
(typeAt
testFile
(1, 18, 1, 28, "concat3 \"a")
"concat3 \"a :: [Char] -> [Char] -> [Char]\n")
issue
":type-at 2 arguments within a function call"
"https://github.com/commercialhaskell/intero/issues/29"
(typeAt
testFile
(1, 26, 1, 35, "\"aa\" \"bb\"")
"\"aa\" \"bb\" :: [Char] -> [Char]\n")
issue
":type-at 2 lines within a do bloc"
"https://github.com/commercialhaskell/intero/issues/29"
(typeAt
testFile
(4, 8, 5, 10, "{{multiline}}")
"{{multiline}} :: IO ()\n")
issue
":type-at part of a line within a do bloc (1)"
"https://github.com/commercialhaskell/intero/issues/29"
(typeAt testFile (4, 8, 4, 10, " 1") " 1 :: IO ()\n")
issue
":type-at part of a line within a do block (2)"
"https://github.com/commercialhaskell/intero/issues/29"
(typeAt testFile (4, 9, 4, 10, "1") "1 :: Integer\n")
issue
":type-at with operator resolution"
"https://github.com/commercialhaskell/intero/issues/47"
(typeAt
(unlines
[ "data X = X"
, "instance Show X where"
, " show _ = show (1::Int,())"
, "p s = id s * s == id s * s"])
(3, 18, 3, 19, "1")
"1 :: Int\n"))
where
testFile :: String
testFile =
unlines
[ "test = putStrLn (concat3 \"aa\" \"bb\" \"cc\")"
, "concat3 a b c = a ++ b ++ c"
, "foo = do"
, " print 1"
, " print 2"
, " print 3"
, ""]
-- | List all types in all modules loaded.
alltypes :: Spec
alltypes =
describe
"All Types"
(do it
":all-types"
(do result <-
withIntero
[]
(\dir repl -> do
writeFile (dir ++ "/X.hs") "x = 123\ny = show 'c'"
_ <- repl (":l X.hs")
repl ":all-types")
shouldBe
result
(unlines
[ "X.hs:(2,1)-(2,2): String"
, "X.hs:(1,1)-(1,2): Integer"
, "X.hs:(2,5)-(2,9): Char -> String"
, "X.hs:(2,10)-(2,13): Char"
, "X.hs:(2,5)-(2,13): String"
, "X.hs:(1,5)-(1,8): Integer"])))
-- | Are we on ghc8_2 or above?
ghc8_2 :: Bool
#if __GLASGOW_HASKELL__ >= 802
ghc8_2 = True
#else
ghc8_2 = False
#endif
-- | Find uses of a variable.
use :: Spec
use =
describe
"Uses"
(do it
":uses X.hs 1 1 1 1 x -- from definition site"
(uses
"x = 'a' : x"
(1, 1, 1, 1, "x")
id
(unlines
["X.hs:(1,1)-(1,2)", "X.hs:(1,1)-(1,2)", "X.hs:(1,11)-(1,12)"]))
it
":uses X.hs 1 11 1 12 x -- from use site"
(uses
"x = 'a' : x"
(1, 11, 1, 12, "x")
id
(if ghc8_2
then unlines ["X.hs:(1,1)-(1,2)","X.hs:(1,1)-(1,2)","X.hs:(1,11)-(1,12)"]
else unlines ["X.hs:(1,1)-(1,2)", "X.hs:(1,11)-(1,12)"]))
it
":uses X.hs 1 5 1 6 id -- package definition"
(uses
"x = id"
(1, 5, 1, 6, "id")
(\i -> subRegex (mkRegex "-[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+") i "")
(unlines ["base:GHC.Base"]))
it
":uses X.hs 1 5 1 6 id -- shadowed package definition"
(uses
"x = id where id = ()"
(1, 5, 1, 7, "id")
id
(unlines
["X.hs:(1,14)-(1,16)", "X.hs:(1,14)-(1,16)", "X.hs:(1,5)-(1,7)"]))
issue
":uses on type constructor (in data decl)"
"https://github.com/commercialhaskell/intero/issues/3"
(uses
(unlines ["data X = X", "foo :: X -> X", "foo x = X"])
(1, 6, 1, 7, "X")
lines
["X.hs:(1,1)-(1,11)"])
issue
":uses on type constructor (in sig)"
"https://github.com/commercialhaskell/intero/issues/3"
(uses
(unlines ["data X = X", "foo :: X -> X", "foo x = X"])
(2, 8, 2, 9, "X")
lines
["X.hs:(1,1)-(1,11)"])
issue
":uses on data constructor (in expression)"
"https://github.com/commercialhaskell/intero/issues/3"
(uses
(unlines ["data X = X", "foo :: X -> X", "foo x = X"])
(3, 9, 3, 10, "X")
lines
(if ghc8_2
then ["X.hs:(1,1)-(1,11)"]
else ["X.hs:(1,10)-(1,11)", "X.hs:(3,9)-(3,10)"])))
-- | Find loc-ats of a variable.
definition :: Spec
definition =
describe
"Definition location"
(do it
"From definition site"
(locAt "x = 'a' : x" (1, 1, 1, 1, "x") (unlines ["X.hs:(1,1)-(1,2)"]))
it
"From use site"
(locAt
"x = 'a' : x"
(1, 11, 1, 12, "x")
(unlines (if ghc8_2
then ["X.hs:(1,1)-(1,2)"]
else ["X.hs:(1,1)-(1,12)"])))
it
"To function argument"
(locAt
"f x = 'a' : x"
(1, 13, 1, 14, "x")
(unlines ["X.hs:(1,3)-(1,4)"]))
it
"To pattern match"
(locAt
"f (Just x) = 'a' : x"
(1, 20, 1, 21, "x")
(unlines ["X.hs:(1,9)-(1,10)"]))
it
"To other module"
(locAtMultiple
[("X.hs", "import Y"), ("Y.hs", "module Y where")]
(1, 8, 1, 9, "Y")
(unlines ["." </> "Y.hs:(1,8)-(1,9)"]))
issue
"To unexported thing"
"https://github.com/commercialhaskell/intero/issues/98"
(locAt
(unlines
[ "module X () where"
, "data MyType = MyCons"
, "t :: MyType"
, "t = MyCons :: MyType"
])
(3, 6, 3, 12, "MyType")
(unlines ["X.hs:(2,1)-(2,21)"])))
-- | Test interactive completions.
completion :: Spec
completion = do
describe
"Completion in REPL"
(do issue
":complete repl \"put\""
"https://github.com/commercialhaskell/intero/issues/34"
(eval
":complete repl \"put\""
(unlines ["3 3 \"\"", "\"putChar\"", "\"putStr\"", "\"putStrLn\""]))
issue
":complete repl \"sor\""
"https://github.com/commercialhaskell/intero/issues/34"
(do reply <-
withIntero
[]
(\_ repl -> do
let req = ":complete repl \"sor\""
reply <- repl req
if reply == "0 0 \"\"\n"
then do
_ <- repl "import Data.List"
repl req
else return ("First step failed: " ++ reply))
shouldBe
(filter (/= "\"sortOn\"") (drop 1 (lines reply)))
(["\"sort\"", "\"sortBy\""])))
describe
"Completion in module context"
(do when
ghc8_2
(issue
":complete-at for defered scope names"
"https://github.com/chrisdone/intero/issues/531"
(atFile
":complete-at"
"X.hs"
-- All these type annotations are required for GHC 8.6.5
-- to accept the input without error.
"{-# OPTIONS -fdefer-type-errors #-}\nmodule X where\ng a = fiiila (filu :: Char) a (fi :: Int)\n where fiiila _ _ _ = 123"
(2, 14, 2, 17, "fi")
lines
["fiiila", "filter"]))
it
":complete-at for put*"
(atFile
":complete-at"
"X.hs"
"module X () where\nx = undefined"
(4, 5, 0, 0, "put")
lines
["putChar", "putStr", "putStrLn"])
it
":complete-at for locally imported"
(atFile
":complete-at"
"X.hs"
"module X () where\nimport Data.List\nx = undefined"
(3, 5, 0, 0, "sor")
(take 2 . lines)
["sort", "sortBy"])
it
":complete-at for module-locally defined"
(atFile
":complete-at"
"X.hs"
"module X () where\nx = undefined\nmodlocal = ()"
(2, 5, 0, 0, "modl")
lines
["modlocal"])
it
":complete-at for definition-locally defined"
(atFile
":complete-at"
"X.hs"
"module X () where\nx = undefined where locally = let p = 123 in p"
(2, 5, 0, 0, "loc")
lines
["locally"]))
--------------------------------------------------------------------------------
-- Combinators for running and interacting with intero
-- | Find the definition for the thing at point.
locAtMultiple :: [(String, String)] -> (Int, Int, Int, Int, String) -> String -> Expectation
locAtMultiple files (line,col,line',col',name) expected = do
result <-
withIntero
[]
(\dir repl -> do
forM_ files $ \(fileName, fileContents) ->
writeFile (dir ++ "/" ++ fileName) fileContents
_ <- repl (":l " ++ fst (head files))
repl
(":loc-at " ++ fst (head files) ++ " " ++
unwords (map show [line, col, line', col']) ++ " " ++ name))
shouldBe result expected
let x = return ()
x
locAt :: String -> (Int, Int, Int, Int, String) -> String -> Expectation
locAt file = locAtMultiple [("X.hs", file)]
-- | Find use-sites for the given place.
uses
:: (Eq a, Show a)
=> String -> (Int, Int, Int, Int, String) -> (String -> a) -> a -> Expectation
uses file (line,col,line',col',name) preprocess expected = do
result <-
withIntero
[]
(\dir repl -> do
writeFile (dir ++ "/X.hs") file
_ <- repl (":l X.hs")
repl
(":uses X.hs " ++
unwords (map show [line, col, line', col']) ++ " " ++ name))
shouldBe (preprocess result) expected
-- | Test the type at the given place.
typeAt :: String -> (Int, Int, Int, Int, String) -> String -> Expectation
typeAt a b c = do
atFile ":type-at" "X.hs" a b id c
-- | Test the type at the given place (with the given filename).
atFile
:: (Eq a, Show a)
=> String
-> String
-> String
-> (Int, Int, Int, Int, String)
-> (String -> a)
-> a
-> Expectation
atFile cmd fname file (line,col,line',col',name) preprocess expected = do
result <-
withIntero
[]
(\dir repl -> do
writeFile (dir ++ "/" ++ fname) file
_ <- repl (":l " ++ show fname)
repl
(cmd ++
" " ++
(if any isSpace fname
then show fname
else fname) ++
" " ++
unwords (map show [line, col, line', col']) ++
(if null name
then ""
else " " ++ show name)))
shouldBe (preprocess result) expected
-- | Make a quick interaction with intero.
eval :: String -- ^ Input.
-> String -- ^ Expected output.
-> Expectation
eval send recv = do
reply <- withIntero [] (\_ repl -> repl send)
shouldBe (unlines (filter (not . null) (lines reply))) recv
-- | Launch an interactive intero process. Creates a temporary
-- directory in which the computation can work.
withIntero
:: MonadIO m
=> [String] -> (FilePath -> (String -> IO String) -> IO a) -> m a
withIntero arguments cont =
liftIO
(withSystemTempDirectory
"withIntero"
(\dir -> do
(inp,out,err,pid) <-
catch
(runInteractiveProcess
"intero"
("-ignore-dot-ghci" : arguments)
(Just dir)
Nothing)
(\(_ :: IOException) -> error "Couldn't launch intero process.")
hSetBuffering inp NoBuffering
hSetBuffering out NoBuffering
hSetBuffering err NoBuffering
let repl instr = do
catch
(do hPutStrLn inp instr
let getReply = do
mc <-
catch
(fmap Just (hGetChar out))
(\(_ :: IOException) -> return Nothing)
case mc of
Nothing -> hGetAvailable err
Just '\4' -> hGetAvailable err
Just c -> do
cs <- getReply
return (c : cs)
getReply)
(\(_ :: IOException) -> return "")
_ <- repl ":set prompt \"\\4\""
finally
(cont dir repl)
(do ignored (hClose inp)
ignored (hClose out)
ignored (hClose err)
ignored (terminateProcess pid))))
where
ignored m = catch m (\(_ :: IOException) -> return ())
hGetAvailable h = do
available <- catch (hReady h) (\(_ :: IOException) -> return False)
if available
then catch
(do c <- hGetChar h
cs <- hGetAvailable h
return (c : cs))
(\(_ :: IOException) -> return [])
else return []
--------------------------------------------------------------------------------
-- Spec combinators
-- | Specify an issue that needs to be regression tested.
issue
:: Example a
=> String -> t -> a -> SpecWith (Arg a)
issue label _link expectation = it label expectation