mirror of
https://github.com/commercialhaskell/stackage.git
synced 2026-01-12 23:38:29 +01:00
290 lines
12 KiB
Haskell
290 lines
12 KiB
Haskell
{-# LANGUAGE DeriveDataTypeable #-}
|
|
{-# LANGUAGE RecordWildCards #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
module Stackage.Test
|
|
( runTestSuites
|
|
) where
|
|
|
|
import qualified Control.Concurrent as C
|
|
import Control.Exception (Exception, SomeException, handle, throwIO, IOException, try)
|
|
import Control.Monad (replicateM, unless, when, forM_)
|
|
import qualified Data.Map as Map
|
|
import qualified Data.Set as Set
|
|
import qualified Control.Monad.Trans.Writer as W
|
|
import Distribution.Package (Dependency (Dependency))
|
|
import Data.Version (parseVersion, Version (Version))
|
|
import Data.Typeable (Typeable)
|
|
import Stackage.Types
|
|
import Stackage.Util
|
|
import System.Directory (copyFile, createDirectory,
|
|
createDirectoryIfMissing, doesFileExist, findExecutable,
|
|
getDirectoryContents, removeFile,
|
|
renameDirectory, canonicalizePath)
|
|
import System.Exit (ExitCode (ExitSuccess))
|
|
import System.FilePath ((<.>), (</>), takeDirectory)
|
|
import System.IO (IOMode (WriteMode, AppendMode),
|
|
withBinaryFile)
|
|
import System.Process (readProcess, runProcess, waitForProcess, createProcess, proc, cwd)
|
|
import Text.ParserCombinators.ReadP (readP_to_S)
|
|
import Data.IORef (IORef, readIORef, atomicModifyIORef, newIORef)
|
|
import qualified Data.ByteString.Lazy as L
|
|
import qualified Data.ByteString.Lazy.Char8 as L8
|
|
import qualified Distribution.PackageDescription as PD
|
|
import Distribution.PackageDescription.Parse (ParseResult (ParseOk),
|
|
parsePackageDescription)
|
|
|
|
runTestSuites :: BuildSettings -> BuildPlan -> IO ()
|
|
runTestSuites settings' bp = do
|
|
settings <- fixBuildSettings settings'
|
|
let selected' = Map.filterWithKey notSkipped $ bpPackages bp
|
|
let testdir = "runtests"
|
|
docdir = "haddock"
|
|
rm_r testdir
|
|
rm_r docdir
|
|
createDirectory testdir
|
|
createDirectory docdir
|
|
|
|
putStrLn "Determining package dependencies"
|
|
selected <- mapM (addDependencies settings (Map.keysSet selected') testdir)
|
|
$ Map.toList selected'
|
|
putStrLn "Running test suites"
|
|
|
|
copyBuiltInHaddocks docdir
|
|
|
|
cabalVersion <- getCabalVersion
|
|
haddockFilesRef <- newIORef []
|
|
allPass <- parFoldM
|
|
(testWorkerThreads settings)
|
|
(runTestSuite cabalVersion settings testdir docdir bp haddockFilesRef)
|
|
(&&)
|
|
True
|
|
selected
|
|
unless allPass $ error $ "There were failures, please see the logs in " ++ testdir
|
|
where
|
|
notSkipped p _ = p `Set.notMember` bpSkippedTests bp
|
|
|
|
addDependencies :: BuildSettings
|
|
-> Set PackageName -- ^ all packages to be installed
|
|
-> FilePath -- ^ testdir
|
|
-> (PackageName, SelectedPackageInfo)
|
|
-> IO (PackageName, Set PackageName, SelectedPackageInfo)
|
|
addDependencies settings allPackages testdir (packageName, spi) = do
|
|
package' <- replaceTarball (tarballDir settings) package
|
|
deps <- handle (\e -> print (e :: IOException) >> return Set.empty)
|
|
$ getDeps allPackages testdir packageName package package'
|
|
return (packageName, deps, spi)
|
|
where
|
|
package = packageVersionString (packageName, spiVersion spi)
|
|
|
|
getDeps :: Set PackageName -- ^ all packages to be installed
|
|
-> FilePath -> PackageName -> String -> String -> IO (Set PackageName)
|
|
getDeps allPackages testdir (PackageName name) nameVer loc = do
|
|
(Nothing, Nothing, Nothing, ph) <- createProcess
|
|
(proc "cabal" ["unpack", loc, "--verbose=0"]) { cwd = Just testdir }
|
|
ec <- waitForProcess ph
|
|
unless (ec == ExitSuccess) $ error $ "Unable to unpack: " ++ loc
|
|
lbs <- L.readFile $ testdir </> nameVer </> name <.> "cabal"
|
|
case parsePackageDescription $ L8.unpack lbs of
|
|
ParseOk _ gpd -> return $ Set.intersection allPackages $ allLibraryDeps gpd
|
|
_ -> return Set.empty
|
|
|
|
allLibraryDeps :: PD.GenericPackageDescription -> Set PackageName
|
|
allLibraryDeps =
|
|
maybe Set.empty (W.execWriter . goTree) . PD.condLibrary
|
|
where
|
|
goTree tree = do
|
|
mapM_ goDep $ PD.condTreeConstraints tree
|
|
forM_ (PD.condTreeComponents tree) $ \(_, y, z) -> do
|
|
goTree y
|
|
maybe (return ()) goTree z
|
|
|
|
goDep (Dependency pn _) = W.tell $ Set.singleton pn
|
|
|
|
getCabalVersion :: IO CabalVersion
|
|
getCabalVersion = do
|
|
output <- readProcess "cabal" ["--numeric-version"] ""
|
|
case filter (null . snd) $ readP_to_S parseVersion $ filter notCRLF output of
|
|
(Version (x:y:_) _, _):_ -> return $ CabalVersion x y
|
|
_ -> error $ "Invalid cabal version: " ++ show output
|
|
where
|
|
notCRLF '\n' = False
|
|
notCRLF '\r' = False
|
|
notCRLF _ = True
|
|
|
|
parFoldM :: Int -- ^ number of threads
|
|
-> ((PackageName, payload) -> IO c)
|
|
-> (a -> c -> a)
|
|
-> a
|
|
-> [(PackageName, Set PackageName, payload)]
|
|
-> IO a
|
|
parFoldM threadCount0 f g a0 bs0 = do
|
|
ma <- C.newMVar a0
|
|
mbs <- C.newMVar bs0
|
|
signal <- C.newEmptyMVar
|
|
completed <- newIORef Set.empty
|
|
tids <- replicateM threadCount0 $ C.forkIO $ worker completed ma mbs signal
|
|
wait threadCount0 signal tids
|
|
|
|
unrun <- C.takeMVar mbs
|
|
when (not $ null unrun) $
|
|
error $ "The following tests were not run: " ++ unwords
|
|
[x | (PackageName x, _, _) <- unrun]
|
|
C.takeMVar ma
|
|
where
|
|
worker completedRef ma mbs signal =
|
|
handle
|
|
(C.putMVar signal . Just)
|
|
(loop >> C.putMVar signal Nothing)
|
|
where
|
|
loop = do
|
|
mb <- C.modifyMVar mbs $ \bs -> do
|
|
completed <- readIORef completedRef
|
|
return $ case findReady completed bs of
|
|
-- There's a workload ready with no deps
|
|
Just (b, bs') -> (bs', Just b)
|
|
-- No workload with no deps
|
|
Nothing -> (bs, Nothing)
|
|
case mb of
|
|
Nothing -> return ()
|
|
Just (name, _, payload) -> do
|
|
c <- f (name, payload)
|
|
C.modifyMVar_ ma $ \a -> return $! g a c
|
|
atomicModifyIORef completedRef $ \s -> (Set.insert name s, ())
|
|
loop
|
|
wait threadCount signal tids
|
|
| threadCount == 0 = return ()
|
|
| otherwise = do
|
|
me <- C.takeMVar signal
|
|
case me of
|
|
Nothing -> wait (threadCount - 1) signal tids
|
|
Just e -> do
|
|
mapM_ C.killThread tids
|
|
throwIO (e :: SomeException)
|
|
|
|
-- | Find a workload whose dependencies have been met.
|
|
findReady :: Ord key
|
|
=> Set key -- ^ workloads already complete
|
|
-> [(key, Set key, value)]
|
|
-> Maybe ((key, Set key, value), [(key, Set key, value)])
|
|
findReady completed =
|
|
loop id
|
|
where
|
|
loop _ [] = Nothing
|
|
loop front (x@(_, deps, _):xs)
|
|
| Set.null $ Set.difference deps completed = Just (x, front xs)
|
|
| otherwise = loop (front . (x:)) xs
|
|
|
|
data TestException = TestException
|
|
deriving (Show, Typeable)
|
|
instance Exception TestException
|
|
|
|
data CabalVersion = CabalVersion Int Int
|
|
deriving (Eq, Ord, Show)
|
|
|
|
runTestSuite :: CabalVersion
|
|
-> BuildSettings
|
|
-> FilePath -- ^ testdir
|
|
-> FilePath -- ^ docdir
|
|
-> BuildPlan
|
|
-> IORef [(String, FilePath)] -- ^ .haddock files
|
|
-> (PackageName, SelectedPackageInfo)
|
|
-> IO Bool
|
|
runTestSuite cabalVersion settings testdir docdir
|
|
bp haddockFilesRef (packageName, SelectedPackageInfo {..}) = do
|
|
-- Set up a new environment that includes the sandboxed bin folder in PATH.
|
|
env' <- getModifiedEnv settings
|
|
let menv = Just $ addSandbox env'
|
|
addSandbox = (("HASKELL_PACKAGE_SANDBOX", packageDir settings):)
|
|
|
|
let run cmd args wdir handle' = do
|
|
ph <- runProcess cmd args (Just wdir) menv Nothing (Just handle') (Just handle')
|
|
ec <- waitForProcess ph
|
|
unless (ec == ExitSuccess) $ throwIO TestException
|
|
|
|
passed <- handle (\TestException -> return False) $ do
|
|
case cabalFileDir settings of
|
|
Nothing -> return ()
|
|
Just cfd -> do
|
|
let PackageName name = packageName
|
|
basename = name ++ ".cabal"
|
|
src = dir </> basename
|
|
dst = cfd </> basename
|
|
createDirectoryIfMissing True cfd
|
|
copyFile src dst
|
|
getHandle WriteMode $ run "cabal" (addCabalArgs settings BSTest ["configure", "--enable-tests"]) dir
|
|
|
|
-- Try building docs first in case tests have an expected failure.
|
|
when (buildDocs settings) $ do
|
|
hfs <- readIORef haddockFilesRef
|
|
let hfsOpts = flip map hfs $ \(pkgVer, hf) -> concat
|
|
[ "--haddock-options=--read-interface="
|
|
, "../"
|
|
, pkgVer
|
|
, "/,"
|
|
, hf
|
|
]
|
|
getHandle AppendMode $ run "cabal"
|
|
( "haddock"
|
|
: "--hyperlink-source"
|
|
: "--html"
|
|
: "--hoogle"
|
|
: "--html-location=../$pkg-$version/"
|
|
: hfsOpts) dir
|
|
let PackageName packageName' = packageName
|
|
handle (\(_ :: IOException) -> return ()) $ renameDirectory
|
|
(dir </> "dist" </> "doc" </> "html" </> packageName')
|
|
(docdir </> package)
|
|
|
|
enewPath <- try $ canonicalizePath $ docdir </> package </> packageName' <.> "haddock"
|
|
case enewPath :: Either IOException FilePath of
|
|
Left _ -> return () -- print e
|
|
Right newPath -> atomicModifyIORef haddockFilesRef $ \hfs'
|
|
-> ((package, newPath) : hfs', ())
|
|
|
|
when spiHasTests $ do
|
|
getHandle AppendMode $ run "cabal" ["build"] dir
|
|
getHandle AppendMode $ run "cabal" (concat
|
|
[ ["test"]
|
|
, if cabalVersion >= CabalVersion 1 20
|
|
then ["--show-details=streaming"] -- FIXME temporary workaround for https://github.com/haskell/cabal/issues/1810
|
|
else []
|
|
]) dir
|
|
return True
|
|
let expectedFailure = packageName `Set.member` bpExpectedFailures bp
|
|
if passed
|
|
then do
|
|
removeFile logfile
|
|
when expectedFailure $ putStrLn $ " " ++ package ++ " passed, but I didn't think it would."
|
|
else unless expectedFailure $ putStrLn $ concat
|
|
[ "Test suite failed: "
|
|
, package
|
|
, "("
|
|
, unMaintainer spiMaintainer
|
|
, githubMentions spiGithubUser
|
|
, ")"
|
|
]
|
|
rm_r dir
|
|
return $! passed || expectedFailure
|
|
where
|
|
logfile = testdir </> package <.> "log"
|
|
dir = testdir </> package
|
|
getHandle mode = withBinaryFile logfile mode
|
|
package = packageVersionString (packageName, spiVersion)
|
|
|
|
copyBuiltInHaddocks docdir = do
|
|
Just ghc <- findExecutable "ghc"
|
|
copyTree (takeDirectory ghc </> "../share/doc/ghc/html/libraries") docdir
|
|
where
|
|
copyTree src dest = do
|
|
entries <- fmap (filter (\s -> s /= "." && s /= ".."))
|
|
$ getDirectoryContents src
|
|
forM_ entries $ \entry -> do
|
|
let src' = src </> entry
|
|
dest' = dest </> entry
|
|
isFile <- doesFileExist src'
|
|
if isFile
|
|
then copyFile src' dest'
|
|
else do
|
|
createDirectory dest'
|
|
copyTree src' dest'
|