packages feed

test-sandbox 0.0.1.8 → 0.0.1.9

raw patch · 5 files changed

+450/−61 lines, 5 filesdep +QuickCheckdep +hastachedep +heredocdep ~basedep ~processdep ~transformersPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: QuickCheck, hastache, heredoc, hspec, regex-posix, template-haskell, test-sandbox, text

Dependency ranges changed: base, process, transformers

API changes (from Hackage documentation)

+ Test.Sandbox: runSandbox :: Sandbox a -> SandboxStateRef -> IO (Either String a)
+ Test.Sandbox: runSandbox' :: SandboxStateRef -> Sandbox a -> IO a
+ Test.Sandbox: withSandbox :: (SandboxStateRef -> IO a) -> IO a
+ Test.Sandbox.Internals: cleanUpKey :: String
+ Test.Sandbox.Internals: cleanUpProcesses :: Sandbox ()
+ Test.Sandbox.Internals: errorHandler :: String -> IO a
+ Test.Sandbox.Internals: getAvailablePids :: Sandbox [ProcessID]
+ Test.Sandbox.Internals: isCleanUp :: Sandbox Bool
+ Test.Sandbox.Internals: runSB :: Sandbox a -> ExceptT String (ReaderT SandboxStateRef IO) a
+ Test.Sandbox.Internals: runSandbox' :: SandboxStateRef -> Sandbox a -> IO a
+ Test.Sandbox.Internals: spPGid :: SandboxedProcess -> Maybe ProcessGroupID
+ Test.Sandbox.Internals: spPid :: SandboxedProcess -> Maybe ProcessID
+ Test.Sandbox.Process: ProcessInfo :: ProcessID -> String -> ProcessID -> ProcessGroupID -> ProcessInfo
+ Test.Sandbox.Process: cleanUpProcessGroupIDs :: [ProcessGroupID] -> IO ()
+ Test.Sandbox.Process: data ProcessInfo
+ Test.Sandbox.Process: getProcessGroupIDs :: IO [ProcessGroupID]
+ Test.Sandbox.Process: getProcessIDs :: [ProcessGroupID] -> IO [ProcessID]
+ Test.Sandbox.Process: getProcessInfo :: String -> Maybe ProcessInfo
+ Test.Sandbox.Process: getProcessInfos :: IO [ProcessInfo]
+ Test.Sandbox.Process: instance Eq ProcessInfo
+ Test.Sandbox.Process: instance Read ProcessInfo
+ Test.Sandbox.Process: instance Show ProcessInfo
+ Test.Sandbox.Process: piPgid :: ProcessInfo -> ProcessGroupID
+ Test.Sandbox.Process: piPid :: ProcessInfo -> ProcessID
+ Test.Sandbox.Process: piPpid :: ProcessInfo -> ProcessID
+ Test.Sandbox.Process: piStat :: ProcessInfo -> String
- Test.Sandbox.Internals: Sandbox :: ErrorT String (ReaderT SandboxStateRef IO) a -> Sandbox a
+ Test.Sandbox.Internals: Sandbox :: ExceptT String (ReaderT SandboxStateRef IO) a -> Sandbox a
- Test.Sandbox.Internals: SandboxedProcess :: String -> FilePath -> [String] -> Maybe Int -> Maybe Capture -> Maybe SandboxedProcessInstance -> SandboxedProcess
+ Test.Sandbox.Internals: SandboxedProcess :: String -> FilePath -> [String] -> Maybe Int -> Maybe Capture -> Maybe SandboxedProcessInstance -> Maybe ProcessID -> Maybe ProcessGroupID -> SandboxedProcess
- Test.Sandbox.Internals: runSandbox :: Sandbox a -> ErrorT String (ReaderT SandboxStateRef IO) a
+ Test.Sandbox.Internals: runSandbox :: Sandbox a -> SandboxStateRef -> IO (Either String a)

Files

src/Test/Sandbox.hs view
@@ -1,3 +1,4 @@+{-#LANGUAGE ScopedTypeVariables#-} {- |    Module    : Test.Sandbox    Maintainer: Benjamin Surma <benjamin.surma@gmail.com>@@ -33,7 +34,12 @@    -- * Initialization   , sandbox+  , withSandbox +  -- * Calling sandbox on IO+  , runSandbox+  , runSandbox'+   -- * Registering processes   , register @@ -80,8 +86,7 @@ import Control.Exception.Lifted import Control.Monad import Control.Monad.Trans (liftIO)-import Control.Monad.Trans.Error (runErrorT)-import Control.Monad.Trans.Reader (runReaderT)+import Control.Monad.Reader (ask) import Control.Monad.Error.Class (catchError, throwError) import qualified Data.ByteString.Char8 as B import Data.Default@@ -91,25 +96,38 @@ import Data.Serialize (Serialize) import Network hiding (sendTo) import Prelude hiding (error)-import qualified Prelude (error) import System.Exit import System.IO import System.IO.Temp import System.Posix hiding (release)+import System.Environment  import Test.Sandbox.Internals +cleanUp :: Sandbox ()+cleanUp = do+  stopAll+  whenM isCleanUp $ do+    cleanUpProcesses+ -- | Creates a sandbox and execute the given actions in the IO monad. sandbox :: String    -- ^ Name of the sandbox environment         -> Sandbox a -- ^ Action to perform         -> IO a sandbox name actions = withSystemTempDirectory (name ++ "_") $ \dir -> do   env <- newSandboxState name dir-  (runReaderT . runErrorT . runSandbox) (actions `finally` stopAll) env >>= either+  runSandbox (actions `finally` cleanUp) env >>= either     (\error -> do hPutStrLn stderr error                   throwIO $ userError error)     return +withSandbox :: (SandboxStateRef -> IO a) -> IO a+withSandbox actions = do+  name <- getProgName+  sandbox name $ do+    ref <- ask+    liftIO $ actions ref+ -- | Optional parameters when registering a process in the Sandbox monad. data ProcessSettings = ProcessSettings {     psWait :: Maybe Int        -- ^ Time to wait (in s.) before checking that the process is still up@@ -155,14 +173,14 @@   displayBanner   sp <- getProcess process   whenM isVerbose $ liftIO $ putStr ("Starting process " ++ process ++ "... ") >> hFlush stdout-  updateProcess =<< startProcess sp+  _ <- updateProcess =<< startProcess sp   whenM isVerbose $ liftIO $ putStrLn "Done."  -- | Starts all registered processes (in their registration order) startAll :: Sandbox () startAll = uninterruptibleMask_ $ do   displayBanner-  whenM isVerbose $ liftIO $ putStr "Starting all sandbox processes... " >> hFlush stdout+  whenM isVerbose $ liftIO $ putStrLn "Starting all sandbox processes... " >> hFlush stdout   silently $ do env <- get                 mapM_ start (ssProcessOrder env)   whenM isVerbose $ liftIO $ putStrLn "Done."@@ -182,9 +200,9 @@      -> Sandbox () stop process = uninterruptibleMask_ $ do   sp <- getProcess process-  whenM isVerbose $ liftIO $ putStr ("Stopping process " ++ process ++ "... ") >> hFlush stdout-  updateProcess =<< stopProcess sp-  whenM isVerbose $ liftIO $ putStrLn "Done."+  whenM isVerbose $ liftIO $ putStrLn ("Stopping process " ++ process ++ "("++ show (spPid sp) ++ ")... ") >> hFlush stdout+  _ <- updateProcess =<< stopProcess sp+  whenM isVerbose $ liftIO $ putStrLn "Done." >> hFlush stdout  -- | Sends a POSIX signal to a process signal :: String     -- ^ Process name@@ -200,8 +218,8 @@ stopAll :: Sandbox () stopAll = uninterruptibleMask_ $ do   whenM isVerbose $ liftIO $ putStr "Stopping all sandbox processes... " >> hFlush stdout-  silently $ do env <- get-                mapM_ stop (reverse $ ssProcessOrder env)+  env <- get+  mapM_ stop (reverse $ ssProcessOrder env)   whenM isVerbose $ liftIO $ putStrLn "Done."  -- | Returns the effective binary path of a registered process.@@ -253,7 +271,7 @@   let port' = fromIntegral port   bindable <- liftIO $ isBindable (fromIntegral port)   if bindable then do env <- get-                      put (env { ssAllocatedPorts = M.insert name port' $ ssAllocatedPorts env })+                      _ <- put (env { ssAllocatedPorts = M.insert name port' $ ssAllocatedPorts env })                       return port'     else throwError $ "Unable to bind port " ++ show port @@ -264,7 +282,7 @@ setFile name contents = do   env <- get   (file, env') <- liftIO $ setFile' name contents env-  put env'+  _ <- put env'   return file  -- | Returns the path of a file previously created by setFile.@@ -284,7 +302,7 @@              -> Sandbox b withVariable key value action = bracket (do env <- get                                             let old = M.lookup key $ ssVariables env-                                            setVariable key value+                                            _ <- setVariable key value                                             return old)                                         (\old -> case old of                                                    Nothing -> unsetVariable key
src/Test/Sandbox/Internals.hs view
@@ -4,21 +4,23 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE ScopedTypeVariables #-}  module Test.Sandbox.Internals where  import Control.Applicative (Applicative) import Control.Concurrent-import Control.Exception.Lifted+import Control.Exception.Lifted hiding (throwTo) import Control.Monad import Control.Monad.Base (MonadBase)-import Control.Monad.Error (MonadError, catchError, throwError)+import Control.Monad.Except (MonadError, catchError, throwError) import Control.Monad.Loops import Control.Monad.Reader (MonadReader, ask) import Control.Monad.Trans (MonadIO, liftIO) import Control.Monad.Trans.Control (MonadBaseControl (..))-import Control.Monad.Trans.Error (ErrorT)-import Control.Monad.Trans.Reader (ReaderT)+import Control.Monad.Trans.Except (ExceptT, runExceptT)+import Control.Monad.Trans.Reader (ReaderT, runReaderT) import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as B import Data.Char@@ -31,8 +33,6 @@ import GHC.IO.Handle import Network import Network.Socket-import Prelude hiding (error)-import qualified Prelude (error) import System.Directory import System.Exit import System.FilePath@@ -43,18 +43,32 @@ import System.Process.Internals (withProcessHandle, ProcessHandle__(OpenHandle)) import System.Random import System.Random.Shuffle+import Test.Sandbox.Process  type SandboxStateRef = IORef SandboxState  newtype Sandbox a = Sandbox {-    runSandbox :: ErrorT String (ReaderT SandboxStateRef IO) a+    runSB :: ExceptT String (ReaderT SandboxStateRef IO) a   } deriving (Applicative, Functor, Monad, MonadBase IO, MonadError String, MonadReader (IORef SandboxState), MonadIO)  instance MonadBaseControl IO Sandbox where-  newtype StM Sandbox a = StMSandbox { runStMSandbox :: StM (ErrorT String (ReaderT SandboxStateRef IO)) a }-  liftBaseWith f = Sandbox . liftBaseWith $ \run -> f (liftM StMSandbox . run . runSandbox)+  newtype StM Sandbox a = StMSandbox { runStMSandbox :: StM (ExceptT String (ReaderT SandboxStateRef IO)) a }+  liftBaseWith f = Sandbox . liftBaseWith $ \run -> f (liftM StMSandbox . run . runSB)   restoreM = Sandbox . restoreM . runStMSandbox +runSandbox :: Sandbox a -> SandboxStateRef -> IO (Either String a)+runSandbox = runReaderT . runExceptT . runSB++errorHandler :: String -> IO a+errorHandler error' = do+  hPutStrLn stderr error'+  throwIO $ userError error'++runSandbox' :: SandboxStateRef -> Sandbox a -> IO a+runSandbox' env' action = do+  val <- runSandbox action env'+  either errorHandler return val+ data SandboxState = SandboxState {     ssName :: String   , ssDataDir :: FilePath@@ -73,6 +87,8 @@   , spWait :: Maybe Int   , spCapture :: Maybe Capture   , spInstance :: Maybe SandboxedProcessInstance+  , spPid :: Maybe ProcessID+  , spPGid :: Maybe ProcessGroupID   }  data Capture = CaptureStdout@@ -93,19 +109,19 @@  pretty :: SandboxState -> String pretty env =-  header env ++-  "-- Data directory: " ++ ssDataDir env ++ "\n\-  \-- Allocated ports: " ++ unwords (map show $ M.assocs $ ssAllocatedPorts env) ++ "\n\-  \-- Configuration files: " ++ unwords (M.elems $ ssFiles env) ++ "\n\-  \-- Registered processes: " ++ unwords (ssProcessOrder env)+  header env+  ++ "-- Data directory: " ++ ssDataDir env ++ "\n"+  ++ "-- Allocated ports: " ++ unwords (map show $ M.assocs $ ssAllocatedPorts env) ++ "\n"+  ++ "-- Configuration files: " ++ unwords (M.elems $ ssFiles env) ++ "\n"+  ++ "-- Registered processes: " ++ unwords (ssProcessOrder env)   ++ footer  header :: SandboxState -> String header te =-  "\n\-  \##------------------------------------------------------------------------------\n\-  \ ## " ++ title ++ replicate (72 - length title) ' ' ++ "  --\n\-  \## ##---------------------------------------------------------------------------\n"+  "\n"+  ++ "##------------------------------------------------------------------------------\n"+  ++ " ## " ++ title ++ replicate (72 - length title) ' ' ++ "  --\n"+  ++ "## ##---------------------------------------------------------------------------\n"   where title = ssName te ++ " system test environment"  footer :: String@@ -116,7 +132,11 @@ newSandboxState name dir = do   gen <- newStdGen   let availablePorts = shuffle' userPorts (length userPorts) gen-                       where userPorts = [49152..65535]+                       where userPorts = [5001..32767]+      -- Ephemeral port is used by operating system which allocates it for tcp-client.+      -- The port area is below.+      -- IANN(freebsd and win7): [49152.. 65535],Linux: [32768..61000],WinXP: [1025..5000]+      -- Do not use these area for "userPorts" to avoid conflict.   newIORef $ SandboxState name dir M.empty [] M.empty availablePorts M.empty M.empty  registerProcess ::@@ -130,9 +150,10 @@   env <- get   if isJust (M.lookup name (ssProcesses env)) then     throwError $ "Process " ++ name ++ " is already registered in the test environment."-    else do let sp = SandboxedProcess name bin args wait capture Nothing-            put env { ssProcesses = M.insert name sp (ssProcesses env)-                    , ssProcessOrder = ssProcessOrder env ++ [name] }+    else do let sp = SandboxedProcess name bin args wait capture Nothing Nothing Nothing+            _ <- put env { ssProcesses = M.insert name sp (ssProcesses env)+                         , ssProcessOrder = ssProcessOrder env ++ [name]+                         }             return sp  isValidProcessName :: String -> Bool@@ -155,7 +176,7 @@                      Just oh' -> liftM Just $ liftIO $ hGetContents oh'                      Nothing -> return Nothing               let sp' = sp { spInstance = Just $ StoppedInstance ec' o }-              updateProcess sp'+              _ <- updateProcess sp'               return sp'             Nothing -> return sp         _ -> return sp@@ -164,7 +185,7 @@ updateProcess :: SandboxedProcess -> Sandbox SandboxedProcess updateProcess sp = do   env <- get-  put env { ssProcesses = M.insert (spName sp) sp (ssProcesses env) }+  _ <- put env { ssProcesses = M.insert (spName sp) sp (ssProcesses env) }   return sp  secondInµs :: Int@@ -211,16 +232,56 @@   case ssAvailablePorts env of     [] -> throwError "No user ports left."     ports -> do (port, ports') <- liftIO $ takeBindablePort' ports-                put env { ssAllocatedPorts = M.insert name port $ ssAllocatedPorts env-                        , ssAvailablePorts = ports' }+                _ <- put env { ssAllocatedPorts = M.insert name port $ ssAllocatedPorts env+                             , ssAvailablePorts = ports' }                 return port   where takeBindablePort' pl = do           pl' <- dropWhileM (liftM not . isBindable) pl           return (head pl', tail pl') ++#if defined __LINUX__ isBindable :: PortNumber -> IO Bool+isBindable port = do+  ports <- getPorts+  return $ not $ S.member (fromIntegral port) $ S.unions $ (map S.singleton $ ports)+  where+    getPort :: String -> Maybe Int+    getPort v =+      if v =~ pattern+        then+          case (v =~ pattern) of+            [[_str,port']] ->+              case readHex port' of+                [(port'',_)] -> Just port''+                _ -> Nothing+            _ -> Nothing+        else Nothing+      where+        pattern = "^ *[0-9]+: [0-9A-F]+:([0-9A-F]+) .*"++    getPorts :: IO [Int]+    getPorts = do+      let files = [+            "/proc/net/tcp",+            "/proc/net/tcp6",+            "/proc/net/udp",+            "/proc/net/udp6"]+      list <- forM files $ \file -> do {+        v <- readFile file ;+        v `seq` return $ catMaybes $ map getPort $ lines v+        } `catch` (\(_ :: SomeException) -> return [])+      return $ flatten list+      where+        flatten :: [[a]] -> [a]+        flatten [] = []+        flatten (x:xs) = x ++ flatten xs++#else+isBindable :: PortNumber -> IO Bool isBindable p = withSocketsDo $ do   s <- socket AF_INET Stream defaultProtocol+  setSocketOption s ReuseAddr 1   localhost <- inet_addr "127.0.0.1"   let sa = SockAddrInet p localhost   r <- (bind s sa >> isBound s)@@ -228,6 +289,10 @@   close s   return $! r +#endif+++ startProcess :: SandboxedProcess -> Sandbox SandboxedProcess startProcess sp =   case spInstance sp of@@ -248,16 +313,23 @@                                                              CaptureStderr -> return (Just hRO, Inherit, UseHandle hRW)                                                              CaptureBoth -> return (Just hRO, UseHandle hRW, UseHandle hRW)                                     Nothing -> return (Nothing, Inherit, Inherit)-      (Just ih, _, _, ph) <- liftIO $ createProcess $ (proc bin args) { std_in = CreatePipe+      (Just ih, _, _, ph) <- liftIO $ createProcess $ ((proc bin args) {create_group = True} ) { std_in = CreatePipe                                                                       , std_out = hOutRW                                                                       , std_err = hErrRW }       when (isJust $ spWait sp) $ liftIO . threadDelay $ fromJust (spWait sp) * secondInµs       errno <- liftIO $ getProcessExitCode ph       case errno of-        Nothing -> updateProcess sp { spInstance = Just $ RunningInstance ph ih hOutRO }-        Just errno' -> throwError $ "Process " ++ spName sp ++ " not running.\n\-                                    \ - command-line: " ++ formatCommandLine bin args ++ "\n\-                                    \ - exit code: " ++ show errno' +        Nothing -> do+          pid <- liftIO $ hGetProcessID ph+          pgid <- liftIO $ getProcessGroupIDOf pid+          updateProcess sp {+            spInstance = Just $ RunningInstance ph ih hOutRO+          , spPid = Just pid+          , spPGid = Just pgid+          }+        Just errno' -> throwError $ "Process " ++ spName sp ++ " not running.\n"+                                 ++ " - command-line: " ++ formatCommandLine bin args ++ "\n"+                                 ++ " - exit code: " ++ show errno'   formatCommandLine :: String -> [String] -> String formatCommandLine bin args = unwords $ bin : args@@ -267,13 +339,33 @@   case spInstance sp of     Just (RunningInstance ph _ _) -> do       let wait = if isNothing $ spWait sp then 50000 else fromJust (spWait sp) * secondInµs `div` 5+      whenM isVerbose $ liftIO $ putStrLn ("sending sigterm : " ++  spName sp)       liftIO $ do terminateProcess ph                   threadDelay wait       stillRunning <- liftM isNothing $ liftIO $ getProcessExitCode ph-      when stillRunning $ liftIO $ killProcess ph+      when stillRunning $ do+        whenM isVerbose $ liftIO $ putStrLn ("sending sigkill : " ++  spName sp)+        liftIO $ killProcess ph       stopProcess =<< getProcess (spName sp)     _ -> return sp +#if defined(__MACOSX__) ||  defined(__WIN32__)+#else+getAvailablePids :: Sandbox [ProcessID]+getAvailablePids = do+  stat <- get+  let pgids = catMaybes (map (\(_k,v) -> spPGid v) (M.toList (ssProcesses stat)))+  pids <- liftIO $ getProcessIDs pgids+  return pids+#endif++cleanUpProcesses :: Sandbox ()+cleanUpProcesses = do+  stat <- get+  let pgids = catMaybes (map (\(_k,v) -> spPGid v) (M.toList (ssProcesses stat)))+  whenM isVerbose $ liftIO $ putStrLn ("Starting to kill process groups " ++ show pgids)+  liftIO $ cleanUpProcessGroupIDs pgids+ hSignalProcess :: Signal -> ProcessHandle -> IO () hSignalProcess s h = do   pid <- hGetProcessID h@@ -285,7 +377,11 @@ hGetProcessID :: ProcessHandle -> IO ProcessID hGetProcessID h = withProcessHandle h $ \x ->   case x of+#if MIN_VERSION_process(1,2,0)+    OpenHandle pid -> return pid+#else     OpenHandle pid -> return (x, pid)+#endif     _ -> throwIO $ userError "Unable to retrieve child process ID."  interactWithProcess :: SandboxedProcess -> String -> Int -> Sandbox String@@ -301,15 +397,15 @@ getProcessInputHandle sp =     case spInstance sp of       Just (RunningInstance _ ih _) -> return ih-      _ -> throwError $ "No such handle for " ++ spName sp ++ ". \-                        \Is the process started?"+      _ -> throwError $ "No such handle for " ++ spName sp ++ ". "+                     ++ "Is the process started?"  getProcessCapturedOutputHandle :: SandboxedProcess -> Sandbox Handle getProcessCapturedOutputHandle sp =   case spInstance sp of     Just (RunningInstance _ _ (Just oh)) -> return oh-    _ -> throwError $ "No captured output handle for " ++ spName sp ++ ". \-                      \Is capture activated?"+    _ -> throwError $ "No captured output handle for " ++ spName sp ++ ". "+                   ++ "Is capture activated?"  getProcessBinary :: SandboxedProcess -> Sandbox FilePath getProcessBinary sp = do@@ -317,8 +413,8 @@   case existing of     exBin:_ -> return exBin     [] -> throwError $ "Unable to find the executable for the test process \""-                       ++ spName sp ++ "\"\r\n\-                       \Considered paths were: " ++ show bins+                    ++ spName sp ++ "\"\r\n"+                    ++ "Considered paths were: " ++ show bins   where bins = getProcessCandidateBinaries sp  findExecutables :: [FilePath] -> IO [FilePath]@@ -362,7 +458,7 @@             -> Sandbox a setVariable name new = do   env <- get-  put $ env { ssVariables = M.insert name (encode new) (ssVariables env) }+  _ <- put $ env { ssVariables = M.insert name (encode new) (ssVariables env) }   return new  -- | Checks that a custom sandbox variable is set.@@ -397,6 +493,12 @@ verbosityKey :: String verbosityKey = "__VERBOSITY__" +isCleanUp :: Sandbox Bool+isCleanUp = getVariable cleanUpKey True++cleanUpKey :: String+cleanUpKey = "__CLEANUP__"+ displayBanner :: Sandbox () displayBanner = do   displayed <- checkVariable var@@ -407,9 +509,10 @@ installSignalHandlers :: Sandbox () installSignalHandlers = do   installed <- checkVariable var-  unless installed $ liftIO . void $ do installHandler sigTERM handler Nothing-                                        installHandler sigQUIT handler Nothing-                                        installHandler sigABRT handler Nothing+  unless installed $ liftIO . void $ do _ <- installHandler sigTERM handler Nothing+                                        _ <- installHandler sigQUIT handler Nothing+                                        _ <- installHandler sigABRT handler Nothing+                                        return ()   void $ setVariable var True   where var = "__HANDLERS_INSTALLED__"         handler = Catch $ signalProcess sigINT =<< getProcessID
+ src/Test/Sandbox/Process.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE CPP #-}++module Test.Sandbox.Process where+import System.Posix.Types+import System.Posix.Signals+import Text.Regex.Posix+import Data.Maybe+import Control.Exception+import Control.Monad+import System.Directory+import qualified Data.Set as S++#if defined(__MACOSX__) ||  defined(__WIN32__)+#else+data ProcessInfo = ProcessInfo {+   piPid  :: ProcessID+,  piStat :: String+,  piPpid :: ProcessID+,  piPgid :: ProcessGroupID+} deriving (Show,Eq,Read)++getProcessInfo :: String -> Maybe ProcessInfo+getProcessInfo v =+  if v =~ pattern+    then+      case v =~ pattern of+        [[_str,pid,stat,ppid,pgid]] -> Just $ ProcessInfo (read pid) stat (read ppid) (read pgid)+        _ -> Nothing+    else+      Nothing+  where+    pattern = "^([0-9]+) \\([^\\)]*\\) ([RSDZTW]) ([0-9]+) ([0-9]+) [0-9]+ .*"++getProcessInfos :: IO [ProcessInfo]+getProcessInfos = do+  dirs <- getDirectoryContents "/proc"+  let processes = filter ( =~ "[0-9]+") dirs+  stats <- forM processes $ \ps -> do {+    file <- (readFile $ "/proc/" ++ ps ++ "/stat") ;+    file `seq` return $ getProcessInfo file+    } `catch` (\(_ :: SomeException) -> return Nothing)+  return $ catMaybes stats++getProcessGroupIDs :: IO [ProcessGroupID]+getProcessGroupIDs = do+  infos <- getProcessInfos+  return $ map (\info -> piPgid info) infos++getProcessIDs :: [ProcessGroupID] -> IO [ProcessID]+getProcessIDs pgids = do+  infos <- getProcessInfos+  let pgids' = S.fromList $ pgids+  return $ map (\info -> piPid info) $ filter (\info -> S.member (piPgid info) pgids') infos+#endif++cleanUpProcessGroupIDs :: [ProcessGroupID] -> IO ()+cleanUpProcessGroupIDs pgids = do+  forM_ pgids $ \pgid -> do+    signalProcessGroup sigKILL pgid `catch`  (\(_::SomeException) -> return ())+
test-sandbox.cabal view
@@ -1,5 +1,5 @@ Name:           test-sandbox-Version:        0.0.1.8+Version:        0.0.1.9 Cabal-Version:  >= 1.14 Category:       Testing Synopsis:       Sandbox for system tests@@ -25,18 +25,42 @@ Source-Repository this     Type:       git     Location:   https://github.com/gree/haskell-test-sandbox-    Tag:        test-sandbox_0.0.1.8+    Tag:        test-sandbox_0.0.1.9  Library     Exposed-modules:    Test.Sandbox                         Test.Sandbox.Internals+                        Test.Sandbox.Process      Build-Depends:      base >=4 && <5, bytestring, cereal, containers,                         data-default, directory, filepath, lifted-base,-                        monad-control, monad-loops, mtl, network, process >= 1.1 && < 1.2,-                        random, random-shuffle, temporary, transformers,-                        transformers-base, unix+                        monad-control, monad-loops, mtl, network, process,+                        random, random-shuffle, temporary, transformers >= 0.4,+                        transformers-base, unix, regex-posix      Hs-source-dirs:     src+    Default-Language:   Haskell2010+    ghc-options:       -Wall +test-suite test+    type:              exitcode-stdio-1.0+    main-is:           test.hs+    hs-source-dirs:    test,dist/build/autogen+    ghc-options:       -Wall++    build-depends: base+                 , test-sandbox+                 , hspec+                 , template-haskell+                 , heredoc+                 , hastache+                 , text+                 , containers+                 , unix+                 , mtl+                 , transformers+                 , regex-posix+                 , directory+                 , process+                 , QuickCheck     Default-Language:   Haskell2010
+ test/test.hs view
@@ -0,0 +1,183 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE CPP #-}+import Test.Hspec+import Test.QuickCheck+import Test.QuickCheck.Monadic (assert,monadicIO)+import Test.Sandbox+import qualified Test.Sandbox.Internals as I+import Text.Heredoc+import qualified Text.Hastache as H+import qualified Text.Hastache.Context as H+import qualified Data.Text.Lazy as TL+import qualified Data.Text as T+import qualified Data.Map as M+import System.Posix.Files+import Data.IORef+import Data.Char+import Control.Concurrent++a2b :: String -> String+a2b [] = []+a2b ('a':xs) = 'b':xs+a2b (x:xs) = x:a2b xs++a2btest :: I.SandboxStateRef -> [Char] -> Property+a2btest ref str' = a2btest' ref $ filter isAlphaNum str'++a2btest' :: I.SandboxStateRef -> [Char] -> Property+a2btest' ref str' =+  monadicIO $ do+    v <- liftIO $ runSandbox' ref $ interactWith "sed_regex" (str' ++ "\n") 1+    assert $ v == ((a2b str') ++ "\n")++main :: IO ()+main = withSandbox $ \gref -> do+  hspec $ do+    describe "Basic Test" $ do+      it "interactive Test by sandbox" $ do+        sandbox "hogehoge" $ do+          start =<< register "sed_regex" "sed" [ "-u", "s/a/b/" ] def { psCapture = Just CaptureStdout }+          v <- interactWith "sed_regex" "a\n" 1+          liftIO $ v `shouldBe` "b\n"+      it "interactive Test by withSandbox" $ do+        withSandbox $ \ref -> do+          val <- runSandbox' ref $ do+            start =<< register "sed_regex" "sed" [ "-u", "s/a/b/" ] def { psCapture = Just CaptureStdout }+            interactWith "sed_regex" "a\n" 1+          val `shouldBe` "b\n"+      it "interactive Test : QuickCheck(setup)" $ do+        val <- runSandbox' gref $ do+          start =<< register "sed_regex" "sed" [ "-u", "s/a/b/" ] def { psCapture = Just CaptureStdout }+          interactWith "sed_regex" "a\n" 1+        val `shouldBe` "b\n"+      it "interactive Test : QuickCheck(run)" $+        property $ a2btest gref+      it "setFile" $ do+        withSandbox $ \ref -> do+          let content =+                  [str|#!/bin/env bash+                      |while true ; do echo hhh ; sleep 1;done+                      |]+          putStr content+          file <- runSandbox' ref $ setFile "str1" content+          readFile file `shouldReturn`  content+      it "set/getVariable" $ do+        withSandbox $ \ref -> do+          runSandbox' ref $ do+            v <- I.isVerbose+            liftIO $ v `shouldBe` True+            _ <- setVariable I.verbosityKey False+            v' <- I.isVerbose+            liftIO $ v' `shouldBe` False+          runSandbox' ref I.isVerbose `shouldReturn` False+          _ <- runSandbox' ref $ setVariable I.verbosityKey True+          runSandbox' ref I.isVerbose `shouldReturn` True+#if defined(__MACOSX__) ||  defined(__WIN32__)+#else+    describe "signal test" $ do+      it "send kill signal for process groups" $ do+        val <- newIORef $ error "do not eval this"+        withSandbox $ \ref -> runSandbox' ref $ do+          file <- setFile "str1"+                  [str|#!/usr/bin/env bash+                      |trap "echo catch signal" 1 2 3 15+                      |while true ; do echo hhh ; sleep 1;done+                      |]+          liftIO $ setExecuteMode file+          fil2 <- setFile' "str2"+                  [("file",file)]+                  [str|#!/usr/bin/env bash+                      |trap "echo catch signal" 1 2 3 15+                      |{{file}}&+                      |{{file}}&+                      |{{file}}&+                      |while true ; do echo hhh ; sleep 1;done+                      |]+          liftIO $ setExecuteMode fil2+          _ <- register "scr1" fil2 [] def+          startAll+          liftIO $ writeIORef val ref+          pids <- I.getAvailablePids+          liftIO $ threadDelay $ 1 * 1000 * 1000+          liftIO $ (length pids >= 4) `shouldBe` True+        ref' <- readIORef val+        pids <- runSandbox' ref' $ I.getAvailablePids+        length pids `shouldBe` 0++      it "send kill signal for process groups" $ do+        sandbox "test" $ do+          file <- setFile "str1"+                  [str|#!/usr/bin/env bash+                      |trap "echo catch signal" 1 2 3 15+                      |while true ; do echo hhh ; sleep 1;done+                      |]+          liftIO $ setExecuteMode file+          fil2 <- setFile' "str2"+                  [("file",file)]+                  [str|#!/usr/bin/env bash+                      |trap "echo catch signal" 1 2 3 15+                      |{{file}}&+                      |{{file}}&+                      |{{file}}&+                      |while true ; do echo hhh ; sleep 1;done+                      |]+          liftIO $ setExecuteMode fil2+          _ <- register "scr1" fil2 [] def+          startAll+          pids <- I.getAvailablePids+          liftIO $ (length pids >= 4) `shouldBe` True+      it "not send kill signal for process groups" $ do+        val <- newIORef $ error "do not eval this"+        withSandbox $ \ref -> runSandbox' ref $ do+          file <- setFile "str1"+                  [str|#!/usr/bin/env bash+                      |trap "echo catch signal" 1 2 3 15+                      |while true ; do echo hhh ; sleep 1;done+                      |]+          liftIO $ setExecuteMode file+          fil2 <- setFile' "str2"+                  [("file",file)]+                  [str|#!/usr/bin/env bash+                      |trap "echo catch signal" 1 2 3 15+                      |{{file}}&+                      |{{file}}&+                      |{{file}}&+                      |while true ; do echo hhh ; sleep 1;done+                      |]+          liftIO $ setExecuteMode fil2+          _ <- register "scr1" fil2 [] def+          startAll+          _ <- setVariable I.cleanUpKey False+          pids <- I.getAvailablePids+          liftIO $ writeIORef val ref+          liftIO $ do+            (length pids >= 4) `shouldBe` True+        ref' <- readIORef val+        pids <- runSandbox' ref' $ I.getAvailablePids+        (length pids > 0 ) `shouldBe` True+        pids' <- runSandbox' ref' $ do+          I.cleanUpProcesses+          I.getAvailablePids+        length pids' `shouldBe` 0+#endif++setFile' :: H.MuVar a+         => String+         -> [(String, a)]+         -> String+         -> Sandbox FilePath+setFile' filename keyValues template  = do+  str' <- H.hastacheStr+          H.defaultConfig+          (H.encodeStr template)+          (H.mkStrContext context')+  setFile filename $ T.unpack $ TL.toStrict str'+  where+    context' str' = (M.fromList (map (\(k,v) -> (k,H.MuVariable v)) keyValues)) M.! str'++setExecuteMode :: FilePath -> IO ()+setExecuteMode file = do+  stat <- getFileStatus file+  setFileMode file (fileMode stat `unionFileModes` ownerExecuteMode)