packages feed

lhc-0.8: lhc-regress/Main.hs

{-# LANGUAGE CPP #-}
module Main where

import Setup
import TestCase

import System.Environment
import System.Process
import System.FilePath
import System.IO
import System.Exit
import System.Directory
import Control.Monad
import Control.Exception.Extensible
import System.Timeout
import Control.Concurrent
import Text.Printf
import qualified Data.ByteString.Char8 as B

data TestResult = CompileError String
                | ProgramError String String
                | KnownFailure
                | TimeOut
                | Success
                deriving Show

isSuccess Success = True
isSuccess KnownFailure = True
isSuccess _ = False

data Stats = Stats { successfulTests :: Int
                   , expectedFailures :: Int
                   , unexpectedFailures :: Int
                   , testsNotExecuted :: Int
                   }

newStats :: Int -> Stats
newStats nTests = Stats 0 0 0 nTests

successfulTest :: Stats -> Stats
successfulTest stats = stats{ successfulTests = successfulTests stats + 1
                            , testsNotExecuted = testsNotExecuted stats - 1 }

expectedFailure :: Stats -> Stats
expectedFailure stats = stats{ expectedFailures = expectedFailures stats + 1
                             , testsNotExecuted = testsNotExecuted stats - 1 }

unexpectedFailure :: Stats -> Stats
unexpectedFailure stats = stats{ unexpectedFailures = unexpectedFailures stats + 1
                               , testsNotExecuted = testsNotExecuted stats - 1 }

hasFailures :: Stats -> Bool
hasFailures stats = unexpectedFailures stats /= 0

ppStats :: Stats -> String
ppStats stats = printf ("Successful tests:    %d\n"++
                        "Expected failures:   %d\n"++
                        "Unexpected failures: %d\n"++
                        "Omitted tests:       %d\n")
                  (successfulTests stats)
                  (expectedFailures stats)
                  (unexpectedFailures stats)
                  (testsNotExecuted stats)

updateStats :: TestResult -> Stats -> Stats
updateStats Success = successfulTest
updateStats KnownFailure = expectedFailure
updateStats _ = unexpectedFailure

main :: IO ()
main = do (cfg,paths) <- parseArguments =<< getArgs
          workChan <- newChan
          resultChan <- newChan
          tests <- forM paths findTestCases
          let nTests = length (concat tests)
          writeList2Chan workChan (concat tests)
          when (cfgVerbose cfg >= 1) $ putStrLn $ "Testsuite consists of " ++ show nTests ++ " tests."
          workers <- replicateM (max 1 (cfgThreads cfg)) $ forkIO $ forever $
            do test <- readChan workChan
               result <- runTestCase cfg test
               writeChan resultChan (test,result)

          results <- getChanContents resultChan
          manager cfg (newStats nTests) (take nTests results)
            `finally` mapM_ killThread workers


errMsg = "Some tests failed to perform as expected."

manager cfg stats rest | hasFailures stats && (not (cfgComplete cfg) || null rest)
  = do when (cfgVerbose cfg == 1) $ putStrLn ""
       when (cfgVerbose cfg >= 1) $ do putStrLn errMsg
                                       putStr (ppStats stats)
       exitFailure

manager cfg stats [] | cfgVerbose cfg >= 3 = do putStrLn "No unexpected failures"
                                                putStr (ppStats stats)
manager cfg stats [] | cfgVerbose cfg >= 1 = do putStrLn ""
                                                putStr (ppStats stats)
manager cfg stats [] = return ()

manager cfg stats ((tc,result):rest)
  = do case () of () | cfgVerbose cfg >= 3 -> case result of
                                                Success      -> printf "%20s: %s\n" (testCaseName tc) "OK."
                                                KnownFailure -> printf "%20s: %s\n" (testCaseName tc) "Known failure."
                                                TimeOut      -> printf "%20s: %s\n" (testCaseName tc) "TimeOut."
                                                CompileError str | cfgVerbose cfg >= 4 -> printf "%20s: %s\n" (testCaseName tc) str
                                                ProgramError short str | cfgVerbose cfg >= 4 -> printf "%20s: %s:\n%s" (testCaseName tc) short str
                                                CompileError str -> printf "%20s: %s\n" (testCaseName tc) "Compile failure."
                                                ProgramError short str -> printf "%20s: %s\n" (testCaseName tc) short
                     | cfgVerbose cfg >= 1 -> if isSuccess result then putStr "." else putStr "*"
                     | otherwise -> return ()
       hFlush stdout
       manager cfg (updateStats result stats) rest

-- FIXME: Get a proper temporary directory.
runTestCase :: Config -> TestCase -> IO TestResult
runTestCase cfg tc
  = bracket (createDirectoryIfMissing True testDir)
            (\_ -> removeDirectoryRecursive testDir) $ \_ -> checkFail $ withTimeout $
    do let args = [ "eval"
                  , testCasePath tc `replaceExtension` "hcr" ] ++
                  cfgLHCOptions cfg ++
                  testCaseArgs tc
           ghcArgs = ["-fforce-recomp","-fext-core","-O2","-c",testCasePath tc]
       when (cfgVerbose cfg >= 4) $ putStrLn $ unwords ("ghc":ghcArgs)
       (ret,out,err) <- execProcess "lhc" ghcArgs B.empty
       case ret of
         ExitFailure c -> return $ CompileError $ unlines $ ["ghc failed with: " ++ show c, B.unpack err]
         ExitSuccess
           -> do when (cfgVerbose cfg >= 4) $ putStrLn $ unwords (cfgLHCPath cfg:args)
                 (ret,out,err) <- execProcess (cfgLHCPath cfg) args B.empty
                 case (testCaseStdout tc, testCaseStderr tc) of
                   (Just expectedOut,_) | expectedOut /= out -> return $ ProgramError "Unexpected stdout" $ B.unpack out
                   (_,Just expectedErr) | expectedErr /= err -> return $ ProgramError "Unexpected stderr" $ B.unpack err
                   _ -> return Success
  where name = dropExtension (takeFileName (testCasePath tc))
        testDir = cfgTempDir cfg </> name
        progName = testDir </> name
        checkFail io = do ret <- io
                          if testCaseMustFail tc
                             then case ret of
                                    Success -> return $ ProgramError "Known bug succeeded." ""
                                    other   -> return KnownFailure
                             else return ret
        withTimeout io = do ret <- timeout (10^6 * cfgTestTimeout cfg) io
                            case ret of
                              Nothing  -> return TimeOut
                              Just val -> return val

-- This differs from System.Process by terminating the program if an exception is raised.
execProcess :: FilePath -> [String] -> B.ByteString -> IO (ExitCode, B.ByteString, B.ByteString)
execProcess cmd args input = do
  (inh, outh, errh, pid) <- runInteractiveProcess cmd args Nothing Nothing
  handle (\e -> do terminateProcess pid
                   throw (e::SomeException)) $ do
  outVar <- newEmptyMVar
  forkIO $ B.hGetContents outh >>= putMVar outVar
  errVar <- newEmptyMVar
  forkIO $ B.hGetContents errh >>= putMVar errVar

  when (not (B.null input)) $ do B.hPutStr inh input >> hFlush inh
  hClose inh

  out <- takeMVar outVar
  err <- takeMVar errVar
  ret <- waitForProcess pid
  return (ret, out, err)