1 {-# LANGUAGE OverloadedStrings #-}
2 {-# LANGUAGE FlexibleContexts #-}
3 -- | A testing tool for replaying recorded client logs back to a server,
4 -- and validating that the server output matches up with another log.
5 module Language.Haskell.LSP.Test.Recorded
10 import Control.Concurrent
11 import Control.Monad.Trans.Class
12 import Control.Monad.Trans.Reader
14 import Language.Haskell.LSP.Control as Control
15 import qualified Data.ByteString.Lazy.Char8 as B
16 import Language.Haskell.LSP.Core
17 import qualified Language.Haskell.LSP.Types as LSP
23 import System.Directory
25 import Language.Haskell.LSP.Test.Files
26 import Language.Haskell.LSP.Test.Parsing
28 -- | Replays a recorded client output and
29 -- makes sure it matches up with an expected response.
31 :: FilePath -- ^ The client output to replay to the server.
32 -> FilePath -- ^ The expected response from the server.
36 -- need to keep hold of current directory since haskell-lsp changes it
37 prevDir <- getCurrentDirectory
39 (Just serverIn, Just serverOut, _, serverProc) <- createProcess
40 (proc "hie" ["--lsp", "-l", "/tmp/hie.log"]) { std_in = CreatePipe , std_out = CreatePipe }
42 hSetBuffering serverIn NoBuffering
43 hSetBuffering serverOut NoBuffering
45 -- whether to send the next request
46 reqSema <- newEmptyMVar :: IO (MVar LSP.LspIdRsp)
47 -- whether to send the next response
48 rspSema <- newEmptyMVar :: IO (MVar LSP.LspId)
49 let semas = (reqSema, rspSema)
51 didPass <- newEmptyMVar
53 -- the recorded client input to the server
54 clientRecIn <- openFile cfp ReadMode
55 serverRecIn <- openFile sfp ReadMode
56 null <- openFile "/dev/null" WriteMode
59 (clientMsgs, fileMap) <- swapFiles emptyFileMap clientRecIn
61 tmpDir <- getTemporaryDirectory
62 (_, mappedClientRecIn) <- openTempFile tmpDir "clientRecInMapped"
63 mapM_ (B.hPut mappedClientRecIn . addHeader) clientMsgs
64 hSeek mappedClientRecIn AbsoluteSeek 0
66 (expectedMsgs, _) <- swapFiles fileMap serverRecIn
69 forkIO $ runReaderT (listenServer expectedMsgs serverOut semas) didPass
71 -- start client replay
73 Control.runWithHandles mappedClientRecIn
75 (const $ Right (), const $ return Nothing)
76 (handlers serverIn semas)
81 -- todo: we shouldn't do this, we should check all notifications were delivered first
84 result <- takeMVar didPass
85 terminateProcess serverProc
88 setCurrentDirectory prevDir
92 -- | The internal monad for tests that can fail or pass,
93 -- ending execution early.
94 type Session = ReaderT (MVar Bool) IO
96 failSession :: String -> Session ()
97 failSession reason = do
98 lift $ putStrLn reason
100 lift $ putMVar passVar False
102 passSession :: Session ()
105 lift $ putMVar passVar True
107 -- | Listens to the server output, makes sure it matches the record and
108 -- signals any semaphores
109 listenServer :: [B.ByteString] -> Handle -> (MVar LSP.LspIdRsp, MVar LSP.LspId) -> Session ()
110 listenServer [] _ _ = passSession
111 listenServer expectedMsgs h semas@(reqSema, rspSema) = do
112 msg <- lift $ getNextMessage h
114 newExpectedMsgs <- case decode msg of
116 Nothing -> case decode msg of
117 Just m -> notification m
118 Nothing -> case decode msg of
120 Nothing -> failSession "Malformed message" >> return expectedMsgs
122 listenServer newExpectedMsgs h semas
125 where jsonEqual :: (FromJSON a, Eq a) => a -> B.ByteString -> Bool
126 jsonEqual x y = Just x == decode y
128 deleteFirstJson _ [] = []
129 deleteFirstJson msg (x:xs)
130 | jsonEqual msg x = xs
131 | otherwise = x:deleteFirstJson msg xs
133 -- firstExpected :: Show a => a
134 firstExpected = head $ filter (not . isNotification) expectedMsgs
136 response :: LSP.ResponseMessage Value -> Session [B.ByteString]
138 lift $ putStrLn $ "Got response for id " ++ show (res ^. LSP.id)
144 lift $ putMVar reqSema (res ^. LSP.id) -- unblock the handler waiting to send a request
146 return $ deleteFirstJson res expectedMsgs
148 request :: LSP.RequestMessage LSP.ServerMethod Value Value -> Session [B.ByteString]
150 lift $ putStrLn $ "Got request for id " ++ show (req ^. LSP.id) ++ " " ++ show (req ^. LSP.method)
156 lift $ putMVar rspSema (req ^. LSP.id) -- unblock the handler waiting for a response
158 return $ deleteFirstJson req expectedMsgs
160 notification :: LSP.NotificationMessage LSP.ServerMethod Value -> Session [B.ByteString]
162 lift $ putStrLn $ "Got notification " ++ show (n ^. LSP.method)
164 return $ deleteFirstJson n expectedMsgs
166 checkOrder msg = unless (inRightOrder msg expectedMsgs) $ do
167 let expected = decode firstExpected
168 _ = expected == Just msg -- make expected type same as res
169 failSession ("Out of order\nExpected\n" ++ show expected ++ "\nGot\n" ++ show msg ++ "\n")
172 isNotification :: B.ByteString -> Bool
174 isJust (decode msg :: Maybe (LSP.NotificationMessage Value Value))
176 -- TODO: QuickCheck tests?
177 -- | Checks wether or not the message appears in the right order
178 -- @ N1 N2 N3 REQ1 N4 N5 REQ2 RES1 @
179 -- given N2, notification order doesn't matter.
180 -- @ N1 N3 REQ1 N4 N5 REQ2 RES1 @
182 -- @ N1 N3 N4 N5 REQ2 RES1 @
184 -- @ N1 N3 N4 N5 XXXX RES1 @ False!
185 -- Order of requests and responses matter
186 inRightOrder :: (FromJSON a, Eq a) => a -> [B.ByteString] -> Bool
188 inRightOrder _ [] = error "Why is this empty"
189 -- inRightOrder (LSP.NotificationMessage _ _ _) _ = True
191 inRightOrder received (expected:msgs)
192 | Just received == decode expected = True
193 | isNotification expected = inRightOrder received msgs
197 handlers :: Handle -> (MVar LSP.LspIdRsp, MVar LSP.LspId) -> Handlers
198 handlers serverH (reqSema, rspSema) = def
201 hoverHandler = Just request
202 , completionHandler = Just request
203 , completionResolveHandler = Just request
204 , signatureHelpHandler = Just request
205 , definitionHandler = Just request
206 , referencesHandler = Just request
207 , documentHighlightHandler = Just request
208 , documentSymbolHandler = Just request
209 , workspaceSymbolHandler = Just request
210 , codeActionHandler = Just request
211 , codeLensHandler = Just request
212 , codeLensResolveHandler = Just request
213 , documentFormattingHandler = Just request
214 , documentRangeFormattingHandler = Just request
215 , documentTypeFormattingHandler = Just request
216 , renameHandler = Just request
217 , documentLinkHandler = Just request
218 , documentLinkResolveHandler = Just request
219 , executeCommandHandler = Just request
220 , initializeRequestHandler = Just request
222 , didChangeConfigurationParamsHandler = Just notification
223 , didOpenTextDocumentNotificationHandler = Just notification
224 , didChangeTextDocumentNotificationHandler = Just notification
225 , didCloseTextDocumentNotificationHandler = Just notification
226 , didSaveTextDocumentNotificationHandler = Just notification
227 , didChangeWatchedFilesNotificationHandler = Just notification
228 , initializedHandler = Just notification
229 , willSaveTextDocumentNotificationHandler = Just notification
230 , cancelNotificationHandler = Just notification
231 , exitNotificationHandler = Just notification
233 , responseHandler = Just response
237 -- TODO: May need to prevent premature exit notification being sent
238 -- notification msg@(LSP.NotificationMessage _ LSP.Exit _) = do
239 -- putStrLn "Will send exit notification soon"
240 -- threadDelay 10000000
241 -- B.hPut serverH $ addHeader (encode msg)
242 notification msg@(LSP.NotificationMessage _ m _) = do
243 B.hPut serverH $ addHeader (encode msg)
245 putStrLn $ "Sent a notification " ++ show m
247 request msg@(LSP.RequestMessage _ id m _) = do
249 when (m == LSP.TextDocumentDocumentSymbol) $ threadDelay 5000000
251 B.hPut serverH $ addHeader (encode msg)
252 putStrLn $ "Sent a request id " ++ show id ++ ": " ++ show m ++ "\nWaiting for a response"
254 rspId <- takeMVar reqSema
255 when (LSP.responseId id /= rspId)
262 response msg@(LSP.ResponseMessage _ id _ _) = do
263 putStrLn $ "Waiting for request id " ++ show id ++ " from the server"
264 reqId <- takeMVar rspSema
265 if LSP.responseId reqId /= id
266 then error $ "Expected id " ++ show reqId ++ ", got " ++ show reqId
268 B.hPut serverH $ addHeader (encode msg)
269 putStrLn $ "Sent response to request id " ++ show id