packages feed

haskell-tools-daemon (empty) → 0.4.0.0

raw patch · 7 files changed

+650/−0 lines, 7 filesdep +HUnitdep +aesondep +basesetup-changed

Dependencies added: HUnit, aeson, base, bytestring, containers, directory, filepath, ghc, ghc-paths, haskell-tools-ast, haskell-tools-daemon, haskell-tools-prettyprint, haskell-tools-refactor, mtl, network, references, split, tasty, tasty-hunit

Files

+ LICENSE view
@@ -0,0 +1,29 @@+Haskell-Tools is Copyright (c) Boldizsár Németh, 2016, all rights reserved,
+and is distributed as free software under the following license.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions, and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions, and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- The names of the copyright holders may not be used to endorse or
+promote products derived from this software without specific prior
+written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Language/Haskell/Tools/Refactor/Daemon.hs view
@@ -0,0 +1,239 @@+{-# LANGUAGE ScopedTypeVariables
+           , OverloadedStrings 
+           , DeriveGeneric
+           , LambdaCase
+           , TemplateHaskell
+           , FlexibleContexts
+           #-}
+module Language.Haskell.Tools.Refactor.Daemon where
+
+import Data.Aeson hiding ((.=))
+import Data.ByteString.Lazy.Char8 (ByteString)
+import Data.ByteString.Lazy.Char8 (unpack)
+import qualified Data.ByteString.Lazy.Char8 as BS
+import GHC.Generics
+
+import Network.Socket hiding (send, sendTo, recv, recvFrom, KeepAlive)
+import Network.Socket.ByteString.Lazy
+import Control.Exception
+import Data.Aeson hiding ((.=))
+import Data.Map (Map, (!), member, insert)
+import qualified Data.Map as Map
+import GHC.Generics
+
+import System.IO
+import System.IO.Error
+import System.FilePath
+import System.Directory
+import Data.IORef
+import Data.List hiding (insert)
+import Data.Tuple
+import Data.Maybe
+import Control.Applicative ((<|>))
+import Control.Monad
+import Control.Monad.State
+import Control.Concurrent.MVar
+import Control.Monad.IO.Class
+import System.Environment
+import Debug.Trace
+
+import GHC hiding (loadModule)
+import Bag (bagToList)
+import SrcLoc (realSrcSpanStart)
+import ErrUtils (errMsgSpan)
+import DynFlags (gopt_set)
+import GHC.Paths ( libdir )
+import GhcMonad (GhcMonad(..), Session(..), reflectGhc, modifySession)
+import HscTypes (SourceError, srcErrorMessages, hsc_mod_graph)
+import FastString (unpackFS)
+
+import Control.Reference
+
+import Language.Haskell.Tools.AST
+import Language.Haskell.Tools.Refactor.GetModules
+import Language.Haskell.Tools.Refactor.Prepare
+import Language.Haskell.Tools.Refactor.Perform
+import Language.Haskell.Tools.Refactor.RefactorBase
+import Language.Haskell.Tools.Refactor.Session
+import Language.Haskell.Tools.PrettyPrint
+import Language.Haskell.Tools.Refactor.Daemon.State
+
+-- TODO: handle boot files
+
+runDaemonCLI :: IO ()
+runDaemonCLI = getArgs >>= runDaemon
+
+runDaemon :: [String] -> IO ()
+runDaemon args = withSocketsDo $
+    do let finalArgs = args ++ drop (length args) defaultArgs
+           isSilent = read (finalArgs !! 1)
+       addrinfos <- getAddrInfo
+                    (Just (defaultHints {addrFlags = [AI_PASSIVE]}))
+                    Nothing (Just (finalArgs !! 0))
+       let serveraddr = head addrinfos
+       sock <- socket (addrFamily serveraddr) Stream defaultProtocol
+       setSocketOption sock ReuseAddr 1
+       bind sock (addrAddress serveraddr)
+       listen sock 1
+       clientLoop isSilent sock
+
+defaultArgs = ["4123", "True"]
+
+clientLoop :: Bool -> Socket -> IO ()
+clientLoop isSilent sock
+  = do (conn,_) <- accept sock
+       ghcSess <- initGhcSession
+       state <- newMVar initSession
+       serverLoop isSilent ghcSess state conn
+       sessionData <- readMVar state
+       when (not (sessionData ^. exiting))
+         $ clientLoop isSilent sock
+
+serverLoop :: Bool -> Session -> MVar DaemonSessionState -> Socket -> IO ()
+serverLoop isSilent ghcSess state sock =
+    do msg <- recv sock 2048
+       when (not $ BS.null msg) $ do -- null on TCP means closed connection
+         when (not isSilent) $ putStrLn $ "message received: " ++ show (unpack msg)
+         let msgs = BS.split '\n' msg
+         continue <- forM msgs $ \msg -> respondTo ghcSess state (sendAll sock . (`BS.snoc` '\n')) msg
+         sessionData <- readMVar state
+         when (not (sessionData ^. exiting) && all (== True) continue)
+           $ serverLoop isSilent ghcSess state sock
+  `catch` interrupted sock
+  where interrupted = \s ex -> do
+                        let err = show (ex :: IOException)
+                        when (not isSilent) $ do
+                          putStrLn "Closing down socket"
+                          hPutStrLn stderr $ "Some exception caught: " ++ err
+
+respondTo :: Session -> MVar DaemonSessionState -> (ByteString -> IO ()) -> ByteString -> IO Bool
+respondTo ghcSess state next mess
+  | BS.null mess = return True
+  | otherwise
+  = case decode mess of
+      Nothing -> do next $ encode $ ErrorMessage $ "MALFORMED MESSAGE: " ++ unpack mess
+                    return True
+      Just req -> modifyMVar state (\st -> swap <$> reflectGhc (runStateT (updateClient (next . encode) req) st) ghcSess)
+
+-- | This function does the real job of acting upon client messages in a stateful environment of a client
+updateClient :: (ResponseMsg -> IO ()) -> ClientMessage -> StateT DaemonSessionState Ghc Bool
+updateClient resp KeepAlive = liftIO (resp KeepAliveResponse) >> return True
+updateClient resp Disconnect = liftIO (resp Disconnected) >> return False
+updateClient resp (AddPackages packagePathes) = do
+    existing <- gets (map ms_mod . (^? refSessMCs & traversal & filtered isTheAdded & mcModules & traversal & modRecMS))
+    needToReload <- (filter (\ms -> not $ ms_mod ms `elem` existing)) 
+                      <$> getReachableModules (\ms -> ms_mod ms `elem` existing)
+    modify $ refSessMCs .- filter (not . isTheAdded) -- remove the added package from the database
+    forM_ existing $ \mn -> removeTarget (TargetModule (GHC.moduleName mn))
+    modifySession (\s -> s { hsc_mod_graph = filter (not . (`elem` existing) . ms_mod) (hsc_mod_graph s) })
+    (modules, ignoredMods) <- loadPackagesFrom (return . getModSumOrig) packagePathes
+    mapM_ (reloadModule (\_ -> return ())) needToReload -- don't report consequent reloads (not expected)
+    liftIO $ resp 
+      $ if not (null ignoredMods) 
+          then ErrorMessage 
+                 $ "The following modules are ignored: " 
+                     ++ concat (intersperse ", " ignoredMods)
+                     ++ ". Multiple modules with the same qualified name are not supported."
+          else LoadedModules modules
+    return True
+  where isTheAdded mc = (mc ^. mcRoot) `elem` packagePathes
+
+updateClient resp (RemovePackages packagePathes) = do
+    mcs <- gets (^. refSessMCs)
+    let existing = map ms_mod (mcs ^? traversal & filtered isRemoved & mcModules & traversal & modRecMS)
+    lift $ forM_ existing (\modName -> removeTarget (TargetModule (GHC.moduleName modName)))
+    lift $ deregisterDirs (mcs ^? traversal & filtered isRemoved & mcSourceDirs & traversal)
+    modify $ refSessMCs .- filter (not . isRemoved)
+    modifySession (\s -> s { hsc_mod_graph = filter (not . (`elem` existing) . ms_mod) (hsc_mod_graph s) })
+    mods <- lift getModuleGraph
+    return True
+  where isRemoved mc = (mc ^. mcRoot) `elem` packagePathes
+
+updateClient resp (ReLoad changed removed) =
+  do removedMods <- gets (map ms_mod . filter ((`elem` removed) . getModSumOrig) . (^? refSessMCs & traversal & mcModules & traversal & modRecMS))
+     lift $ forM_ removedMods (\modName -> removeTarget (TargetModule (GHC.moduleName modName)))
+     modify $ refSessMCs & traversal & mcModules 
+                .- Map.filter (\m -> maybe True (not . (`elem` removed) . getModSumOrig) (m ^? modRecMS))
+     modifySession (\s -> s { hsc_mod_graph = filter (not . (`elem` removedMods) . ms_mod) (hsc_mod_graph s) })
+     reloadChangedModules (\ms -> resp (LoadedModules [getModSumOrig ms]))
+                          (\ms -> getModSumOrig ms `elem` changed)
+     return True
+
+updateClient _ Stop = modify (exiting .= True) >> return False
+
+updateClient resp (PerformRefactoring refact modPath selection args) = do
+    (Just actualMod, otherMods) <- getFileMods modPath
+    let cmd = analyzeCommand refact (selection:args)
+    res <- lift $ performCommand cmd actualMod otherMods
+    case res of
+      Left err -> liftIO $ resp $ ErrorMessage err
+      Right diff -> do changedMods <- catMaybes <$> applyChanges diff
+                       liftIO $ resp $ ModulesChanged (map snd changedMods)
+                       void $ reloadChanges (map ((^. sfkModuleName) . fst) changedMods)
+    return True
+  where applyChanges changes = do 
+          forM changes $ \case 
+            ModuleCreated n m otherM -> do 
+              mcs <- gets (^. refSessMCs)
+              Just (_, otherMR) <- gets (lookupModInSCs otherM . (^. refSessMCs))
+
+              let Just otherMS = otherMR ^? modRecMS
+                  Just mc = lookupModuleColl (otherM ^. sfkModuleName) mcs
+              modify $ refSessMCs & traversal & filtered (\mc' -> (mc' ^. mcId) == (mc ^. mcId)) & mcModules 
+                         .- Map.insert (SourceFileKey NormalHs n) (ModuleNotLoaded False)
+              otherSrcDir <- liftIO $ getSourceDir otherMS
+              let loc = toFileName otherSrcDir n
+              liftIO $ withBinaryFile loc WriteMode (`hPutStr` prettyPrint m)
+              lift $ addTarget (Target (TargetModule (GHC.mkModuleName n)) True Nothing)
+              return $ Just (SourceFileKey NormalHs n, loc)
+            ContentChanged (n,m) -> do
+              Just (_, mr) <- gets (lookupModInSCs n . (^. refSessMCs))
+              let Just ms = mr ^? modRecMS
+              liftIO $ withBinaryFile (getModSumOrig ms) WriteMode (`hPutStr` prettyPrint m)
+              return $ Just (n, getModSumOrig ms)
+            ModuleRemoved mod -> do
+              Just (_,m) <- gets (lookupModInSCs (SourceFileKey NormalHs mod) . (^. refSessMCs))
+              let modName = GHC.moduleName $ fromJust $ fmap semanticsModule (m ^? typedRecModule) <|> fmap semanticsModule (m ^? renamedRecModule)
+              ms <- getModSummary modName
+              lift $ removeTarget (TargetModule modName)
+              modify $ (refSessMCs .- removeModule mod)
+              liftIO $ removeFile (getModSumOrig ms)
+              return Nothing
+          
+        reloadChanges changedMods 
+          = reloadChangedModules (\ms -> resp $ LoadedModules [getModSumOrig ms]) (\ms -> modSumName ms `elem` changedMods)
+
+initGhcSession :: IO Session
+initGhcSession = Session <$> (newIORef =<< runGhc (Just libdir) (initGhcFlags >> getSession))
+
+data ClientMessage
+  = KeepAlive
+  | AddPackages { addedPathes :: [FilePath] }
+  | RemovePackages { removedPathes :: [FilePath] }
+  | PerformRefactoring { refactoring :: String
+                       , modulePath :: FilePath
+                       , editorSelection :: String
+                       , details :: [String]
+                       }
+  | Stop
+  | Disconnect
+  | ReLoad { changedModules :: [FilePath]
+           , removedModules :: [FilePath]
+           }
+  -- ReLoadAll -- re-load all modules
+  -- Reset -- completely re-initialize the refactor sesson
+  deriving (Show, Generic)
+
+
+instance FromJSON ClientMessage 
+
+data ResponseMsg
+  = KeepAliveResponse
+  | ErrorMessage { errorMsg :: String }
+  | CompilationProblem { errorMsg :: String }
+  | ModulesChanged { moduleChanges :: [FilePath] }
+  | LoadedModules { loadedModules :: [FilePath] }
+  | Disconnected
+  deriving (Show, Generic)
+
+instance ToJSON ResponseMsg
+ Language/Haskell/Tools/Refactor/Daemon/State.hs view
@@ -0,0 +1,18 @@+{-# LANGUAGE TemplateHaskell #-}
+module Language.Haskell.Tools.Refactor.Daemon.State where
+
+import Control.Reference
+
+import Language.Haskell.Tools.Refactor.GetModules
+import Language.Haskell.Tools.Refactor.Session
+
+data DaemonSessionState 
+  = DaemonSessionState { _refactorSession :: RefactorSessionState
+                       , _exiting :: Bool
+                       }
+
+makeReferences ''DaemonSessionState
+
+instance IsRefactSessionState DaemonSessionState where
+  refSessMCs = refactorSession & refSessMCs
+  initSession = DaemonSessionState initSession False
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple
+main = defaultMain
+ exe/Main.hs view
@@ -0,0 +1,8 @@+module Main where
+
+import System.Environment
+
+import Language.Haskell.Tools.Refactor.Daemon
+
+main :: IO ()
+main = runDaemonCLI
+ haskell-tools-daemon.cabal view
@@ -0,0 +1,59 @@+name:                haskell-tools-daemon
+version:             0.4.0.0
+synopsis:            Background process for Haskell-tools refactor that editors can connect to.
+description:         Background process for Haskell-tools refactor that editors can connect to.
+homepage:            https://github.com/haskell-tools/haskell-tools
+license:             BSD3
+license-file:        LICENSE
+author:              Boldizsar Nemeth
+maintainer:          nboldi@elte.hu
+category:            Language
+build-type:          Simple
+cabal-version:       >=1.10
+
+library
+  ghc-options:         -O2
+  build-depends:       base                      >= 4.9   && < 5.0
+                     , aeson                     >= 0.11  && < 1.0
+                     , bytestring                >= 0.10  && < 1.0
+                     , filepath                  >= 1.4   && < 2.0
+                     , containers                >= 0.5   && < 0.6
+                     , mtl                       >= 2.2   && < 2.3
+                     , split                     >= 0.2   && < 1.0
+                     , directory                 >= 1.2   && < 1.3
+                     , ghc                       >= 8.0   && < 8.1
+                     , ghc-paths                 >= 0.1   && < 0.2
+                     , references                >= 0.3.2 && < 1.0
+                     , network                   >= 2.6   && < 3.0
+                     , haskell-tools-ast         >= 0.4   && < 0.5
+                     , haskell-tools-prettyprint >= 0.4   && < 0.5
+                     , haskell-tools-refactor    >= 0.4   && < 0.5
+  exposed-modules:     Language.Haskell.Tools.Refactor.Daemon
+                     , Language.Haskell.Tools.Refactor.Daemon.State
+  default-language:    Haskell2010
+
+
+executable ht-daemon
+  ghc-options:         -O2
+  build-depends:       base                      >= 4.9 && < 5.0
+                     , haskell-tools-daemon      >= 0.4 && < 0.5
+  hs-source-dirs:      exe
+  main-is:             Main.hs
+  default-language:    Haskell2010
+
+test-suite haskell-tools-daemon-tests
+  type:                exitcode-stdio-1.0
+  ghc-options:         -with-rtsopts=-M2g -O2
+  hs-source-dirs:      test
+  main-is:             Main.hs  
+  build-depends:       base                      >= 4.9  && < 4.10
+                     , HUnit                     >= 1.3  && < 1.4
+                     , tasty                     >= 0.11 && < 0.12
+                     , tasty-hunit               >= 0.9  && < 0.10
+                     , directory                 >= 1.2  && < 1.3
+                     , filepath                  >= 1.4  && < 2.0
+                     , bytestring                >= 0.10 && < 0.11
+                     , network                   >= 2.6  && < 2.7
+                     , aeson                     >= 0.11 && < 0.12
+                     , haskell-tools-daemon      >= 0.4  && < 0.5
+  default-language:    Haskell2010
+ test/Main.hs view
@@ -0,0 +1,295 @@+{-# LANGUAGE StandaloneDeriving, LambdaCase #-}
+module Main where
+
+import Test.Tasty
+import Test.Tasty.HUnit
+import System.Exit
+import System.Directory
+import System.FilePath
+import Control.Monad
+import Control.Exception
+import Control.Concurrent
+import Control.Concurrent.MVar
+import Network.Socket hiding (KeepAlive, send, recv)
+import Network.Socket.ByteString.Lazy as Sock
+import qualified Data.ByteString.Lazy.Char8 as BS
+import qualified Data.List as List
+import Data.Aeson
+import Data.Maybe
+import System.IO
+import System.Directory
+
+import Debug.Trace
+
+import Language.Haskell.Tools.Refactor.Daemon
+
+main :: IO ()
+main = do -- create one daemon process for the whole testing session
+          -- with separate processes it is not a problem
+          forkIO $ runDaemon ["4123", "True"]
+          tr <- canonicalizePath testRoot
+          defaultMain (allTests tr)
+          stopDaemon
+
+allTests :: FilePath -> TestTree
+allTests testRoot
+  = localOption (mkTimeout ({- 10s -} 1000 * 1000 * 10)) 
+      $ testGroup "daemon-tests" 
+          [ testGroup "simple-tests" 
+              $ map (makeDaemonTest . (\(label, input, output) -> (Nothing, label, input, output))) simpleTests
+          , testGroup "loading-tests" 
+              $ map (makeDaemonTest . (\(label, input, output) -> (Nothing, label, input, output))) loadingTests
+          , testGroup "refactor-tests" 
+              $ map (makeDaemonTest . (\(label, dir, input, output) -> (Just (testRoot </> dir), label, input, output))) (refactorTests testRoot)
+          , testGroup "reload-tests" 
+              $ map makeReloadTest reloadingTests
+          , selfLoadingTest
+          ]
+
+testSuffix = "_test"
+
+simpleTests :: [(String, [ClientMessage], [ResponseMsg])]
+simpleTests = 
+  [ ( "empty-test", [], [] )
+  , ( "keep-alive", [KeepAlive], [KeepAliveResponse] )
+  ]
+
+loadingTests :: [(String, [ClientMessage], [ResponseMsg])]
+loadingTests =
+  [ ( "load-package"
+    , [AddPackages [testRoot </> "has-cabal"]]
+    , [LoadedModules [testRoot </> "has-cabal" </> "A.hs"]] )
+  , ( "no-cabal"
+    , [AddPackages [testRoot </> "no-cabal"]]
+    , [LoadedModules [testRoot </> "no-cabal" </> "A.hs"]] )
+  , ( "source-dir"
+    , [AddPackages [testRoot </> "source-dir"]]
+    , [LoadedModules [testRoot </> "source-dir" </> "src" </> "A.hs"]] )
+  , ( "source-dir-outside"
+    , [AddPackages [testRoot </> "source-dir-outside"]]
+    , [LoadedModules [testRoot </> "source-dir-outside" </> ".." </> "src" </> "A.hs"]] )
+  , ( "multi-packages"
+    , [ AddPackages [ testRoot </> "multi-packages" </> "package1"
+                    , testRoot </> "multi-packages" </> "package2" ]]
+    , [ LoadedModules [ testRoot </> "multi-packages" </> "package2" </> "B.hs"
+                      , testRoot </> "multi-packages" </> "package1" </> "A.hs"]] )
+  , ( "multi-packages-flags"
+    , [ AddPackages [ testRoot </> "multi-packages-flags" </> "package1"
+                    , testRoot </> "multi-packages-flags" </> "package2" ]]
+    , [ LoadedModules [ testRoot </> "multi-packages-flags" </> "package2" </> "B.hs"
+                      , testRoot </> "multi-packages-flags" </> "package1" </> "A.hs"]] )
+  , ( "multi-packages-dependent"
+    , [ AddPackages [ testRoot </> "multi-packages-dependent" </> "package1"
+                    , testRoot </> "multi-packages-dependent" </> "package2" ]]
+    , [ LoadedModules [ testRoot </> "multi-packages-dependent" </> "package1" </> "A.hs"
+                      , testRoot </> "multi-packages-dependent" </> "package2" </> "B.hs"]] )
+  , ( "has-th"
+    , [AddPackages [testRoot </> "has-th"]]
+    , [LoadedModules [testRoot </> "has-th" </> "TH.hs", testRoot </> "has-th" </> "A.hs"]] )
+  , ( "th-added-later"
+    , [ AddPackages [testRoot </> "th-added-later" </> "package1"]
+      , AddPackages [testRoot </> "th-added-later" </> "package2"] 
+      ]
+    , [ LoadedModules [testRoot </> "th-added-later" </> "package1" </> "A.hs"] 
+      , LoadedModules [testRoot </> "th-added-later" </> "package2" </> "B.hs"]] )
+  ]
+
+sourceRoot = ".." </> ".." </> "src"
+
+selfLoadingTest :: TestTree
+selfLoadingTest = localOption (mkTimeout ({- 2 min -} 1000 * 1000 * 60 * 2)) $ testCase "self-load" $ do  
+    actual <- communicateWithDaemon 
+                [ Right $ AddPackages (map (sourceRoot </>) ["ast" {-, "backend-ghc", "prettyprint", "rewrite", "refactor", "cli", "daemon" -} ] ) ]
+    assertBool ("The expected result is a nonempty response message list that does not contain errors. Actual result: " ++ show actual) 
+               (not (null actual) && all (\case ErrorMessage {} -> False; _ -> True) actual)
+
+
+refactorTests :: FilePath -> [(String, FilePath, [ClientMessage], [ResponseMsg])]
+refactorTests testRoot =
+  [ ( "simple-refactor", "simple-refactor"
+    , [ AddPackages [ testRoot </> "simple-refactor" ++ testSuffix ]
+      , PerformRefactoring "RenameDefinition" (testRoot </> "simple-refactor" ++ testSuffix </> "A.hs") "3:1-3:2" ["y"]
+      ]
+    , [ LoadedModules [ testRoot </> "simple-refactor" ++ testSuffix </> "A.hs" ]
+      , ModulesChanged [ testRoot </> "simple-refactor" ++ testSuffix </> "A.hs" ] 
+      , LoadedModules [ testRoot </> "simple-refactor" ++ testSuffix </> "A.hs" ]
+      ] )
+  , ( "hs-boots", "hs-boots"
+    , [ AddPackages [ testRoot </> "hs-boots" ++ testSuffix ]
+      , PerformRefactoring "RenameDefinition" (testRoot </> "hs-boots" ++ testSuffix </> "A.hs") "5:1-5:2" ["aa"]
+      ]
+    , [ LoadedModules [ testRoot </> "hs-boots" ++ testSuffix </> "B.hs-boot", testRoot </> "hs-boots" ++ testSuffix </> "A.hs-boot"
+                      , testRoot </> "hs-boots" ++ testSuffix </> "A.hs", testRoot </> "hs-boots" ++ testSuffix </> "B.hs" ]
+      , ModulesChanged [ testRoot </> "hs-boots" ++ testSuffix </> "A.hs", testRoot </> "hs-boots" ++ testSuffix </> "B.hs"
+                       , testRoot </> "hs-boots" ++ testSuffix </> "A.hs-boot" ] 
+      , LoadedModules [ testRoot </> "hs-boots" ++ testSuffix </> "A.hs-boot" ]
+      , LoadedModules [ testRoot </> "hs-boots" ++ testSuffix </> "B.hs-boot" ]
+      , LoadedModules [ testRoot </> "hs-boots" ++ testSuffix </> "A.hs" ]
+      , LoadedModules [ testRoot </> "hs-boots" ++ testSuffix </> "B.hs" ]
+      ] )
+  , ( "remove-module", "simple-refactor"
+    , [ AddPackages [ testRoot </> "simple-refactor" ++ testSuffix ]
+      , PerformRefactoring "RenameDefinition" (testRoot </> "simple-refactor" ++ testSuffix </> "A.hs") "1:8-1:9" ["AA"]
+      ]
+    , [ LoadedModules [ testRoot </> "simple-refactor" ++ testSuffix </> "A.hs" ]
+      , ModulesChanged [ testRoot </> "simple-refactor" ++ testSuffix </> "AA.hs" ] 
+      , LoadedModules [ testRoot </> "simple-refactor" ++ testSuffix </> "AA.hs" ]
+      ] )
+  ]
+
+reloadingTests :: [(String, FilePath, [ClientMessage], IO (), [ClientMessage], [ResponseMsg])]
+reloadingTests =
+  [ ( "reloading-module", testRoot </> "reloading", [ AddPackages [ testRoot </> "reloading" ++ testSuffix ] ]
+    , writeFile (testRoot </> "reloading" ++ testSuffix </> "C.hs") "module C where\nc = ()" 
+    , [ ReLoad [testRoot </> "reloading" ++ testSuffix </> "C.hs"] []
+      , PerformRefactoring "RenameDefinition" (testRoot </> "reloading" ++ testSuffix </> "C.hs") "2:1-2:2" ["d"] 
+      ]
+    , [ LoadedModules [ testRoot </> "reloading" ++ testSuffix </> "C.hs"
+                      , testRoot </> "reloading" ++ testSuffix </> "B.hs"
+                      , testRoot </> "reloading" ++ testSuffix </> "A.hs" ]
+      , LoadedModules [ testRoot </> "reloading" ++ testSuffix </> "C.hs" ]
+      , LoadedModules [ testRoot </> "reloading" ++ testSuffix </> "B.hs" ]
+      , LoadedModules [ testRoot </> "reloading" ++ testSuffix </> "A.hs" ]
+      , ModulesChanged [ testRoot </> "reloading" ++ testSuffix </> "C.hs"
+                       , testRoot </> "reloading" ++ testSuffix </> "B.hs" ]
+      , LoadedModules [ testRoot </> "reloading" ++ testSuffix </> "C.hs" ]
+      , LoadedModules [ testRoot </> "reloading" ++ testSuffix </> "B.hs" ]
+      , LoadedModules [ testRoot </> "reloading" ++ testSuffix </> "A.hs" ]
+      ]
+    )
+  , ( "reloading-package", testRoot </> "changing-cabal"
+    , [ AddPackages [ testRoot </> "changing-cabal" ++ testSuffix ] ]
+    , appendFile (testRoot </> "changing-cabal" ++ testSuffix </> "some-test-package.cabal") ", B" 
+    , [ AddPackages [testRoot </> "changing-cabal" ++ testSuffix]
+      , PerformRefactoring "RenameDefinition" (testRoot </> "changing-cabal" ++ testSuffix </> "A.hs") "3:1-3:2" ["z"] 
+      ]
+    , [ LoadedModules [ testRoot </> "changing-cabal" ++ testSuffix </> "A.hs" ]
+      , LoadedModules [ testRoot </> "changing-cabal" ++ testSuffix </> "A.hs"
+                      , testRoot </> "changing-cabal" ++ testSuffix </> "B.hs" ]
+      , ModulesChanged [ testRoot </> "changing-cabal" ++ testSuffix </> "A.hs"
+                       , testRoot </> "changing-cabal" ++ testSuffix </> "B.hs" ]
+      , LoadedModules [ testRoot </> "changing-cabal" ++ testSuffix </> "A.hs" ]
+      , LoadedModules [ testRoot </> "changing-cabal" ++ testSuffix </> "B.hs" ]
+      ]
+    )
+  , ( "reloading-remove", testRoot </> "reloading", [ AddPackages [ testRoot </> "reloading" ++ testSuffix ] ]
+    , do removeFile (testRoot </> "reloading" ++ testSuffix </> "A.hs")
+         removeFile (testRoot </> "reloading" ++ testSuffix </> "B.hs")
+    , [ ReLoad [testRoot </> "reloading" ++ testSuffix </> "C.hs"] 
+               [testRoot </> "reloading" ++ testSuffix </> "A.hs", testRoot </> "reloading" ++ testSuffix </> "B.hs"]
+      , PerformRefactoring "RenameDefinition" (testRoot </> "reloading" ++ testSuffix </> "C.hs") "3:1-3:2" ["d"] 
+      ]
+    , [ LoadedModules [ testRoot </> "reloading" ++ testSuffix </> "C.hs"
+                      , testRoot </> "reloading" ++ testSuffix </> "B.hs"
+                      , testRoot </> "reloading" ++ testSuffix </> "A.hs" ]
+      , LoadedModules [ testRoot </> "reloading" ++ testSuffix </> "C.hs" ]
+      , ModulesChanged [ testRoot </> "reloading" ++ testSuffix </> "C.hs" ]
+      , LoadedModules [ testRoot </> "reloading" ++ testSuffix </> "C.hs" ]
+      ]
+    )
+  , ( "remove-package", testRoot </> "multi-packages-dependent"
+    , [ AddPackages [ testRoot </> "multi-packages-dependent" ++ testSuffix </> "package1"
+                    , testRoot </> "multi-packages-dependent" ++ testSuffix </> "package2" ]]
+    , removeDirectoryRecursive (testRoot </> "multi-packages-dependent" ++ testSuffix </> "package2")
+    , [ RemovePackages [testRoot </> "multi-packages-dependent" ++ testSuffix </> "package2"] 
+      , PerformRefactoring "RenameDefinition" (testRoot </> "multi-packages-dependent" ++ testSuffix </> "package1" </> "A.hs") 
+                                              "3:1-3:2" ["d"] 
+      ]
+    , [ LoadedModules [ testRoot </> "multi-packages-dependent" ++ testSuffix </> "package1" </> "A.hs"
+                      , testRoot </> "multi-packages-dependent" ++ testSuffix </> "package2" </> "B.hs" ]
+      , ModulesChanged [ testRoot </> "multi-packages-dependent" ++ testSuffix </> "package1" </> "A.hs" ]
+      , LoadedModules [ testRoot </> "multi-packages-dependent" ++ testSuffix </> "package1" </> "A.hs" ]
+      ]
+    )
+  ]
+
+makeDaemonTest :: (Maybe FilePath, String, [ClientMessage], [ResponseMsg]) -> TestTree
+makeDaemonTest (Nothing, label, input, expected) = testCase label $ do  
+    actual <- communicateWithDaemon (map Right input)
+    assertEqual "" expected actual
+makeDaemonTest (Just dir, label, input, expected) = testCase label $ do 
+    exists <- doesDirectoryExist (dir ++ testSuffix)
+    -- clear the target directory from possible earlier test runs
+    when exists $ removeDirectoryRecursive (dir ++ testSuffix)
+    copyDir dir (dir ++ testSuffix)
+    actual <- communicateWithDaemon (map Right input)
+    assertEqual "" expected actual
+  `finally` removeDirectoryRecursive (dir ++ testSuffix)
+
+makeReloadTest :: (String, FilePath, [ClientMessage], IO (), [ClientMessage], [ResponseMsg]) -> TestTree
+makeReloadTest (label, dir, input1, io, input2, expected) = testCase label $ do  
+    exists <- doesDirectoryExist (dir ++ testSuffix)
+    -- clear the target directory from possible earlier test runs
+    when exists $ removeDirectoryRecursive (dir ++ testSuffix)
+    copyDir dir (dir ++ testSuffix)
+    actual <- communicateWithDaemon (map Right input1 ++ [Left io] ++ map Right input2)
+    assertEqual "" expected actual
+  `finally` removeDirectoryRecursive (dir ++ testSuffix)
+
+communicateWithDaemon :: [Either (IO ()) ClientMessage] -> IO [ResponseMsg]
+communicateWithDaemon msgs = withSocketsDo $ do
+  addrInfo <- getAddrInfo Nothing (Just "127.0.0.1") (Just "4123")
+  let serverAddr = head addrInfo
+  sock <- socket (addrFamily serverAddr) Stream defaultProtocol
+  connect sock (addrAddress serverAddr)
+  intermedRes <- sequence (map (either (\io -> do sendAll sock (encode KeepAlive)
+                                                  r <- readSockResponsesUntil sock KeepAliveResponse BS.empty
+                                                  io
+                                                  return r)
+                               ((>> return []) . sendAll sock . (`BS.snoc` '\n') . encode)) msgs)
+  sendAll sock $ encode Disconnect
+  resps <- readSockResponsesUntil sock Disconnected BS.empty
+  close sock
+  return (concat intermedRes ++ resps)
+
+readSockResponsesUntil :: Socket -> ResponseMsg -> BS.ByteString -> IO [ResponseMsg]
+readSockResponsesUntil sock rsp bs
+  = do resp <- recv sock 2048
+       let fullBS = bs `BS.append` resp
+       if BS.null resp 
+         then return []
+         else
+           let splitted = BS.split '\n' fullBS
+               recognized = catMaybes $ map decode splitted
+            in if rsp `elem` recognized 
+                 then return $ List.delete rsp recognized 
+                 else readSockResponsesUntil sock rsp fullBS
+
+stopDaemon :: IO ()
+stopDaemon = withSocketsDo $ do
+  addrInfo <- getAddrInfo Nothing (Just "127.0.0.1") (Just "4123")
+  let serverAddr = head addrInfo
+  sock <- socket (addrFamily serverAddr) Stream defaultProtocol
+  connect sock (addrAddress serverAddr)
+
+  sendAll sock $ encode Stop
+  close sock
+
+testRoot = ".." </> ".." </> "examples" </> "Project"
+
+deriving instance Eq ResponseMsg
+instance FromJSON ResponseMsg
+instance ToJSON ClientMessage
+
+copyDir ::  FilePath -> FilePath -> IO ()
+copyDir src dst = do
+  createDirectory dst
+  content <- getDirectoryContents src
+  let xs = filter (`notElem` [".", ".."]) content
+  forM_ xs $ \name -> do
+    let srcPath = src </> name
+    let dstPath = dst </> name
+    isDirectory <- doesDirectoryExist srcPath
+    if isDirectory
+      then copyDir srcPath dstPath
+      else copyFile srcPath dstPath
+
+commonPrefix :: (Eq e) => [e] -> [e] -> [e]
+commonPrefix _ [] = []
+commonPrefix [] _ = []
+commonPrefix (x:xs) (y:ys)
+  | x == y    = x : commonPrefix xs ys
+  | otherwise = []
+
+longestCommonPrefix :: (Eq a) => [[a]] -> [a]
+longestCommonPrefix = foldl1 commonPrefix