packages feed

haskell-tools-cli 0.3.0.1 → 0.4.0.0

raw patch · 6 files changed

+454/−140 lines, 6 filesdep +aesondep +bytestringdep +criteriondep −HUnitdep ~filepathdep ~haskell-tools-astdep ~haskell-tools-cliPVP ok

version bump matches the API change (PVP)

Dependencies added: aeson, bytestring, criterion, knob, tasty, tasty-hunit, time

Dependencies removed: HUnit

Dependency ranges changed: filepath, haskell-tools-ast, haskell-tools-cli, haskell-tools-prettyprint, haskell-tools-refactor

API changes (from Hackage documentation)

- Language.Haskell.Tools.Refactor.Session: RefactorSessionState :: Map (String, String, IsBoot) (UnnamedModule IdDom) -> Maybe (String, String, IsBoot) -> Bool -> Bool -> RefactorSessionState
- Language.Haskell.Tools.Refactor.Session: [_actualMod] :: RefactorSessionState -> Maybe (String, String, IsBoot)
- Language.Haskell.Tools.Refactor.Session: [_dryMode] :: RefactorSessionState -> Bool
- Language.Haskell.Tools.Refactor.Session: [_exiting] :: RefactorSessionState -> Bool
- Language.Haskell.Tools.Refactor.Session: [_refSessMods] :: RefactorSessionState -> Map (String, String, IsBoot) (UnnamedModule IdDom)
- Language.Haskell.Tools.Refactor.Session: actualMod :: Lens RefactorSessionState RefactorSessionState (Maybe (String, String, IsBoot)) (Maybe (String, String, IsBoot))
- Language.Haskell.Tools.Refactor.Session: data RefactorSessionState
- Language.Haskell.Tools.Refactor.Session: dryMode :: Lens RefactorSessionState RefactorSessionState Bool Bool
- Language.Haskell.Tools.Refactor.Session: exiting :: Lens RefactorSessionState RefactorSessionState Bool Bool
- Language.Haskell.Tools.Refactor.Session: initSession :: RefactorSessionState
- Language.Haskell.Tools.Refactor.Session: refSessMods :: Lens RefactorSessionState RefactorSessionState (Map (String, String, IsBoot) (UnnamedModule IdDom)) (Map (String, String, IsBoot) (UnnamedModule IdDom))
- Language.Haskell.Tools.Refactor.Session: type RefactorSession = StateT RefactorSessionState
+ Language.Haskell.Tools.Refactor.CLI: instance Language.Haskell.Tools.Refactor.Session.IsRefactSessionState Language.Haskell.Tools.Refactor.CLI.CLISessionState
- Language.Haskell.Tools.Refactor.CLI: refactorSession :: [String] -> IO String
+ Language.Haskell.Tools.Refactor.CLI: refactorSession :: Handle -> Handle -> [String] -> IO ()

Files

Language/Haskell/Tools/Refactor/CLI.hs view
@@ -1,6 +1,8 @@ {-# LANGUAGE LambdaCase
            , TupleSections
            , FlexibleContexts
+           , TemplateHaskell
+           , TypeFamilies
            #-}
 module Language.Haskell.Tools.Refactor.CLI (refactorSession) where
 
@@ -11,9 +13,12 @@ import Data.List
 import Data.List.Split
 import Control.Monad.State
+import Control.Applicative ((<|>))
 import Control.Reference
 
 import GHC
+import ErrUtils
+import Digraph
 import HscTypes as GHC
 import Module as GHC
 import GHC.Paths ( libdir )
@@ -26,124 +31,141 @@ 
 import Debug.Trace
 
-tryOut = refactorSession [ "-dry-run", "-one-shot", "-module-name=Language.Haskell.Tools.AST", "-refactoring=OrganizeImports"
-                         , "-package", "ghc", "src/ast", "src/backend-ghc", "src/prettyprint", "src/rewrite", "src/refactor"]
+type CLIRefactorSession = StateT CLISessionState Ghc
 
-refactorSession :: [String] -> IO String
-refactorSession args = runGhc (Just libdir) $ flip evalStateT initSession $
+data CLISessionState = 
+  CLISessionState { _refactState :: RefactorSessionState
+                  , _actualMod :: Maybe SourceFileKey
+                  , _exiting :: Bool
+                  , _dryMode :: Bool
+                  }
+
+makeReferences ''CLISessionState
+
+tryOut = refactorSession stdin stdout 
+           [ "-dry-run", "-one-shot", "-module-name=Language.Haskell.Tools.AST", "-refactoring=OrganizeImports"
+           , "src/ast", "src/backend-ghc", "src/prettyprint", "src/rewrite", "src/refactor"]
+
+refactorSession :: Handle -> Handle -> [String] -> IO ()
+refactorSession input output args = runGhc (Just libdir) $ handleSourceError printSrcErrors 
+                                                         $ flip evalStateT initSession $
   do lift $ initGhcFlags
      workingDirsAndHtFlags <- lift $ useFlags args
      let (htFlags, workingDirs) = partition (\f -> head f == '-') workingDirsAndHtFlags
-     if null workingDirs then return usageMessage
-                         else do initializeSession workingDirs htFlags
-                                 runSession htFlags
+     if null workingDirs then liftIO $ hPutStrLn output usageMessage
+                         else do initializeSession output workingDirs htFlags
+                                 runSession input output htFlags
      
-  where initializeSession :: [FilePath] -> [String] -> RefactorSession Ghc ()
-        initializeSession workingDirs flags = do
-          moduleNames <- liftIO $ concat <$> mapM getModules workingDirs
-          lift $ useDirs (concatMap fst moduleNames)
-          lift $ setTargets $ map (\mod -> (Target (TargetModule (GHC.mkModuleName mod)) True Nothing)) 
-                                  (concatMap snd moduleNames)
-          liftIO $ putStrLn "Compiling modules. This may take some time. Please wait."
-          lift $ load LoadAllTargets
-          allMods <- lift getModuleGraph
-          mods <- lift $ forM allMods loadModule
-          liftIO $ putStrLn "All modules loaded. Use 'SelectModule module-name' to select a module"
-          modify $ refSessMods .= Map.fromList mods
-          liftIO $ hSetBuffering stdout NoBuffering
-          when ("-dry-run" `elem` flags) $ modify (dryMode .= True)
+  where printSrcErrors err = do dfs <- getSessionDynFlags 
+                                liftIO $ printBagOfErrors dfs (srcErrorMessages err)
 
-        loadModule :: ModSummary -> Ghc ((FilePath, String, IsBoot), TypedModule)
-        loadModule ms = 
-          do mm <- parseTyped ms
-             let modName = GHC.moduleNameString $ moduleName $ ms_mod ms
-                 wd = srcDirFromRoot (fromJust $ ml_hs_file $ ms_location ms) modName            
-             liftIO $ putStrLn ("Loaded module: " ++ modName)
-             return ((wd, modName, case ms_hsc_src ms of HsSrcFile -> NormalHs; _ -> IsHsBoot), mm)
+        initializeSession :: Handle -> [FilePath] -> [String] -> CLIRefactorSession ()
+        initializeSession output workingDirs flags = do
+          liftIO $ hSetBuffering output NoBuffering
+          liftIO $ hPutStrLn output "Compiling modules. This may take some time. Please wait."
+          (_, ignoredMods) <- loadPackagesFrom (\ms -> liftIO $ hPutStrLn output ("Loaded module: " ++ modSumName ms)) workingDirs
+          when (not $ null ignoredMods) 
+            $ liftIO $ hPutStrLn output 
+            $ "The following modules are ignored: " 
+                ++ concat (intersperse ", " $ ignoredMods)
+                ++ ". Multiple modules with the same qualified name are not supported."
+          liftIO $ hPutStrLn output "All modules loaded. Use 'SelectModule module-name' to select a module"
+          when ("-dry-run" `elem` flags) $ modify (dryMode .= True)
 
-        runSession :: [String] -> RefactorSession Ghc String
-        runSession flags | "-one-shot" `elem` flags
+        runSession :: Handle -> Handle -> [String] -> CLIRefactorSession ()
+        runSession _ output flags | "-one-shot" `elem` flags
           = let modName = catMaybes $ map (\f -> case splitOn "=" f of ["-module-name", mod] -> Just mod; _ -> Nothing) flags
                 refactoring = catMaybes $ map (\f -> case splitOn "=" f of ["-refactoring", ref] -> Just ref; _ -> Nothing) flags
              in case (modName, refactoring) of 
                   ([modName],[refactoring]) ->
-                    do performSessionCommand (LoadModule modName)
-                       command <- readSessionCommand (takeWhile (/='"') $ dropWhile (=='"') $ refactoring)
-                       performSessionCommand command
-                  _ -> return usageMessage
-        runSession _ = runSessionLoop
+                    do performSessionCommand output (LoadModule modName)
+                       command <- readSessionCommand output (takeWhile (/='"') $ dropWhile (=='"') $ refactoring)
+                       performSessionCommand output command
+                  _ -> liftIO $ hPutStrLn output usageMessage
+        runSession input output _ = runSessionLoop input output
 
-        runSessionLoop :: RefactorSession Ghc String
-        runSessionLoop = do 
+        runSessionLoop :: Handle -> Handle -> CLIRefactorSession ()
+        runSessionLoop input output = do 
           actualMod <- gets (^. actualMod)
-          liftIO $ putStr (maybe "no-module-selected" (\(_,m,_) -> m) actualMod ++ ">")
-          cmd <- liftIO $ getLine 
-          sessionComm <- readSessionCommand cmd
-          liftIO . putStrLn =<< performSessionCommand sessionComm
+          liftIO $ hPutStr output (maybe "no-module-selected> " (\sfk -> (sfk ^. sfkModuleName) ++ "> ") actualMod)
+          cmd <- liftIO $ hGetLine input 
+          sessionComm <- readSessionCommand output cmd
+          performSessionCommand output sessionComm
           doExit <- gets (^. exiting)
-          when (not doExit) (void runSessionLoop)
-          return ""
+          when (not doExit) (void (runSessionLoop input output))
 
         usageMessage = "Usage: ht-refact [ht-flags, ghc-flags] package-pathes\n"
                          ++ "ht-flags: -dry-run -one-shot -module-name=modulename -refactoring=\"refactoring\""
 
 data RefactorSessionCommand 
   = LoadModule String
+  | Skip
   | Exit
   | RefactorCommand RefactorCommand
   deriving Show
 
-readSessionCommand :: Monad m => String -> RefactorSession m RefactorSessionCommand
-readSessionCommand cmd = case splitOn " " cmd of 
+readSessionCommand :: Handle -> String -> CLIRefactorSession RefactorSessionCommand
+readSessionCommand output cmd = case splitOn " " cmd of 
     ["SelectModule", mod] -> return $ LoadModule mod
     ["Exit"] -> return Exit
     _ -> do actualMod <- gets (^. actualMod)
-            case actualMod of Just (wd,m,_) -> return $ RefactorCommand $ readCommand (toFileName wd m) cmd
-                              Nothing -> error "Set the actual module first"
+            case actualMod of Just _ -> return $ RefactorCommand $ readCommand cmd
+                              Nothing -> do liftIO $ hPutStrLn output "Set the actual module first"
+                                            return Skip
 
-performSessionCommand :: RefactorSessionCommand -> RefactorSession Ghc String
-performSessionCommand (LoadModule mod) = do fnd <- gets (find (\(_,m,hs) -> m == mod && hs == NormalHs) . Map.keys . (^. refSessMods))
-                                            if isJust fnd then modify $ actualMod .= fnd
-                                                          else liftIO $ putStrLn ("Cannot find module: " ++ mod)
-                                            return ""
-performSessionCommand Exit = do modify $ exiting .= True
-                                return ""
-performSessionCommand (RefactorCommand cmd) 
-  = do RefactorSessionState { _refSessMods = mods, _actualMod = Just act@(_, mod, _) } <- get
-       res <- lift $ performCommand cmd (mod, mods Map.! act) (map (\((_,m,_),mod) -> (m,mod)) $ Map.assocs (Map.delete act mods))
+performSessionCommand :: Handle -> RefactorSessionCommand -> CLIRefactorSession ()
+performSessionCommand output (LoadModule modName) = do 
+  mod <- gets (lookupModInSCs (SourceFileKey NormalHs modName) . (^. refSessMCs))
+  if isJust mod then modify $ actualMod .= fmap fst mod
+                else liftIO $ hPutStrLn output ("Cannot find module: " ++ modName)
+performSessionCommand _ Skip = return ()
+performSessionCommand _ Exit = modify $ exiting .= True
+performSessionCommand output (RefactorCommand cmd) 
+  = do actMod <- gets (^. actualMod)
+       (Just actualMod, otherMods) <- getMods actMod
+       res <- lift $ performCommand cmd actualMod otherMods
        inDryMode <- gets (^. dryMode)
-       case res of Left err -> return err
-                   Right resMods -> performChanges inDryMode resMods
-                     
-  where performChanges False resMods = do 
-          mss <- forM resMods $ \case 
+       case res of Left err -> liftIO $ hPutStrLn output err
+                   Right resMods -> performChanges output inDryMode resMods
+
+  where performChanges output False resMods = do 
+          changedMods <- forM resMods $ \case 
+            ModuleCreated n m otherM -> do 
+              Just (_, otherMR) <- gets (lookupModInSCs otherM . (^. refSessMCs))
+              let Just otherMS = otherMR ^? modRecMS
+              otherSrcDir <- liftIO $ getSourceDir otherMS
+              let loc = srcDirFromRoot otherSrcDir n
+              liftIO $ withBinaryFile loc WriteMode (`hPutStr` prettyPrint m)
+              return (SourceFileKey NormalHs n)
             ContentChanged (n,m) -> do
-              let modName = semanticsModule $ m ^. semantics
+              let modName = semanticsModule m
               ms <- getModSummary modName (isBootModule $ m ^. semantics)
-              let isBoot = case ms_hsc_src ms of HsSrcFile -> NormalHs; _ -> IsHsBoot
-              Just (workingDir,_,_) <- gets (find (\(_,m,b) -> m == n && b == isBoot) . Map.keys . (^. refSessMods))
-              liftIO $ withBinaryFile ((case isBoot of NormalHs -> toFileName; IsHsBoot -> toBootFileName) workingDir n) 
-                                      WriteMode (`hPutStr` prettyPrint m)
-              return $ Just (n, workingDir, modName, isBoot)
+              let file = fromJust $ ml_hs_file $ ms_location ms
+              liftIO $ withBinaryFile file WriteMode (`hPutStr` prettyPrint m)
+              return n
             ModuleRemoved mod -> do
-              Just (workingDir,_,_) <- gets (find (\(_,m,b) -> m == mod) . Map.keys . (^. refSessMods))
-              liftIO $ removeFile (toFileName workingDir mod)
-              modify $ refSessMods .- Map.delete (workingDir, mod, IsHsBoot) . Map.delete (workingDir, mod, NormalHs)
-              return Nothing
-          lift $ load LoadAllTargets
-          forM_ (catMaybes mss) $ \(n, workingDir, modName, isBoot) -> do
-              -- TODO: add target if module is added as a change
-              ms <- getModSummary modName (isBoot == IsHsBoot)
-              newm <- lift $ parseTyped ms
-              modify $ refSessMods .- Map.insert (workingDir, n, isBoot) newm
-              liftIO $ putStrLn ("Re-loaded module: " ++ n)
-          return ""
-        performChanges True resMods = concat <$> forM resMods (liftIO . \case 
+              Just (_,m) <- gets (lookupModInSCs (SourceFileKey NormalHs mod) . (^. refSessMCs))
+              case ( fmap semanticsModule (m ^? typedRecModule) <|> fmap semanticsModule (m ^? renamedRecModule)
+                   , fmap isBootModule (m ^? typedRecModule) <|> fmap isBootModule (m ^? renamedRecModule)) of 
+                (Just modName, Just isBoot) -> do
+                  ms <- getModSummary modName isBoot
+                  let file = fromJust $ ml_hs_file $ ms_location ms
+                  modify $ (refSessMCs .- removeModule mod)
+                  liftIO $ removeFile file
+                _ -> do liftIO $ hPutStrLn output ("Module " ++ mod ++ " could not be removed.")
+              return (SourceFileKey NormalHs mod)
+          void $ reloadChangedModules (hPutStrLn output . ("Re-loaded module: " ++) . modSumName) 
+                   (\ms -> keyFromMS ms `elem` changedMods)
+        performChanges output True resMods = forM_ resMods (liftIO . \case 
           ContentChanged (n,m) -> do
-            return $ "### UModule changed: " ++ n ++ "\n### new content:\n" ++ prettyPrint m
+            hPutStrLn output $ "### Module changed: " ++ (n ^. sfkModuleName) ++ "\n### new content:\n" ++ prettyPrint m
           ModuleRemoved mod ->
-            return $ "### UModule removed: " ++ mod)
+            hPutStrLn output $ "### Module removed: " ++ mod)
 
         getModSummary name boot
           = do allMods <- lift getModuleGraph
                return $ fromJust $ find (\ms -> ms_mod ms == name && (ms_hsc_src ms == HsSrcFile) /= boot) allMods 
+
+instance IsRefactSessionState CLISessionState where
+  refSessMCs = refactState & _refSessMCs
+  initSession = CLISessionState initSession Nothing False False
− Language/Haskell/Tools/Refactor/Session.hs
@@ -1,24 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}
-module Language.Haskell.Tools.Refactor.Session where
-
-import Data.Map as Map
-import Control.Monad.State
-import Control.Reference
-
-import Language.Haskell.Tools.AST (IdDom)
-import Language.Haskell.Tools.Refactor (IsBoot(..))
-import Language.Haskell.Tools.Refactor.RefactorBase (UnnamedModule)
-
-type RefactorSession = StateT RefactorSessionState
-
-data RefactorSessionState
-  = RefactorSessionState { _refSessMods :: Map.Map (String, String, IsBoot) (UnnamedModule IdDom)
-                         , _actualMod :: Maybe (String, String, IsBoot)
-                         , _exiting :: Bool
-                         , _dryMode :: Bool
-                         }
-
-initSession :: RefactorSessionState
-initSession = RefactorSessionState Map.empty Nothing False False
-
-makeReferences ''RefactorSessionState
+ benchmark/Main.hs view
@@ -0,0 +1,174 @@+
+{-# LANGUAGE LambdaCase
+           , ViewPatterns
+           , TypeFamilies
+           , RecordWildCards
+           , DeriveGeneric
+           #-}
+module Main where
+
+import Criterion.Measurement hiding (runBenchmark)
+import Criterion.Types hiding(measure)
+
+import qualified Data.ByteString.Lazy.Char8 as LazyBS (pack, unpack)
+import qualified Data.ByteString.Char8 as BS (pack, unpack)
+import Data.Aeson
+import Data.Knob
+import GHC.Generics
+import System.FilePath
+import System.Directory
+import System.IO
+import Control.Monad
+import Control.Exception
+import System.Environment
+import Data.List
+import Data.List.Split
+import Data.Time.Clock
+import Data.Time.Calendar
+
+import Language.Haskell.Tools.Refactor.CLI
+
+rootDir = ".." </> ".." </> "examples"
+
+main :: IO ()
+main = do
+  args <- getArgs
+  (year, month, day) <- date
+  cases <- bms2Mcases (Date {..}) bms
+  case args of 
+    [file] -> writeFile file (show $ encode cases)
+    _ -> putStrLn $ LazyBS.unpack $ encode cases
+  putStrLn "Execution times (cycles):"
+  mapM_ (\c -> putStrLn $ "# " ++ bmId (bm c) ++ ": " ++ showGrouped (measCycles (ms c))) cases
+    where showGrouped = reverse . concat . intersperse " " . chunksOf 3 . reverse . show
+
+date :: IO (Integer,Int,Int) -- (year,month,day)
+date = getCurrentTime >>= return . toGregorian . utctDay
+
+data Date = Date { year  :: Integer
+                 , month :: Int
+                 , day   :: Int
+                 }
+  deriving (Eq, Show, Generic)
+
+data BM = BM { bmId       :: String
+             , workingDir :: FilePath
+             , refactors  :: [String]
+             }
+  deriving (Eq, Show, Generic)
+
+data BMCase = BMCase { bm :: BM
+                     , ms :: Measured
+                     , dt :: Date
+                     }
+  deriving (Eq, Show, Generic)
+
+instance FromJSON Date
+instance ToJSON Date
+instance FromJSON BM
+instance ToJSON BM
+instance FromJSON BMCase
+instance ToJSON BMCase
+
+bms :: [BM]
+bms = [ BM { bmId = "full-1", workingDir = rootDir </> "CppHs", refactors = [
+          "SelectModule Language.Preprocessor.Cpphs.CppIfdef"
+        , "ExtractBinding 182:8-182:36 parseResult"
+        , "RenameDefinition 181:1 gDefined"
+        , "GenerateSignature 51:5-51:5"
+        , "GenerateSignature 50:5-50:5"
+        , "GenerateSignature 49:5-49:5"
+        , "ExtractBinding 47:46-47:64 linesFixed"
+        , "RenameDefinition 46:1 cppIfDef"
+        , "OrganizeImports"
+        , "Exit"
+        ]  }
+      , BM { bmId = "full-2", workingDir = rootDir </> "CppHs", refactors = [
+          "SelectModule Language.Preprocessor.Cpphs.MacroPass"
+        , "ExtractBinding 96:29-96:47 tokenizeTT"
+        , "ExtractBinding 90:11-90:67 fun"
+        , "OrganizeImports"
+        , "Exit"
+        ]  }
+      , BM { bmId = "full-3", workingDir = rootDir </> "CppHs", refactors = [
+          "SelectModule Language.Preprocessor.Cpphs.CppIfdef"
+        , "ExtractBinding 182:8-182:36 parseResult"
+        , "RenameDefinition 181:1 gDefined"
+        , "SelectModule Language.Preprocessor.Cpphs.MacroPass"
+        , "ExtractBinding 96:29-96:47 tokenizeTT"
+        , "SelectModule Language.Preprocessor.Cpphs.CppIfdef"
+        , "GenerateSignature 51:5-51:5"
+        , "GenerateSignature 50:5-50:5"
+        , "GenerateSignature 49:5-49:5"
+        , "SelectModule Language.Preprocessor.Cpphs.MacroPass"
+        , "ExtractBinding 90:11-90:67 fun"
+        , "SelectModule Language.Preprocessor.Cpphs.CppIfdef"
+        , "ExtractBinding 47:46-47:64 linesFixed"
+        , "RenameDefinition 46:1 cppIfDef"
+        , "OrganizeImports"
+        , "Exit"
+        ]  }
+      , BM { bmId = "3xGenerateTypeSignature", workingDir = rootDir </> "CppHs", refactors = [
+          "SelectModule Language.Preprocessor.Cpphs.CppIfdef"
+        , "GenerateSignature 51:5-51:5"
+        , "GenerateSignature 50:5-50:5"
+        , "GenerateSignature 49:5-49:5"
+        , "Exit"
+        ]  }
+      , BM { bmId ="selects", workingDir = rootDir </> "CppHs", refactors = [
+          "SelectModule Language.Preprocessor.Cpphs.CppIfdef"
+        , "SelectModule Language.Preprocessor.Cpphs.MacroPass"
+        , "SelectModule Language.Preprocessor.Cpphs.CppIfdef"
+        , "SelectModule Language.Preprocessor.Cpphs.MacroPass"
+        , "SelectModule Language.Preprocessor.Cpphs.CppIfdef"
+        , "SelectModule Language.Preprocessor.Cpphs.MacroPass"
+        , "SelectModule Language.Preprocessor.Cpphs.CppIfdef"
+        , "SelectModule Language.Preprocessor.Cpphs.MacroPass"
+        , "SelectModule Language.Preprocessor.Cpphs.CppIfdef"
+        , "Exit"
+        ]  }
+      , BM { bmId ="empty", workingDir = rootDir </> "CppHs", refactors = [ "Exit" ] }
+      ]
+
+
+
+bms2Mcases :: Date -> [BM] -> IO [BMCase]
+bms2Mcases = mapM . bm2Mcase
+
+runBenchmark :: Benchmarkable -> IO Measured
+runBenchmark bm = fst <$> (measure bm 1)
+
+bm2Mms :: BM -> IO Measured
+bm2Mms (BM { .. }) = runBenchmark $ benchmakable workingDir refactors
+
+bm2Mcase :: Date -> BM -> IO BMCase
+bm2Mcase d bm = BMCase bm <$> (bm2Mms bm) <*> (return d)
+
+benchmakable :: String -> [String] -> Benchmarkable -- IO (Either String String)
+benchmakable wd rfs = Benchmarkable $ \ _ -> do
+  makeCliTest wd rfs
+
+makeCliTest :: String -> [String] -> IO ()
+makeCliTest wd rfs = do   
+    copyDir wd (wd ++ "_orig")
+    inKnob <- newKnob (BS.pack $ unlines rfs)
+    inHandle <- newFileHandle inKnob "<input>" ReadMode
+    outKnob <- newKnob (BS.pack [])
+    outHandle <- newFileHandle outKnob "<output>" WriteMode
+    refactorSession inHandle outHandle [wd]
+  `finally` do removeDirectoryRecursive wd
+               renameDirectory (wd ++ "_orig") wd
+
+
+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
exe/Main.hs view
@@ -1,8 +1,9 @@ module Main where
 
+import System.IO
 import System.Environment
 
 import Language.Haskell.Tools.Refactor.CLI
 
 main :: IO ()
-main = putStrLn =<< refactorSession =<< getArgs
+main = refactorSession stdin stdout =<< getArgs
haskell-tools-cli.cabal view
@@ -1,5 +1,5 @@ name:                haskell-tools-cli
-version:             0.3.0.1
+version:             0.4.0.0
 synopsis:            Command-line frontend for Haskell-tools Refact
 description:         Command-line frontend for Haskell-tools Refact. Not meant as a final product, only for demonstration purposes.
 homepage:            https://github.com/haskell-tools/haskell-tools
@@ -12,38 +12,60 @@ cabal-version:       >=1.10
 
 library
+  ghc-options:         -O2
   build-depends:       base                      >= 4.9 && < 4.10
                      , containers                >= 0.5 && < 0.6
                      , mtl                       >= 2.2 && < 2.3
                      , split                     >= 0.2 && < 0.3
                      , directory                 >= 1.2 && < 1.3
+                     , filepath                  >= 1.4 && < 2
                      , ghc                       >= 8.0 && < 8.1
                      , ghc-paths                 >= 0.1 && < 0.2
                      , references                >= 0.3 && < 0.4
-                     , haskell-tools-ast         >= 0.3 && < 0.4
-                     , haskell-tools-prettyprint >= 0.3 && < 0.4
-                     , haskell-tools-refactor    >= 0.3 && < 0.4
+                     , 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.CLI
-                     , Language.Haskell.Tools.Refactor.Session
   default-language:    Haskell2010
 
 
 executable ht-refact
+  ghc-options:         -O2
   build-depends:       base                      >= 4.9 && < 4.10
-                     , haskell-tools-cli         >= 0.3 && < 0.4
+                     , haskell-tools-cli         >= 0.4 && < 0.5
   hs-source-dirs:      exe
   main-is:             Main.hs
   default-language:    Haskell2010
   
 test-suite haskell-tools-cli-tests
   type:                exitcode-stdio-1.0
-  ghc-options:         -with-rtsopts=-M2g
+  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
-                     , haskell-tools-cli         >= 0.3 && < 0.4
+                     , haskell-tools-cli         >= 0.4 && < 0.5
+                     , knob                      >= 0.1 && < 0.2
+                     , bytestring                >= 0.10 && < 0.11
   default-language:    Haskell2010
+
+benchmark cli-benchmark
+  type:                exitcode-stdio-1.0
+  ghc-options:         -with-rtsopts=-M2g -O2
+  build-depends:       base                      >= 4.9 && < 4.10
+                     , haskell-tools-cli         >= 0.4 && < 0.5
+                     , criterion                 >= 1.1 && < 1.2
+                     , time                      >= 1.6 && < 1.7
+                     , aeson                     >= 0.11 && < 0.12
+                     , directory                 >= 1.2 && < 1.3
+                     , filepath                  >= 1.4 && < 2.0
+                     , knob                      >= 0.1 && < 0.2
+                     , bytestring                >= 0.10 && < 0.11
+                     , split                     >= 0.2 && < 0.3
+  hs-source-dirs:      benchmark
+  main-is:             Main.hs  
+
 
test/Main.hs view
@@ -1,40 +1,159 @@ module Main where
 
-import Test.HUnit
+import Test.Tasty
+import Test.Tasty.HUnit
+
 import System.Exit
+import System.Directory
 import System.FilePath
+import Control.Monad
+import Control.Exception
+import qualified Data.List as List
+import Data.Knob
+import Data.ByteString.Char8 (pack, unpack)
+import System.IO
 
 import Language.Haskell.Tools.Refactor.CLI
 
 main :: IO ()
-main = run allTests
-
-run :: [Test] -> IO ()
-run tests = do results <- runTestTT $ TestList tests
-               if errors results + failures results > 0 
-                  then exitFailure
-                  else exitSuccess
+main = do nightlyTests <- benchTests
+          defaultMain $ testGroup "cli tests" (allTests ++ nightlyTests)
 
-allTests :: [Test]
+allTests :: [TestTree]
 allTests = map makeCliTest cliTests
 
-makeCliTest :: (String, [String], String) -> Test
-makeCliTest (dir, args, expected) = TestLabel dir $ TestCase $ do 
-    res <- refactorSession (args ++ [dir])
-    assertEqual "" (filter (/= '\r') expected) (filter (/= '\r') res)
+makeCliTest :: ([FilePath], [String], String, String) -> TestTree
+makeCliTest (dirs, args, input, output) = let dir = joinPath $ longestCommonPrefix $ map splitDirectories dirs
+                                              testdirs = map (((dir ++ "_test") </>) . makeRelative dir) dirs
+  in testCase dir $ do   
+    exists <- doesDirectoryExist (dir ++ "_test")
+    when exists $ removeDirectoryRecursive (dir ++ "_test")
+    copyDir dir (dir ++ "_test")
+    inKnob <- newKnob (pack input)
+    inHandle <- newFileHandle inKnob "<input>" ReadMode
+    outKnob <- newKnob (pack [])
+    outHandle <- newFileHandle outKnob "<output>" WriteMode
+    res <- refactorSession inHandle outHandle (args ++ testdirs)
+    actualOut <- Data.Knob.getContents outKnob
+    assertEqual "" (filter (/= '\r') output) (filter (/= '\r') $ unpack actualOut)
+  `finally` removeDirectoryRecursive (dir ++ "_test")
 
-cliTests :: [(String, [String], String)]
+cliTests :: [([FilePath], [String], String, String)]
 cliTests 
-  = [ ( ".." </> ".." </> "examples" </> "Project" </> "source-dir"
+  = [ ( [testRoot </> "Project" </> "source-dir"]
       , ["-dry-run", "-one-shot", "-module-name=A", "-refactoring=\"GenerateSignature 3:1-3:1\""] 
-      , "### UModule changed: A\n### new content:\nmodule A where\n\nx :: ()\nx = ()")
-    , ( ".." </> ".." </> "examples" </> "Project" </> "source-dir-outside"
+      , "", prefixText ["A"] ++ "### Module changed: A\n### new content:\nmodule A where\n\nx :: ()\nx = ()\n")
+    , ( [testRoot </> "Project" </> "source-dir-outside"]
       , ["-dry-run", "-one-shot", "-module-name=A", "-refactoring=\"GenerateSignature 3:1-3:1\""] 
-      , "### UModule changed: A\n### new content:\nmodule A where\n\nx :: ()\nx = ()")
-    , ( ".." </> ".." </> "examples" </> "Project" </> "no-cabal"
+      , "", prefixText ["A"] ++ "### Module changed: A\n### new content:\nmodule A where\n\nx :: ()\nx = ()\n")
+    , ( [testRoot </> "Project" </> "no-cabal"]
       , ["-dry-run", "-one-shot", "-module-name=A", "-refactoring=\"GenerateSignature 3:1-3:1\""] 
-      , "### UModule changed: A\n### new content:\nmodule A where\n\nx :: ()\nx = ()")
-    , ( ".." </> ".." </> "examples" </> "Project" </> "has-cabal"
+      , "", prefixText ["A"] ++ "### Module changed: A\n### new content:\nmodule A where\n\nx :: ()\nx = ()\n")
+    , ( [testRoot </> "Project" </> "has-cabal"]
       , ["-dry-run", "-one-shot", "-module-name=A", "-refactoring=\"GenerateSignature 3:1-3:1\""] 
-      , "### UModule changed: A\n### new content:\nmodule A where\n\nx :: ()\nx = ()")
-    ]+      , "", prefixText ["A"] ++ "### Module changed: A\n### new content:\nmodule A where\n\nx :: ()\nx = ()\n")
+    , ( [testRoot </> "Project" </> "selection"], [] 
+      , "SelectModule C\nSelectModule B\nRenameDefinition 5:1-5:2 bb\nSelectModule C\nRenameDefinition 3:1-3:2 cc\nExit"
+      , prefixText ["C","B"] ++ "no-module-selected> C> B> " 
+          ++ reloads ["B"] ++ "B> C> "
+          ++ reloads ["C", "B"] ++ "C> "
+          )
+    , ( [testRoot </> "Project" </> "reloading"], [] 
+      , "SelectModule C\nRenameDefinition 3:1-3:2 cc\nSelectModule B\nRenameDefinition 5:1-5:2 bb\nExit"
+      , prefixText ["C","B","A"] ++ "no-module-selected> C> " 
+          ++ reloads ["C", "B", "A"] ++ "C> B> "
+          ++ reloads ["B", "A"] ++ "B> ")
+    , ( map ((testRoot </> "Project" </> "multi-packages") </>) ["package1", "package2"]
+      , ["-dry-run", "-one-shot", "-module-name=A", "-refactoring=\"RenameDefinition 3:1-3:2 xx\""], ""
+      , prefixText ["B", "A"] ++ "### Module changed: A\n### new content:\nmodule A where\n\nxx = ()\n" 
+      )
+    , ( map ((testRoot </> "Project" </> "multi-packages-flags") </>) ["package1", "package2"]
+      , ["-dry-run", "-one-shot", "-module-name=A", "-refactoring=\"RenameDefinition 3:1-3:2 xx\""], ""
+      , prefixText ["B", "A"] ++ "### Module changed: A\n### new content:\nmodule A where\n\nxx = \\case () -> ()\n"
+      )
+    , ( map ((testRoot </> "Project" </> "multi-packages-same-module") </>) ["package1", "package2"]
+      , ["-dry-run", "-one-shot", "-module-name=A", "-refactoring=\"RenameDefinition 3:1-3:2 xx\""], ""
+      , "Compiling modules. This may take some time. Please wait.\nLoaded module: A\n" 
+          ++ "The following modules are ignored: A. Multiple modules with the same qualified name are not supported.\n"
+          ++ "All modules loaded. Use 'SelectModule module-name' to select a module\n" 
+          ++ "### Module changed: A\n### new content:\nmodule A where\n\nxx = ()\n"
+      )
+    ]
+
+benchTests :: IO [TestTree]
+benchTests 
+  = forM ["full-1", "full-2", "full-3"] $ \id -> do
+      commands <- readFile ("bench-tests" </> id <.> "txt")
+      return $ makeCliTest ([".." </> ".." </> "examples" </> "CppHs"], [], filter (/='\r') commands, expectedOut id)
+
+expectedOut "full-1" 
+  = prefixText cppHsMods ++ "no-module-selected> Language.Preprocessor.Cpphs.CppIfdef> "
+      ++ concat (replicate 8 (reloads cppIfDefReloads ++ "Language.Preprocessor.Cpphs.CppIfdef> "))
+expectedOut "full-2" 
+  = prefixText cppHsMods ++ "no-module-selected> Language.Preprocessor.Cpphs.MacroPass> "
+      ++ concat (replicate 3 (reloads macroPassReloads ++ "Language.Preprocessor.Cpphs.MacroPass> "))
+expectedOut "full-3"
+  = prefixText cppHsMods ++ "no-module-selected> Language.Preprocessor.Cpphs.CppIfdef> "
+      ++ concat (replicate 2 (reloads cppIfDefReloads ++ "Language.Preprocessor.Cpphs.CppIfdef> "))
+      ++ "Language.Preprocessor.Cpphs.MacroPass> "
+      ++ reloads macroPassReloads ++ "Language.Preprocessor.Cpphs.MacroPass> "
+      ++ "Language.Preprocessor.Cpphs.CppIfdef> "
+      ++ concat (replicate 3 (reloads cppIfDefReloads ++ "Language.Preprocessor.Cpphs.CppIfdef> "))
+      ++ "Language.Preprocessor.Cpphs.MacroPass> "
+      ++ reloads macroPassReloads ++ "Language.Preprocessor.Cpphs.MacroPass> "
+      ++ "Language.Preprocessor.Cpphs.CppIfdef> "
+      ++ concat (replicate 3 (reloads cppIfDefReloads ++ "Language.Preprocessor.Cpphs.CppIfdef> "))
+
+cppIfDefReloads = [ "Language.Preprocessor.Cpphs.CppIfdef" 
+                  , "Language.Preprocessor.Cpphs.RunCpphs"
+                  , "Language.Preprocessor.Cpphs" ]
+macroPassReloads = "Language.Preprocessor.Cpphs.MacroPass" : cppIfDefReloads
+
+cppHsMods = [ "Language.Preprocessor.Cpphs.Options"
+            , "Language.Preprocessor.Cpphs.SymTab"
+            , "Language.Preprocessor.Cpphs.Position"
+            , "Language.Preprocessor.Cpphs.ReadFirst"
+            , "Language.Preprocessor.Cpphs.HashDefine"
+            , "Language.Preprocessor.Cpphs.Tokenise"
+            , "Language.Preprocessor.Cpphs.MacroPass"
+            , "Language.Preprocessor.Cpphs.CppIfdef"
+            , "Language.Preprocessor.Unlit"
+            , "Language.Preprocessor.Cpphs.RunCpphs"
+            , "Language.Preprocessor.Cpphs" ]
+
+testRoot = ".." </> ".." </> "examples"
+
+prefixText :: [String] -> String
+prefixText mods 
+  = "Compiling modules. This may take some time. Please wait.\n" 
+      ++ concatMap (\m -> "Loaded module: " ++ m ++ "\n") mods 
+      ++ "All modules loaded. Use 'SelectModule module-name' to select a module\n"
+
+reloads :: [String] -> String
+reloads mods = concatMap (\m -> "Re-loaded module: " ++ m ++ "\n") mods 
+
+copyDir ::  FilePath -> FilePath -> IO ()
+copyDir src dst = do
+  exists <- doesDirectoryExist dst
+  -- clear the target directory from possible earlier test runs
+  when exists $ removeDirectoryRecursive dst
+  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