{-# LANGUAGE FlexibleContexts #-}
{-# OPTIONS #-}
module Language.C.Test.ParseTests (
time, lineCount, withFileExt,
runCPP,
parseTestTemplate, runParseTest,
ppTestTemplate, runPrettyPrint,
equivTestTemplate,runEquivTest,
compileTestTemplate, runCompileTest,
) where
import Control.Exception
import Control.Monad.State
import Data.List
import System.Process
import System.Directory
import System.Exit
import System.FilePath (takeBaseName, takeExtension)
import System.IO
import System.Process
import Language.C
import Language.C.Test.Environment
import Language.C.Test.Framework
import Language.C.Test.GenericAST
import Language.C.Test.TestMonad
lineCount :: FilePath -> IO Int
lineCount = liftM (length . lines) . readFile
withFileExt :: FilePath -> String -> FilePath
withFileExt filename ext = (stripExt filename) ++ "." ++ ext where
stripExt fn =
let basefn = takeBaseName fn in
case (dropWhile (/= '.') . reverse) basefn of
('.' : s : ss) -> reverse (s : ss)
_ -> basefn
runCPP :: FilePath -> [String] -> TestMonad (FilePath,FilePath)
runCPP origFile cppArgs = do
cFile <- withTempFile_ (takeExtension origFile) $ \_ -> return ()
copySuccess <- liftIOCatched (copyFile origFile cFile)
case copySuccess of
Left err -> errorOnInit cppArgs $ "Copy failed: " ++ show err
Right () -> dbgMsg $ "Copy: " ++ origFile ++ " ==> " ++ cFile ++ "\n"
preFile <- case isPreprocessedFile cFile of
False -> do
dbgMsg $ "Preprocessing " ++ origFile ++ "\n"
preFile <- withTempFile_ ".i" $ \_hnd -> return ()
(preErrReport, gccExitcode) <- withTempFile ".cpp_report" $ \errReportHnd -> do
liftIO $ runProcess "gcc" (["-E", "-o", preFile] ++ cppArgs ++ [origFile])
Nothing Nothing
Nothing Nothing (Just errReportHnd)
>>= waitForProcess
case gccExitcode of
ExitSuccess -> do
modify $ addTmpFile preFile
return preFile
ExitFailure fCode -> do
modify $ addTmpFile preErrReport
errReport <- liftIO $ readFile preErrReport
errorOnInit cppArgs $ "C preprocessor failed: " ++ "`gcc -E -o " ++ preFile ++ " " ++ (unwords cppArgs) ++ " " ++ origFile ++
"' returned exit code `" ++ show fCode ++ "'\n" ++
"Output from stderr: " ++ errReport
True -> return cFile
return (cFile,preFile)
parseTestTemplate :: Test
parseTestTemplate = Test
{
testName = "parse",
testDescr = "parse the given preprocessed c file",
preferredScale = Kilo,
inputUnit = linesOfCode
}
runParseTest :: FilePath -> Position -> TestMonad (Either (String,FilePath) (CTranslUnit,PerfMeasure)) runParseTest preFile initialPos = do
dbgMsg $ "Starting Parse of " ++ preFile ++ "\n"
((parse,input),elapsed) <-
time $ do input <- liftIO$ readInputStream preFile
parse <- parseEval input initialPos
return (parse,input)
dbgMsg $ "Parse result : " ++ eitherStatus parse ++ "\n"
case parse of
Left err@(ParseError (errMsgs, pos)) -> do
report <- reportParseError err (inputStreamToString input)
return $ Left $ (unlines (("Parse error in " ++ show pos) : errMsgs), report)
Right header ->
return $ Right $ (header,PerfMeasure (fromIntegral $ countLines input,elapsed))
reportParseError :: ParseError -> String -> TestMonad FilePath
reportParseError (ParseError (errMsgs,pos)) input = do
withTempFile_ ".report" $ \hnd -> liftIO $ do
pwd <- getCurrentDirectory
contextMsg <- getContextInfo pos
hPutStr hnd $ "Failed to parse " ++ (posFile pos)
++ "\nwith message:\n" ++ concat errMsgs ++ " " ++ show pos
++ "\n" ++ contextMsg
++ "\nWorking dir: " ++ pwd
++ "\nPreprocessed input follows:\n\n" ++ input
ppTestTemplate :: Test
ppTestTemplate = Test
{
testName = "pretty-print",
testDescr = "pretty-print the given AST",
preferredScale = Kilo,
inputUnit = linesOfCode
}
runPrettyPrint :: CTranslUnit -> TestMonad ((FilePath, FilePath), PerfMeasure)
runPrettyPrint ast = do
dbgMsg "Pretty Print ..."
(fullExport,t) <-
time $
withTempFile_ "pp.c" $ \hnd ->
liftIO $ hPutStrLn hnd $ show (pretty ast)
modify $ addTmpFile fullExport
locs <- liftIO $ lineCount fullExport
dbgMsg $ " to " ++ fullExport ++ " (" ++ show locs ++ " lines)"++ "\n"
dbgMsg $ "Pretty Print [report] ... "
smallExport <- withTempFile_ "ppr.c" $ \hnd ->
liftIO $ hPutStrLn hnd $ show (prettyUsingInclude ast)
dbgMsg $ "to " ++ smallExport ++ "\n"
lc <- liftIO $ lineCount fullExport
return ((fullExport,smallExport), PerfMeasure (fromIntegral lc,t))
equivTestTemplate :: Test
equivTestTemplate = Test
{
testName = "equivalence check",
testDescr = "check if two ASTs are equivalent",
preferredScale = Unit,
inputUnit = topLevelDeclarations
}
runEquivTest :: CTranslUnit -> CTranslUnit -> TestMonad (Either (String, Maybe FilePath) PerfMeasure)
runEquivTest (CTranslUnit decls1 _) (CTranslUnit decls2 _) = do
dbgMsg $ "Check AST equivalence\n"
(result,t) <- time $ do
let ast1 = map (toGenericAST . normalizeAST) decls1
let ast2 = map (toGenericAST . normalizeAST) decls2
if (length ast1 /= length ast2)
then
return $ Left ("Length mismatch: " ++ show (length ast1) ++ " vs. " ++ show (length ast2), Nothing)
else
case find (\(_, (d1,d2)) -> d1 /= d2) (zip [0..] (zip ast1 ast2)) of
Just (ix, (decl1,decl2)) -> do
declf1 <- withTempFile_ ".1.ast" $ \hnd -> liftIO $ hPutStrLn hnd (show $ pretty decl1)
declf2 <- withTempFile_ ".2.ast" $ \hnd -> liftIO $ hPutStrLn hnd (show $ pretty decl2)
modify $ (addTmpFile declf1 . addTmpFile declf2)
diff <- withTempFile_ ".ast_diff" $ \_hnd -> return ()
decl1Src <- liftIO $ getDeclSrc decls1 ix
decl2Src <- liftIO $ getDeclSrc decls2 ix
liftIO $ do
appendFile diff ("Original declaration: \n" ++ decl1Src ++ "\n")
appendFile diff ("Pretty printed declaration: \n" ++ decl2Src ++ "\n")
_ <- system $ "diff -u '" ++ declf1 ++ "' '" ++ declf2 ++ "' >> '" ++ diff ++ "'" return ()
return $ Left ("Declarations do not match: ", Just diff)
Nothing -> return $ Right (length ast1)
return $ either Left (\decls -> Right $ PerfMeasure (fromIntegral decls, t)) result
getDeclSrc :: [CExtDecl] -> Int -> IO String
getDeclSrc decls ix = case drop ix decls of
[] -> error "getDeclSrc : Bad ix"
[decl] -> readFilePos (posOf decl) Nothing
(decl:(declNext:_)) | fileOf decl /= fileOf declNext -> readFilePos (posOf decl) Nothing
| otherwise -> readFilePos (posOf decl) (Just (posRow $ posOf declNext))
where
fileOf = posFile . posOf
readFilePos pos mLineNext = do
let lnStart = posRow pos - 1
lns <- liftM (drop lnStart . lines) $ readFile (posFile pos)
(return.unlines) $
case mLineNext of
Nothing -> lns
(Just lineNext) -> take (lineNext - lnStart - 1) lns
compileTestTemplate :: Test
compileTestTemplate = Test
{
testName = "compile",
testDescr = "compile the given file using gcc",
preferredScale = Kilo,
inputUnit = linesOfCode
}
runCompileTest :: [String] -> FilePath -> TestMonad (Either (String, FilePath) (FilePath, PerfMeasure))
runCompileTest args file = do
dbgMsg "Compile ..."
outFile <- withTempFile_ ".o" $ \_hnd -> return ()
(gccErrReport,(exitCode,t)) <- withTempFile ".cc_report" $ \errReportHnd -> do
time $ liftIO $ runProcess "gcc" (["-o", outFile,"-std=gnu99"] ++ args ++ [file])
Nothing Nothing
Nothing Nothing (Just errReportHnd)
>>= waitForProcess
case exitCode of
ExitSuccess -> do
liftIO $ removeFile gccErrReport
locs <- liftIO $ lineCount file
return . Right $ (outFile, PerfMeasure (fromIntegral locs, t))
ExitFailure fCode -> do
return . Left $ ("gcc "++concat (intersperse " " args)++" "++file++ " failed with exit code "++show fCode,
gccErrReport)
parseEval :: InputStream -> Position -> TestMonad (Either ParseError CTranslUnit)
parseEval input initialPos =
case parseC input initialPos of
Left err -> return $ Left err
Right ok -> return $ Right ok
eitherStatus :: Either a b -> String
eitherStatus = either (const "ERROR") (const "ok")
getContextInfo :: Position -> IO String
getContextInfo pos = do
content <- readFile (posFile pos) `catch` handleIOError
return $
case splitAt (posRow pos - 1) (lines content) of
([],[]) -> "/* No Input */"
([],ctxLine : post) -> showContext [] ctxLine (take 1 post)
(pre,[]) -> showContext [last pre] "/* End Of File */" []
(pre,ctxLine : post) -> showContext [last pre] ctxLine (take 1 post)
where
handleIOError :: IOException -> IO String
handleIOError _ = return ""
showContext preCtx ctx postCtx = unlines $ preCtx ++ [ctx, replicate (posColumn pos - 1) ' ' ++ "^^^"] ++ postCtx