packages feed

lsp-test-0.11.0.0: test/Test.hs

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}

import           Test.Hspec
import           Data.Aeson
import           Data.Default
import qualified Data.HashMap.Strict as HM
import           Data.Either
import           Data.Maybe
import qualified Data.Text as T
import           Control.Applicative.Combinators
import           Control.Concurrent
import           Control.Monad.IO.Class
import           Control.Monad
import           Control.Lens hiding (List)
import           Language.Haskell.LSP.Messages
import           Language.Haskell.LSP.Test
import           Language.Haskell.LSP.Types
import           Language.Haskell.LSP.Types.Lens hiding
  (capabilities, message, rename, applyEdit)
import qualified Language.Haskell.LSP.Types.Lens as LSP
import           Language.Haskell.LSP.Types.Capabilities as LSP
import           System.Directory
import           System.FilePath
import           System.Timeout

{-# ANN module ("HLint: ignore Reduce duplication" :: String) #-}
{-# ANN module ("HLint: ignore Unnecessary hiding" :: String) #-}


main = findServer >>= \serverExe -> hspec $ do
  describe "Session" $ do
    it "fails a test" $ do
      let session = runSession serverExe fullCaps "test/data/renamePass" $ do
                      openDoc "Desktop/simple.hs" "haskell"
                      anyRequest
        in session `shouldThrow` anySessionException
    it "initializeResponse" $ runSession serverExe fullCaps "test/data/renamePass" $ do
      rsp <- initializeResponse
      liftIO $ rsp ^. result `shouldSatisfy` isRight

    it "runSessionWithConfig" $
      runSession serverExe didChangeCaps "test/data/renamePass" $ return ()

    describe "withTimeout" $ do
      it "times out" $
        let sesh = runSession serverExe fullCaps "test/data/renamePass" $ do
                    openDoc "Desktop/simple.hs" "haskell"
                    -- won't receive a request - will timeout
                    -- incoming logging requests shouldn't increase the
                    -- timeout
                    withTimeout 5 $ skipManyTill anyMessage message :: Session ApplyWorkspaceEditRequest
          -- wait just a bit longer than 5 seconds so we have time
          -- to open the document
          in timeout 6000000 sesh `shouldThrow` anySessionException

      it "doesn't time out" $
        let sesh = runSession serverExe fullCaps "test/data/renamePass" $ do
                    openDoc "Desktop/simple.hs" "haskell"
                    withTimeout 5 $ skipManyTill anyMessage publishDiagnosticsNotification
          in void $ timeout 6000000 sesh

      it "further timeout messages are ignored" $ runSession serverExe fullCaps "test/data/renamePass" $ do
        doc <- openDoc "Desktop/simple.hs" "haskell"
        -- shouldn't timeout
        withTimeout 3 $ getDocumentSymbols doc
        -- longer than the original timeout
        liftIO $ threadDelay (5 * 10^6)
        -- shouldn't throw an exception
        getDocumentSymbols doc
        return ()

      it "overrides global message timeout" $
        let sesh =
              runSessionWithConfig (def { messageTimeout = 5 }) serverExe fullCaps "test/data/renamePass" $ do
                doc <- openDoc "Desktop/simple.hs" "haskell"
                -- shouldn't time out in here since we are overriding it
                withTimeout 10 $ liftIO $ threadDelay 7000000
                getDocumentSymbols doc
                return True
        in sesh `shouldReturn` True

      it "unoverrides global message timeout" $
        let sesh =
              runSessionWithConfig (def { messageTimeout = 5 }) serverExe fullCaps "test/data/renamePass" $ do
                doc <- openDoc "Desktop/simple.hs" "haskell"
                -- shouldn't time out in here since we are overriding it
                withTimeout 10 $ liftIO $ threadDelay 7000000
                getDocumentSymbols doc
                -- should now timeout
                skipManyTill anyMessage message :: Session ApplyWorkspaceEditRequest
            isTimeout (Timeout _) = True
            isTimeout _ = False
        in sesh `shouldThrow` isTimeout


    describe "SessionException" $ do
      it "throw on time out" $
        let sesh = runSessionWithConfig (def {messageTimeout = 10}) serverExe fullCaps "test/data/renamePass" $ do
                skipMany loggingNotification
                _ <- message :: Session ApplyWorkspaceEditRequest
                return ()
        in sesh `shouldThrow` anySessionException

      it "don't throw when no time out" $ runSessionWithConfig (def {messageTimeout = 5}) serverExe fullCaps "test/data/renamePass" $ do
        loggingNotification
        liftIO $ threadDelay $ 6 * 1000000
        _ <- openDoc "Desktop/simple.hs" "haskell"
        return ()

      describe "UnexpectedMessageException" $ do
        it "throws when there's an unexpected message" $
          let selector (UnexpectedMessage "Publish diagnostics notification" (NotLogMessage _)) = True
              selector _ = False
            in runSession serverExe fullCaps "test/data/renamePass" publishDiagnosticsNotification `shouldThrow` selector
        it "provides the correct types that were expected and received" $
          let selector (UnexpectedMessage "ResponseMessage WorkspaceEdit" (RspDocumentSymbols _)) = True
              selector _ = False
              sesh = do
                doc <- openDoc "Desktop/simple.hs" "haskell"
                sendRequest TextDocumentDocumentSymbol (DocumentSymbolParams doc Nothing)
                skipMany anyNotification
                message :: Session RenameResponse -- the wrong type
            in runSession serverExe fullCaps "test/data/renamePass" sesh
              `shouldThrow` selector

  -- This is too fickle at the moment
  -- describe "replaySession" $
  --   it "passes a test" $
  --     replaySession serverExe "test/data/renamePass"
  --   it "fails a test" $
  --     let selector (ReplayOutOfOrder _ _) = True
  --         selector _ = False
  --       in replaySession serverExe "test/data/renameFail" `shouldThrow` selector

  -- describe "manual javascript session" $
  --   it "passes a test" $
  --     runSession "javascript-typescript-stdio" fullCaps "test/data/javascriptPass" $ do
  --       doc <- openDoc "test.js" "javascript"

  --       noDiagnostics

  --       Right (fooSymbol:_) <- getDocumentSymbols doc

  --       liftIO $ do
  --         fooSymbol ^. name `shouldBe` "foo"
  --         fooSymbol ^. kind `shouldBe` SkFunction

  describe "text document VFS" $
    it "sends back didChange notifications" $
      runSession serverExe def "test/data/refactor" $ do
        doc <- openDoc "Main.hs" "haskell"

        let args = toJSON (doc ^. uri)
            reqParams = ExecuteCommandParams "doAnEdit" (Just (List [args])) Nothing
        request_ WorkspaceExecuteCommand reqParams

        editReq <- message :: Session ApplyWorkspaceEditRequest
        liftIO $ do
          let (Just cs) = editReq ^. params . edit . changes
              [(u, List es)] = HM.toList cs
          u `shouldBe` doc ^. uri
          es `shouldBe` [TextEdit (Range (Position 0 0) (Position 0 5)) "howdy"]
        contents <- documentContents doc
        liftIO $ contents `shouldBe` "howdy:: IO Int\nmain = return (42)\n"

  describe "getDocumentEdit" $
    it "automatically consumes applyedit requests" $
      runSession serverExe fullCaps "test/data/refactor" $ do
        doc <- openDoc "Main.hs" "haskell"

        let args = toJSON (doc ^. uri)
            reqParams = ExecuteCommandParams "doAnEdit" (Just (List [args])) Nothing
        request_ WorkspaceExecuteCommand reqParams
        contents <- getDocumentEdit doc
        liftIO $ contents `shouldBe` "howdy:: IO Int\nmain = return (42)\n"

  describe "getCodeActions" $
    it "works" $ runSession serverExe fullCaps "test/data/refactor" $ do
      doc <- openDoc "Main.hs" "haskell"
      waitForDiagnostics
      [CACodeAction action] <- getCodeActions doc (Range (Position 1 14) (Position 1 18))
      liftIO $ action ^. title `shouldBe` "Delete this"

  describe "getAllCodeActions" $
    it "works" $ runSession serverExe fullCaps "test/data/refactor" $ do
      doc <- openDoc "Main.hs" "haskell"
      _ <- waitForDiagnostics
      actions <- getAllCodeActions doc
      liftIO $ do
        let [CACodeAction action] = actions
        action ^. title `shouldBe` "Delete this"
        action ^. command . _Just . command `shouldBe` "deleteThis"

  describe "getDocumentSymbols" $
    it "works" $ runSession serverExe fullCaps "test/data/renamePass" $ do
      doc <- openDoc "Desktop/simple.hs" "haskell"

      skipMany loggingNotification

      Left (mainSymbol:_) <- getDocumentSymbols doc

      liftIO $ do
        mainSymbol ^. name `shouldBe` "foo"
        mainSymbol ^. kind `shouldBe` SkObject
        mainSymbol ^. range `shouldBe` mkRange 0 0 3 6

  describe "applyEdit" $ do
    it "increments the version" $ runSession serverExe docChangesCaps "test/data/renamePass" $ do
      doc <- openDoc "Desktop/simple.hs" "haskell"
      VersionedTextDocumentIdentifier _ (Just oldVersion) <- getVersionedDoc doc
      let edit = TextEdit (Range (Position 1 1) (Position 1 3)) "foo"
      VersionedTextDocumentIdentifier _ (Just newVersion) <- applyEdit doc edit
      liftIO $ newVersion `shouldBe` oldVersion + 1
    it "changes the document contents" $ runSession serverExe fullCaps "test/data/renamePass" $ do
      doc <- openDoc "Desktop/simple.hs" "haskell"
      let edit = TextEdit (Range (Position 0 0) (Position 0 2)) "foo"
      applyEdit doc edit
      contents <- documentContents doc
      liftIO $ contents `shouldSatisfy` T.isPrefixOf "foodule"

  -- describe "getCompletions" $
  --   it "works" $ runSession serverExe def "test/data/renamePass" $ do
  --     doc <- openDoc "Desktop/simple.hs" "haskell"

  --     -- wait for module to be loaded
  --     skipMany loggingNotification
  --     noDiagnostics
  --     noDiagnostics

  --     comps <- getCompletions doc (Position 5 5)
  --     let item = head (filter (\x -> x ^. label == "interactWithUser") comps)
  --     liftIO $ do
  --       item ^. label `shouldBe` "interactWithUser"
  --       item ^. kind `shouldBe` Just CiFunction
  --       item ^. detail `shouldBe` Just "Items -> IO ()\nMain"

  -- describe "getReferences" $
  --   it "works" $ runSession serverExe fullCaps "test/data/renamePass" $ do
  --     doc <- openDoc "Desktop/simple.hs" "haskell"
  --     let pos = Position 40 3 -- interactWithUser
  --         uri = doc ^. LSP.uri
  --     refs <- getReferences doc pos True
  --     liftIO $ refs `shouldContain` map (Location uri) [
  --         mkRange 41 0 41 16
  --       , mkRange 75 6 75 22
  --       , mkRange 71 6 71 22
  --       ]

  -- describe "getDefinitions" $
  --   it "works" $ runSession serverExe fullCaps "test/data/renamePass" $ do
  --     doc <- openDoc "Desktop/simple.hs" "haskell"
  --     let pos = Position 49 25 -- addItem
  --     defs <- getDefinitions doc pos
  --     liftIO $ defs `shouldBe` [Location (doc ^. uri) (mkRange 28 0 28 7)]

  -- describe "getTypeDefinitions" $
  --   it "works" $ runSession serverExe fullCaps "test/data/renamePass" $ do
  --     doc <- openDoc "Desktop/simple.hs" "haskell"
  --     let pos = Position 20 23  -- Quit value
  --     defs <- getTypeDefinitions doc pos
  --     liftIO $ defs `shouldBe` [Location (doc ^. uri) (mkRange 10 0 14 19)]  -- Type definition

  describe "waitForDiagnosticsSource" $
    it "works" $ runSession serverExe fullCaps "test/data" $ do
      openDoc "Error.hs" "haskell"
      [diag] <- waitForDiagnosticsSource "dummy-server"
      liftIO $ do
        diag ^. severity `shouldBe` Just DsWarning
        diag ^. source `shouldBe` Just "dummy-server"

  -- describe "rename" $ do
  --   it "works" $ pendingWith "HaRe not in hie-bios yet"
  --   it "works on javascript" $
  --     runSession "javascript-typescript-stdio" fullCaps "test/data/javascriptPass" $ do
  --       doc <- openDoc "test.js" "javascript"
  --       rename doc (Position 2 11) "bar"
  --       documentContents doc >>= liftIO . (`shouldContain` "function bar()") . T.unpack

  describe "getHover" $
    it "works" $ runSession serverExe fullCaps "test/data/renamePass" $ do
      doc <- openDoc "Desktop/simple.hs" "haskell"
      hover <- getHover doc (Position 45 9)
      liftIO $ hover `shouldSatisfy` isJust

  -- describe "getHighlights" $
  --   it "works" $ runSession serverExe fullCaps "test/data/renamePass" $ do
  --     doc <- openDoc "Desktop/simple.hs" "haskell"
  --     skipManyTill loggingNotification $ count 2 noDiagnostics
  --     highlights <- getHighlights doc (Position 27 4) -- addItem
  --     liftIO $ length highlights `shouldBe` 4

  -- describe "formatDoc" $
  --   it "works" $ runSession serverExe fullCaps "test/data" $ do
  --     doc <- openDoc "Format.hs" "haskell"
  --     oldContents <- documentContents doc
  --     formatDoc doc (FormattingOptions 4 True)
  --     documentContents doc >>= liftIO . (`shouldNotBe` oldContents)

  -- describe "formatRange" $
  --   it "works" $ runSession serverExe fullCaps "test/data" $ do
  --     doc <- openDoc "Format.hs" "haskell"
  --     oldContents <- documentContents doc
  --     formatRange doc (FormattingOptions 4 True) (Range (Position 1 10) (Position 2 10))
  --     documentContents doc >>= liftIO . (`shouldNotBe` oldContents)

  describe "closeDoc" $
    it "works" $
      let sesh =
            runSession serverExe fullCaps "test/data" $ do
              doc <- openDoc "Format.hs" "haskell"
              closeDoc doc
              -- need to evaluate to throw
              documentContents doc >>= liftIO . print
      in sesh `shouldThrow` anyException

  describe "satisfy" $
    it "works" $ runSession serverExe fullCaps "test/data" $ do
      openDoc "Format.hs" "haskell"
      let pred (NotLogMessage _) = True
          pred _ = False
      void $ satisfy pred

  describe "ignoreLogNotifications" $
    it "works" $
      runSessionWithConfig (defaultConfig { ignoreLogNotifications = True }) serverExe  fullCaps "test/data" $ do
        openDoc "Format.hs" "haskell"
        void publishDiagnosticsNotification

  describe "dynamic capabilities" $ do
    it "keeps track" $ runSession serverExe fullCaps "test/data" $ do
      loggingNotification -- initialized log message

      createDoc ".register" "haskell" ""
      message :: Session RegisterCapabilityRequest

      doc <- createDoc "Foo.watch" "haskell" ""
      NotLogMessage msg <- loggingNotification
      liftIO $ msg ^. params . LSP.message `shouldBe` "got workspace/didChangeWatchedFiles"

      caps <- getRegisteredCapabilities
      liftIO $ caps `shouldBe`
        [ Registration "0" WorkspaceDidChangeWatchedFiles $ Just $ toJSON $
          DidChangeWatchedFilesRegistrationOptions $ List
          [ FileSystemWatcher "*.watch" (Just (WatchKind True True True)) ]
        ]

      -- now unregister it by sending a specific createDoc
      createDoc ".unregister" "haskell" ""
      message :: Session UnregisterCapabilityRequest

      createDoc "Bar.watch" "haskell" ""
      void $ sendRequest TextDocumentHover $ TextDocumentPositionParams doc (Position 0 0) Nothing
      count 0 $ loggingNotification
      void $ anyResponse

    it "handles absolute patterns" $ runSession serverExe fullCaps "" $ do
      curDir <- liftIO $ getCurrentDirectory

      loggingNotification -- initialized log message

      createDoc ".register.abs" "haskell" ""
      message :: Session RegisterCapabilityRequest

      doc <- createDoc (curDir </> "Foo.watch") "haskell" ""
      NotLogMessage msg <- loggingNotification
      liftIO $ msg ^. params . LSP.message `shouldBe` "got workspace/didChangeWatchedFiles"

      -- now unregister it by sending a specific createDoc
      createDoc ".unregister.abs" "haskell" ""
      message :: Session UnregisterCapabilityRequest

      createDoc (curDir </> "Bar.watch") "haskell" ""
      void $ sendRequest TextDocumentHover $ TextDocumentPositionParams doc (Position 0 0) Nothing
      count 0 $ loggingNotification
      void $ anyResponse


mkRange :: Int -> Int -> Int -> Int -> Range
mkRange sl sc el ec = Range (Position sl sc) (Position el ec)

didChangeCaps :: ClientCapabilities
didChangeCaps = def { _workspace = Just workspaceCaps }
  where
    workspaceCaps = def { _didChangeConfiguration = Just configCaps }
    configCaps = DidChangeConfigurationClientCapabilities (Just True)

docChangesCaps :: ClientCapabilities
docChangesCaps = def { _workspace = Just workspaceCaps }
  where
    workspaceCaps = def { _workspaceEdit = Just editCaps }
    editCaps = WorkspaceEditClientCapabilities (Just True)


findExeRecursive :: FilePath -> FilePath -> IO (Maybe FilePath)
findExeRecursive exe dir = do
  me <- listToMaybe <$> findExecutablesInDirectories [dir] exe
  case me of
    Just e -> return (Just e)
    Nothing -> do
      subdirs <- (fmap (dir </>)) <$> listDirectory dir >>= filterM doesDirectoryExist
      foldM (\acc subdir -> case acc of
                              Just y -> pure $ Just y
                              Nothing -> findExeRecursive exe subdir)
            Nothing
            subdirs

-- | So we can find the dummy-server with cabal run
-- since it doesnt put build tools on the path (only cabal test)
findServer = do
  let serverName = "dummy-server"
  e <- findExecutable serverName
  e' <- findExeRecursive serverName "dist-newstyle"
  pure $ fromJust $ e <|> e'