{-# OPTIONS_GHC -cpp #-}
module Distribution.ShellHarness ( runTests ) where

import Prelude hiding( catch )
import System.Directory ( getCurrentDirectory, setPermissions,
                          Permissions(..), getDirectoryContents,
                          findExecutable, createDirectoryIfMissing )
import System.Environment ( getEnv, getEnvironment )
import System.Exit ( ExitCode (..), exitWith )
import System.FilePath
import System.IO
import System.Process ( ProcessHandle, runInteractiveCommand,
                        runInteractiveProcess, waitForProcess,
                        getProcessExitCode )
import Data.Maybe
import Data.List ( isInfixOf, isPrefixOf, (\\), nubBy )
import Control.Concurrent
#if __GLASGOW_HASKELL__ >= 610
import Control.OldException
#else
import Control.Exception
#endif
import Control.Monad

runTests :: String -> [String] -> IO Bool
runTests cwd tests = do
     fails <- run tests
     if  "bugs" `isInfixOf` cwd
         then if (length tests /= length fails)
                 then do putStrLn $ "Some bug tests passed:"
                         mapM_ putStrLn (tests \\ fails)
                         return False
                 else do putStrLn "All bug tests OK"
                         return True
         else if fails /= []
                 then do putStrLn "Some tests failed:"
                         mapM_ putStrLn fails
                         return False
                 else do putStrLn "All tests OK"
                         return True

run :: [String] -> IO [String]
run tests= do
    cwd <-  getCurrentDirectory
    path <- getEnv "PATH"
    env <- getEnvironment
    darcs_path <- get_darcs_path
    let myenv = [("HOME",cwd)
                ,("DARCS_TESTING_HOME",cwd)
                ,("PWD",cwd)
                ,("EMAIL","tester")
                ,("DARCSEMAIL","tester")
                ,("PATH",(darcs_path++":"++path))
                ,("DARCS_DONT_COLOR","1")
                ,("DARCS_DONT_ESCAPE_ANYTHING","1")]
    bash <- find_bash
    let shell = takeWhile (/= '\n') bash
    putStrLn $ "Using bash shell in '"++shell++"'"
    set_prefs
    run_helper shell tests []  (set_env myenv env)
  where get_darcs_path = do
           env <- getEnvironment
           cwd <-  getCurrentDirectory
           case lookup "DARCS" env of
                Nothing -> return (cwd ++ "/..")
                Just d  -> return $ takeDirectory d
        set_prefs = do
            finally (catch (appendFile ".darcs/defaults" "\nALL --ignore-times\n")
                           (\e -> fail $ "Unable to set preferences: "
                                         ++ show e))
                    (createDirectoryIfMissing  False ".darcs")
run_helper :: String -> [String] -> [String] ->
                  [(String,String)] -> IO [String]
run_helper _ [] fails _ = return fails
run_helper shell (test:ts) fails env = do
    putStr $ "Running " ++ test ++ " ..." ++ (replicate (36 - (length test)) ' ')
    (output,success) <- backtick shell test env
    if success then do putStrLn " passed."
                       cleanup_dirs
                       run_helper shell ts fails env
               else do putStrLn " failed."
                       putStrLn $ "Probable reason :" ++ output
                       cleanup_dirs
                       run_helper shell ts (fails++[test]) env
  where cleanup_dirs :: IO ()
        cleanup_dirs =
          do dirfiles <- getDirectoryContents (fromJust $ lookup "PWD" env)
             let tempfiles = (filter ("temp" `isPrefixOf`) dirfiles) ++
                             (filter ("tmp" `isPrefixOf`) dirfiles)
             mapM_ (\x-> 
                  setPermissions x (Permissions 
                                   {readable = True
                                   ,writable = True
                                   ,executable = False
                                   ,searchable = True}
                                   )
                 ) tempfiles

backtick :: String -> String -> [(String, String)]-> IO (String,Bool)
backtick cmd args env = do
   (exitcode,res) <- backtick_helper cmd args env
   case exitcode of
        ExitSuccess -> return (res, True)
        ExitFailure code -> return (res, False)

backtick_helper :: String -> String -> [(String,String)] ->
                                      IO (ExitCode, String)
backtick_helper cmd args env = process_wrapper (runInteractiveProcess
                                                   cmd [args] Nothing
                                                   (Just env)
                                               ) ""

find_bash =
   do sh <- findExecutable "bash"
      case sh of
          Just p -> return p
          Nothing -> error "Could not find bash in PATH"

-- | Run a process with a list of arguments and return anything from
-- /stderr/ or /stdout/
process_wrapper :: IO (Handle, Handle, Handle, ProcessHandle) ->
                   String -> IO (ExitCode, String)
process_wrapper f _ = do
       (_,o,e,pid) <- f
       hSetBuffering o LineBuffering
       hSetBuffering e LineBuffering
       ch <- newChan
       -- WARNING: beware of hokeyness ahead!
       let readWrite i = do x <- hGetLine i
                            writeChan ch $ Just x
                            readWrite i
                         `catch` \_ -> writeChan ch Nothing
           readEO = do x <- readChan ch
                       case x of
                         Just l -> do y <- readEO
                                      return $ l:y
                         Nothing -> readEO'
           readEO' = do x <- readChan ch
                        case x of
                          Just l -> do y <- readEO'
                                       return $ l:y
                          Nothing -> return []
       forkIO $ readWrite o
       forkIO $ readWrite e
       outerr <- readEO
       ec <- waitForProcessNonBlocking pid
       threadDelay 1000
       case ec of
         ExitFailure 127 -> fail $ "timeout running command\n\n"
                                   ++unlines outerr
         _ -> return (ec, unlines outerr)

--
-- waitForProcess uses a very hokey heuristic to try to avoid burning too
-- much CPU time in a busy wait, while also not adding too much extra
-- latency.

waitForProcessNonBlocking :: ProcessHandle -> IO ExitCode
waitForProcessNonBlocking = if rtsSupportsBoundThreads
                            then waitForProcess
                            else wfp 0
    where wfp n pid = do mec <- getProcessExitCode pid
                         case mec of
                           Just ec -> return ec
                           Nothing -> do threadDelay n
                                         wfp (min 100000 (n+1+n`div`4)) pid



set_env :: [(String,String)] -> [(String,String)] -> [(String, String)]
set_env es env = nubBy (\(x,_) (y,_) -> x == y) (es ++ env)

