1 {-# LANGUAGE OverloadedStrings #-}
2 -- | A testing tool for replaying recorded client logs back to a server,
3 -- and validating that the server output matches up with another log.
4 module Language.Haskell.LSP.Test.Recorded
9 import Control.Concurrent
10 import Control.Monad.Trans.Class
11 import Control.Monad.Trans.Reader
13 import Language.Haskell.LSP.Control as Control
14 import qualified Data.ByteString.Lazy.Char8 as B
15 import Language.Haskell.LSP.Core
16 import qualified Language.Haskell.LSP.Types as LSP
23 import System.Directory
26 -- | Replays a recorded client output and
27 -- makes sure it matches up with an expected response.
29 :: FilePath -- ^ The client output to replay to the server.
30 -> FilePath -- ^ The expected response from the server.
34 -- need to keep hold of current directory since haskell-lsp changes it
35 prevDir <- getCurrentDirectory
37 (Just serverIn, Just serverOut, _, serverProc) <- createProcess
38 (proc "hie" ["--lsp", "-l", "/tmp/hie.log", "-d"]) { std_in = CreatePipe
39 , std_out = CreatePipe
42 hSetBuffering serverIn NoBuffering
43 hSetBuffering serverOut NoBuffering
46 -- whether to send the next request
47 reqSema <- newEmptyMVar :: IO (MVar LSP.LspIdRsp)
48 -- whether to send the next response
49 rspSema <- newEmptyMVar :: IO (MVar LSP.LspId)
50 let semas = (reqSema, rspSema)
52 didPass <- newEmptyMVar
54 -- the recorded client input to the server
55 clientRecIn <- openFile cfp ReadMode
56 serverRecIn <- openFile sfp ReadMode
57 null <- openFile "/dev/null" WriteMode
59 expectedMsgs <- getAllMessages serverRecIn
62 forkIO $ runReaderT (listenServer expectedMsgs serverOut semas) didPass
64 -- start client replay
66 Control.runWithHandles clientRecIn
68 (const $ Right (), const $ return Nothing)
69 (handlers serverIn semas)
74 -- todo: we shouldn't do this, we should check all notifications were delivered first
77 result <- takeMVar didPass
78 terminateProcess serverProc
81 setCurrentDirectory prevDir
85 -- | The internal monad for tests that can fail or pass,
86 -- ending execution early.
87 type Session = ReaderT (MVar Bool) IO
89 failSession :: String -> Session ()
90 failSession reason = do
91 lift $ putStrLn reason
93 lift $ putMVar passVar False
95 passSession :: Session ()
98 lift $ putMVar passVar True
100 -- | Listens to the server output, makes sure it matches the record and
101 -- signals any semaphores
102 listenServer :: [B.ByteString] -> Handle -> (MVar LSP.LspIdRsp, MVar LSP.LspId) -> Session ()
103 listenServer [] _ _ = passSession
104 listenServer expectedMsgs h semas@(reqSema, rspSema) = do
105 msg <- lift $ getNextMessage h
106 lift $ putStrLn $ "Remaining messages " ++ show (length expectedMsgs)
107 if inRightOrder msg expectedMsgs
110 whenResponse msg $ \res -> lift $ do
111 putStrLn $ "Got response for id " ++ show (res ^. LSP.id)
112 putMVar reqSema (res ^. LSP.id) -- unblock the handler waiting to send a request
114 whenRequest msg $ \req -> lift $ do
115 putStrLn $ "Got request for id " ++ show (req ^. LSP.id) ++ " " ++ show (req ^. LSP.method)
116 putMVar rspSema (req ^. LSP.id) -- unblock the handler waiting for a response
118 whenNotification msg $ \n -> lift $ putStrLn $ "Got notification " ++ show (n ^. LSP.method)
120 unless (msg `elem` expectedMsgs) $ failSession "Got an unexpected message"
122 listenServer (delete msg expectedMsgs) h semas
124 let reason = "Got: " ++ show msg ++ "\n Expected: " ++ show (head (filter (not . isNotification) expectedMsgs))
125 in failSession reason
127 isNotification :: B.ByteString -> Bool
129 isJust (decode msg :: Maybe (LSP.NotificationMessage Value Value))
131 whenResponse :: B.ByteString -> (LSP.ResponseMessage Value -> Session ()) -> Session ()
132 whenResponse msg f = case decode msg :: Maybe (LSP.ResponseMessage Value) of
133 Just msg' -> when (isJust (msg' ^. LSP.result)) (f msg')
137 :: B.ByteString -> (LSP.RequestMessage Value Value Value -> Session ()) -> Session ()
139 forM_ (decode msg :: (Maybe (LSP.RequestMessage Value Value Value)))
141 whenNotification :: B.ByteString -> (LSP.NotificationMessage Value Value -> Session ()) -> Session ()
142 whenNotification msg = forM_ (decode msg :: (Maybe (LSP.NotificationMessage Value Value)))
144 -- TODO: QuickCheck tests?
145 -- | Checks wether or not the message appears in the right order
146 -- @ N1 N2 N3 REQ1 N4 N5 REQ2 RES1 @
147 -- given N2, notification order doesn't matter.
148 -- @ N1 N3 REQ1 N4 N5 REQ2 RES1 @
150 -- @ N1 N3 N4 N5 REQ2 RES1 @
152 -- @ N1 N3 N4 N5 XXXX RES1 @ False!
153 -- Order of requests and responses matter
154 inRightOrder :: B.ByteString -> [B.ByteString] -> Bool
155 inRightOrder _ [] = error "why is this empty"
156 inRightOrder received msgs = received `elem` valid
158 valid = takeWhile canSkip msgs ++ firstNonSkip
159 -- we don't care about the order of notifications
160 canSkip = isNotification
161 nonSkip = dropWhile canSkip msgs
162 firstNonSkip | null nonSkip = []
163 | otherwise = [head nonSkip]
165 getAllMessages :: Handle -> IO [B.ByteString]
166 getAllMessages h = do
171 msg <- getNextMessage h
172 (msg :) <$> getAllMessages h
174 -- | Fetches the next message bytes based on
175 -- the Content-Length header
176 getNextMessage :: Handle -> IO B.ByteString
177 getNextMessage h = do
178 headers <- getHeaders h
179 case read . init <$> lookup "Content-Length" headers of
180 Nothing -> error "Couldn't read Content-Length header"
181 Just size -> B.hGet h size
184 handlers :: Handle -> (MVar LSP.LspIdRsp, MVar LSP.LspId) -> Handlers
185 handlers serverH (reqSema, rspSema) = def
188 hoverHandler = Just request
189 , completionHandler = Just request
190 , completionResolveHandler = Just request
191 , signatureHelpHandler = Just request
192 , definitionHandler = Just request
193 , referencesHandler = Just request
194 , documentHighlightHandler = Just request
195 , documentSymbolHandler = Just request
196 , workspaceSymbolHandler = Just request
197 , codeActionHandler = Just request
198 , codeLensHandler = Just request
199 , codeLensResolveHandler = Just request
200 , documentFormattingHandler = Just request
201 , documentRangeFormattingHandler = Just request
202 , documentTypeFormattingHandler = Just request
203 , renameHandler = Just request
204 , documentLinkHandler = Just request
205 , documentLinkResolveHandler = Just request
206 , executeCommandHandler = Just request
207 , initializeRequestHandler = Just request
209 , didChangeConfigurationParamsHandler = Just notification
210 , didOpenTextDocumentNotificationHandler = Just notification
211 , didChangeTextDocumentNotificationHandler = Just notification
212 , didCloseTextDocumentNotificationHandler = Just notification
213 , didSaveTextDocumentNotificationHandler = Just notification
214 , didChangeWatchedFilesNotificationHandler = Just notification
215 , initializedHandler = Just notification
216 , willSaveTextDocumentNotificationHandler = Just notification
217 , cancelNotificationHandler = Just notification
218 , exitNotificationHandler = Just notification
220 , responseHandler = Just response
223 -- TODO: May need to prevent premature exit notification being sent
224 -- notification msg@(LSP.NotificationMessage _ LSP.Exit _) = do
225 -- putStrLn "Will send exit notification soon"
226 -- threadDelay 10000000
227 -- B.hPut serverH $ addHeader (encode msg)
228 notification msg@(LSP.NotificationMessage _ m _) = do
229 B.hPut serverH $ addHeader (encode msg)
231 putStrLn $ "Sent a notification " ++ show m
233 request msg@(LSP.RequestMessage _ id m _) = do
234 B.hPut serverH $ addHeader (encode msg)
235 putStrLn $ "Sent a request id " ++ show id ++ ": " ++ show m ++ "\nWaiting for a response"
237 rspId <- takeMVar reqSema
238 when (LSP.responseId id /= rspId)
245 response msg@(LSP.ResponseMessage _ id _ _) = do
246 putStrLn $ "Waiting for request id " ++ show id ++ " from the server"
247 reqId <- takeMVar rspSema
248 if LSP.responseId reqId /= id
249 then error $ "Expected id " ++ show reqId ++ ", got " ++ show reqId
251 B.hPut serverH $ addHeader (encode msg)
252 putStrLn $ "Sent response to request id " ++ show id
254 addHeader :: B.ByteString -> B.ByteString
255 addHeader content = B.concat
257 , B.pack $ show $ B.length content
263 getHeaders :: Handle -> IO [(String, String)]
266 let (name, val) = span (/= ':') l
267 if null val then return [] else ((name, drop 2 val) :) <$> getHeaders h