packages feed

intero-0.1.10: src/test/Main.hs

{-# LANGUAGE ScopedTypeVariables #-}
-- | Test that various commands work properly.

module Main where

import Control.Exception
import Control.Monad.IO.Class
import Data.Char
import System.IO
import System.IO.Temp
import System.Process
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

-- | 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/chrisdone/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/chrisdone/intero/issues/28"
                     (eval ":type-at"
                           "\n<no location info>: Expected a span: \"<module-name/filepath>\" <start line> <start column> <end line> <end column> \"<sample string>\"\n"))

-- | 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 ‘GHC.Base’\n")
               it ":k Just" (eval ":k Maybe" "Maybe :: * -> *\n"))

-- | 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 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/chrisdone/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/chrisdone/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/chrisdone/intero/issues/29"
                     (typeAt testFile (1,29,1,32,"\" \"") "\" \" :: [Char] -> [Char]\n")
               issue ":type-at funtion + half of its first argument"
                     "https://github.com/chrisdone/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/chrisdone/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/chrisdone/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/chrisdone/intero/issues/29"
                     (typeAt testFile (4,8,4,10," 1") " 1 :: IO ()\n")
               issue ":type-at part of a line within a do bloc (2)"
                     "https://github.com/chrisdone/intero/issues/29"
                     (typeAt testFile (4,9,4,10,"1") "1 :: Integer\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"])))

-- | 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
                        (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)"])))

-- | Find loc-ats of a variable.
definition :: Spec
definition =
  describe "Definition location"
           (do it ":loc-at X.hs 1 1 1 1 x -- from definition site"
                  (locAt "x = 'a' : x"
                         (1,1,1,1,"x")
                         (unlines ["X.hs:(1,1)-(1,2)"]))
               it ":loc-at X.hs 1 11 1 12 x -- from use site"
                  (locAt "x = 'a' : x"
                         (1,11,1,12,"x")
                         (unlines ["X.hs:(1,1)-(1,12)"]))
               it ":loc-at X.hs 1 11 1 12 x -- to function argument"
                  (locAt "f x = 'a' : x"
                         (1,13,1,14,"x")
                         (unlines ["X.hs:(1,3)-(1,4)"]))
               it ":loc-at X.hs 1 11 1 12 x -- to pattern match"
                  (locAt "f (Just x) = 'a' : x"
                         (1,20,1,21,"x")
                         (unlines ["X.hs:(1,9)-(1,10)"])))

-- | Test interactive completions.
completion :: Spec
completion =
  do describe "Complete basic Prelude identifiers"
              (issue ":complete repl \"put\""
                     "https://github.com/chrisdone/intero/issues/34"
                     (eval ":complete repl \"put\""
                           (unlines ["3 3 \"\""
                                    ,"\"putChar\""
                                    ,"\"putStr\""
                                    ,"\"putStrLn\""])))
     describe "Completion in module context"
              (do 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.
locAt
  :: String -> (Int,Int,Int,Int,String) -> String -> Expectation
locAt file (line,col,line',col',name) expected =
  do result <-
       withIntero
         []
         (\dir repl ->
            do writeFile (dir ++ "/X.hs") file
               _ <- repl (":l X.hs")
               repl (":loc-at X.hs " ++
                     unwords (map show [line,col,line',col']) ++ " " ++ name))
     shouldBe result expected
     let x = return ()
     x

-- | 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 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