lsp-test 0.10.3.0 → 0.11.0.0
raw patch · 8 files changed
+497/−198 lines, 8 filesdep +Globdep ~haskell-lspnew-component:exe:dummy-serverPVP ok
version bump matches the API change (PVP)
Dependencies added: Glob
Dependency ranges changed: haskell-lsp
API changes (from Hackage documentation)
- Language.Haskell.LSP.Test: openDoc' :: FilePath -> String -> Text -> Session TextDocumentIdentifier
+ Language.Haskell.LSP.Test: createDoc :: FilePath -> String -> Text -> Session TextDocumentIdentifier
+ Language.Haskell.LSP.Test: getRegisteredCapabilities :: Session [Registration]
Files
- ChangeLog.md +7/−0
- README.md +12/−10
- lsp-test.cabal +30/−3
- src/Language/Haskell/LSP/Test.hs +71/−5
- src/Language/Haskell/LSP/Test/Parsing.hs +2/−3
- src/Language/Haskell/LSP/Test/Session.hs +61/−26
- test/Test.hs +199/−151
- test/dummy-server/Main.hs +115/−0
ChangeLog.md view
@@ -1,5 +1,12 @@ # Revision history for lsp-test +## 0.11.0.0 -- 2020-05-14++* Replace `openDoc'` with `createDoc` which now sends+ `workspace/didChangeWatchedFiles` notifications if the server has registered+ for it+* Add `getRegisteredCapabilities`+ ## 0.10.3.0 -- 2020-05-04 * Build with new haskell-lsp-0.22
README.md view
@@ -1,4 +1,4 @@-# lsp-test [](https://github.com/bubba/lsp-test/actions) [](https://hackage.haskell.org/package/lsp-test-0.1.0.0)+# lsp-test [](https://github.com/bubba/lsp-test/actions) [](https://hackage.haskell.org/package/lsp-test) lsp-test is a functional testing framework for Language Server Protocol servers. ```haskell@@ -35,17 +35,19 @@ ``` Try out the example tests in the `example` directory with `cabal test`.-For more examples check the [Wiki](https://github.com/bubba/lsp-test/wiki/Introduction)+For more examples check the [Wiki](https://github.com/bubba/lsp-test/wiki/Introduction). -## Developing-The tests are integration tests, so make sure you have the following language servers installed and on your PATH:-### [haskell-ide-engine](https://github.com/haskell/haskell-ide-engine)-- Check out a relatively recent version of the repo, or see `.travis.yml` to get the exact commit used for CI.-- `stack install`-### [javascript-typescript-langserver](https://github.com/sourcegraph/javascript-typescript-langserver)-`npm i -g javascript-typescript-langserver`+Whilst writing your tests you may want to debug them to see what's going wrong.+You can set the `logMessages` and `logStdErr` options in `SessionConfig` to see what the server is up to.+There are also corresponding environment variables so you can turn them on from the command line:+```+LSP_TEST_LOG_MESSAGES=1 cabal test+``` -Then run the tests with `cabal test` or `stack test`.+## Developing+The tests for lsp-test use a dummy server found in `test/dummy-server/`.+Run the tests with `cabal test` or `stack test`.+Tip: If you want to filter the tests, use `cabal run test:tests -- -m "foo"` ## Troubleshooting Seeing funny stuff when running lsp-test via stack? If your server is built upon Haskell tooling, [keep in mind that stack sets some environment variables related to GHC, and you may want to unset them.](https://github.com/alanz/haskell-ide-engine/blob/bfb16324d396da71000ef81d51acbebbdaa854ab/test/utils/TestUtils.hs#L290-L298)
lsp-test.cabal view
@@ -1,5 +1,5 @@ name: lsp-test-version: 0.10.3.0+version: 0.11.0.0 synopsis: Functional test framework for LSP servers. description: A test framework for writing tests against@@ -7,14 +7,16 @@ @Language.Haskell.LSP.Test@ launches your server as a subprocess and allows you to simulate a session down to the wire, and @Language.Haskell.LSP.Test@ can replay captured sessions from <haskell-lsp https://hackage.haskell.org/package/haskell-lsp>.- It's currently used for testing in <https://github.com/haskell/haskell-ide-engine haskell-ide-engine>.+ To see examples of it in action, check out <https://github.com/haskell/haskell-ide-engine haskell-ide-engine>,+ <https://github.com/haskell/haskell-language-server haskell-language-server> and+ <https://github.com/digital-asset/ghcide ghcide>. homepage: https://github.com/bubba/lsp-test#readme license: BSD3 license-file: LICENSE author: Luke Lau maintainer: luke_lau@icloud.com bug-reports: https://github.com/bubba/lsp-test/issues-copyright: 2019 Luke Lau+copyright: 2020 Luke Lau category: Testing build-type: Simple cabal-version: 2.0@@ -26,6 +28,11 @@ type: git location: https://github.com/bubba/lsp-test/ +Flag DummyServer+ Description: Build the dummy server executable used in testing+ Default: False+ Manual: True+ library hs-source-dirs: src exposed-modules: Language.Haskell.LSP.Test@@ -48,6 +55,7 @@ , Diff , directory , filepath+ , Glob ^>= 0.10 , lens , mtl , parser-combinators >= 1.2@@ -69,6 +77,22 @@ Language.Haskell.LSP.Test.Session ghc-options: -W +executable dummy-server+ main-is: Main.hs+ hs-source-dirs: test/dummy-server+ ghc-options: -W+ build-depends: base >= 4.10 && < 5+ , haskell-lsp+ , data-default+ , aeson+ , unordered-containers+ , directory+ , filepath+ default-language: Haskell2010+ scope: private+ if !flag(DummyServer)+ buildable: False+ test-suite tests type: exitcode-stdio-1.0 main-is: Test.hs@@ -83,4 +107,7 @@ , aeson , unordered-containers , text+ , directory+ , filepath default-language: Haskell2010+ build-tool-depends: lsp-test:dummy-server
src/Language/Haskell/LSP/Test.hs view
@@ -41,8 +41,8 @@ -- ** Initialization , initializeResponse -- ** Documents+ , createDoc , openDoc- , openDoc' , closeDoc , changeDoc , documentContents@@ -82,6 +82,8 @@ , applyEdit -- ** Code lenses , getCodeLenses+ -- ** Capabilities+ , getRegisteredCapabilities ) where import Control.Applicative.Combinators@@ -90,12 +92,13 @@ import Control.Monad.IO.Class import Control.Exception import Control.Lens hiding ((.=), List)+import qualified Data.Map.Strict as Map import qualified Data.Text as T import qualified Data.Text.IO as T import Data.Aeson import Data.Default import qualified Data.HashMap.Strict as HashMap-import qualified Data.Map as Map+import Data.List import Data.Maybe import Language.Haskell.LSP.Types import Language.Haskell.LSP.Types.Lens hiding@@ -114,6 +117,7 @@ import System.IO import System.Directory import System.FilePath+import qualified System.FilePath.Glob as Glob -- | Starts a new session. --@@ -345,7 +349,61 @@ initializeResponse :: Session InitializeResponse initializeResponse = initRsp <$> ask >>= (liftIO . readMVar) --- | Opens a text document and sends a notification to the client.+-- | /Creates/ a new text document. This is different from 'openDoc'+-- as it sends a workspace/didChangeWatchedFiles notification letting the server+-- know that a file was created within the workspace, __provided that the server+-- has registered for it__, and the file matches any patterns the server+-- registered for.+-- It /does not/ actually create a file on disk, but is useful for convincing+-- the server that one does exist.+--+-- @since 11.0.0.0+createDoc :: FilePath -- ^ The path to the document to open, __relative to the root directory__.+ -> String -- ^ The text document's language identifier, e.g. @"haskell"@.+ -> T.Text -- ^ The content of the text document to create.+ -> Session TextDocumentIdentifier -- ^ The identifier of the document just created.+createDoc file languageId contents = do+ dynCaps <- curDynCaps <$> get+ rootDir <- asks rootDir+ caps <- asks sessionCapabilities+ absFile <- liftIO $ canonicalizePath (rootDir </> file)+ let regs = filter (\r -> r ^. method == WorkspaceDidChangeWatchedFiles) $+ Map.elems dynCaps+ watchHits :: FileSystemWatcher -> Bool+ watchHits (FileSystemWatcher pattern kind) =+ -- If WatchKind is exlcuded, defaults to all true as per spec+ fileMatches pattern && createHits (fromMaybe (WatchKind True True True) kind)++ fileMatches pattern = Glob.match (Glob.compile pattern) relOrAbs+ -- If the pattern is absolute then match against the absolute fp+ where relOrAbs+ | isAbsolute pattern = absFile+ | otherwise = file++ createHits (WatchKind create _ _) = create++ regHits :: Registration -> Bool+ regHits reg = isJust $ do+ opts <- reg ^. registerOptions+ fileWatchOpts <- case fromJSON opts :: Result DidChangeWatchedFilesRegistrationOptions of+ Success x -> Just x+ Error _ -> Nothing+ if foldl' (\acc w -> acc || watchHits w) False (fileWatchOpts ^. watchers)+ then Just ()+ else Nothing++ clientCapsSupports =+ caps ^? workspace . _Just . didChangeWatchedFiles . _Just . dynamicRegistration . _Just+ == Just True+ shouldSend = clientCapsSupports && foldl' (\acc r -> acc || regHits r) False regs++ when shouldSend $+ sendNotification WorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $+ List [ FileEvent (filePathToUri file) FcCreated ]+ openDoc' file languageId contents++-- | Opens a text document that /exists on disk/, and sends a+-- textDocument/didOpen notification to the server. openDoc :: FilePath -> String -> Session TextDocumentIdentifier openDoc file languageId = do context <- ask@@ -354,6 +412,7 @@ openDoc' file languageId contents -- | This is a variant of `openDoc` that takes the file content as an argument.+-- Use this is the file exists /outside/ of the current workspace. openDoc' :: FilePath -> String -> T.Text -> Session TextDocumentIdentifier openDoc' file languageId contents = do context <- ask@@ -363,13 +422,13 @@ sendNotification TextDocumentDidOpen (DidOpenTextDocumentParams item) pure $ TextDocumentIdentifier uri --- | Closes a text document and sends a notification to the client.+-- | Closes a text document and sends a textDocument/didOpen notification to the server. closeDoc :: TextDocumentIdentifier -> Session () closeDoc docId = do let params = DidCloseTextDocumentParams (TextDocumentIdentifier (docId ^. uri)) sendNotification TextDocumentDidClose params --- | Changes a text document and sends a notification to the client+-- | Changes a text document and sends a textDocument/didOpen notification to the server. changeDoc :: TextDocumentIdentifier -> [TextDocumentContentChangeEvent] -> Session () changeDoc docId changes = do verDoc <- getVersionedDoc docId@@ -611,3 +670,10 @@ rsp <- request TextDocumentCodeLens (CodeLensParams tId Nothing) :: Session CodeLensResponse case getResponseResult rsp of List res -> pure res++-- | Returns a list of capabilities that the server has requested to /dynamically/+-- register during the 'Session'.+--+-- @since 0.11.0.0+getRegisteredCapabilities :: Session [Registration]+getRegisteredCapabilities = (Map.elems . curDynCaps) <$> get
src/Language/Haskell/LSP/Test/Parsing.hs view
@@ -75,7 +75,7 @@ satisfyMaybe pred = do skipTimeout <- overridingTimeout <$> get- timeoutId <- curTimeoutId <$> get+ timeoutId <- getCurTimeoutId unless skipTimeout $ do chan <- asks messageChan timeout <- asks (messageTimeout . config)@@ -85,8 +85,7 @@ x <- Session await - unless skipTimeout $- modify $ \s -> s { curTimeoutId = timeoutId + 1 }+ unless skipTimeout (bumpTimeoutId timeoutId) modify $ \s -> s { lastReceivedMessage = Just x }
src/Language/Haskell/LSP/Test/Session.hs view
@@ -23,6 +23,8 @@ , sendMessage , updateState , withTimeout+ , getCurTimeoutId+ , bumpTimeoutId , logMsg , LogMsgType(..) )@@ -61,6 +63,7 @@ import Language.Haskell.LSP.Types.Capabilities import Language.Haskell.LSP.Types import Language.Haskell.LSP.Types.Lens+import qualified Language.Haskell.LSP.Types.Lens as LSP import Language.Haskell.LSP.VFS import Language.Haskell.LSP.Test.Compat import Language.Haskell.LSP.Test.Decoding@@ -121,7 +124,9 @@ { serverIn :: Handle , rootDir :: FilePath- , messageChan :: Chan SessionMessage+ , messageChan :: Chan SessionMessage -- ^ Where all messages come through+ -- Keep curTimeoutId in SessionContext, as its tied to messageChan+ , curTimeoutId :: MVar Int -- ^ The current timeout we are waiting on , requestMap :: MVar RequestMap , initRsp :: MVar InitializeResponse , config :: SessionConfig@@ -139,16 +144,29 @@ instance Monad m => HasReader r (ConduitM a b (StateT s (ReaderT r m))) where ask = lift $ lift Reader.ask +getCurTimeoutId :: (HasReader SessionContext m, MonadIO m) => m Int+getCurTimeoutId = asks curTimeoutId >>= liftIO . readMVar++-- Pass this the timeoutid you *were* waiting on+bumpTimeoutId :: (HasReader SessionContext m, MonadIO m) => Int -> m ()+bumpTimeoutId prev = do+ v <- asks curTimeoutId+ -- when updating the curtimeoutid, account for the fact that something else+ -- might have bumped the timeoutid in the meantime+ liftIO $ modifyMVar_ v (\x -> pure (max x (prev + 1)))+ data SessionState = SessionState { curReqId :: LspId , vfs :: VFS , curDiagnostics :: Map.Map NormalizedUri [Diagnostic]- , curTimeoutId :: Int , overridingTimeout :: Bool -- ^ The last received message from the server. -- Used for providing exception information , lastReceivedMessage :: Maybe FromServerMessage+ , curDynCaps :: Map.Map T.Text Registration+ -- ^ The capabilities that the server has dynamically registered with us so+ -- far } class Monad m => HasState s m where@@ -166,15 +184,19 @@ get = Session (lift State.get) put = Session . lift . State.put -instance Monad m => HasState s (ConduitM a b (StateT s m))+instance Monad m => HasState s (StateT s m) where+ get = State.get+ put = State.put++instance (Monad m, (HasState s m)) => HasState s (ConduitM a b m) where- get = lift State.get- put = lift . State.put+ get = lift get+ put = lift . put -instance Monad m => HasState s (ConduitParser a (StateT s m))+instance (Monad m, (HasState s m)) => HasState s (ConduitParser a m) where- get = lift State.get- put = lift . State.put+ get = lift get+ put = lift . put runSession :: SessionContext -> SessionState -> Session a -> IO (a, SessionState) runSession context state (Session session) = runReaderT (runStateT conduit state) context@@ -200,7 +222,7 @@ watchdog :: ConduitM SessionMessage FromServerMessage (StateT SessionState (ReaderT SessionContext IO)) () watchdog = Conduit.awaitForever $ \msg -> do- curId <- curTimeoutId <$> get+ curId <- getCurTimeoutId case msg of ServerMessage sMsg -> yield sMsg TimeoutMessage tId -> when (curId == tId) $ lastReceivedMessage <$> get >>= throw . Timeout@@ -229,25 +251,28 @@ reqMap <- newMVar newRequestMap messageChan <- newChan+ timeoutIdVar <- newMVar 0 initRsp <- newEmptyMVar mainThreadId <- myThreadId - let context = SessionContext serverIn absRootDir messageChan reqMap initRsp config caps- initState vfs = SessionState (IdInt 0) vfs- mempty 0 False Nothing+ let context = SessionContext serverIn absRootDir messageChan timeoutIdVar reqMap initRsp config caps+ initState vfs = SessionState (IdInt 0) vfs mempty False Nothing mempty runSession' ses = initVFS $ \vfs -> runSession context (initState vfs) ses - errorHandler = throwTo mainThreadId :: SessionException -> IO()+ errorHandler = throwTo mainThreadId :: SessionException -> IO () serverListenerLauncher = forkIO $ catch (serverHandler serverOut context) errorHandler server = (Just serverIn, Just serverOut, Nothing, serverProc)- serverAndListenerFinalizer tid =- finally (timeout (messageTimeout config * 1000000)+ serverAndListenerFinalizer tid = do+ finally (timeout (messageTimeout config * 1^6) (runSession' exitServer))- (cleanupProcess server >> killThread tid)+ -- Make sure to kill the listener first, before closing+ -- handles etc via cleanupProcess+ (killThread tid >> cleanupProcess server) - (result, _) <- bracket serverListenerLauncher serverAndListenerFinalizer+ (result, _) <- bracket serverListenerLauncher+ serverAndListenerFinalizer (const $ runSession' session) return result @@ -258,12 +283,25 @@ updateState :: (MonadIO m, HasReader SessionContext m, HasState SessionState m) => FromServerMessage -> m ()++-- Keep track of dynamic capability registration+updateState (ReqRegisterCapability req) = do+ let List newRegs = (\r -> (r ^. LSP.id, r)) <$> req ^. params . registrations+ modify $ \s ->+ s { curDynCaps = Map.union (Map.fromList newRegs) (curDynCaps s) }++updateState (ReqUnregisterCapability req) = do+ let List unRegs = (^. LSP.id) <$> req ^. params . unregistrations+ modify $ \s ->+ let newCurDynCaps = foldr' Map.delete (curDynCaps s) unRegs+ in s { curDynCaps = newCurDynCaps }+ updateState (NotPublishDiagnostics n) = do let List diags = n ^. params . diagnostics doc = n ^. params . uri- modify (\s ->+ modify $ \s -> let newDiags = Map.insert (toNormalizedUri doc) diags (curDiagnostics s)- in s { curDiagnostics = newDiags })+ in s { curDiagnostics = newDiags } updateState (ReqApplyWorkspaceEdit r) = do @@ -336,21 +374,20 @@ logMsg LogClient msg liftIO $ B.hPut h (addHeader $ encode msg) --- | Execute a block f that will throw a 'Timeout' exception+-- | Execute a block f that will throw a 'Language.Haskell.LSP.Test.Exception.Timeout' exception -- after duration seconds. This will override the global timeout -- for waiting for messages to arrive defined in 'SessionConfig'. withTimeout :: Int -> Session a -> Session a withTimeout duration f = do chan <- asks messageChan- timeoutId <- curTimeoutId <$> get+ timeoutId <- getCurTimeoutId modify $ \s -> s { overridingTimeout = True } liftIO $ forkIO $ do threadDelay (duration * 1000000) writeChan chan (TimeoutMessage timeoutId) res <- f- modify $ \s -> s { curTimeoutId = timeoutId + 1,- overridingTimeout = False- }+ bumpTimeoutId timeoutId+ modify $ \s -> s { overridingTimeout = False } return res data LogMsgType = LogServer | LogClient@@ -375,5 +412,3 @@ | otherwise = Cyan showPretty = B.unpack . encodePretty--
test/Test.hs view
@@ -15,37 +15,38 @@ import Control.Monad.IO.Class import Control.Monad import Control.Lens hiding (List)-import GHC.Generics import Language.Haskell.LSP.Messages import Language.Haskell.LSP.Test-import Language.Haskell.LSP.Test.Replay import Language.Haskell.LSP.Types-import Language.Haskell.LSP.Types.Lens as LSP hiding+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 = hspec $ do++main = findServer >>= \serverExe -> hspec $ do describe "Session" $ do- it "fails a test" $- let session = runSession "hie" fullCaps "test/data/renamePass" $ do+ it "fails a test" $ do+ let session = runSession serverExe fullCaps "test/data/renamePass" $ do openDoc "Desktop/simple.hs" "haskell"- skipMany loggingNotification anyRequest in session `shouldThrow` anySessionException- it "initializeResponse" $ runSession "hie" fullCaps "test/data/renamePass" $ do+ it "initializeResponse" $ runSession serverExe fullCaps "test/data/renamePass" $ do rsp <- initializeResponse- liftIO $ rsp ^. result `shouldSatisfy` isLeft+ liftIO $ rsp ^. result `shouldSatisfy` isRight it "runSessionWithConfig" $- runSession "hie" didChangeCaps "test/data/renamePass" $ return ()+ runSession serverExe didChangeCaps "test/data/renamePass" $ return () describe "withTimeout" $ do it "times out" $- let sesh = runSession "hie" fullCaps "test/data/renamePass" $ do+ 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@@ -56,15 +57,13 @@ in timeout 6000000 sesh `shouldThrow` anySessionException it "doesn't time out" $- let sesh = runSession "hie" fullCaps "test/data/renamePass" $ do+ 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 "hie" fullCaps "test/data/renamePass" $ do+ it "further timeout messages are ignored" $ runSession serverExe fullCaps "test/data/renamePass" $ do doc <- openDoc "Desktop/simple.hs" "haskell"- -- warm up the cache- getDocumentSymbols doc -- shouldn't timeout withTimeout 3 $ getDocumentSymbols doc -- longer than the original timeout@@ -75,7 +74,7 @@ it "overrides global message timeout" $ let sesh =- runSessionWithConfig (def { messageTimeout = 5 }) "hie" fullCaps "test/data/renamePass" $ do+ 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@@ -85,7 +84,7 @@ it "unoverrides global message timeout" $ let sesh =- runSessionWithConfig (def { messageTimeout = 5 }) "hie" fullCaps "test/data/renamePass" $ do+ 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@@ -99,15 +98,15 @@ describe "SessionException" $ do it "throw on time out" $- let sesh = runSessionWithConfig (def {messageTimeout = 10}) "hie" fullCaps "test/data/renamePass" $ do+ 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}) "hie" fullCaps "test/data/renamePass" $ do+ it "don't throw when no time out" $ runSessionWithConfig (def {messageTimeout = 5}) serverExe fullCaps "test/data/renamePass" $ do loggingNotification- liftIO $ threadDelay $ 10 * 1000000+ liftIO $ threadDelay $ 6 * 1000000 _ <- openDoc "Desktop/simple.hs" "haskell" return () @@ -115,7 +114,7 @@ it "throws when there's an unexpected message" $ let selector (UnexpectedMessage "Publish diagnostics notification" (NotLogMessage _)) = True selector _ = False- in runSession "hie" fullCaps "test/data/renamePass" publishDiagnosticsNotification `shouldThrow` selector+ 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@@ -124,40 +123,38 @@ sendRequest TextDocumentDocumentSymbol (DocumentSymbolParams doc Nothing) skipMany anyNotification message :: Session RenameResponse -- the wrong type- in runSession "hie" fullCaps "test/data/renamePass" sesh+ in runSession serverExe fullCaps "test/data/renamePass" sesh `shouldThrow` selector - describe "replaySession" $- -- This is too fickle at the moment- -- it "passes a test" $- -- replaySession "hie" "test/data/renamePass"- it "fails a test" $- let selector (ReplayOutOfOrder _ _) = True- selector _ = False- in replaySession "hie" "test/data/renameFail" `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"+ -- describe "manual javascript session" $+ -- it "passes a test" $+ -- runSession "javascript-typescript-stdio" fullCaps "test/data/javascriptPass" $ do+ -- doc <- openDoc "test.js" "javascript" - noDiagnostics+ -- noDiagnostics - Right (fooSymbol:_) <- getDocumentSymbols doc+ -- Right (fooSymbol:_) <- getDocumentSymbols doc - liftIO $ do- fooSymbol ^. name `shouldBe` "foo"- fooSymbol ^. kind `shouldBe` SkFunction+ -- liftIO $ do+ -- fooSymbol ^. name `shouldBe` "foo"+ -- fooSymbol ^. kind `shouldBe` SkFunction describe "text document VFS" $ it "sends back didChange notifications" $- runSession "hie" def "test/data/refactor" $ do+ runSession serverExe def "test/data/refactor" $ do doc <- openDoc "Main.hs" "haskell" - let args = toJSON $ AOP (doc ^. uri)- (Position 1 14)- "Redundant bracket"- reqParams = ExecuteCommandParams "applyrefact:applyOne" (Just (List [args])) Nothing+ let args = toJSON (doc ^. uri)+ reqParams = ExecuteCommandParams "doAnEdit" (Just (List [args])) Nothing request_ WorkspaceExecuteCommand reqParams editReq <- message :: Session ApplyWorkspaceEditRequest@@ -165,169 +162,154 @@ let (Just cs) = editReq ^. params . edit . changes [(u, List es)] = HM.toList cs u `shouldBe` doc ^. uri- es `shouldBe` [TextEdit (Range (Position 1 0) (Position 1 18)) "main = return 42"]-- noDiagnostics-+ es `shouldBe` [TextEdit (Range (Position 0 0) (Position 0 5)) "howdy"] contents <- documentContents doc- liftIO $ contents `shouldBe` "main :: IO Int\nmain = return 42\n"+ liftIO $ contents `shouldBe` "howdy:: IO Int\nmain = return (42)\n" describe "getDocumentEdit" $ it "automatically consumes applyedit requests" $- runSession "hie" fullCaps "test/data/refactor" $ do+ runSession serverExe fullCaps "test/data/refactor" $ do doc <- openDoc "Main.hs" "haskell" - let args = toJSON $ AOP (doc ^. uri)- (Position 1 14)- "Redundant bracket"- reqParams = ExecuteCommandParams "applyrefact:applyOne" (Just (List [args])) Nothing+ let args = toJSON (doc ^. uri)+ reqParams = ExecuteCommandParams "doAnEdit" (Just (List [args])) Nothing request_ WorkspaceExecuteCommand reqParams contents <- getDocumentEdit doc- liftIO $ contents `shouldBe` "main :: IO Int\nmain = return 42\n"- noDiagnostics+ liftIO $ contents `shouldBe` "howdy:: IO Int\nmain = return (42)\n" describe "getCodeActions" $- it "works" $ runSession "hie" fullCaps "test/data/refactor" $ do+ 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` "Apply hint:Redundant bracket"+ liftIO $ action ^. title `shouldBe` "Delete this" describe "getAllCodeActions" $- it "works" $ runSession "hie" fullCaps "test/data/refactor" $ do+ 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` "Apply hint:Redundant bracket"- action ^. command . _Just . command `shouldSatisfy` T.isSuffixOf ":applyrefact:applyOne"+ action ^. title `shouldBe` "Delete this"+ action ^. command . _Just . command `shouldBe` "deleteThis" describe "getDocumentSymbols" $- it "works" $ runSession "hie" fullCaps "test/data/renamePass" $ do+ it "works" $ runSession serverExe fullCaps "test/data/renamePass" $ do doc <- openDoc "Desktop/simple.hs" "haskell" skipMany loggingNotification - noDiagnostics- Left (mainSymbol:_) <- getDocumentSymbols doc liftIO $ do- mainSymbol ^. name `shouldBe` "main"- mainSymbol ^. kind `shouldBe` SkFunction- mainSymbol ^. range `shouldBe` Range (Position 3 0) (Position 5 30)+ mainSymbol ^. name `shouldBe` "foo"+ mainSymbol ^. kind `shouldBe` SkObject+ mainSymbol ^. range `shouldBe` mkRange 0 0 3 6 describe "applyEdit" $ do- it "increments the version" $ runSession "hie" docChangesCaps "test/data/renamePass" $ 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 "hie" fullCaps "test/data/renamePass" $ do+ 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 "hie" def "test/data/renamePass" $ do- doc <- openDoc "Desktop/simple.hs" "haskell"+ -- 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+ -- -- 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"+ -- 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 "hie" 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 "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 "hie" 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 "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 "hie" 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 "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 "hie" fullCaps "test/data" $ do+ it "works" $ runSession serverExe fullCaps "test/data" $ do openDoc "Error.hs" "haskell"- [diag] <- waitForDiagnosticsSource "bios"+ [diag] <- waitForDiagnosticsSource "dummy-server" liftIO $ do- diag ^. severity `shouldBe` Just DsError- diag ^. source `shouldBe` Just "bios"-- 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+ diag ^. severity `shouldBe` Just DsWarning+ diag ^. source `shouldBe` Just "dummy-server" - -- runSession "hie" fullCaps "test/data" $ do- -- doc <- openDoc "Rename.hs" "haskell"- -- rename doc (Position 1 0) "bar"- -- documentContents doc >>= liftIO . shouldBe "main = bar\nbar = return 42\n"+ -- 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 "hie" fullCaps "test/data/renamePass" $ do+ it "works" $ runSession serverExe fullCaps "test/data/renamePass" $ do doc <- openDoc "Desktop/simple.hs" "haskell"- -- hover returns nothing until module is loaded- skipManyTill loggingNotification $ count 2 noDiagnostics- hover <- getHover doc (Position 45 9) -- putStrLn+ hover <- getHover doc (Position 45 9) liftIO $ hover `shouldSatisfy` isJust - describe "getHighlights" $- it "works" $ runSession "hie" 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 "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 "hie" fullCaps "test/data" $ do- doc <- openDoc "Format.hs" "haskell"- oldContents <- documentContents doc- formatDoc doc (FormattingOptions 4 True)- documentContents doc >>= liftIO . (`shouldNotBe` oldContents)+ -- 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 "hie" 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 "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 "hie" fullCaps "test/data" $ do+ runSession serverExe fullCaps "test/data" $ do doc <- openDoc "Format.hs" "haskell" closeDoc doc -- need to evaluate to throw@@ -335,7 +317,7 @@ in sesh `shouldThrow` anyException describe "satisfy" $- it "works" $ runSession "hie" fullCaps "test/data" $ do+ it "works" $ runSession serverExe fullCaps "test/data" $ do openDoc "Format.hs" "haskell" let pred (NotLogMessage _) = True pred _ = False@@ -343,10 +325,60 @@ describe "ignoreLogNotifications" $ it "works" $- runSessionWithConfig (defaultConfig { ignoreLogNotifications = True }) "hie" fullCaps "test/data" $ do+ 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@@ -361,8 +393,24 @@ workspaceCaps = def { _workspaceEdit = Just editCaps } editCaps = WorkspaceEditClientCapabilities (Just True) -data ApplyOneParams = AOP- { file :: Uri- , start_pos :: Position- , hintTitle :: String- } deriving (Generic, ToJSON)++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'
+ test/dummy-server/Main.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE OverloadedStrings #-}+import Data.Aeson+import Data.Default+import Data.List (isSuffixOf)+import qualified Data.HashMap.Strict as HM+import Language.Haskell.LSP.Core+import Language.Haskell.LSP.Control+import Language.Haskell.LSP.Messages+import Language.Haskell.LSP.Types+import Control.Concurrent+import Control.Monad+import System.Directory+import System.FilePath++main = do+ lfvar <- newEmptyMVar+ let initCbs = InitializeCallbacks+ { onInitialConfiguration = const $ Right ()+ , onConfigurationChange = const $ Right ()+ , onStartup = \lf -> do+ putMVar lfvar lf++ return Nothing+ }+ options = def+ { executeCommandCommands = Just ["doAnEdit"]+ }+ run initCbs (handlers lfvar) options Nothing++handlers :: MVar (LspFuncs ()) -> Handlers+handlers lfvar = def+ { initializedHandler = pure $ \_ -> send $ NotLogMessage $ fmServerLogMessageNotification MtLog "initialized"+ , hoverHandler = pure $ \req -> send $+ RspHover $ makeResponseMessage req (Just (Hover (HoverContents (MarkupContent MkPlainText "hello")) Nothing))+ , documentSymbolHandler = pure $ \req -> send $+ RspDocumentSymbols $ makeResponseMessage req $ DSDocumentSymbols $+ List [ DocumentSymbol "foo"+ Nothing+ SkObject+ Nothing+ (mkRange 0 0 3 6)+ (mkRange 0 0 3 6)+ Nothing+ ]+ , didOpenTextDocumentNotificationHandler = pure $ \noti -> do+ let NotificationMessage _ _ (DidOpenTextDocumentParams doc) = noti+ TextDocumentItem uri _ _ _ = doc+ Just fp = uriToFilePath uri+ diag = Diagnostic (mkRange 0 0 0 1)+ (Just DsWarning)+ (Just (NumberValue 42))+ (Just "dummy-server")+ "Here's a warning"+ Nothing+ Nothing+ when (".hs" `isSuffixOf` fp) $ void $ forkIO $ do+ threadDelay (2 * 10^6)+ send $ NotPublishDiagnostics $+ fmServerPublishDiagnosticsNotification $ PublishDiagnosticsParams uri $ List [diag]++ -- also act as a registerer for workspace/didChangeWatchedFiles+ when (".register" `isSuffixOf` fp) $ do+ reqId <- readMVar lfvar >>= getNextReqId+ send $ ReqRegisterCapability $ fmServerRegisterCapabilityRequest reqId $+ RegistrationParams $ List $+ [ Registration "0" WorkspaceDidChangeWatchedFiles $ Just $ toJSON $+ DidChangeWatchedFilesRegistrationOptions $ List+ [ FileSystemWatcher "*.watch" (Just (WatchKind True True True)) ]+ ]+ when (".register.abs" `isSuffixOf` fp) $ do+ curDir <- getCurrentDirectory+ reqId <- readMVar lfvar >>= getNextReqId+ send $ ReqRegisterCapability $ fmServerRegisterCapabilityRequest reqId $+ RegistrationParams $ List $+ [ Registration "1" WorkspaceDidChangeWatchedFiles $ Just $ toJSON $+ DidChangeWatchedFilesRegistrationOptions $ List+ [ FileSystemWatcher (curDir </> "*.watch") (Just (WatchKind True True True)) ]+ ]++ -- also act as an unregisterer for workspace/didChangeWatchedFiles+ when (".unregister" `isSuffixOf` fp) $ do+ reqId <- readMVar lfvar >>= getNextReqId+ send $ ReqUnregisterCapability $ fmServerUnregisterCapabilityRequest reqId $+ UnregistrationParams $ List [ Unregistration "0" "workspace/didChangeWatchedFiles" ]+ when (".unregister.abs" `isSuffixOf` fp) $ do+ reqId <- readMVar lfvar >>= getNextReqId+ send $ ReqUnregisterCapability $ fmServerUnregisterCapabilityRequest reqId $+ UnregistrationParams $ List [ Unregistration "1" "workspace/didChangeWatchedFiles" ]+ , executeCommandHandler = pure $ \req -> do+ send $ RspExecuteCommand $ makeResponseMessage req Null+ reqId <- readMVar lfvar >>= getNextReqId+ let RequestMessage _ _ _ (ExecuteCommandParams "doAnEdit" (Just (List [val])) _) = req+ Success docUri = fromJSON val+ edit = List [TextEdit (mkRange 0 0 0 5) "howdy"]+ send $ ReqApplyWorkspaceEdit $ fmServerApplyWorkspaceEditRequest reqId $+ ApplyWorkspaceEditParams $ WorkspaceEdit (Just (HM.singleton docUri edit))+ Nothing+ , codeActionHandler = pure $ \req -> do+ let RequestMessage _ _ _ params = req+ CodeActionParams _ _ cactx _ = params+ CodeActionContext diags _ = cactx+ caresults = fmap diag2caresult diags+ diag2caresult d = CACodeAction $+ CodeAction "Delete this"+ Nothing+ (Just (List [d]))+ Nothing+ (Just (Command "" "deleteThis" Nothing))+ send $ RspCodeAction $ makeResponseMessage req caresults+ , didChangeWatchedFilesNotificationHandler = pure $ \_ ->+ send $ NotLogMessage $ fmServerLogMessageNotification MtLog "got workspace/didChangeWatchedFiles"+ }+ where send msg = readMVar lfvar >>= \lf -> (sendFunc lf) msg++mkRange sl sc el ec = Range (Position sl sc) (Position el ec)