packages feed

fibon-0.1.0: tools/fibon-run/Fibon/Run/Actions.hs

module Fibon.Run.Actions (
      runBundle
    , buildBundle
    , sanityCheckBundle
    , FibonError
    , Action(..)
    , ActionRunner
)
where

import Data.List
import Data.Maybe
import Data.Time.Clock.POSIX
import Fibon.BenchmarkInstance
import Fibon.Result
import Fibon.Run.BenchmarkBundle
import Fibon.Run.BenchmarkRunner as Runner
import qualified Fibon.Run.Log as Log
import qualified Fibon.Run.SysTools as SysTools
import Control.Monad.Error
import Control.Monad.Reader
import System.Directory
import System.Exit
import System.FilePath
import System.Process

type FibonRunMonad = ErrorT FibonError (ReaderT BenchmarkBundle IO)

data Action =
    Sanity
  | Build
  | Run
  deriving (Read, Show, Eq, Ord, Enum)

type ActionRunner a = (BenchmarkBundle -> IO (Either FibonError a))

data ActionResult =
    SanityComplete
  | BuildComplete BuildData
  | RunComplete   RunData
  deriving(Show)

data FibonError =
    BuildError   String
  | SanityError  String
  | RunError     String
  | OtherError   String -- ^ For general IO exceptions
  deriving (Show)
instance Error FibonError where
  strMsg = OtherError

sanityCheckBundle :: BenchmarkBundle -> IO (Either FibonError ())
sanityCheckBundle bb = runFibonMonad bb $ do
  SanityComplete <- runAction Sanity
  return ()

buildBundle :: BenchmarkBundle -> IO (Either FibonError BuildData)
buildBundle bb = runFibonMonad bb $ do
  SanityComplete   <- runAction Sanity
  BuildComplete br <- runAction Build
  return br

runBundle :: BenchmarkBundle -> IO (Either FibonError FibonResult)
runBundle bb = runFibonMonad bb $ do
  SanityComplete   <- runAction Sanity
  BuildComplete br <- runAction Build
  RunComplete   rr <- runAction Run
  return $ FibonResult (bundleName bb) br rr

runFibonMonad :: BenchmarkBundle
              -> ErrorT FibonError (ReaderT BenchmarkBundle IO) a
              -> IO (Either FibonError a)
runFibonMonad bb a = runReaderT (runErrorT a) bb

runAction :: Action -> FibonRunMonad ActionResult
runAction Sanity = do
  io $ Log.notice "  Checking..."
  sanityCheck
  return SanityComplete
runAction Build = do
  io $ Log.notice "  Building..."
  prepConfigure
  runConfigure
  r <- runBuild
  return $ BuildComplete r
runAction Run = do
  io $ Log.notice "  Running..."
  prepRun
  r <- runRun
  return $ RunComplete r

sanityCheck :: FibonRunMonad ()
sanityCheck = do
  bb <- ask
  let bmPath = pathToBench bb
  io $ Log.info ("Checking for directory:\n"++bmPath)
  bdExists <- io $ doesDirectoryExist bmPath
  unless bdExists (throwError $ pathDoesNotExist bmPath)
  io $ Log.info ("Checking for cabal file in:\n"++bmPath)
  dirContents <- io $ getDirectoryContents bmPath
  let cabalFile = find (".cabal" `isSuffixOf`) dirContents
  case cabalFile of
    Just f  -> do io $ Log.info ("Found cabal file: "++f)
                  checkForExpectedOutFiles
    Nothing -> throwError cabalFileDoesNotExist
  where
  pathDoesNotExist bmP  = SanityError("Directory:\n"++bmP++" does not exist")
  cabalFileDoesNotExist = SanityError "Can not find cabal file"

checkForExpectedOutFiles :: FibonRunMonad ()
checkForExpectedOutFiles = do
  bb <- ask
  io $ Log.info "Checking for diff files"
  let expectedOut = (output . benchDetails) bb
      fs = diffFiles expectedOut
  missingFiles <- io $ filterM (missing bb) fs
  case missingFiles of
    [] -> return ()
    ms -> throwError $ SanityError("Missing expected output files: "++show ms)
  where
  missing bb f = do
    Log.info $ "Checking for expected output file: " ++ f
    e1 <- doesFileExist $ (pathToAllOutputFiles bb)  </> f
    e2 <- doesFileExist $ (pathToSizeOutputFiles bb) </> f
    return (not e1 && not e2)
  diffFiles =
    catMaybes . map (\o -> case o of (_, Diff f) -> Just f ; _ -> Nothing)

prepConfigure :: FibonRunMonad ()
prepConfigure = do
  bb <- ask
  let ud = (workDir bb) </> (unique bb)
  udExists <- io $ doesDirectoryExist ud
  unless udExists (io $ createDirectory ud)

runConfigure :: FibonRunMonad ()
runConfigure = do
  _ <- runCabalCommand "configure" configureFlags
  return ()

runBuild :: FibonRunMonad BuildData
runBuild = do
  time <- runCabalCommand "build" buildFlags
  size <- runSizeCommand
  return $ BuildData {buildTime = time, buildSize = size}

prepRun :: FibonRunMonad ()
prepRun = do
  mapM_ copyFiles [
      pathToSizeInputFiles
    , pathToAllInputFiles
    , pathToSizeOutputFiles
    , pathToAllOutputFiles
    ]

runRun :: FibonRunMonad RunData
runRun =  do
  bb <- ask
  res <- io $ Runner.run bb
  io $ Log.info (show res)
  case res of
    Success s d -> return     $ RunData  {summary = s, details = d}
    Failure msg -> throwError $ RunError (summarize msg)
  where
  summarize = concat . intersperse "\n" . map  simplify
  simplify (MissingOutput f) = "Missing output file: "++f
  simplify (DiffError     _ )= "Output differs from expected."
  simplify (Timeout         )= "Timeout"

copyFiles :: (BenchmarkBundle -> FilePath)
          -> FibonRunMonad ()
copyFiles pathSelector = do
  bb <- ask
  let srcPath = pathSelector bb
      dstPath = pathToExeBuildDir bb
      cp f    = do
        io $ copyFile (srcPath </> baseName) (dstPath </> baseName)
        where baseName = snd (splitFileName f)
  dExists <- io $ doesDirectoryExist srcPath
  if not dExists
    then do return ()
    else do
      io $ Log.info ("Copying files\n  from: "++srcPath++"\n  to: "++dstPath)
      files <- io $ getDirectoryContents srcPath
      let realFiles = filter (\f -> f /= "." && f /= "..") files
      io $ Log.info ("Copying files: "++(show realFiles))
      mapM_ cp realFiles
      return ()

runCabalCommand :: String
                -> (FlagConfig -> [String])
                -> FibonRunMonad Double
runCabalCommand cmd flagsSelector = do
  bb <- ask
  let fullArgs = ourArgs ++ userArgs
      userArgs = (flagsSelector . fullFlags) bb
      ourArgs  = [cmd, "--builddir="++(pathToCabalWorkDir bb)]
  (_, time) <- timeInDir (pathToBench bb) $ exec SysTools.cabal fullArgs
  return time

runSizeCommand :: FibonRunMonad String
runSizeCommand = do
  bb <- ask
  exec (SysTools.size) [(pathToExe bb)]


timeInDir :: FilePath -> FibonRunMonad a -> FibonRunMonad (a, Double)
timeInDir fp action = do
  dir <- io $ getCurrentDirectory
  io $ setCurrentDirectory fp
  start <- io $ getTime
  r <- action
  end <- io $ getTime
  io $ setCurrentDirectory dir
  let !delta = end - start
  return (r, delta)

io :: IO a -> FibonRunMonad a
io = liftIO

exec :: FilePath -> [String] -> FibonRunMonad String
exec cmd args = do
  (exit, out, err) <- io $ readProcessWithExitCode cmd args []
  io $ Log.info ("COMMAND: "++fullCommand)
  io $ Log.info ("STDOUT: \n"++out)
  io $ Log.info ("STDERR: \n"++err)
  case exit of
    ExitSuccess   -> return out
    ExitFailure _ -> throwError $ BuildError msg
  where
  msg         = "Failed running command: " ++ fullCommand 
  fullCommand = cmd ++ stringify args


joinWith :: a -> [[a]] -> [a]
joinWith a = concatMap (a:)

stringify :: [String] -> String
stringify = joinWith ' '

getTime :: IO Double
getTime = (fromRational . toRational) `fmap` getPOSIXTime