-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE RankNTypes #-}
-- | A testing tool for replaying captured client logs back to a server,
-- and validating that the server output matches up with another log.
module Language.Haskell.LSP.Test.Replay
import Prelude hiding (id)
import Control.Concurrent
+import Control.Exception
import Control.Monad.IO.Class
import qualified Data.ByteString.Lazy.Char8 as B
+import qualified Data.Text as T
+import Data.UUID
import Language.Haskell.LSP.Capture
import Language.Haskell.LSP.Messages
import Language.Haskell.LSP.Types hiding (error)
import Data.Aeson
import Data.List
import Data.Maybe
-import Control.Lens
+import Control.Lens hiding (List)
import Control.Monad
import System.IO
import System.FilePath
+import System.Random
import Language.Haskell.LSP.Test
import Language.Haskell.LSP.Test.Files
-import Language.Haskell.LSP.Test.Parsing
+import Language.Haskell.LSP.Test.Decoding
+import Language.Haskell.LSP.Test.Messages
+import Language.Haskell.LSP.Test.Server
-- | Replays a captured client output and
-- makes sure it matches up with an expected response.
-- The session directory should have a captured session file in it
-- named "session.log".
-replaySession :: FilePath -- ^ The recorded session directory.
+replaySession :: String -- ^ The command to run the server.
+ -> FilePath -- ^ The recorded session directory.
-> IO Bool
-replaySession sessionDir = do
+replaySession serverExe sessionDir = do
entries <- B.lines <$> B.readFile (sessionDir </> "session.log")
-- decode session
let unswappedEvents = map (fromJust . decode) entries
- events <- swapFiles sessionDir unswappedEvents
+ withServer serverExe $ \serverIn serverOut pid -> do
+
+ events <- swapUUIDs pid <$> swapFiles sessionDir unswappedEvents
let clientEvents = filter isClientMsg events
serverEvents = filter isServerMsg events
serverMsgs = filter (not . shouldSkip) $ map (\(FromServer _ msg) -> msg) serverEvents
requestMap = getRequestMap clientMsgs
-
reqSema <- newEmptyMVar
rspSema <- newEmptyMVar
passVar <- newEmptyMVar :: IO (MVar Bool)
- forkIO $ runSessionWithHandler (listenServer serverMsgs requestMap reqSema rspSema passVar) sessionDir $
- sendMessages clientMsgs reqSema rspSema
+ threadId <- forkIO $
+ runSessionWithHandles serverIn
+ serverOut
+ (listenServer serverMsgs requestMap reqSema rspSema passVar)
+ sessionDir
+ (sendMessages clientMsgs reqSema rspSema)
- takeMVar passVar
+ result <- takeMVar passVar
+ killThread threadId
+ return result
where
isClientMsg (FromClient _ _) = True
sendMessages :: [FromClientMessage] -> MVar LspId -> MVar LspIdRsp -> Session ()
sendMessages [] _ _ = return ()
sendMessages (nextMsg:remainingMsgs) reqSema rspSema =
- case nextMsg of
- ReqInitialize m -> request m
- ReqShutdown m -> request m
- ReqHover m -> request m
- ReqCompletion m -> request m
- ReqCompletionItemResolve m -> request m
- ReqSignatureHelp m -> request m
- ReqDefinition m -> request m
- ReqFindReferences m -> request m
- ReqDocumentHighlights m -> request m
- ReqDocumentSymbols m -> request m
- ReqWorkspaceSymbols m -> request m
- ReqCodeAction m -> request m
- ReqCodeLens m -> request m
- ReqCodeLensResolve m -> request m
- ReqDocumentFormatting m -> request m
- ReqDocumentRangeFormatting m -> request m
- ReqDocumentOnTypeFormatting m -> request m
- ReqRename m -> request m
- ReqExecuteCommand m -> request m
- ReqDocumentLink m -> request m
- ReqDocumentLinkResolve m -> request m
- ReqWillSaveWaitUntil m -> request m
- RspApplyWorkspaceEdit m -> response m
- RspFromClient m -> response m
- NotInitialized m -> notification m
- NotExit m -> notification m
- NotCancelRequestFromClient m -> notification m
- NotDidChangeConfiguration m -> notification m
- NotDidOpenTextDocument m -> notification m
- NotDidChangeTextDocument m -> notification m
- NotDidCloseTextDocument m -> notification m
- NotWillSaveTextDocument m -> notification m
- NotDidSaveTextDocument m -> notification m
- NotDidChangeWatchedFiles m -> notification m
- UnknownFromClientMessage m -> liftIO $ error $ "Unknown message was recorded from the client" ++ show m
+ handleClientMessage request response notification nextMsg
where
-- TODO: May need to prevent premature exit notification being sent
notification msg@(NotificationMessage _ Exit _) = do
if responseId reqId /= id
then error $ "Expected id " ++ show reqId ++ ", got " ++ show reqId
else do
- sendResponse' msg
+ sendResponse msg
liftIO $ putStrLn $ "Sent response to request id " ++ show id
sendMessages remainingMsgs reqSema rspSema
listenServer :: [FromServerMessage] -> RequestMap -> MVar LspId -> MVar LspIdRsp -> MVar Bool -> Handle -> Session ()
listenServer [] _ _ _ passVar _ = liftIO $ putMVar passVar True
listenServer expectedMsgs reqMap reqSema rspSema passVar serverOut = do
- msgBytes <- liftIO $ getNextMessage serverOut
+
+ let handler :: IOException -> IO B.ByteString
+ handler _ = putMVar passVar False >> return B.empty
+
+ msgBytes <- liftIO $ catch (getNextMessage serverOut) handler
let msg = decodeFromServerMsg reqMap msgBytes
- case msg of
- ReqRegisterCapability m -> request m
- ReqApplyWorkspaceEdit m -> request m
- ReqShowMessage m -> request m
- ReqUnregisterCapability m -> request m
- RspInitialize m -> response m
- RspShutdown m -> response m
- RspHover m -> response m
- RspCompletion m -> response m
- RspCompletionItemResolve m -> response m
- RspSignatureHelp m -> response m
- RspDefinition m -> response m
- RspFindReferences m -> response m
- RspDocumentHighlights m -> response m
- RspDocumentSymbols m -> response m
- RspWorkspaceSymbols m -> response m
- RspCodeAction m -> response m
- RspCodeLens m -> response m
- RspCodeLensResolve m -> response m
- RspDocumentFormatting m -> response m
- RspDocumentRangeFormatting m -> response m
- RspDocumentOnTypeFormatting m -> response m
- RspRename m -> response m
- RspExecuteCommand m -> response m
- RspError m -> response m
- RspDocumentLink m -> response m
- RspDocumentLinkResolve m -> response m
- RspWillSaveWaitUntil m -> response m
- NotPublishDiagnostics m -> notification m
- NotLogMessage m -> notification m
- NotShowMessage m -> notification m
- NotTelemetry m -> notification m
- NotCancelRequestFromServer m -> notification m
+ handleServerMessage request response notification msg
if shouldSkip msg
then listenServer expectedMsgs reqMap reqSema rspSema passVar serverOut
print msg
putStrLn "Expected one of:"
mapM_ print $ takeWhile (not . isNotification) expectedMsgs
- print $ head $ dropWhile (not . isNotification) expectedMsgs
+ print $ head $ dropWhile isNotification expectedMsgs
putMVar passVar False
where
- response :: Show a => ResponseMessage a -> Session ()
+ response :: ResponseMessage a -> Session ()
response res = do
liftIO $ putStrLn $ "Got response for id " ++ show (res ^. id)
liftIO $ putMVar rspSema (res ^. id) -- unblock the handler waiting to send a request
- request :: (Show a, Show b) => RequestMessage ServerMethod a b -> Session ()
+ request :: RequestMessage ServerMethod a b -> Session ()
request req = do
liftIO
$ putStrLn
liftIO $ putMVar reqSema (req ^. id) -- unblock the handler waiting for a response
- notification :: Show a => NotificationMessage ServerMethod a -> Session ()
+ notification :: NotificationMessage ServerMethod a -> Session ()
notification n = liftIO $ putStrLn $ "Got notification " ++ show (n ^. method)
shouldSkip (NotShowMessage _) = True
shouldSkip (ReqShowMessage _) = True
shouldSkip _ = False
+
+-- | Swaps out the expected UUIDs to match the current process ID
+swapUUIDs :: Int -> [Event] -> [Event]
+swapUUIDs _ [] = []
+swapUUIDs pid (FromServer t (RspInitialize rsp):xs) = FromServer t (RspInitialize swapped):swapUUIDs pid xs
+ where swapped = case newCommands of
+ Just cmds -> result . _Just . capabilities . executeCommandProvider . _Just . commands .~ cmds $ rsp
+ Nothing -> rsp
+ oldCommands = rsp ^? result . _Just . capabilities . executeCommandProvider . _Just . commands
+ newCommands = fmap (fmap swap) oldCommands
+ swap cmd
+ | isUuid cmd = T.append uuid $ T.dropWhile (/= ':') cmd
+ | otherwise = cmd
+ uuid = toText $ fst $ random $ mkStdGen pid
+ isUuid = isJust . fromText . T.takeWhile (/= ':')
+swapUUIDs pid (x:xs) = x:swapUUIDs pid xs
\ No newline at end of file