name: lsp-test
version: 0.1.0.0
synopsis: Functional test framework for LSP servers.
--- description:
+description:
+ A test framework for writing tests against
+ <https://microsoft.github.io/language-server-protocol/ Language Server Protocol servers>.
+ @lsp-test@ launches your server as a subprocess and allows you to simulate a session
+ down to the wire.
+ Used for testing in <https://github.com/haskell/haskell-ide-engine haskell-ide-engine>.
+ > runSession "hie" fullCaps "path/to/root/dir" $ do
+ > doc <- openDoc "Desktop/simple.hs" "haskell"
+ > diags <- waitForDiagnostics
+ > let pos = Position 12 5
+ > params = TextDocumentPositionParams doc
+ > hover <- request TextDocumentHover params
homepage: https://github.com/Bubba/haskell-lsp-test#readme
license: BSD3
license-file: LICENSE
copyright: 2018 Luke Lau
category: Testing
build-type: Simple
-cabal-version: >=1.10
+cabal-version: 2.0
extra-source-files: README.md
+tested-with: GHC == 8.2.2 , GHC == 8.4.2 , GHC == 8.4.3
+
+source-repository head
+ type: git
+ location: https://github.com/Bubba/haskell-lsp-test/
library
hs-source-dirs: src
Stability : experimental
Portability : POSIX
-A framework for testing
-<https://github.com/Microsoft/language-server-protocol Language Server Protocol servers>
-functionally.
+Provides the framework to start functionally testing
+<https://github.com/Microsoft/language-server-protocol Language Server Protocol servers>.
+You should import "Language.Haskell.LSP.Types" alongside this.
-}
module Language.Haskell.LSP.Test
(
, defaultConfig
, module Language.Haskell.LSP.Test.Capabilities
-- ** Exceptions
- , SessionException(..)
- , anySessionException
+ , module Language.Haskell.LSP.Test.Exceptions
, withTimeout
-- * Sending
, request
, sendNotification
, sendResponse
-- * Receving
- , message
- , anyRequest
- , anyResponse
- , anyNotification
- , anyMessage
- , loggingNotification
- , publishDiagnosticsNotification
- -- * Combinators
- , satisfy
+ , module Language.Haskell.LSP.Test.Parsing
-- * Utilities
+ -- | Quick helper functions for common tasks.
+ -- ** Initialization
, initializeResponse
-- ** Documents
, openDoc
import qualified Yi.Rope as Rope
-- | Starts a new session.
+--
+-- > runSession "hie" fullCaps "path/to/root/dir" $ do
+-- > doc <- openDoc "Desktop/simple.hs" "haskell"
+-- > diags <- waitForDiagnostics
+-- > let pos = Position 12 5
+-- > params = TextDocumentPositionParams doc
+-- > hover <- request TextDocumentHover params
runSession :: String -- ^ The command to run the server.
-> LSP.ClientCapabilities -- ^ The capabilities that the client should declare.
-> FilePath -- ^ The filepath to the root directory for the session.
-> IO a
runSession = runSessionWithConfig def
--- | Starts a new sesion with a client with the specified capabilities.
+-- | Starts a new sesion with a custom configuration.
runSessionWithConfig :: SessionConfig -- ^ Configuration options for the session.
-> String -- ^ The command to run the server.
-> LSP.ClientCapabilities -- ^ The capabilities that the client should declare.
-> a -- ^ The notification parameters.
-> Session ()
--- | Open a virtual file if we send a did open text document notification
+-- Open a virtual file if we send a did open text document notification
sendNotification TextDocumentDidOpen params = do
let params' = fromJust $ decode $ encode params
n :: DidOpenTextDocumentNotification
modify (\s -> s { vfs = newVFS })
sendMessage n
--- | Close a virtual file if we send a close text document notification
+-- Close a virtual file if we send a close text document notification
sendNotification TextDocumentDidClose params = do
let params' = fromJust $ decode $ encode params
n :: DidCloseTextDocumentNotification
sendNotification method params = sendMessage (NotificationMessage "2.0" method params)
+-- | Sends a response to the server.
sendResponse :: ToJSON a => ResponseMessage a -> Session ()
sendResponse = sendMessage
let (List diags) = diagsNot ^. params . LSP.diagnostics
return diags
+-- | The same as 'waitForDiagnostics', but will only match a specific
+-- 'Language.Haskell.LSP.Types._source'.
waitForDiagnosticsSource :: String -> Session [Diagnostic]
waitForDiagnosticsSource src = do
diags <- waitForDiagnostics
let params = TextDocumentPositionParams doc pos
in getResponseResult <$> request TextDocumentDefinition params
--- ^ Renames the term at the specified position.
+-- | Renames the term at the specified position.
rename :: TextDocumentIdentifier -> Position -> String -> Session ()
rename doc pos newName = do
let params = RenameParams doc pos (T.pack newName)
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE OverloadedStrings #-}
-module Language.Haskell.LSP.Test.Parsing where
+
+module Language.Haskell.LSP.Test.Parsing
+ ( -- $receiving
+ message
+ , anyRequest
+ , anyResponse
+ , anyNotification
+ , anyMessage
+ , loggingNotification
+ , publishDiagnosticsNotification
+ , responseForId
+ ) where
import Control.Applicative
import Control.Concurrent
import qualified Data.Text as T
import Data.Typeable
import Language.Haskell.LSP.Messages
-import Language.Haskell.LSP.Types as LSP hiding (error)
+import Language.Haskell.LSP.Types as LSP hiding (error, message)
import Language.Haskell.LSP.Test.Messages
import Language.Haskell.LSP.Test.Session
+-- $receiving
+-- To receive a message, just specify the type that expect:
+--
+-- @
+-- msg1 <- message :: Session ApplyWorkspaceEditRequest
+-- msg2 <- message :: Session HoverResponse
+-- @
+--
+-- 'Language.Haskell.LSP.Test.Session' is actually just a parser
+-- that operates on messages under the hood. This means that you
+-- can create and combine parsers to match speicifc sequences of
+-- messages that you expect.
+--
+-- For example, if you wanted to match either a definition or
+-- references request:
+--
+-- > defOrImpl = (message :: Session DefinitionRequest)
+-- > <|> (message :: Session ReferencesRequest)
+--
+-- If you wanted to match any number of telemetry
+-- notifications immediately followed by a response:
+--
+-- @
+-- logThenDiags =
+-- skipManyTill (message :: Session TelemetryNotification)
+-- anyResponse
+-- @
+
satisfy :: (FromServerMessage -> Bool) -> Session FromServerMessage
satisfy pred = do
anyResponse :: Session FromServerMessage
anyResponse = named "Any response" $ satisfy isServerResponse
+-- | Matches a response for a specific id.
responseForId :: forall a. FromJSON a => LspId -> Session (ResponseMessage a)
responseForId lid = named (T.pack $ "Response for id: " ++ show lid) $ do
let parser = decode . encodeMsg :: FromServerMessage -> Maybe (ResponseMessage a)
x <- satisfy (maybe False (\z -> z ^. LSP.id == responseId lid) . parser)
return $ castMsg x
+-- | Matches any type of message.
anyMessage :: Session FromServerMessage
anyMessage = satisfy (const True)
shouldSkip (ReqShowMessage _) = True
shouldSkip _ = False
+-- | Matches a 'Language.Haskell.LSP.Test.PublishDiagnosticsNotification'
+-- (textDocument/publishDiagnostics) notification.
publishDiagnosticsNotification :: Session PublishDiagnosticsNotification
publishDiagnosticsNotification = named "Publish diagnostics notification" $ do
NotPublishDiagnostics diags <- satisfy test
-- You can send and receive messages to the server within 'Session' via 'getMessage',
-- 'sendRequest' and 'sendNotification'.
--
--- @
--- runSession \"path\/to\/root\/dir\" $ do
--- docItem <- getDocItem "Desktop/simple.hs" "haskell"
--- sendNotification TextDocumentDidOpen (DidOpenTextDocumentParams docItem)
--- diagnostics <- getMessage :: Session PublishDiagnosticsNotification
--- @
+
type Session = ParserStateReader FromServerMessage SessionState SessionContext IO
-- | Stuff you can configure for a 'Session'.