-- Test use and abuse of the ide-backend API (such as two calls to shutdown etc) -- -- Since many of these tests have a non-standard use of the API they often do -- not use the withAvailableSession infrastructure. module TestSuite.Tests.API (testGroupAPI) where import Control.Concurrent import Control.Exception import Data.Monoid import System.Exit import Test.Tasty import Test.HUnit import qualified Data.ByteString.Lazy.Char8 as L (unlines) import qualified Data.Text as T import IdeSession import TestSuite.State import TestSuite.Session import TestSuite.Assertions testGroupAPI :: TestSuiteEnv -> TestTree testGroupAPI env = testGroup "API use and abuse" $ [ stdTest env "Duplicate shutdown" testDuplicateShutdown , stdTest env "Permit a session within a session and duplicated shutdownSession" testNestedSessions , stdTest env "Reject updateSession after shutdownSession" testRejectUpdateSession , stdTest env "Reject getSourceErrors after shutdownSession" testRejectGetSourceErrors , stdTest env "Reject runStmt after shutdownSession" testRejectRunStmt , stdTest env "Fail on empty package DB" testEmptyPackageDB , stdTest env "Two calls to runStmt" testTwiceRunStmt , stdTest env "Make sure we can terminate the IDE session when code is running" test_Terminate_CodeRunning , stdTest env "getSourceErrors during run" test_getSourceErrors_CodeRunning , stdTest env "getLoadedModules during run" test_getLoadedModules_CodeRunning , stdTest env "getLoadedModules while configGenerateModInfo off" test_getLoadedModules_ConfigGenerateModInfoOff , stdTest env "Call runWait after termination (normal termination)" test_runWait_AfterTermination , stdTest env "Call runWait after termination (interrupted)" test_runWait_AfterTermination_Int , stdTest env "Call runWait after termination (restarted session)" test_runWait_AfterTermination_Restarted ] ++ exeTests env [ stdTest env "Two calls to runExe" testTwiceRunExe , stdTest env "Make sure we can terminate the IDE session when exe is running" test_Terminate_ExeRunning ] testDuplicateShutdown :: TestSuiteEnv -> Assertion testDuplicateShutdown env = withSession (defaultServerConfig env) $ -- withSession itself also does a shutdownSession shutdownSession testNestedSessions :: TestSuiteEnv -> Assertion testNestedSessions env = withSession (defaultServerConfig env) $ \session -> do loadModulesFrom session "TestSuite/inputs/ABnoError" withSession (defaultServerConfig env) $ \s2 -> do withSession (defaultServerConfig env) $ \s3 -> do withSession (defaultServerConfig env) $ \_s4 -> do let update2 = loadModule "M.hs" "a = unknownX" updateSessionD s2 update2 1 assertOneError s2 withSession (defaultServerConfig env) $ \s5 -> do let update3 = loadModule "M.hs" "a = 3" updateSessionD s3 update3 1 assertNoErrors session shutdownSession s5 -- <-- duplicate "nested" shutdown testRejectUpdateSession :: TestSuiteEnv -> Assertion testRejectUpdateSession env = withSession (defaultServerConfig env) $ \session -> do shutdownSession session assertRaises "updateSessionD session mempty" (== userError "Session already shut down.") (updateSessionD session mempty 0) testRejectGetSourceErrors :: TestSuiteEnv -> Assertion testRejectGetSourceErrors env = withSession (defaultServerConfig env) $ \session -> do shutdownSession session assertRaises "getSourceErrors session" (== userError "Session already shut down.") (getSourceErrors session) testRejectRunStmt :: TestSuiteEnv -> Assertion testRejectRunStmt env = withSession (defaultServerConfig env) $ \session -> do shutdownSession session assertRaises "runStmt session Main main" (== userError "State not idle") (runStmt session "Main" "main") testEmptyPackageDB :: TestSuiteEnv -> Assertion testEmptyPackageDB env = assertRaises "" (\e -> e == userError "Invalid package DB stack: []") (withSession cfg $ \_ -> return ()) where cfg = (defaultServerConfig env) { testSuiteServerPackageDBStack = Just [] } testTwiceRunStmt :: TestSuiteEnv -> Assertion testTwiceRunStmt env = withAvailableSession env $ \session -> do updateSessionD session upd 1 assertNoErrors session do runActions <- runStmt session "M" "echo" supplyStdin runActions "ECHO!\n" (output, result) <- runWaitAll runActions assertEqual "" RunOk result assertEqual "" "ECHO!\n" output do runActions <- runStmt session "M" "echoReverse" supplyStdin runActions "!OHCE\n" (output, result) <- runWaitAll runActions assertEqual "" RunOk result assertEqual "" "ECHO!\n" output where upd = (updateCodeGeneration True) <> (updateSourceFile "M.hs" . L.unlines $ [ "module M where" , "echo :: IO ()" , "echo = getLine >>= putStrLn" , "echoReverse :: IO ()" , "echoReverse = getLine >>= putStrLn . reverse" ]) testTwiceRunExe :: TestSuiteEnv -> Assertion testTwiceRunExe env = withAvailableSession env $ \session -> do updateSessionD session upd 1 assertNoErrors session let m = "M" updExe = buildExe [] [(T.pack m, "M.hs")] updateSessionD session updExe 2 do runActions <- runExe session "M" supplyStdin runActions "!OHCE\n" (output, result) <- runWaitAll runActions assertEqual "" result ExitSuccess assertEqual "" "ECHO!\n" output do runActions <- runExe session "M" supplyStdin runActions "!OHCE\n" (output, result) <- runWaitAll runActions assertEqual "" result ExitSuccess assertEqual "" "ECHO!\n" output where upd = (updateCodeGeneration True) <> (updateSourceFile "M.hs" . L.unlines $ [ "module M where" , "main :: IO ()" , "main = getLine >>= putStrLn . reverse" ]) test_Terminate_CodeRunning :: TestSuiteEnv -> Assertion test_Terminate_CodeRunning env = do runActions <- withAvailableSession' env dontReuse $ \session -> do updateSessionD session upd 1 assertNoErrors session runStmt session "M" "echo" forceCancel runActions where upd = (updateCodeGeneration True) <> (updateSourceFile "M.hs" . L.unlines $ [ "module M where" , "echo :: IO ()" , "echo = (getLine >>= putStrLn) >> echo" ]) test_Terminate_ExeRunning :: TestSuiteEnv -> Assertion test_Terminate_ExeRunning env = do runActions <- withAvailableSession' env dontReuse $ \session -> do updateSessionD session upd 1 let m = "M" updExe = buildExe [] [(T.pack m, "M.hs")] updateSessionD session updExe 2 assertNoErrors session runExe session "M" forceCancel runActions where upd = (updateCodeGeneration True) <> (updateSourceFile "M.hs" . L.unlines $ [ "module M where" , "main :: IO ()" , "main = (getLine >>= putStrLn) >> main" ]) test_getSourceErrors_CodeRunning :: TestSuiteEnv -> Assertion test_getSourceErrors_CodeRunning env = withAvailableSession env $ \session -> do updateSessionD session upd 1 assertSourceErrors' session ["Top-level binding with no type signature"] errs <- getSourceErrors session do runActions <- runStmt session "M" "loop" errs' <- getSourceErrors session assertEqual "Running code does not affect getSourceErrors" errs errs' forceCancel runActions ifTestingExe env $ do let m = "M" updExe = buildExe [] [(T.pack m, "M.hs")] updateSessionD session updExe 2 errs' <- getSourceErrors session assertEqual "" errs errs' runActions <- runExe session m errs'' <- getSourceErrors session assertEqual "Running exes does not affect getSourceErrors" errs errs'' forceCancel runActions where upd = (updateCodeGeneration True) <> (updateSourceFile "M.hs" . L.unlines $ [ "{-# OPTIONS_GHC -Wall #-}" , "module M where" , "loop = loop" , "main :: IO ()" , "main = loop" ]) test_getLoadedModules_CodeRunning :: TestSuiteEnv -> Assertion test_getLoadedModules_CodeRunning env = withAvailableSession env $ \session -> do updateSessionD session upd 1 mods <- getLoadedModules session assertEqual "" [T.pack "M"] mods runActions <- runStmt session "M" "loop" mods' <- getLoadedModules session assertEqual "Running code does not affect getLoadedModules" mods mods' forceCancel runActions where upd = (updateCodeGeneration True) <> (updateSourceFile "M.hs" . L.unlines $ [ "{-# OPTIONS_GHC -Wall #-}" , "module M where" , "loop = loop" ]) test_runWait_AfterTermination :: TestSuiteEnv -> Assertion test_runWait_AfterTermination env = withAvailableSession env $ \session -> do updateSessionD session upd 1 assertNoErrors session do runActions <- runStmt session "M" "hello" (output, result) <- runWaitAll runActions assertEqual "" RunOk result assertEqual "" "Hello World\n" output result' <- runWait runActions assertEqual "" result' (Right RunOk) ifTestingExe env $ do let m = "M" updExe = buildExe [] [(T.pack m, "M.hs")] updateSessionD session updExe 2 runActionsExe <- runExe session m (outExe, statusExe) <- runWaitAll runActionsExe assertEqual "Output from runExe" "Hello World\n" outExe assertEqual "after runExe" ExitSuccess statusExe result2 <- runWait runActionsExe assertEqual "" result2 (Right ExitSuccess) where upd = (updateCodeGeneration True) <> (updateSourceFile "M.hs" . L.unlines $ [ "module M where" , "hello :: IO ()" , "hello = putStrLn \"Hello World\"" , "main :: IO ()" , "main = hello" ]) test_runWait_AfterTermination_Int :: TestSuiteEnv -> Assertion test_runWait_AfterTermination_Int env = withAvailableSession env $ \session -> do updateSessionD session upd 1 assertNoErrors session do runActions <- runStmt session "M" "loop" threadDelay 1000000 interrupt runActions resOrEx <- runWait runActions case resOrEx of Right result -> assertBool "" (isAsyncException result) _ -> assertFailure $ "Unexpected run result: " ++ show resOrEx resOrEx' <- runWait runActions case resOrEx' of Right result -> assertBool "" (isAsyncException result) _ -> assertFailure $ "Unexpected run result in repeat call: " ++ show resOrEx' ifTestingExe env $ do let m = "M" updExe = buildExe [] [(T.pack m, "M.hs")] updateSessionD session updExe 2 runActionsExe <- runExe session m threadDelay 1000000 interrupt runActionsExe resOrEx <- runWait runActionsExe case resOrEx of Right result -> assertEqual "after runExe" (ExitFailure (-2)) result -- SIGINT _ -> assertFailure $ "Unexpected run result: " ++ show resOrEx result' <- runWait runActionsExe assertEqual "" result' (Right $ ExitFailure (-2)) -- SIGINT where upd = (updateCodeGeneration True) <> (updateSourceFile "M.hs" . L.unlines $ [ "module M where" , "import Control.Concurrent (threadDelay)" , "loop :: IO ()" , "loop = threadDelay 100000 >> loop" , "main :: IO ()" , "main = loop" ]) test_runWait_AfterTermination_Restarted :: TestSuiteEnv -> Assertion test_runWait_AfterTermination_Restarted env = withAvailableSession env $ \session -> do updateSessionD session upd 1 assertNoErrors session do runActions <- runStmt session "M" "loop" threadDelay 1000000 restartSession session forceCancel runActions resOrEx2 <- runWait runActions case resOrEx2 of Right RunForceCancelled -> return () _ -> assertFailure $ "Unexpected run result: " ++ show resOrEx2 resOrEx' <- runWait runActions case resOrEx' of Right RunForceCancelled -> return () _ -> assertFailure $ "Unexpected run result in repeat call: " ++ show resOrEx' updateSessionD session mempty 1 -- needed to load the code again ifTestingExe env $ do let m = "M" updExe = buildExe [] [(T.pack m, "M.hs")] updateSessionD session updExe 3 -- TODO: Really we only expect 2 (#189) runActionsExe <- runExe session m threadDelay 1000000 restartSession session -- restartSession would not suffice, since session restart -- doesn't stop the exe, so we need to interrupt manually. interrupt runActionsExe resOrEx <- runWait runActionsExe case resOrEx of Right result -> assertEqual "after runExe" (ExitFailure (-2)) result -- SIGKILL _ -> assertFailure $ "Unexpected run result: " ++ show resOrEx result' <- runWait runActionsExe assertEqual "" result' (Right $ ExitFailure (-2)) -- SIGKILL where upd = (updateCodeGeneration True) <> (updateSourceFile "M.hs" . L.unlines $ [ "module M where" , "import Control.Concurrent (threadDelay)" , "loop :: IO ()" , "loop = threadDelay 100000 >> loop" , "main :: IO ()" , "main = loop" ]) test_getLoadedModules_ConfigGenerateModInfoOff :: TestSuiteEnv -> Assertion test_getLoadedModules_ConfigGenerateModInfoOff env = withAvailableSession' env (withModInfo False) $ \session -> do updateSessionD session upd 1 assertNoErrors session assertLoadedModules session "" ["M"] where upd = (updateCodeGeneration True) <> (updateSourceFile "M.hs" . L.unlines $ [ "module M where" , "hello :: IO ()" , "hello = putStrLn \"Hello World\"" ]) {------------------------------------------------------------------------------- Auxiliary -------------------------------------------------------------------------------} withSession :: TestSuiteServerConfig -> (IdeSession -> IO a) -> IO a withSession cfg = bracket (startNewSession cfg) shutdownSession