Update recorded playback to build upon new session
[lsp-test.git] / src / Language / Haskell / LSP / Test / Recorded.hs
index 9c89a78d8d12710de7ded927f8723c7fdccad5e8..c028478dc1e7411c3cea400d57d9dbee8e5e452e 100644 (file)
@@ -4,66 +4,36 @@
 -- | A testing tool for replaying recorded client logs back to a server,
 -- and validating that the server output matches up with another log.
 module Language.Haskell.LSP.Test.Recorded
-  ( replay,
-    sendNextRequest
+  ( replaySession
   )
 where
 
+import           Prelude hiding (id)
 import           Control.Concurrent
-import           Control.Monad.Trans.Class
-import           Control.Monad.Trans.Reader
-import           Control.Monad.Trans.State
 import           Control.Monad.IO.Class
 import qualified Data.ByteString.Lazy.Char8    as B
 import           Language.Haskell.LSP.Capture
 import           Language.Haskell.LSP.Messages
-import qualified Language.Haskell.LSP.Types    as LSP
+import           Language.Haskell.LSP.Types hiding (error)
 import           Data.Aeson
+import           Data.List
 import           Data.Maybe
 import           Control.Lens
 import           Control.Monad
 import           System.IO
-import           System.Directory
 import           System.FilePath
-import           System.Process
+import           Language.Haskell.LSP.Test
 import           Language.Haskell.LSP.Test.Files
 import           Language.Haskell.LSP.Test.Parsing
 
-data SessionContext = SessionContext
-  {
-    reqSema :: MVar LSP.LspId,
-    rspSema :: MVar LSP.LspIdRsp,
-    serverIn :: Handle
-  }
-type Session = StateT [FromClientMessage] (ReaderT SessionContext IO)
 
 -- | Replays a recorded client output and 
 -- makes sure it matches up with an expected response.
-replay :: FilePath -- ^ The recorded session directory.
-       -> Session a
-       -> IO ()
-replay sessionDir session = do
+replaySession :: FilePath -- ^ The recorded session directory.
+              -> IO Bool
+replaySession sessionDir = do
 
-  let sessionFp = sessionDir </> "session.log"
-
-  -- need to keep hold of current directory since haskell-lsp changes it
-  prevRootDir <- getCurrentDirectory
-
-  (Just serverIn, Just serverOut, _, serverProc) <- createProcess
-    (proc "hie" ["--lsp", "-l", "/tmp/hie.log"]) { std_in  = CreatePipe
-                                                 , std_out = CreatePipe
-                                                 }
-
-  hSetBuffering serverIn  NoBuffering
-  hSetBuffering serverOut NoBuffering
-
-  -- whether to send the next request
-  reqSema <- newEmptyMVar :: IO (MVar LSP.LspIdRsp)
-  -- whether to send the next response
-  rspSema <- newEmptyMVar :: IO (MVar LSP.LspId)
-  let semas = (reqSema, rspSema)
-
-  entries <- B.lines <$> B.readFile sessionFp
+  entries <- B.lines <$> B.readFile (sessionDir </> "session.log")
 
   -- decode session
   let unswappedEvents = map (fromJust . decode) entries
@@ -71,17 +41,18 @@ replay sessionDir session = do
   events <- swapFiles sessionDir unswappedEvents
 
   let clientEvents = map (\(FromClient _ msg) -> msg) $ filter isClientMsg events
+      serverEvents = map (\(FromServer _ msg) -> msg) $ filter isServerMsg events
       requestMap = getRequestMap clientEvents
 
-  -- listen to server
-  forkIO $ listenServer serverOut requestMap semas
 
-  runReaderT (runStateT session clientEvents) (SessionContext rspSema reqSema serverIn)
+  reqSema <- newEmptyMVar
+  rspSema <- newEmptyMVar
+  passVar <- newEmptyMVar :: IO (MVar Bool)
 
-  terminateProcess serverProc
+  forkIO $ runSessionWithHandler (listenServer serverEvents requestMap reqSema rspSema passVar) sessionDir $
+    sendMessages clientEvents reqSema rspSema
   
-  -- restore directory
-  setCurrentDirectory prevRootDir
+  takeMVar passVar
 
   where
     isClientMsg (FromClient _ _) = True
@@ -90,10 +61,9 @@ replay sessionDir session = do
     isServerMsg (FromServer _ _) = True
     isServerMsg _                = False
 
-sendNextRequest :: Session ()
-sendNextRequest = do
-  (nextMsg:remainingMsgs) <- get
-  put remainingMsgs
+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
@@ -129,65 +99,60 @@ sendNextRequest = do
     NotWillSaveTextDocument     m -> notification m
     NotDidSaveTextDocument      m -> notification m
     NotDidChangeWatchedFiles    m -> notification m
-    UnknownFromClientMessage m ->
-      error $ "Unknown message was recorded from the client" ++ show m
+    UnknownFromClientMessage m -> liftIO $ error $ "Unknown message was recorded from the client" ++ show m
  where
   -- TODO: May need to prevent premature exit notification being sent
-  notification msg@(LSP.NotificationMessage _ LSP.Exit _) = do
-    context <- lift ask
-
-    liftIO $ do
-      putStrLn "Will send exit notification soon"
-      threadDelay 10000000
-      B.hPut (serverIn context) $ addHeader (encode msg)
+  notification msg@(NotificationMessage _ Exit _) = do
+    liftIO $ putStrLn "Will send exit notification soon"
+    liftIO $ threadDelay 10000000
+    sendNotification' msg
     
-  notification msg@(LSP.NotificationMessage _ m _) = do
-    context <- lift ask
+    liftIO $ error "Done"
 
-    liftIO $ B.hPut (serverIn context) $ addHeader (encode msg)
+  notification msg@(NotificationMessage _ m _) = do
+    sendNotification' msg
 
     liftIO $ putStrLn $ "Sent a notification " ++ show m
     
-    sendNextRequest
-
-  request msg@(LSP.RequestMessage _ id m _) = do
-    context <- lift ask
+    sendMessages remainingMsgs reqSema rspSema
 
-    liftIO $ do
-      when (m == LSP.TextDocumentDocumentSymbol) $ threadDelay 5000000
+  request msg@(RequestMessage _ id m _) = do
+    liftIO $ print $ addHeader $ encode msg
 
-      B.hPut (serverIn context) $ addHeader (encode msg)
-      putStrLn $  "Sent a request id " ++ show id ++ ": " ++ show m ++ "\nWaiting for a response"
+    sendRequest' msg
+    liftIO $ putStrLn $  "Sent a request id " ++ show id ++ ": " ++ show m ++ "\nWaiting for a response"
 
-      rspId <- takeMVar (rspSema context)
-      when (LSP.responseId id /= rspId) $ 
-        error $ "Expected id " ++ show id ++ ", got " ++ show rspId
+    rsp <- liftIO $ takeMVar rspSema
+    when (responseId id /= rsp) $ 
+      error $ "Expected id " ++ show id ++ ", got " ++ show rsp
     
-  response msg@(LSP.ResponseMessage _ id _ _) = do
-    context <- lift ask
+    sendMessages remainingMsgs reqSema rspSema
 
-    liftIO $ do
-      putStrLn $ "Waiting for request id " ++ show id ++ " from the server"
-      reqId <- takeMVar (reqSema context)
-      if LSP.responseId reqId /= id
+  response msg@(ResponseMessage _ id _ _) = do
+    liftIO $ putStrLn $ "Waiting for request id " ++ show id ++ " from the server"
+    reqId <- liftIO $ takeMVar reqSema
+    if responseId reqId /= id
       then error $ "Expected id " ++ show reqId ++ ", got " ++ show reqId
       else do
-          B.hPut (serverIn context) $ addHeader (encode msg)
-          putStrLn $ "Sent response to request id " ++ show id
+        sendResponse' msg
+        liftIO $ putStrLn $ "Sent response to request id " ++ show id
 
-    sendNextRequest
+    sendMessages remainingMsgs reqSema rspSema
 
 
--- | Listens to the server output, makes sure it matches the record and
--- signals any semaphores
-listenServer :: Handle -> RequestMap -> (MVar LSP.LspIdRsp, MVar LSP.LspId) -> IO ()
-listenServer h reqMap semas@(reqSema, rspSema) = do
-  msgBytes <- getNextMessage h
+isNotification :: FromServerMessage -> Bool
+isNotification (NotPublishDiagnostics      _) = True
+isNotification (NotLogMessage              _) = True
+isNotification (NotShowMessage             _) = True
+isNotification (NotCancelRequestFromServer _) = True
+isNotification _                              = False
 
+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 msg = decodeFromServerMsg reqMap msgBytes
 
-  print msg
-
   case msg of
     ReqRegisterCapability       m -> request m
     ReqApplyWorkspaceEdit       m -> request m
@@ -222,62 +187,50 @@ listenServer h reqMap semas@(reqSema, rspSema) = do
     NotTelemetry                m -> notification m
     NotCancelRequestFromServer  m -> notification m
   
-  listenServer h reqMap semas
+  if inRightOrder msg expectedMsgs
+    then listenServer (delete msg expectedMsgs) reqMap reqSema rspSema passVar serverOut
+    else liftIO $ do
+      putStrLn "Out of order"
+      putStrLn "Got:"
+      print msg
+      putStrLn "Expected one of:"
+      mapM_ print $ takeWhile (not . isNotification) expectedMsgs
+      print $ head $ dropWhile (not . isNotification) expectedMsgs
+      putMVar passVar False
 
   where
-  response :: Show a => LSP.ResponseMessage a -> IO ()
+  response :: Show a => ResponseMessage a -> Session ()
   response res = do
-    putStrLn $ "Got response for id " ++ show (res ^. LSP.id)
+    liftIO $ putStrLn $ "Got response for id " ++ show (res ^. id)
 
-    print res
+    liftIO $ print res
 
-    putMVar reqSema (res ^. LSP.id) -- unblock the handler waiting to send a request
+    liftIO $ putMVar rspSema (res ^. id) -- unblock the handler waiting to send a request
 
-  request :: Show a => LSP.RequestMessage LSP.ServerMethod a b -> IO ()
+  request :: (Show a, Show b) => RequestMessage ServerMethod a b -> Session ()
   request req = do
-    putStrLn
+    liftIO
+      $  putStrLn
       $  "Got request for id "
-      ++ show (req ^. LSP.id)
+      ++ show (req ^. id)
       ++ " "
-      ++ show (req ^. LSP.method)
+      ++ show (req ^. method)
 
-    print req
+    liftIO $ print req
 
-    putMVar rspSema (req ^. LSP.id) -- unblock the handler waiting for a response
+    liftIO $ putMVar reqSema (req ^. id) -- unblock the handler waiting for a response
 
-  notification :: Show a => LSP.NotificationMessage LSP.ServerMethod a -> IO ()
+  notification :: Show a => NotificationMessage ServerMethod a -> Session ()
   notification n = do
-    putStrLn $ "Got notification " ++ show (n ^. LSP.method)
-    print n
-
-  --   lift
-  --     $  putStrLn
-  --     $  show (length (filter isNotification expectedMsgs) - 1)
-  --     ++ " notifications remaining"
-
-  -- checkOrder msg = unless (inRightOrder msg expectedMsgs) $ failSession
-  --   (  "Out of order\nExpected\n"
-  --   ++ show firstExpected
-  --   ++ "\nGot\n"
-  --   ++ show msg
-  --   ++ "\n"
-  --   )
-
-  -- markReceived :: FromServerMessage -> Session [FromServerMessage]
-  -- markReceived msg =
-  --   let new = delete msg expectedMsgs
-  --   in  if new == expectedMsgs
-  --         then failSession ("Unexpected message: " ++ show msg) >> return new
-  --         else return new
-
-  -- firstExpected = head $ filter (not . isNotification) expectedMsgs
+    liftIO $ putStrLn $ "Got notification " ++ show (n ^. method)
+    liftIO $ print n
+
+    liftIO
+      $  putStrLn
+      $  show (length (filter isNotification expectedMsgs) - 1)
+      ++ " notifications remaining"
+
 
-isNotification :: FromServerMessage -> Bool
-isNotification (NotPublishDiagnostics      _) = True
-isNotification (NotLogMessage              _) = True
-isNotification (NotShowMessage             _) = True
-isNotification (NotCancelRequestFromServer _) = True
-isNotification _                              = False
 
 -- TODO: QuickCheck tests?
 -- | Checks wether or not the message appears in the right order
@@ -298,19 +251,3 @@ inRightOrder received (expected : msgs)
   | received == expected    = True
   | isNotification expected = inRightOrder received msgs
   | otherwise               = False
\ No newline at end of file
-
--- | The internal monad for tests that can fail or pass,
--- ending execution early.
--- type Session = ReaderT (MVar Bool) IO
-
--- -- TODO: Make return type polymoprhic more like error
--- failSession :: String -> Session ()
--- failSession reason = do
---   lift $ putStrLn reason
---   passVar <- ask
---   lift $ putMVar passVar False
-
--- passSession :: Session ()
--- passSession = do
---   passVar <- ask
---   lift $ putMVar passVar True
\ No newline at end of file