packages feed

ddc-war-0.2.1.1: DDC/War/Interface/Controller.hs

-- | Gang controller and user interface.
module DDC.War.Interface.Controller
	( Config        (..)
	, controller)
where
import DDC.War.Driver
import BuildBox.Pretty
import BuildBox.Control.Gang
import System.IO
import Control.Concurrent
import Control.Concurrent.STM.TChan
import Control.Monad.STM
import Control.Monad
import Data.Maybe
import Data.List
import DDC.War.Interface.VT100      as VT100
import qualified System.Cmd


data Config
        = Config
        { -- | Allow the user to control the driver from the console, 
          --   and ask what to do interactively if a file is different than expected.
          configInteractive     :: Bool 

          -- | Use VT100 colors in the output.
        , configColoredOutput   :: Bool

          -- | Pad test names out to this column width.
        , configFormatPathWidth :: Int 

          -- | Suppress this prefix from the front of displayed test names.
          --   Test names usually have fully qualified paths,
          --   but we wont want to display the "/Users/whoever/devel/code/ddc/test"
          --   prefix.
        , configSuppressPrefix  :: String }
        deriving Show

	

-- | Gang controller and user interface for the war test driver.
--   This should be forked into its own thread, so it runs concurrently
--   with the gang that is actually executing the tests.
controller 
	:: Config
	-> Gang
	-> Int		       -- ^ Total number of chains.
	-> TChan Result        -- ^ Channel to receive results from.
	-> IO [Result]

controller config gang chainsTotal chanResult
 = liftM reverse $ go_start []
 where	
	-- See if there is an input on the console.
        go_start :: [Result] -> IO [Result]
	go_start jobResults

         | configInteractive config
         = hReady stdin >>= \gotInput
           -> if gotInput
                then go_input jobResults
                else go_checkResult jobResults

         | otherwise
         = go_checkResult jobResults

	
	-- We've got input on the console, wait for already running tests then bail out.
	go_input jobResults
	 = do	putStrLn "Interrupt. Waiting for running jobs (CTRL-C kills)..."
	 	flushGang gang
	 	return jobResults


	-- See if any job results have been written to our input channel.
	go_checkResult jobResults
 	 =  (atomically $ isEmptyTChan chanResult) >>= \empty'
	 -> if empty'
	     then do
		gangState	<- getGangState gang
		if gangState == GangFinished

		 -- No job results in channel, and the gang has finished.
		 -- We're done, so return to caller.
		 then do
			return jobResults

		 -- No job results in channel, but the gang is still running.
		 -- Spin until something else happens.
		 else do
			threadDelay 10000
			go_start jobResults

	     -- We've got a job result from the input channel.
	     -- Read the result and pass it off to the result handler.
	     else do
		jobResult <- atomically $ readTChan chanResult
		keepGoing <- handleResult config gang chainsTotal jobResult
		if keepGoing
		 then go_start (jobResult : jobResults)
		 else do
			killGang gang
			return (jobResult : jobResults)


-- | Handle a job result.
--   Returns True if the controller should continue, 
--   or False if we should shut down and return to the caller.
handleResult :: Config -> Gang -> Int -> Result -> IO Bool
handleResult config gang chainsTotal
        (Result chainIx jobIx jobId actionName product')
 | JobId testName wayName       <- jobId
 , ProductStatus status _       <- product'
 = do   let testName2    = fromMaybe testName 
                                (stripPrefix (configSuppressPrefix config) testName)

        let width        = configFormatPathWidth config

        putStrLn 
         $ render 
         $ parens (padR (length $ show chainsTotal)
                        (ppr $ chainIx) 
                        <> text "."
                        <> ppr jobIx
                        <> text "/" 
                        <> ppr chainsTotal)
           <+> padL width (text testName2)
           <+> padL 5     (text wayName)
           <+> padL 8     (text actionName)
           <+> colorizeStatus config actionName (render status)

        hFlush stdout
        return True

 -- If a file is different than expected in interactive mode,
 --   then ask the user what to do about it.
 | ProductDiff fileRef fileOut fileDiff <- product'
 , configInteractive config
 = do	
	putStr	$  "\n"
		++ "-- Output Differs  -------------------------------------------------------------\n"
		++ "   expected file: " ++ fileRef	++	"\n"
		++ "     actual file: " ++ fileOut	++ 	"\n"
		++ replicate 80 '-' ++ "\n"

	-- Show the difference
	str	<- readFile fileDiff
	putStr	str
	hFlush stdout
	
	-- Ask the user what to do about it, 
	--  and pause the gang while we're waiting for user input
	pauseGang gang
	keepGoing	<- handleResult_askDiff fileRef fileOut fileDiff
	when keepGoing
	 $ resumeGang gang

	return keepGoing

 -- If a file is different than expected in batch mode,
 --   then just print the status.
 | ProductDiff{}        <- product'
 = handleResult config gang chainsTotal 
        $ Result chainIx jobIx jobId actionName
        $ ProductStatus (text "failed") False

 -- Bogus pattern match to avoid warning.
 | otherwise
 = error "DDC.War.Interface.Controller.handleResult: no match"


handleResult_askDiff :: FilePath -> FilePath -> FilePath -> IO Bool
handleResult_askDiff fileRef fileOut fileDiff
 = do	putStr	$  replicate 80 '-' ++ "\n"
		++ "    (ENTER) continue   (e) show expected    (a) show actual\n"
		++ "    (CONTROL-C) quit   (u) update expected\n"
		++ "\n"
		++ "? "

	hFlush stdout
	cmd	<- hGetLine stdin

	let result
		-- continue, ignoring that the test gave a different result.
		| ""		<- cmd
		= return True

		-- Print the expected output
		| ('e': _)	<- cmd
		= do	str	<- readFile fileRef
			putStr	$  replicate 80 '-' ++ "\n"
			putStr	str
			handleResult_askDiff fileRef fileOut fileDiff

		-- Print the actual output
		| ('a': _)	<- cmd
		= do	str	<- readFile fileOut
			putStr	$  replicate 80 '-' ++ "\n"
			putStr	str
			handleResult_askDiff fileRef fileOut fileDiff

		-- Update the expected output with the actual one
		| ('u': _)	<- cmd
		= do	System.Cmd.system 
				$ "cp " ++ fileOut ++ " " ++ fileRef

			return True

		-- Invalid Command
		| otherwise
		= do	putStr	 $ "Invalid command.\n"
			handleResult_askDiff fileRef fileOut fileDiff
	
	result


colorizeStatus :: Config -> String -> String -> Doc
colorizeStatus config jobName status
        | not $ configColoredOutput config
        = text status

        | isPrefixOf "ok" status
        = colorDoc [Foreground Green] (text status)

        | isPrefixOf "success" status
        , jobName == "run"
        = colorDoc [Foreground Green] (text status)

        | isPrefixOf "success" status
        , jobName == "compile"
        = colorDoc [Foreground Blue]  (text status)

        | isPrefixOf "failed"  status
        = colorDoc [Foreground Red]   (text status)

        | otherwise
        = text status


colorDoc :: [VT100.Mode] -> Doc -> Doc
colorDoc vmode doc
        = text 
        $ concat [ setMode vmode
                 , render doc
                 , setMode [Reset] ]