packages feed

crucible-llvm-0.10: test/TestBehavior.hs

-- | See @test/behavior/README.md@

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}

module TestBehavior (behaviorTests) where

import           Control.Monad ( void, when, unless )
import qualified Data.ByteString.Char8 as BS
import qualified Data.List as List
import           Data.Parameterized.Context ( pattern Empty, pattern (:>) )
import qualified Data.Parameterized.Map as MapF
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
import           Data.Time.Clock ( NominalDiffTime )
import qualified Data.Vector as V
import           Lens.Micro ((^.))
import qualified Oughta
import           System.Directory ( listDirectory )
import           System.Exit ( ExitCode(..) )
import           System.FilePath ( (-<.>), takeFileName, takeExtension, (</>) )
import qualified System.IO as IO
import qualified System.Process as Proc

import qualified Test.Tasty as T
import           Test.Tasty.HUnit ( testCase, (@=?) )

-- LLVM parsing
import qualified Text.LLVM.AST as L
import           Data.LLVM.BitCode ( parseBitCodeFromFileWithWarnings )

-- Crucible
import           Lang.Crucible.Backend ( backendGetSym, getProofObligations )
import qualified Lang.Crucible.Backend as CB
import qualified Lang.Crucible.Simulator as CS
import           Lang.Crucible.Simulator ( timeoutFeature, genericToExecutionFeature )
import           Lang.Crucible.Simulator.ExecutionTree ( ExecResult(..) )
import qualified Lang.Crucible.CFG.Core as CC

-- LLVM
import           Lang.Crucible.LLVM ( registerLazyModule )
import qualified Lang.Crucible.LLVM as CL
import qualified Lang.Crucible.LLVM.Globals as LLVMG
import qualified Lang.Crucible.LLVM.Intrinsics as Intrinsics
import qualified Lang.Crucible.LLVM.MemModel as LLVMMem
import qualified Lang.Crucible.LLVM.SymIO as SymIO
import qualified Lang.Crucible.LLVM.Translation as Trans
import           What4.Interface ( bvOne )

-- Reuse from existing tests
import           MemSetup ( withTranslatedModule )


behaviorTests :: IO T.TestTree
behaviorTests = do
  let behaviorDir = "test/behavior"
  files <- listDirectory behaviorDir
  let cFiles = List.sort $ filter (\f -> takeExtension f == ".c") files
  let testTrees = map (\f -> testBehaviorFile (behaviorDir </> f)) cFiles

  let gccDir = "test/behavior/gcc-c-torture"
  extFiles <- listDirectory gccDir
  let cExtFiles = List.sort $ filter (\f -> takeExtension f == ".c") extFiles
  let gccTests = map (\f -> testBehaviorFileExternal (gccDir </> f)) cExtFiles

  return $ T.testGroup "Behavior tests"
    [ T.testGroup "Manual tests" testTrees
    , T.testGroup "GCC tests" gccTests
    ]


testBehaviorFile :: FilePath -> T.TestTree
testBehaviorFile cFile = testCase (takeFileName cFile) $ do
  (outputLuaProg, llvmLuaProg) <- extractLuaProgs cFile

  exePath <- compileExe cFile
  bcPath <- cimpileBc cFile

  nativeOut <- runExe exePath
  symbolicOut <- runBc bcPath

  nativeOut @=? symbolicOut
  Oughta.check' Oughta.defaultHooks outputLuaProg (Oughta.Output $ BS.pack nativeOut)

  llPath <- compileLl cFile
  llvmIr <- BS.readFile llPath
  Oughta.check' Oughta.defaultHooks llvmLuaProg (Oughta.Output llvmIr)


-- | Test external test files that don't have expected output comments
-- Just verify that both native and symbolic execution succeed
-- These tests use abort() on failure rather than output checks
testBehaviorFileExternal :: FilePath -> T.TestTree
testBehaviorFileExternal cFile = testCase (takeFileName cFile) $ do
  exePath <- compileExe cFile
  bcPath <- cimpileBc cFile
  nativeOut <- runExe exePath
  symbolicOut <- runBc bcPath
  nativeOut @=? symbolicOut

cflags :: [String]
cflags = ["-O1", "-Wno-implicit-function-declaration", "-Wno-implicit-int"]

compileFile :: String -> FilePath -> [String] -> IO FilePath
compileFile outputExt cFile additionalArgs = do
  let outPath = cFile -<.> outputExt
  let args = cflags ++ additionalArgs ++ ["-o", outPath, cFile]
  (exitCode, stdout, stderr) <- Proc.readProcessWithExitCode "clang" args ""
  when (exitCode /= ExitSuccess) $
    fail $ unlines $
      [ "Compilation failed!"
      , "clang " ++ unwords args
      , "stdout:"
      , stdout
      , "stderr:"
      , stderr
      ]
  return outPath

compileExe :: FilePath -> IO FilePath
compileExe cFile =
  compileFile ".exe" cFile ["-fsanitize=undefined"]

cimpileBc :: FilePath -> IO FilePath
cimpileBc cFile =
  compileFile ".bc" cFile ["-emit-llvm", "-fno-discard-value-names", "-c"]

compileLl :: FilePath -> IO FilePath
compileLl cFile =
  compileFile ".ll" cFile ["-emit-llvm", "-fno-discard-value-names", "-S"]

runExe :: FilePath -> IO String
runExe exePath = do
  (exitCode, stdout, stderr) <- Proc.readProcessWithExitCode exePath [] ""
  when (exitCode /= ExitSuccess) $
    fail $ unlines $
      [ "Native execution failed!"
      , exePath
      , "stdout:"
      , stdout
      , "stderr:"
      , stderr
      ]
  return stdout

-- | @(outputLuaProg, llvmLuaProg)@
extractLuaProgs :: FilePath -> IO (Oughta.LuaProgram, Oughta.LuaProgram)
extractLuaProgs cFile = do
  content <- TextIO.readFile cFile
  let contentLines = lines (Text.unpack content)
      hasOutputChecks = any ("/// " `List.isInfixOf`) contentLines
      hasLlvmChecks = any ("//- " `List.isInfixOf`) contentLines

  unless hasOutputChecks $
    fail $ "Test file " ++ cFile ++ " must have output checks (/// comments)"
  unless hasLlvmChecks $
    fail $ "Test file " ++ cFile ++ " must have LLVM IR checks (//- comments)"

  let outputLuaProg = Oughta.fromLineComments cFile "/// " content
      llvmLuaProg = Oughta.fromLineComments cFile "//- " content

  return (outputLuaProg, llvmLuaProg)


ppAbortedResult :: CS.AbortedResult sym ext -> String
ppAbortedResult ar = case ar of
  CS.AbortedExec reason _ -> show (CB.ppAbortExecReason reason)
  CS.AbortedExit code _ -> "exit " ++ show code
  CS.AbortedBranch _ _ res1 res2 ->
    unlines
    [ "branch:"
    , ppAbortedResult res1
    , ppAbortedResult res2
    ]

parseBc :: FilePath -> IO L.Module
parseBc file =
  parseBitCodeFromFileWithWarnings file >>= \case
    Left err -> fail $ "Couldn't parse LLVM bitcode from file " ++ file ++ "\n" ++ show err
    Right (m, _warnings) -> return m


-- | Symbolically execute an LLVM bitcode file and capture stdout
runBc :: FilePath -> IO String
runBc bcPath = do
  llvmMod <- parseBc bcPath

  let outPath = bcPath -<.> ".out"
  outHandle <- IO.openFile outPath IO.WriteMode
  IO.hSetBuffering outHandle IO.LineBuffering

  withTranslatedModule @() llvmMod $ \trans ctx halloc bak -> do
    let memVar = Trans.llvmMemVar ctx

    CC.AnyCFG mainCfg <-
      Trans.getTranslatedCFG trans "main" >>=
        \case
          Nothing -> fail "Could not find 'main' function in module"
          Just (_, cfg, _warns) -> return cfg

    mem <- LLVMG.initializeAllMemory bak ctx llvmMod
    mem' <- LLVMG.populateAllGlobals bak (trans ^. Trans.globalInitMap) mem

    let intrinsicTypes = MapF.union Intrinsics.llvmIntrinsicTypes SymIO.llvmSymIOIntrinsicTypes
    let fns = CS.fnBindingsFromList []
    let impl = CL.llvmExtensionImpl ?memOpts
    let simCtx = CS.initSimContext bak intrinsicTypes halloc outHandle fns impl ()

    mainArgs <-
      case CC.cfgArgTypes mainCfg of
        -- main(int argc, char** argv)
        (Empty :> LLVMMem.LLVMPointerRepr w :> LLVMMem.PtrRepr) -> do
          let sym = backendGetSym bak
          argc_ <- LLVMMem.llvmPointer_bv sym =<< bvOne sym w
          argv_ <- LLVMMem.mkNullPointer sym LLVMMem.PtrWidth
          let argc = CS.RegEntry (LLVMMem.LLVMPointerRepr w) argc_
          let argv = CS.RegEntry LLVMMem.PtrRepr argv_
          return (CS.RegMap (Empty :> argc :> argv))

        -- main(void)
        Empty -> return CS.emptyRegMap

        -- main() - technically varargs
        (Empty :> CC.VectorRepr elemTy) -> do
          let v = CS.RegEntry (CC.VectorRepr elemTy) V.empty
          return (CS.RegMap (Empty :> v))

        _ -> fail "Unsupported main function signature"

    let ?intrinsicsOpts = Intrinsics.defaultIntrinsicsOptions
    let retType = CC.cfgReturnType mainCfg
    let globSt = CL.llvmGlobals memVar mem'
    let simSt = CS.InitialState simCtx globSt CS.defaultAbortHandler retType $
                  CS.runOverrideSim retType $ do
                    void $ Intrinsics.register_llvm_overrides llvmMod [] [] ctx
                    registerLazyModule (\_ -> return ()) trans
                    CS.regValue <$> CS.callCFG mainCfg mainArgs

    timeoutFeat <- timeoutFeature (5 :: NominalDiffTime)
    let features = [genericToExecutionFeature timeoutFeat]

    execResult <- CS.executeCrucible features simSt
    case execResult of
      FinishedResult {} -> do
        obligations <- getProofObligations bak
        case obligations of
          Nothing -> return ()
          Just _ -> fail "Symbolic execution finished with pending proof obligations"
      AbortedResult _ abortResult -> do
        case abortResult of
          CS.AbortedExit ExitSuccess _ -> return ()
          CS.AbortedExec (CB.EarlyExit _) _ -> return ()  -- exit()
          _ -> fail (ppAbortedResult abortResult)
      TimeoutResult {} -> fail "Symbolic execution timed out"

  IO.hFlush outHandle
  IO.hClose outHandle
  readFile outPath