{-# 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" #-}