build-depends: unix
other-modules: Language.Haskell.LSP.Test.Compat
Language.Haskell.LSP.Test.Decoding
+ Language.Haskell.LSP.Test.Exceptions
Language.Haskell.LSP.Test.Files
Language.Haskell.LSP.Test.Messages
Language.Haskell.LSP.Test.Parsing
-- * Sessions
runSession
, runSessionWithHandles
- , runSessionWithCapabilities
+ , runSessionWithConfig
, Session
+ , SessionConfig(..)
+ , MonadSessionConfig(..)
+ , SessionException(..)
+ , anySessionException
-- * Sending
, sendRequest
, sendNotification
import Data.Default
import qualified Data.Map as Map
import Data.Maybe
-import Language.Haskell.LSP.Types
-import qualified Language.Haskell.LSP.Types as LSP (error, id)
-import Language.Haskell.LSP.TH.ClientCapabilities
+import Language.Haskell.LSP.Types hiding (id, capabilities)
+import qualified Language.Haskell.LSP.Types as LSP
import Language.Haskell.LSP.VFS
import Language.Haskell.LSP.Test.Compat
import Language.Haskell.LSP.Test.Decoding
+import Language.Haskell.LSP.Test.Exceptions
import Language.Haskell.LSP.Test.Parsing
import Language.Haskell.LSP.Test.Session
import Language.Haskell.LSP.Test.Server
-> FilePath -- ^ The filepath to the root directory for the session.
-> Session a -- ^ The session to run.
-> IO a
-runSession = runSessionWithCapabilities def
+runSession = runSessionWithConfig def
-- | Starts a new sesion with a client with the specified capabilities.
-runSessionWithCapabilities :: ClientCapabilities -- ^ The capabilities the client should have.
+runSessionWithConfig :: SessionConfig -- ^ The capabilities the client should have.
-> String -- ^ The command to run the server.
-> FilePath -- ^ The filepath to the root directory for the session.
-> Session a -- ^ The session to run.
-> IO a
-runSessionWithCapabilities caps serverExe rootDir session = do
+runSessionWithConfig config serverExe rootDir session = do
pid <- getProcessID
absRootDir <- canonicalizePath rootDir
(Just $ T.pack absRootDir)
(Just $ filePathToUri absRootDir)
Nothing
- caps
+ (capabilities config)
(Just TraceOff)
- withServer serverExe $ \serverIn serverOut _ -> runSessionWithHandles serverIn serverOut listenServer rootDir $ do
+ withServer serverExe $ \serverIn serverOut _ ->
+ runSessionWithHandles serverIn serverOut listenServer config rootDir $ do
-- Wrap the session around initialize and shutdown calls
sendRequest Initialize initializeParams
x <- liftIO $ readChan c
yield x
chanSource c
+
\ No newline at end of file
--- /dev/null
+module Language.Haskell.LSP.Test.Exceptions where
+
+import Control.Exception
+
+data SessionException = TimeoutException
+ deriving Show
+instance Exception SessionException
+
+anySessionException :: SessionException -> Bool
+anySessionException = const True
\ No newline at end of file
module Language.Haskell.LSP.Test.Parsing where
import Control.Applicative
+import Control.Concurrent
+import Control.Monad.IO.Class
+import Control.Monad.Trans.Class
import Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as B
import Data.Conduit.Parser
import Data.Maybe
import Language.Haskell.LSP.Messages
import Language.Haskell.LSP.Types hiding (error)
+import Language.Haskell.LSP.Test.Exceptions
import Language.Haskell.LSP.Test.Messages
+import Language.Haskell.LSP.Test.Session
+
+satisfy :: (MonadIO m, MonadSessionConfig m) => (a -> Bool) -> ConduitParser a m a
+satisfy pred = do
+ timeout <- timeout <$> lift sessionConfig
+ tId <- liftIO myThreadId
+ liftIO $ forkIO $ do
+ threadDelay (timeout * 1000000)
+ throwTo tId TimeoutException
+ x <- await
+ if pred x
+ then return x
+ else empty
-- | Matches if the message is a notification.
-anyNotification :: Monad m => ConduitParser FromServerMessage m FromServerMessage
+anyNotification :: (MonadIO m, MonadSessionConfig m) => ConduitParser FromServerMessage m FromServerMessage
anyNotification = satisfy isServerNotification
-notification :: forall m a. (Monad m, FromJSON a) => ConduitParser FromServerMessage m (NotificationMessage ServerMethod a)
+notification :: forall m a. (MonadIO m, MonadSessionConfig m, FromJSON a) => ConduitParser FromServerMessage m (NotificationMessage ServerMethod a)
notification = do
let parser = decode . encodeMsg :: FromServerMessage -> Maybe (NotificationMessage ServerMethod a)
x <- satisfy (isJust . parser)
return $ decodeMsg $ encodeMsg x
-- | Matches if the message is a request.
-anyRequest :: Monad m => ConduitParser FromServerMessage m FromServerMessage
+anyRequest :: (MonadIO m, MonadSessionConfig m) => ConduitParser FromServerMessage m FromServerMessage
anyRequest = satisfy isServerRequest
-request :: forall m a b. (Monad m, FromJSON a, FromJSON b) => ConduitParser FromServerMessage m (RequestMessage ServerMethod a b)
+request :: forall m a b. (MonadIO m, MonadSessionConfig m, FromJSON a, FromJSON b) => ConduitParser FromServerMessage m (RequestMessage ServerMethod a b)
request = do
let parser = decode . encodeMsg :: FromServerMessage -> Maybe (RequestMessage ServerMethod a b)
x <- satisfy (isJust . parser)
return $ decodeMsg $ encodeMsg x
-- | Matches if the message is a response.
-anyResponse :: Monad m => ConduitParser FromServerMessage m FromServerMessage
+anyResponse :: (MonadIO m, MonadSessionConfig m) => ConduitParser FromServerMessage m FromServerMessage
anyResponse = satisfy isServerResponse
-response :: forall m a. (Monad m, FromJSON a) => ConduitParser FromServerMessage m (ResponseMessage a)
+response :: forall m a. (MonadIO m, MonadSessionConfig m, FromJSON a) => ConduitParser FromServerMessage m (ResponseMessage a)
response = do
let parser = decode . encodeMsg :: FromServerMessage -> Maybe (ResponseMessage a)
x <- satisfy (isJust . parser)
(decode x)
-- | Matches if the message is a log message notification or a show message notification/request.
-loggingNotification :: Monad m => ConduitParser FromServerMessage m FromServerMessage
+loggingNotification :: (MonadIO m, MonadSessionConfig m) => ConduitParser FromServerMessage m FromServerMessage
loggingNotification = satisfy shouldSkip
where
shouldSkip (NotLogMessage _) = True
shouldSkip (ReqShowMessage _) = True
shouldSkip _ = False
-publishDiagnosticsNotification :: Monad m => ConduitParser FromServerMessage m PublishDiagnosticsNotification
+publishDiagnosticsNotification :: (MonadIO m, MonadSessionConfig m) => ConduitParser FromServerMessage m PublishDiagnosticsNotification
publishDiagnosticsNotification = do
NotPublishDiagnostics diags <- satisfy test
return diags
where test (NotPublishDiagnostics _) = True
test _ = False
\ No newline at end of file
-
-satisfy :: Monad m => (a -> Bool) -> ConduitParser a m a
-satisfy pred = do
- x <- await
- if pred x
- then return x
- else empty
-
import qualified Data.Text as T
import Language.Haskell.LSP.Capture
import Language.Haskell.LSP.Messages
-import Language.Haskell.LSP.Types hiding (error)
+import Language.Haskell.LSP.Types as LSP hiding (error)
import Data.Aeson
+import Data.Default
import Data.List
import Data.Maybe
import Control.Lens hiding (List)
runSessionWithHandles serverIn
serverOut
(listenServer serverMsgs requestMap reqSema rspSema passVar)
+ def
sessionDir
(sendMessages clientMsgs reqSema rspSema)
swapCommands pid (FromServer t (RspInitialize rsp):xs) = FromServer t (RspInitialize swapped):swapCommands pid xs
where swapped = case newCommands of
- Just cmds -> result . _Just . capabilities . executeCommandProvider . _Just . commands .~ cmds $ rsp
+ Just cmds -> result . _Just . LSP.capabilities . executeCommandProvider . _Just . commands .~ cmds $ rsp
Nothing -> rsp
- oldCommands = rsp ^? result . _Just . capabilities . executeCommandProvider . _Just . commands
+ oldCommands = rsp ^? result . _Just . LSP.capabilities . executeCommandProvider . _Just . commands
newCommands = fmap (fmap (swapPid pid)) oldCommands
swapCommands pid (x:xs) = x:swapCommands pid xs
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE FlexibleInstances #-}
module Language.Haskell.LSP.Test.Session
( Session
- , SessionState(..)
+ , SessionConfig(..)
, SessionContext(..)
+ , SessionState(..)
+ , MonadSessionConfig(..)
, runSessionWithHandles
, get
, put
import Data.Aeson
import Data.Conduit
import Data.Conduit.Parser
+import Data.Default
import Data.Foldable
import Data.List
import qualified Data.HashMap.Strict as HashMap
import Language.Haskell.LSP.Messages
+import Language.Haskell.LSP.TH.ClientCapabilities
import Language.Haskell.LSP.Types
import Language.Haskell.LSP.VFS
import Language.Haskell.LSP.Test.Compat
import System.Directory
import System.IO
+-- | A session representing one instance of launching and connecting to a server.
+--
+-- You can send and receive messages to the server within 'Session' via 'getMessage',
+-- 'sendRequest' and 'sendNotification'.
+--
+-- @
+-- runSession \"path\/to\/root\/dir\" $ do
+-- docItem <- getDocItem "Desktop/simple.hs" "haskell"
+-- sendNotification TextDocumentDidOpen (DidOpenTextDocumentParams docItem)
+-- diagnostics <- getMessage :: Session PublishDiagnosticsNotification
+-- @
+type Session = ParserStateReader FromServerMessage SessionState SessionContext IO
+
+-- | Stuff you can configure for a 'Session'.
+data SessionConfig = SessionConfig
+ {
+ capabilities :: ClientCapabilities, -- ^ Specific capabilities the client should advertise.
+ timeout :: Int -- ^ Maximum time to wait for a request in seconds.
+ }
+
+instance Default SessionConfig where
+ def = SessionConfig def 60
+
+class Monad m => MonadSessionConfig m where
+ sessionConfig :: m SessionConfig
+
+instance Monad m => MonadSessionConfig (StateT SessionState (ReaderT SessionContext m)) where
+ sessionConfig = config <$> lift Reader.ask
+
data SessionContext = SessionContext
{
serverIn :: Handle
, messageChan :: Chan FromServerMessage
, requestMap :: MVar RequestMap
, initRsp :: MVar InitializeResponse
+ , config :: SessionConfig
}
data SessionState = SessionState
type ParserStateReader a s r m = ConduitParser a (StateT s (ReaderT r m))
--- | A session representing one instance of launching and connecting to a server.
---
--- You can send and receive messages to the server within 'Session' via 'getMessage',
--- 'sendRequest' and 'sendNotification'.
---
--- @
--- runSession \"path\/to\/root\/dir\" $ do
--- docItem <- getDocItem "Desktop/simple.hs" "haskell"
--- sendNotification TextDocumentDidOpen (DidOpenTextDocumentParams docItem)
--- diagnostics <- getMessage :: Session PublishDiagnosticsNotification
--- @
-type Session = ParserStateReader FromServerMessage SessionState SessionContext IO
-
type SessionProcessor = ConduitT FromServerMessage FromServerMessage (StateT SessionState (ReaderT SessionContext IO))
-runSession' :: Chan FromServerMessage -> SessionProcessor () -> SessionContext -> SessionState -> Session a -> IO (a, SessionState)
-runSession' chan preprocessor context state session = runReaderT (runStateT conduit state) context
+runSession :: Chan FromServerMessage -> SessionProcessor () -> SessionContext -> SessionState -> Session a -> IO (a, SessionState)
+runSession chan preprocessor context state session = runReaderT (runStateT conduit state) context
where conduit = runConduit $ chanSource chan .| preprocessor .| runConduitParser session
get :: Monad m => ParserStateReader a s r m s
runSessionWithHandles :: Handle -- ^ Server in
-> Handle -- ^ Server out
-> (Handle -> Session ()) -- ^ Server listener
+ -> SessionConfig
-> FilePath
-> Session a
-> IO a
-runSessionWithHandles serverIn serverOut serverHandler rootDir session = do
+runSessionWithHandles serverIn serverOut serverHandler config rootDir session = do
absRootDir <- canonicalizePath rootDir
hSetBuffering serverIn NoBuffering
meaninglessChan <- newChan
initRsp <- newEmptyMVar
- let context = SessionContext serverIn absRootDir messageChan reqMap initRsp
+ let context = SessionContext serverIn absRootDir messageChan reqMap initRsp config
initState = SessionState (IdInt 0) mempty
- threadId <- forkIO $ void $ runSession' meaninglessChan processor context initState (serverHandler serverOut)
- (result, _) <- runSession' messageChan processor context initState session
+ threadId <- forkIO $ void $ runSession meaninglessChan processor context initState (serverHandler serverOut)
+ (result, _) <- runSession messageChan processor context initState session
killThread threadId
{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE FlexibleInstances #-}
module ParsingTests where
import Control.Lens hiding (List)
import Language.Haskell.LSP.Types
import Data.Conduit
import Data.Conduit.Parser
+import Data.Default
import Test.Hspec
type TestSession = ConduitParser FromServerMessage IO
+instance MonadSessionConfig IO where
+ sessionConfig = return def
+
parsingSpec :: Spec
parsingSpec =
describe "diagnostics" $ do
import Data.Default
import qualified Data.HashMap.Strict as HM
import Data.Maybe
+import Control.Concurrent
import Control.Monad.IO.Class
import Control.Lens hiding (List)
import GHC.Generics
import Language.Haskell.LSP.Test
import Language.Haskell.LSP.Test.Replay
import Language.Haskell.LSP.TH.ClientCapabilities
-import Language.Haskell.LSP.Types
+import Language.Haskell.LSP.Types hiding (capabilities)
import ParsingTests
main = hspec $ do
let caps = def { _workspace = Just workspaceCaps }
workspaceCaps = def { _didChangeConfiguration = Just configCaps }
configCaps = DidChangeConfigurationClientCapabilities (Just True)
- runSessionWithCapabilities caps "hie --lsp" "test/data/renamePass" $ return ()
+ conf = def { capabilities = caps }
+ runSessionWithConfig conf "hie --lsp" "test/data/renamePass" $ return ()
+
+ it "times out" $
+ let sesh = runSessionWithConfig (def {timeout = 10}) "hie --lsp" "test/data/renamePass" $ do
+ skipMany loggingNotification
+ _ <- request :: Session ApplyWorkspaceEditRequest
+ return ()
+ in sesh `shouldThrow` anySessionException
+
+ it "doesn't time out" $ runSessionWithConfig (def {timeout = 10}) "hie --lsp" "test/data/renamePass" $ do
+ loggingNotification
+ liftIO $ threadDelay 5
+
describe "replay session" $ do
it "passes a test" $