aihc-cpp-1.0.0.0: test/Test/Progress.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.Progress
( CaseMeta (..),
Outcome (..),
evaluateCase,
loadManifest,
progressSummary,
)
where
import Aihc.Cpp (Config (..), Diagnostic (..), IncludeRequest (..), Result (..), Severity (..), Step (..), defaultConfig, preprocess)
import qualified Control.Exception as E
import qualified Data.ByteString as BS
import Data.Char (isDigit, isSpace)
import Data.List (dropWhileEnd)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import GHC.IO.Handle (hDuplicate, hDuplicateTo)
import Language.Preprocessor.Cpphs (BoolOptions (..), CpphsOptions (..), defaultCpphsOptions, runCpphs)
import System.Directory (doesFileExist, getTemporaryDirectory, removeFile)
import System.FilePath (takeDirectory, (</>))
import System.IO (IOMode (ReadMode), hClose, hFlush, openTempFile, stderr, withFile)
data Expected = ExpectPass | ExpectXFail deriving (Eq, Show)
data Outcome = OutcomePass | OutcomeXFail | OutcomeXPass | OutcomeFail deriving (Eq, Show)
data CaseMeta = CaseMeta
{ caseId :: !String,
caseCategory :: !String,
casePath :: !FilePath,
caseExpected :: !Expected,
caseReason :: !String
}
deriving (Eq, Show)
fixtureRoot :: FilePath
fixtureRoot = "test/Test/Fixtures/progress"
manifestPath :: FilePath
manifestPath = fixtureRoot </> "manifest.tsv"
progressSummary :: [(CaseMeta, Outcome, String)] -> (Int, Int, Int, Int)
progressSummary outcomes =
( count OutcomePass,
count OutcomeXFail,
count OutcomeXPass,
count OutcomeFail
)
where
count wanted = length [() | (_, out, _) <- outcomes, out == wanted]
evaluateCase :: CaseMeta -> IO (CaseMeta, Outcome, String)
evaluateCase meta = do
let sourcePath = fixtureRoot </> casePath meta
source <- BS.readFile sourcePath
ours <- runOurs sourcePath source
oracle <- runOracle sourcePath
let (outcome, details) = classify (caseExpected meta) ours oracle
pure (meta, outcome, details)
classify :: Expected -> Either String Text -> Either String Text -> (Outcome, String)
classify expected ours oracle =
case expected of
ExpectPass ->
case (ours, oracle) of
(Right oursOut, Right oracleOut) ->
case compareLocatedOutput oursOut oracleOut of
Nothing -> (OutcomePass, "")
Just details -> (OutcomeFail, "preprocessed output differs from cpphs oracle: " <> details)
(Left _, Left _) -> (OutcomePass, "")
(Left oursErr, _) -> (OutcomeFail, "ours failed: " <> oursErr)
(_, Left oracleErr) -> (OutcomeFail, "oracle failed: " <> oracleErr)
ExpectXFail ->
case (ours, oracle) of
(Right oursOut, Right oracleOut) ->
case compareLocatedOutput oursOut oracleOut of
Nothing -> (OutcomeXPass, "expected xfail but now matches cpphs")
Just _ -> (OutcomeXFail, "")
(Left _, Left _) -> (OutcomeXPass, "expected xfail but now matches cpphs")
_ -> (OutcomeXFail, "")
data LocatedLine = LocatedLine
{ locatedLineNo :: !Int,
locatedFile :: !FilePath,
locatedText :: !Text
}
deriving (Eq, Show)
compareLocatedOutput :: Text -> Text -> Maybe String
compareLocatedOutput oursOut oracleOut =
firstDifference (toLocatedLines oursOut) (toLocatedLines oracleOut)
firstDifference :: [LocatedLine] -> [LocatedLine] -> Maybe String
firstDifference = go (1 :: Int)
where
go :: Int -> [LocatedLine] -> [LocatedLine] -> Maybe String
go _ [] [] = Nothing
go n (o : os) (r : rs)
| o == r = go (n + 1) os rs
| otherwise =
Just
( "first mismatch at output record "
<> show n
<> ": ours="
<> showLocated o
<> ", oracle="
<> showLocated r
)
go n [] (r : _) =
Just
( "ours ended early at output record "
<> show n
<> ", oracle has "
<> showLocated r
)
go n (o : _) [] =
Just
( "oracle ended early at output record "
<> show n
<> ", ours has "
<> showLocated o
)
showLocated :: LocatedLine -> String
showLocated (LocatedLine ln fp txt) =
"(" <> show ln <> ", " <> show fp <> ", " <> show (T.unpack txt) <> ")"
toLocatedLines :: Text -> [LocatedLine]
toLocatedLines = go 1 "<unknown>" . T.lines
where
go _ _ [] = []
go lineNo filePath (line : rest) =
case parseLinePragma line of
Just (nextLineNo, mFilePath) ->
let filePath' = fromMaybe filePath mFilePath
in go nextLineNo filePath' rest
Nothing ->
LocatedLine lineNo filePath line : go (lineNo + 1) filePath rest
parseLinePragma :: Text -> Maybe (Int, Maybe FilePath)
parseLinePragma raw =
let line = T.strip raw
in case T.stripPrefix "#line" line of
Nothing -> Nothing
Just rest0 ->
let rest1 = T.stripStart rest0
(lineNoTxt, rest2) = T.span isDigit rest1
in if T.null lineNoTxt
then Nothing
else case reads (T.unpack lineNoTxt) of
[(lineNo, "")] ->
let rest3 = T.stripStart rest2
in if T.null rest3
then Just (lineNo, Nothing)
else case T.uncons rest3 of
Just ('"', quoted) ->
let (filePath, suffix) = T.breakOn "\"" quoted
in if T.null suffix
then Nothing
else Just (lineNo, Just (T.unpack filePath))
_ -> Nothing
_ -> Nothing
runOurs :: FilePath -> BS.ByteString -> IO (Either String Text)
runOurs sourcePath source = do
result <- drive (preprocess defaultConfig {configInputFile = sourcePath} source)
let errors = [diagMessage d | d <- resultDiagnostics result, diagSeverity d == Error]
case errors of
[] -> pure (Right (resultOutput result))
(msg : _) -> pure (Left (T.unpack msg))
where
drive (Done result) = pure result
drive (NeedInclude req k) = do
let includeAbsPath = resolveIncludePath sourcePath req
exists <- doesFileExist includeAbsPath
content <- if exists then Just <$> BS.readFile includeAbsPath else pure Nothing
drive (k content)
resolveIncludePath :: FilePath -> IncludeRequest -> FilePath
resolveIncludePath rootPath req =
includeBaseDir </> includePath req
where
includeFromDir = takeDirectory (includeFrom req)
includeBaseDir =
if null includeFromDir
then takeDirectory rootPath
else includeFromDir
runOracle :: FilePath -> IO (Either String Text)
runOracle sourcePath = do
source <- TIO.readFile sourcePath
let cpphsOptions =
defaultCpphsOptions
{ boolopts =
(boolopts defaultCpphsOptions)
{ stripC89 = True,
warnings = False
}
}
(oracleOut, capturedStderr) <-
captureStderrText
( ( E.try $ do
out <- runCpphs cpphsOptions sourcePath (T.unpack source)
_ <- E.evaluate (length out)
pure out
) ::
IO (Either E.SomeException String)
)
pure $
case oracleOut of
Right out -> Right (T.pack out)
Left err ->
Left
( "cpphs failed: "
<> show err
<> stderrSuffix capturedStderr
)
captureStderrText :: IO a -> IO (a, Text)
captureStderrText action = do
tempDir <- getTemporaryDirectory
(tempPath, tempHandle) <- openTempFile tempDir "cpphs-stderr.txt"
originalStderr <- hDuplicate stderr
let restoreStderr = hFlush stderr >> hDuplicateTo originalStderr stderr
cleanup = do
hClose tempHandle
hClose originalStderr
removeFile tempPath
E.bracketOnError
(pure ())
(const (restoreStderr >> cleanup))
$ \() -> do
hFlush stderr
hDuplicateTo tempHandle stderr
result <- action `E.finally` restoreStderr
hClose tempHandle
captured <- withFile tempPath ReadMode TIO.hGetContents
hClose originalStderr
removeFile tempPath
pure (result, captured)
stderrSuffix :: Text -> String
stderrSuffix captured
| T.null stripped = ""
| otherwise = " stderr=" <> T.unpack stripped
where
stripped = T.strip captured
loadManifest :: IO [CaseMeta]
loadManifest = do
raw <- TIO.readFile manifestPath
let rows = filter (not . T.null) (map stripComment (T.lines raw))
mapM parseRow rows
stripComment :: Text -> Text
stripComment line =
let core = fst (T.breakOn "#" line)
in T.strip core
parseRow :: Text -> IO CaseMeta
parseRow row =
case T.splitOn "\t" row of
[cid, cat, pathTxt, expectedTxt] -> parseRowWithReason cid cat pathTxt expectedTxt ""
[cid, cat, pathTxt, expectedTxt, reasonTxt] -> parseRowWithReason cid cat pathTxt expectedTxt reasonTxt
_ -> fail ("Invalid manifest row (expected 4 or 5 tab-separated columns): " <> T.unpack row)
parseRowWithReason :: Text -> Text -> Text -> Text -> Text -> IO CaseMeta
parseRowWithReason cid cat pathTxt expectedTxt reasonTxt = do
let path = T.unpack pathTxt
exists <- doesFileExist (fixtureRoot </> path)
if not exists
then fail ("Manifest references missing case file: " <> path)
else do
expected <-
case expectedTxt of
"pass" -> pure ExpectPass
"xfail" -> pure ExpectXFail
_ -> fail ("Unknown expected value in manifest: " <> T.unpack expectedTxt)
let reason = trim (T.unpack reasonTxt)
case expected of
ExpectXFail | null reason -> fail ("xfail case requires reason: " <> T.unpack cid)
_ -> pure ()
pure
CaseMeta
{ caseId = T.unpack cid,
caseCategory = T.unpack cat,
casePath = path,
caseExpected = expected,
caseReason = reason
}
trim :: String -> String
trim = dropWhile isSpace . dropWhileEnd isSpace