1 {-# LANGUAGE OverloadedStrings #-}
2 {-# LANGUAGE FlexibleContexts #-}
3 {-# LANGUAGE RankNTypes #-}
4 -- | A testing tool for replaying recorded client logs back to a server,
5 -- and validating that the server output matches up with another log.
6 module Language.Haskell.LSP.Test.Recorded
12 import Control.Concurrent
13 import Control.Monad.Trans.Class
14 import Control.Monad.Trans.Reader
15 import Control.Monad.Trans.State
16 import Control.Monad.IO.Class
17 import qualified Data.ByteString.Lazy.Char8 as B
18 import Language.Haskell.LSP.Capture
19 import Language.Haskell.LSP.Messages
20 import qualified Language.Haskell.LSP.Types as LSP
26 import System.Directory
27 import System.FilePath
29 import Language.Haskell.LSP.Test.Files
30 import Language.Haskell.LSP.Test.Parsing
32 data SessionContext = SessionContext
34 reqSema :: MVar LSP.LspId,
35 rspSema :: MVar LSP.LspIdRsp,
38 type Session = StateT [FromClientMessage] (ReaderT SessionContext IO)
40 -- | Replays a recorded client output and
41 -- makes sure it matches up with an expected response.
42 replay :: FilePath -- ^ The recorded session directory.
45 replay sessionDir session = do
47 let sessionFp = sessionDir </> "session.log"
49 -- need to keep hold of current directory since haskell-lsp changes it
50 prevRootDir <- getCurrentDirectory
52 (Just serverIn, Just serverOut, _, serverProc) <- createProcess
53 (proc "hie" ["--lsp", "-l", "/tmp/hie.log"]) { std_in = CreatePipe
54 , std_out = CreatePipe
57 hSetBuffering serverIn NoBuffering
58 hSetBuffering serverOut NoBuffering
60 -- whether to send the next request
61 reqSema <- newEmptyMVar :: IO (MVar LSP.LspIdRsp)
62 -- whether to send the next response
63 rspSema <- newEmptyMVar :: IO (MVar LSP.LspId)
64 let semas = (reqSema, rspSema)
66 entries <- B.lines <$> B.readFile sessionFp
69 let unswappedEvents = map (fromJust . decode) entries
71 events <- swapFiles sessionDir unswappedEvents
73 let clientEvents = map (\(FromClient _ msg) -> msg) $ filter isClientMsg events
74 requestMap = getRequestMap clientEvents
77 forkIO $ listenServer serverOut requestMap semas
79 runReaderT (runStateT session clientEvents) (SessionContext rspSema reqSema serverIn)
81 terminateProcess serverProc
84 setCurrentDirectory prevRootDir
87 isClientMsg (FromClient _ _) = True
90 isServerMsg (FromServer _ _) = True
93 sendNextRequest :: Session ()
95 (nextMsg:remainingMsgs) <- get
98 ReqInitialize m -> request m
99 ReqShutdown m -> request m
100 ReqHover m -> request m
101 ReqCompletion m -> request m
102 ReqCompletionItemResolve m -> request m
103 ReqSignatureHelp m -> request m
104 ReqDefinition m -> request m
105 ReqFindReferences m -> request m
106 ReqDocumentHighlights m -> request m
107 ReqDocumentSymbols m -> request m
108 ReqWorkspaceSymbols m -> request m
109 ReqCodeAction m -> request m
110 ReqCodeLens m -> request m
111 ReqCodeLensResolve m -> request m
112 ReqDocumentFormatting m -> request m
113 ReqDocumentRangeFormatting m -> request m
114 ReqDocumentOnTypeFormatting m -> request m
115 ReqRename m -> request m
116 ReqExecuteCommand m -> request m
117 ReqDocumentLink m -> request m
118 ReqDocumentLinkResolve m -> request m
119 ReqWillSaveWaitUntil m -> request m
120 RspApplyWorkspaceEdit m -> response m
121 RspFromClient m -> response m
122 NotInitialized m -> notification m
123 NotExit m -> notification m
124 NotCancelRequestFromClient m -> notification m
125 NotDidChangeConfiguration m -> notification m
126 NotDidOpenTextDocument m -> notification m
127 NotDidChangeTextDocument m -> notification m
128 NotDidCloseTextDocument m -> notification m
129 NotWillSaveTextDocument m -> notification m
130 NotDidSaveTextDocument m -> notification m
131 NotDidChangeWatchedFiles m -> notification m
132 UnknownFromClientMessage m ->
133 error $ "Unknown message was recorded from the client" ++ show m
135 -- TODO: May need to prevent premature exit notification being sent
136 notification msg@(LSP.NotificationMessage _ LSP.Exit _) = do
140 putStrLn "Will send exit notification soon"
142 B.hPut (serverIn context) $ addHeader (encode msg)
144 notification msg@(LSP.NotificationMessage _ m _) = do
147 liftIO $ B.hPut (serverIn context) $ addHeader (encode msg)
149 liftIO $ putStrLn $ "Sent a notification " ++ show m
153 request msg@(LSP.RequestMessage _ id m _) = do
157 when (m == LSP.TextDocumentDocumentSymbol) $ threadDelay 5000000
159 B.hPut (serverIn context) $ addHeader (encode msg)
160 putStrLn $ "Sent a request id " ++ show id ++ ": " ++ show m ++ "\nWaiting for a response"
162 rspId <- takeMVar (rspSema context)
163 when (LSP.responseId id /= rspId) $
164 error $ "Expected id " ++ show id ++ ", got " ++ show rspId
166 response msg@(LSP.ResponseMessage _ id _ _) = do
170 putStrLn $ "Waiting for request id " ++ show id ++ " from the server"
171 reqId <- takeMVar (reqSema context)
172 if LSP.responseId reqId /= id
173 then error $ "Expected id " ++ show reqId ++ ", got " ++ show reqId
175 B.hPut (serverIn context) $ addHeader (encode msg)
176 putStrLn $ "Sent response to request id " ++ show id
181 -- | Listens to the server output, makes sure it matches the record and
182 -- signals any semaphores
183 listenServer :: Handle -> RequestMap -> (MVar LSP.LspIdRsp, MVar LSP.LspId) -> IO ()
184 listenServer h reqMap semas@(reqSema, rspSema) = do
185 msgBytes <- getNextMessage h
187 let msg = decodeFromServerMsg reqMap msgBytes
192 ReqRegisterCapability m -> request m
193 ReqApplyWorkspaceEdit m -> request m
194 ReqShowMessage m -> request m
195 ReqUnregisterCapability m -> request m
196 RspInitialize m -> response m
197 RspShutdown m -> response m
198 RspHover m -> response m
199 RspCompletion m -> response m
200 RspCompletionItemResolve m -> response m
201 RspSignatureHelp m -> response m
202 RspDefinition m -> response m
203 RspFindReferences m -> response m
204 RspDocumentHighlights m -> response m
205 RspDocumentSymbols m -> response m
206 RspWorkspaceSymbols m -> response m
207 RspCodeAction m -> response m
208 RspCodeLens m -> response m
209 RspCodeLensResolve m -> response m
210 RspDocumentFormatting m -> response m
211 RspDocumentRangeFormatting m -> response m
212 RspDocumentOnTypeFormatting m -> response m
213 RspRename m -> response m
214 RspExecuteCommand m -> response m
215 RspError m -> response m
216 RspDocumentLink m -> response m
217 RspDocumentLinkResolve m -> response m
218 RspWillSaveWaitUntil m -> response m
219 NotPublishDiagnostics m -> notification m
220 NotLogMessage m -> notification m
221 NotShowMessage m -> notification m
222 NotTelemetry m -> notification m
223 NotCancelRequestFromServer m -> notification m
225 listenServer h reqMap semas
228 response :: Show a => LSP.ResponseMessage a -> IO ()
230 putStrLn $ "Got response for id " ++ show (res ^. LSP.id)
234 putMVar reqSema (res ^. LSP.id) -- unblock the handler waiting to send a request
236 request :: Show a => LSP.RequestMessage LSP.ServerMethod a b -> IO ()
239 $ "Got request for id "
240 ++ show (req ^. LSP.id)
242 ++ show (req ^. LSP.method)
246 putMVar rspSema (req ^. LSP.id) -- unblock the handler waiting for a response
248 notification :: Show a => LSP.NotificationMessage LSP.ServerMethod a -> IO ()
250 putStrLn $ "Got notification " ++ show (n ^. LSP.method)
255 -- $ show (length (filter isNotification expectedMsgs) - 1)
256 -- ++ " notifications remaining"
258 -- checkOrder msg = unless (inRightOrder msg expectedMsgs) $ failSession
259 -- ( "Out of order\nExpected\n"
260 -- ++ show firstExpected
266 -- markReceived :: FromServerMessage -> Session [FromServerMessage]
267 -- markReceived msg =
268 -- let new = delete msg expectedMsgs
269 -- in if new == expectedMsgs
270 -- then failSession ("Unexpected message: " ++ show msg) >> return new
273 -- firstExpected = head $ filter (not . isNotification) expectedMsgs
275 isNotification :: FromServerMessage -> Bool
276 isNotification (NotPublishDiagnostics _) = True
277 isNotification (NotLogMessage _) = True
278 isNotification (NotShowMessage _) = True
279 isNotification (NotCancelRequestFromServer _) = True
280 isNotification _ = False
282 -- TODO: QuickCheck tests?
283 -- | Checks wether or not the message appears in the right order
284 -- @ N1 N2 N3 REQ1 N4 N5 REQ2 RES1 @
285 -- given N2, notification order doesn't matter.
286 -- @ N1 N3 REQ1 N4 N5 REQ2 RES1 @
288 -- @ N1 N3 N4 N5 REQ2 RES1 @
290 -- @ N1 N3 N4 N5 XXXX RES1 @ False!
291 -- Order of requests and responses matter
292 inRightOrder :: FromServerMessage -> [FromServerMessage] -> Bool
294 inRightOrder _ [] = error "Why is this empty"
295 -- inRightOrder (LSP.NotificationMessage _ _ _) _ = True
297 inRightOrder received (expected : msgs)
298 | received == expected = True
299 | isNotification expected = inRightOrder received msgs
302 -- | The internal monad for tests that can fail or pass,
303 -- ending execution early.
304 -- type Session = ReaderT (MVar Bool) IO
306 -- -- TODO: Make return type polymoprhic more like error
307 -- failSession :: String -> Session ()
308 -- failSession reason = do
309 -- lift $ putStrLn reason
311 -- lift $ putMVar passVar False
313 -- passSession :: Session ()
316 -- lift $ putMVar passVar True