packages feed

ddc-war-0.2.1.1: DDC/War/Task/Test.hs

module DDC.War.Task.Test
        ( Spec          (..)
        , Result        (..)
        , build)
where
import DDC.War.Create

import qualified DDC.War.Interface.Controller   as Controller

import DDC.War.Driver                           (Chain(..))
import qualified DDC.War.Driver                 as Driver
import qualified DDC.War.Driver.Gang            as Driver

import BuildBox.Control.Gang
import BuildBox.IO.Directory
import BuildBox.Pretty
import BuildBox

import System.Directory
import Control.Concurrent
import Control.Concurrent.STM.TChan
import Control.Monad
import Control.Monad.STM
import Control.Exception
import Data.List

import qualified Data.Sequence          as Seq
import qualified Data.Foldable          as Seq
import qualified Data.Set               as Set
import qualified Data.Traversable       as Seq


-- | Run regression tests.
data Spec
        = Spec
        { -- | Start looking for tests from these directories.
          specTestDirs          :: [FilePath]

          -- | Ways to run each test.
        , specWays              :: [Way]

          -- | Number of concurrent threads to use.
        , specThreads           :: Int

          -- | Ask user what to do about unexpected test outputs interactively.
        , specInteractive       :: Bool 

          -- | Pad test names out to this column width in log files.
        , specFormatPathWidth   :: Int

          -- | Write all test results to this file.
        , specResultsFileAll    :: Maybe FilePath

          -- | Write only failed test results to this file
        , specResultsFileFailed :: Maybe FilePath }
        deriving Show


data Result
        = ResultSuccess
        deriving Show


instance Pretty Result where
 ppr result
  = case result of
        ResultSuccess   -> text "success"


build :: Spec -> Build Result
build spec
 = do
        currentDir      <- io $ getCurrentDirectory

        -- All the starting test directories from the command line.
        testDirs       <- io $ mapM (makeRelativeToCurrentDirectory <=< canonicalizePath)
                        $ specTestDirs spec

        -- Trace all the files reachable from these directories.
        testFilesRaw   <- io $ liftM (join . Seq.fromList)
                        $  mapM traceFilesFrom testDirs
                
        -- Canonicalize all the paths and put them in a set (which sorts them)
        testFilesSet   <- io $ liftM (Set.fromList . Seq.toList)
                        $  Seq.mapM canonicalizePath
                        $  testFilesRaw

        -- Skip over files with 'skip' in the path,
        --  and don't decend into our own build dirs.
        let testFilesSorted
                = filter (not . isInfixOf "skip-")
                $ filter (not . isInfixOf "-skip")
                $ filter (not . isInfixOf "war-")
                $ Set.toList testFilesSet

        let testFilesSortedSet
                = Set.fromList testFilesSorted

        -- Create test chains based on the files we have.
        let ways'
                = case specWays spec of
                   []   -> [Way "std" [] []]
                   ways -> ways

        let chains :: [Chain]
            chains = concat 
                [ concat $ map (\way -> create way testFilesSortedSet file) ways'
                | file <- testFilesSorted]

        -- Suppress this prefix from the front of test names when we display them.
        let prefix = currentDir ++ "/"

        -- Run all the chains.
        results <- io $ runChainsWithControllerIO prefix spec chains
        
        -- Write all results to file if we were asked for it
        let chainsTotal = length chains
        let pathWidth   = specFormatPathWidth spec
        let pprResult   = render . Driver.prettyResult chainsTotal prefix pathWidth
        (case specResultsFileAll spec of
          Nothing       -> return ()
          Just file     -> io   $ writeFile file 
                                $ unlines 
                                $ map pprResult
                                $ results)

        -- Write failed results to file if we were asked for it.
        let wasSuccess p
                = case p of
                        Driver.ProductStatus _ True     -> True
                        _                               -> False
        (case specResultsFileFailed spec of 
          Nothing       -> return ()
          Just file     -> io   $ writeFile file
                                $ unlines
                                $ map pprResult
                                $ filter (not . wasSuccess . Driver.resultProduct)
                                $ results)

        return ResultSuccess


-- | Fork threads to run job chains.
--      We display test results interactivly on the console,
--      as well as allowing the user to interrupt by pressing ENTER.
--
--      In batch mode: if we get a ResultDiff saying a test file is different
--      then just treat it as failed.
--
--      In non-batch mode: if we get a ResultDiff then use the controller
--      to ask the user what to do about it interactively.
--
runChainsWithControllerIO
        :: String               -- ^ Suppress this prefix from the front of test names.
        -> Spec                 -- ^ Build configuration.
        -> [Chain]              -- ^ Chains of jobs to run.
        -> IO [Driver.Result]

runChainsWithControllerIO prefix spec chains
 = do   
        -- Count the total number of chains for the status display.
        let chainsTotal = length chains

        -- Create a new channel to communicate between the test driver  and the
        -- controller. As each test finishes, the driver writes the result to the
        -- channel, and the controller reads the results and displays them 
        -- on the console.
        (chanResult :: TChan Driver.Result)
                <- atomically $ newTChan
        
        -- Fork a gang to run all the job chains.
        gang    <- Driver.forkChainsIO 
                        (specThreads spec) "/tmp"
                        (Just chanResult) chains

        -- Fork the controller to display results and manage user input.
        --   When the controller it done it also writes all the results
        --   it received to an MVar to send them back to the main thread.
        let configController
                = Controller.Config
                { Controller.configFormatPathWidth = specFormatPathWidth spec
                , Controller.configInteractive     = specInteractive spec 
                , Controller.configColoredOutput   = specInteractive spec
                , Controller.configSuppressPrefix  = prefix }

        varResults      <- newEmptyMVar
        forkIO 
         $ do   results <- Controller.controller configController gang chainsTotal chanResult
                putMVar varResults results
         `finally` (putMVar varResults [])

        -- Wait for the controller to finish.
        results <- takeMVar varResults

        -- Wait until the gang is finished running chains, 
        -- or has been killed by the controller.
        joinGang gang

        return results