llvm-analysis-0.3.0: src/LLVM/Analysis/Util/Testing.hs
{-# LANGUAGE ExistentialQuantification, DeriveDataTypeable #-}
-- | Various functions to help test this library and analyses based on
-- it.
--
-- The idea behind the test framework is that each 'TestDescriptor'
-- describes inputs for a test suite and automatically converts the inputs
-- to a summary value, which it compares against an expected value. It
-- reports how many such tests pass/fail.
--
-- More concretely, each test suite specifies:
--
-- * The test input files (via a shell glob)
--
-- * A function to conver a test input file name to a filename
-- containing the expected outut.
--
-- * A summary function to reduce a Module to a summary value
--
-- * A comparison function (usually an assertion from HUnit)
--
-- With these components, the framework reads each input file and
-- converts it to bitcode. It uses the summary function to reduce the
-- Module to a summary value and reads the expected output (using the
-- 'read' function). These two types (the summary and expected
-- output) must be identical. The comparison function is then
-- applied. If it throws an exception, the test is considered to have
-- failed.
--
-- NOTE 1: The result type of the summary function MUST be an instance
-- of 'Read' AND the same as the type found in the expected results
-- file.
--
-- NOTE 2: The test inputs can be C, C++, bitcode, or LLVM assembly
-- files.
module LLVM.Analysis.Util.Testing (
-- * Types
TestDescriptor(..),
BuildException(..),
-- * Actions
testAgainstExpected,
-- * Helpers
buildModule,
readInputAndExpected
) where
import Control.Exception as E
import Control.Monad ( when )
import Data.Typeable ( Typeable )
import System.Directory ( findExecutable )
import System.Environment ( getEnv )
import System.Exit ( ExitCode(ExitSuccess) )
import System.FilePath
import System.FilePath.Glob
import System.IO.Error
import System.IO.Temp
import System.Process as P
import Test.Framework ( defaultMain, Test )
import Test.Framework.Providers.HUnit
import LLVM.Analysis
data BuildException = ClangFailed FilePath ExitCode
| NoBuildMethodForInput FilePath
| OptFailed FilePath ExitCode
| NoOptBinaryFound
deriving (Typeable, Show)
instance Exception BuildException
-- | A description of a set of tests.
data TestDescriptor =
forall a. (Read a) => TestDescriptor {
testPattern :: String, -- ^ A shell glob pattern (relative to the project root) that collects all test inputs
testExpectedMapping :: FilePath -> FilePath, -- ^ A function to apply to an input file name to find the file containing its expected results
testResultBuilder :: Module -> a, -- ^ A function to turn a Module into a summary value of any type
testResultComparator :: String -> a -> a -> IO () -- ^ A function to compare two summary values (throws on failure)
}
-- | An intermediate helper to turn input files into modules and
-- return the expected output.
readInputAndExpected :: (Read a)
=> [String] -- ^ Arguments for opt
-> (FilePath -> IO Module) -- ^ A function to turn a bitcode file bytestring into a Module
-> (FilePath -> FilePath) -- ^ The function to map an input file name to the expected output file
-> FilePath -- ^ The input file
-> IO (FilePath, Module, a)
readInputAndExpected optOpts parseFile expectedFunc inputFile = do
let exFile = expectedFunc inputFile
exContent <- readFile exFile
-- use seq here to force the full evaluation of the read file.
let expected = length exContent `seq` read exContent
m <- buildModule [] optOpts parseFile inputFile
return (inputFile, m, expected)
-- | This is the main test suite entry point. It takes a bitcode
-- parser and a list of test suites.
--
-- The bitcode parser is taken as an input so that this library does
-- not have a direct dependency on any FFI code.
testAgainstExpected :: [String] -- ^ Options for opt
-> (FilePath -> IO Module) -- ^ A function to turn a bitcode file bytestring into a Module
-> [TestDescriptor] -- ^ The list of test suites to run
-> IO ()
testAgainstExpected optOpts parseFile testDescriptors = do
caseSets <- mapM mkDescriptorSet testDescriptors
defaultMain $ concat caseSets
where
mkDescriptorSet :: TestDescriptor -> IO [Test]
mkDescriptorSet TestDescriptor { testPattern = pat
, testExpectedMapping = mapping
, testResultBuilder = br
, testResultComparator = cmp
} = do
-- Glob up all of the files in the test directory with the target extension
testInputFiles <- namesMatching pat
-- Read in the expected results and corresponding modules
inputsAndExpecteds <- mapM (readInputAndExpected optOpts parseFile mapping) testInputFiles
-- Build actual test cases by applying the result builder
mapM (mkTest br cmp) inputsAndExpecteds
mkTest br cmp (file, m, expected) = do
let actual = br m
return $ testCase file $ cmp file expected actual
-- | Optimize the bitcode in the given bytestring using opt with the
-- provided options
optify :: [String] -> FilePath -> FilePath -> IO ()
optify args inp optFile = do
opt <- findOpt
let cmd = P.proc opt ("-o" : optFile : inp : args)
(_, _, _, p) <- createProcess cmd
rc <- waitForProcess p
when (rc /= ExitSuccess) $ E.throwIO $ OptFailed inp rc
-- | Given an input file, bitcode parsing function, and options to
-- pass to opt, return a Module. The input file can be C, C++, or
-- LLVM bitcode.
--
-- Note that this function returns an Either value to report some
-- kinds of errors. It can also raise IOErrors.
buildModule :: [String] -- ^ Front-end options (passed to clang) for the module.
-> [String] -- ^ Optimization options (passed to opt) for the module. opt is not run if the list is empty
-> (FilePath -> IO Module) -- ^ A function to turn a bitcode file into a Module
-> FilePath -- ^ The input file (either bitcode or C/C++)
-> IO Module
buildModule clangOpts optOpts parseFile inputFilePath = do
clang <- catchIOError (getEnv "LLVM_CLANG") (const (return "clang"))
clangxx <- catchIOError (getEnv "LLVM_CLANGXX") (const (return "clang++"))
case takeExtension inputFilePath of
".ll" -> simpleBuilder inputFilePath
".bc" -> simpleBuilder inputFilePath
".c" -> clangBuilder inputFilePath clang
".C" -> clangBuilder inputFilePath clangxx
".cxx" -> clangBuilder inputFilePath clangxx
".cpp" -> clangBuilder inputFilePath clangxx
_ -> E.throwIO $ NoBuildMethodForInput inputFilePath
where
simpleBuilder inp
| null optOpts = parseFile inp
| otherwise =
withSystemTempFile ("opt_" ++ takeFileName inp) $ \optFname _ -> do
optify optOpts inp optFname
parseFile optFname
clangBuilder inp driver =
withSystemTempFile ("base_" ++ takeFileName inp) $ \baseFname _ -> do
let cOpts = clangOpts ++ ["-emit-llvm", "-o" , baseFname, "-c", inp]
(_, _, _, p) <- createProcess $ proc driver cOpts
rc <- waitForProcess p
when (rc /= ExitSuccess) $ E.throwIO $ ClangFailed inputFilePath rc
case null optOpts of
True -> parseFile baseFname
False ->
withSystemTempFile ("opt_" ++ takeFileName inp) $ \optFname _ -> do
optify optOpts baseFname optFname
parseFile optFname
-- | Find a suitable @opt@ binary in the user's PATH
--
-- First consult the LLVM_OPT environment variable. If that is not
-- set, try a few common opt aliases.
findOpt :: IO FilePath
findOpt = do
let fbin = findBin [ "opt", "opt-3.3", "opt-3.2", "opt-3.1", "opt-3.0" ]
catchIOError (getEnv "LLVM_OPT") (const fbin)
where
findBin [] = E.throwIO NoOptBinaryFound
findBin (bin:bins) = do
b <- findExecutable bin
case b of
Just e -> return e
Nothing -> findBin bins
{-# ANN module "HLint: ignore Use if" #-}