1 {-# LANGUAGE OverloadedStrings #-}
2 {-# LANGUAGE RankNTypes #-}
3 {-# LANGUAGE ScopedTypeVariables #-}
4 {-# LANGUAGE ExistentialQuantification #-}
7 -- Module : Language.Haskell.LSP.Test
8 -- Description : A functional testing framework for LSP servers.
9 -- Maintainer : luke_lau@icloud.com
10 -- Stability : experimental
12 -- A framework for testing <https://github.com/Microsoft/language-server-protocol Language Server Protocol servers> at the JSON level.
14 module Language.Haskell.LSP.Test
18 , runSessionWithHandler
31 , publishDiagnosticsNotification
69 import Control.Applicative
71 import Control.Monad.IO.Class
72 import Control.Concurrent
74 import qualified Data.Text as T
75 import qualified Data.Text.IO as T
77 import qualified Data.ByteString.Lazy.Char8 as B
81 import Language.Haskell.LSP.Types
82 import qualified Language.Haskell.LSP.Types as LSP (error)
83 import Language.Haskell.LSP.Messages
84 import Language.Haskell.LSP.Test.Compat
86 import System.Directory
87 import System.FilePath
88 import Language.Haskell.LSP.Test.Decoding
89 import Language.Haskell.LSP.Test.Parsing
90 import Text.Parser.Combinators
92 -- | Starts a new session.
93 runSession :: FilePath -- ^ The filepath to the root directory for the session.
94 -> Session a -- ^ The session to run.
96 runSession rootDir session = do
98 absRootDir <- canonicalizePath rootDir
100 let initializeParams = InitializeParams (Just pid)
101 (Just $ T.pack absRootDir)
102 (Just $ filePathToUri absRootDir)
107 runSessionWithHandler listenServer rootDir $ do
109 -- Wrap the session around initialize and shutdown calls
110 sendRequest (Proxy :: Proxy InitializeRequest) Initialize initializeParams
111 RspInitialize initRsp <- response
112 liftIO $ maybe (return ()) (putStrLn . ("Error while initializing: " ++) . show ) (initRsp ^. LSP.error)
114 sendNotification Initialized InitializedParams
116 -- Run the actual test
119 sendNotification Exit ExitParams
121 -- | An internal version of 'runSession' that allows for a custom handler to listen to the server.
122 -- It also does not automatically send initialize and exit messages.
123 runSessionWithHandler :: (Handle -> Session ())
127 runSessionWithHandler serverHandler rootDir session = do
128 absRootDir <- canonicalizePath rootDir
130 (Just serverIn, Just serverOut, Nothing, serverProc) <- createProcess
131 (proc "hie" ["--lsp", "-d", "-l", "/tmp/hie-test.log"])
132 { std_in = CreatePipe, std_out = CreatePipe }
134 hSetBuffering serverIn NoBuffering
135 hSetBuffering serverOut NoBuffering
137 reqMap <- newMVar newRequestMap
138 messageChan <- newChan
139 meaninglessChan <- newChan
141 let context = SessionContext serverIn absRootDir messageChan reqMap
142 initState = SessionState (IdInt 9)
144 forkIO $ void $ runSession' meaninglessChan context initState (serverHandler serverOut)
145 (result, _) <- runSession' messageChan context initState session
147 terminateProcess serverProc
151 -- | Listens to the server output, makes sure it matches the record and
152 -- signals any semaphores
153 listenServer :: Handle -> Session ()
154 listenServer serverOut = do
155 msgBytes <- liftIO $ getNextMessage serverOut
158 reqMap <- liftIO $ readMVar $ requestMap context
160 liftIO $ writeChan (messageChan context) $ decodeFromServerMsg reqMap msgBytes
162 listenServer serverOut
164 -- | Sends a request to the server.
167 -- sendRequest (Proxy :: Proxy DocumentSymbolRequest)
168 -- TextDocumentDocumentSymbol
169 -- (DocumentSymbolParams docId)
172 :: forall params resp. (ToJSON params, ToJSON resp, FromJSON resp)
173 => Proxy (RequestMessage ClientMethod params resp) -- ^ A proxy to provide more type information about the request.
174 -> ClientMethod -- ^ The request method.
175 -> params -- ^ The request parameters.
176 -> Session LspId -- ^ The id of the request that was sent.
177 sendRequest _ method params = do
178 id <- curReqId <$> get
179 modify $ \c -> c { curReqId = nextId id }
181 let req = RequestMessage "2.0" id method params :: RequestMessage ClientMethod params resp
187 where nextId (IdInt i) = IdInt (i + 1)
188 nextId (IdString s) = IdString $ T.pack $ show $ read (T.unpack s) + 1
190 sendRequest' :: (ToJSON a, ToJSON b) => RequestMessage ClientMethod a b -> Session ()
191 sendRequest' req = do
192 -- Update the request map
193 reqMap <- requestMap <$> ask
194 liftIO $ modifyMVar_ reqMap (return . flip updateRequestMap req)
198 -- | Sends a notification to the server.
199 sendNotification :: ToJSON a
200 => ClientMethod -- ^ The notification method.
201 -> a -- ^ The notification parameters.
203 sendNotification method params =
204 let notif = NotificationMessage "2.0" method params
205 in sendNotification' notif
207 sendNotification' :: (ToJSON a, ToJSON b) => NotificationMessage a b -> Session ()
208 sendNotification' = sendMessage
210 sendResponse :: ToJSON a => ResponseMessage a -> Session ()
211 sendResponse = sendMessage
213 sendMessage :: ToJSON a => a -> Session ()
215 h <- serverIn <$> ask
216 liftIO $ B.hPut h $ addHeader (encode msg)
218 -- | Opens a text document and sends a notification to the client.
219 openDoc :: FilePath -> String -> Session TextDocumentIdentifier
220 openDoc file languageId = do
221 item <- getDocItem file languageId
222 sendNotification TextDocumentDidOpen (DidOpenTextDocumentParams item)
223 TextDocumentIdentifier <$> getDocUri file
225 -- | Reads in a text document as the first version.
226 getDocItem :: FilePath -- ^ The path to the text document to read in.
227 -> String -- ^ The language ID, e.g "haskell" for .hs files.
228 -> Session TextDocumentItem
229 getDocItem file languageId = do
231 let fp = rootDir context </> file
232 contents <- liftIO $ T.readFile fp
233 return $ TextDocumentItem (filePathToUri fp) (T.pack languageId) 0 contents
235 -- | Gets the Uri for the file corrected to the session directory.
236 getDocUri :: FilePath -> Session Uri
239 let fp = rootDir context </> file
240 return $ filePathToUri fp