{-# LANGUAGE QuasiQuotes #-} import GHC import GHC.Paths import Data.IORef import Control.Monad import Data.List import System.Directory import Data.String.Here import Data.String.Utils (strip, replace) import IHaskell.Eval.Parser import IHaskell.Types import IHaskell.IPython import IHaskell.Eval.Evaluate as Eval import IHaskell.Eval.Completion import Test.Hspec import Test.Hspec.HUnit doGhc = runGhc (Just libdir) parses = doGhc . parseString like parser desired = parser >>= (`shouldBe` desired) is string blockType = do result <- doGhc $ parseString string result `shouldBe` [blockType $ strip string] eval string = do outputAccum <- newIORef [] let publish _ displayDatas = modifyIORef outputAccum (displayDatas :) getTemporaryDirectory >>= setCurrentDirectory interpret $ Eval.evaluate 1 string publish out <- readIORef outputAccum return $ reverse out becomes string expected = do let indent (' ':x) = 1 + indent x indent _ = 0 empty = null . strip stringLines = filter (not . empty) $ lines string minIndent = minimum (map indent stringLines) newString = unlines $ map (drop minIndent) stringLines eval newString >>= comparison where comparison results = do when (length results /= length expected) $ expectationFailure $ "Expected result to have " ++ show (length expected) ++ " results. Got " ++ show results let isPlain (Display PlainText _) = True isPlain _ = False forM_ (zip results expected) $ \(result, expected) -> case find isPlain result of Just (Display PlainText str) -> str `shouldBe` expected Nothing -> expectationFailure $ "No plain-text output in " ++ show result completes string expected = completionTarget newString cursorloc `shouldBe` expected where (newString, cursorloc) = case elemIndex '!' string of Nothing -> error "Expected cursor written as '!'." Just idx -> (replace "!" "" string, idx) completionHas string expected = do (matched, completions) <- doGhc $ do initCompleter complete newString cursorloc let existsInCompletion = (`elem` completions) unmatched = filter (not . existsInCompletion) expected unmatched `shouldBe` [] where (newString, cursorloc) = case elemIndex '!' string of Nothing -> error "Expected cursor written as '!'." Just idx -> (replace "!" "" string, idx) initCompleter :: GhcMonad m => m () initCompleter = do flags <- getSessionDynFlags setSessionDynFlags $ flags { hscTarget = HscInterpreted, ghcLink = LinkInMemory } -- Import modules. imports <- mapM parseImportDecl ["import Prelude", "import qualified Control.Monad", "import qualified Data.List as List", "import Data.Maybe as Maybe"] setContext $ map IIDecl imports main :: IO () main = hspec $ do parserTests ipythonTests evalTests completionTests completionTests = do describe "Completion" $ do it "correctly gets the completion identifier without dots" $ do "hello!" `completes` ["hello"] "hello aa!bb goodbye" `completes` ["aa"] "hello aabb! goodbye" `completes` ["aabb"] "aacc! goodbye" `completes` ["aacc"] "hello !aabb goodbye" `completes` [] "!aabb goodbye" `completes` [] it "correctly gets the completion identifier with dots" $ do "hello test.aa!bb goodbye" `completes` ["test", "aa"] "Test.!" `completes` ["Test", ""] "Test.Thing!" `completes` ["Test", "Thing"] "Test.Thing.!" `completes` ["Test", "Thing", ""] "Test.Thing.!nope" `completes` ["Test", "Thing", ""] it "correctly gets the completion type" $ do completionType "import Data." ["Data", ""] `shouldBe` ModuleName "Data" "" completionType "import Prel" ["Prel"] `shouldBe` ModuleName "" "Prel" completionType "import D.B.M" ["D", "B", "M"] `shouldBe` ModuleName "D.B" "M" completionType " import A." ["A", ""] `shouldBe` ModuleName "A" "" completionType "import a.x" ["a", "x"] `shouldBe` Identifier "x" completionType "A.x" ["A", "x"] `shouldBe` Qualified "A" "x" completionType "a.x" ["a", "x"] `shouldBe` Identifier "x" completionType "pri" ["pri"] `shouldBe` Identifier "pri" it "properly completes identifiers" $ do "pri!" `completionHas` ["print"] "ma!" `completionHas` ["map"] "hello ma!" `completionHas` ["map"] "print $ catMa!" `completionHas` ["catMaybes"] it "properly completes qualified identifiers" $ do "Control.Monad.liftM!" `completionHas` [ "Control.Monad.liftM" , "Control.Monad.liftM2" , "Control.Monad.liftM5"] "print $ List.intercal!" `completionHas` ["List.intercalate"] "print $ Data.Maybe.cat!" `completionHas` ["Data.Maybe.catMaybes"] "print $ Maybe.catM!" `completionHas` ["Maybe.catMaybes"] it "properly completes imports" $ do "import Data.!" `completionHas` ["Data.Maybe", "Data.List"] "import Data.M!" `completionHas` ["Data.Maybe"] "import Prel!" `completionHas` ["Prelude"] evalTests = do describe "Code Evaluation" $ do it "evaluates expressions" $ do "3" `becomes` ["3"] "3+5" `becomes` ["8"] "print 3" `becomes` ["3"] [hereLit| let x = 11 z = 10 in x+z |] `becomes` ["21"] it "evaluates multiline expressions" $ do [hereLit| import Control.Monad forM_ [1, 2, 3] $ \x -> print x |] `becomes` ["1\n2\n3"] it "evaluates function declarations silently" $ do [hereLit| fun :: [Int] -> Int fun [] = 3 fun (x:xs) = 10 fun [1, 2] |] `becomes` ["10"] it "evaluates data declarations" $ do [hereLit| data X = Y Int | Z String deriving (Show, Eq) print [Y 3, Z "No"] print (Y 3 == Z "No") |] `becomes` ["[Y 3,Z \"No\"]", "False"] it "evaluates do blocks in expressions" $ do [hereLit| show (show (do Just 10 Nothing Just 100)) |] `becomes` ["\"\\\"Nothing\\\"\""] it "is silent for imports" $ do "import Control.Monad" `becomes` [] "import qualified Control.Monad" `becomes` [] "import qualified Control.Monad as CM" `becomes` [] "import Control.Monad (when)" `becomes` [] it "evaluates directives" $ do ":typ 3" `becomes` ["forall a. Num a => a"] ":in String" `becomes` ["type String = [Char] \t-- Defined in `GHC.Base'"] ipythonTests = do describe "Parse IPython Version" $ do it "parses 2.0.0-dev" $ parseVersion "2.0.0-dev" `shouldBe` [2, 0, 0] it "parses 2.0.0-alpha" $ parseVersion "2.0.0-dev" `shouldBe` [2, 0, 0] it "parses 12.5.10" $ parseVersion "12.5.10" `shouldBe` [12, 5, 10] parserTests = do layoutChunkerTests moduleNameTests parseStringTests layoutChunkerTests = describe "Layout Chunk" $ do it "chunks 'a string'" $ layoutChunks "a string" `shouldBe` ["a string"] it "chunks 'a\\nstring'" $ layoutChunks "a\n string" `shouldBe` ["a\n string"] it "chunks 'a\\n string\\nextra'" $ layoutChunks "a\n string\nextra" `shouldBe` ["a\n string","extra"] it "chunks strings with too many lines" $ layoutChunks "a\n\nstring" `shouldBe` ["a","string"] moduleNameTests = describe "Get Module Name" $ do it "parses simple module names" $ "module A where\nx = 3" `named` ["A"] it "parses module names with dots" $ "module A.B where\nx = 3" `named` ["A", "B"] it "parses module names with exports" $ "module A.B.C ( x ) where x = 3" `named` ["A", "B", "C"] it "errors when given unnamed modules" $ do doGhc (getModuleName "x = 3") `shouldThrow` anyException where named str result = do res <- doGhc $ getModuleName str res `shouldBe` result parseStringTests = describe "Parser" $ do it "parses empty strings" $ parses "" `like` [] it "parses simple imports" $ "import Data.Monoid" `is` Import it "parses simple arithmetic" $ "3 + 5" `is` Expression it "parses :type" $ parses ":type x\n:ty x" `like` [ Directive GetType "x", Directive GetType "x" ] it "parses :info" $ parses ":info x\n:in x" `like` [ Directive GetInfo "x", Directive GetInfo "x" ] it "parses :help and :?" $ parses ":? x\n:help x" `like` [ Directive GetHelp "x", Directive GetHelp "x" ] it "parses :set x" $ parses ":set x" `like` [ Directive HelpForSet "x" ] it "parses :extension x" $ parses ":ex x\n:extension x" `like` [ Directive SetExtension "x", Directive SetExtension "x" ] it "fails to parse :nope" $ parses ":nope goodbye" `like` [ ParseError (Loc 1 1) "Unknown directive: 'nope'." ] it "parses number followed by let stmt" $ parses "3\nlet x = expr" `like` [ Expression "3", Statement "let x = expr" ] it "parses let x in y" $ "let x = 3 in x + 3" `is` Expression it "parses a data declaration" $ "data X = Y Int" `is` Declaration it "parses number followed by type directive" $ parses "3\n:t expr" `like` [ Expression "3", Directive GetType "expr" ] it "parses a <- statement" $ "y <- print 'no'" `is` Statement it "parses a <- stmt followed by let stmt" $ parses "y <- do print 'no'\nlet x = expr" `like` [ Statement "y <- do print 'no'", Statement "let x = expr" ] it "parses <- followed by let followed by expr" $ parses "y <- do print 'no'\nlet x = expr\nexpression" `like` [ Statement "y <- do print 'no'", Statement "let x = expr", Expression "expression" ] it "parses two print statements" $ parses "print yes\nprint no" `like` [ Expression "print yes", Expression "print no" ] it "parses a pattern-maching function declaration" $ "fun [] = 10" `is` Declaration it "parses a function decl followed by an expression" $ parses "fun [] = 10\nprint 'h'" `like` [ Declaration "fun [] = 10", Expression "print 'h'" ] it "parses list pattern matching fun decl" $ "fun (x : xs) = 100" `is` Declaration it "parses two pattern matches as the same declaration" $ "fun [] = 10\nfun (x : xs) = 100" `is` Declaration it "parses a type signature followed by a declaration" $ "fun :: [a] -> Int\nfun [] = 10\nfun (x : xs) = 100" `is` Declaration it "parases a simple module" $ "module A where x = 3" `is` Module it "parses a module with an export" $ "module B (x) where x = 3" `is` Module it "breaks when a let is incomplete" $ parses "let x = 3 in" `like` [ ParseError (Loc 1 13) "parse error (possibly incorrect indentation or mismatched brackets)" ] it "breaks without data kinds" $ parses "data X = 3" `like` [ ParseError (Loc 1 10) "Illegal literal in type (use -XDataKinds to enable): 3" ] it "parses statements after imports" $ do parses "import X\nprint 3" `like` [ Import "import X", Expression "print 3" ] parses "import X\n\nprint 3" `like` [ Import "import X", Expression "print 3" ] it "ignores blank lines properly" $ [hereLit| test arg = hello where x = y z = w |] `is` Declaration it "doesn't break on long strings" $ do let longString = concat $ replicate 20 "hello " ("img ! src \"" ++ longString ++ "\" ! width \"500\"") `is` Expression it "parses do blocks in expression" $ do [hereLit| show (show (do Just 10 Nothing Just 100)) |] `is` Expression