packages feed

hermit-1.0.1: tests/Main.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE ViewPatterns #-}
module Main (main) where

import Constants (hiVersion)
import Control.Monad

import Data.Char (isSpace)
import Data.List
import Data.Maybe (listToMaybe)

import GHC.Paths (ghc)

import HERMIT.Driver

import System.Directory
import System.FilePath as F
#if defined(darwin_HOST_OS)
import System.Info (arch)
#else
import System.Info (arch, os)
#endif
import System.IO
import System.IO.Temp (withSystemTempFile)
import System.Process

import Test.Tasty (TestTree, TestName, defaultMain, testGroup)
import Test.Tasty.Golden (goldenVsFileDiff)

type HermitTestArgs = (FilePath, FilePath, FilePath, [String])

main :: IO ()
main = defaultMain hermitTests

hermitTests :: TestTree
hermitTests = testGroup "HERMIT tests" $ map mkHermitTest testArgs

-- subdirectory names
golden, dump, rootDir, examples :: FilePath
golden   = "golden" </> "golden-ghc-" ++ show hiVersion
dump     = "dump"
rootDir  = "tests"
examples = "examples"

testArgs :: [HermitTestArgs]
testArgs = [ ("concatVanishes", "Flatten.hs", "Flatten.hss", ["-safety=unsafe"])
        , ("concatVanishes", "QSort.hs"  , "QSort.hss"  , ["-safety=unsafe"])
        , ("concatVanishes", "Rev.hs"    , "Rev.hss"    , ["-safety=unsafe"])
        , ("evaluation"    , "Eval.hs"   , "Eval.hss"   , [])
#if __GLASGOW_HASKELL__ < 710
        -- broken on GHC 7.10 due to not satisfying the let/app invariant. I should probably fix this.
        , ("factorial"     , "Fac.hs"    , "Fac.hss"    , [])
#endif
        -- broken due to Core Parser: , ("fib-stream"    , "Fib.hs"    , "Fib.hss"    )
        , ("fib-tuple"     , "Fib.hs"    , "Fib.hss"    , [])
        , ("flatten"       , "Flatten.hs", "Flatten.hec", ["-safety=unsafe"])
        -- for some reason loops in testsuite but not normally: , ("hanoi"         , "Hanoi.hs"  , "Hanoi.hss"  )
        , ("last"          , "Last.hs"   , "Last.hss"   , ["-safety=unsafe"])
        , ("last"          , "Last.hs"   , "NewLast.hss", ["-safety=strict"])
        -- broken due to Core Parser: , ("map"           , "Map.hs"    , "Map.hss"    )
        , ("mean"          , "Mean.hs"   , "Mean.hss"   , [])
        -- TODO: re-enable once fixed , ("nub"           , "Nub.hs"    , "Nub.hss"    , [])
        , ("qsort"         , "QSort.hs"  , "QSort.hss"  , [])
        , ("reverse"       , "Reverse.hs", "Reverse.hss", ["-safety=unsafe"])
        , ("new_reverse"   , "Reverse.hs", "Reverse.hec", [])
        ]

fixName :: FilePath -> FilePath
fixName = map (\c -> if c == '.' then '_' else c)

mkTestScript :: Handle -> FilePath -> IO ()
mkTestScript h hss = do
    hPutStrLn h
        $ unlines [ "set-auto-corelint True"
                  , "set-pp-type Show"
                  , "set-fail-hard True"
                  , "load-and-run \"" ++ hss ++ "\""
                  , "top ; prog"
                  , "display" -- all the bindings
                  , "show-lemmas"
                  , "resume" ]
    hClose h

-- | Get the path to the sandbox database if any
-- Taken from hoogle-index (by Ben Gamari, under BSD3)
getSandboxDb :: IO (Maybe FilePath)
getSandboxDb = do
  dir <- getCurrentDirectory
  let f = dir </> "cabal.sandbox.config"
  ex <- doesFileExist f
  if ex
    then
      (listToMaybe .
       map ((</> archOSCompilerConf) .
            dropFileName .
            dropWhile isSpace .
            tail .
            dropWhile (/= ':')) .
       filter (isPrefixOf "package-db") .
       lines) <$> readFile f
    else return Nothing
  where
    archOSCompilerConf :: String
    archOSCompilerConf = intercalate "-" [arch, theOS, takeFileName ghc, "packages.conf.d"]

    theOS :: String
#if defined(darwin_HOST_OS)
    theOS = "osx" -- System.Info.os gives "darwin", which isn't what is actually
                  -- used, for some silly reason
#else
    theOS = os
#endif

mkHermitTest :: HermitTestArgs -> TestTree
mkHermitTest (dir, hs, hss, extraFlags) =
    goldenVsFileDiff testName diff gfile dfile hermitOutput
  where
    testName :: TestName
    testName = dir </> hs

    fixed, gfile, dfile, pathp :: FilePath
    fixed = fixName (concat [dir, "_", hs, "_", hss])
    gfile = rootDir  </> golden </> fixed <.> "ref"
    dfile = rootDir  </> dump   </> fixed <.> "dump"
    pathp = examples </> dir

    diff :: FilePath -> FilePath -> [String]
    diff ref new = ["diff", "-b", "-U 5", ref, new]

    -- For some incredibly bizarre reason, HERMIT's output can be have different
    -- line orderings depending on if it's been run once before. As far as I can
    -- tell, this is due to the presence of object (.o) and interface (.hi) files.
    -- Wat.
    --
    -- Luckily, removing any object or interface before running HERMIT seems to
    -- provide a guarantee that HERMIT's output will be the same on subsequent runs.
    cleanObjectFiles :: IO ()
    cleanObjectFiles = do
        files <- getDirectoryContents pathp
        forM_ files $ \file ->
            when (takeExtension file `elem` [".o", ".hi"]) $
               removeFile $ pathp </> file

    hermitOutput :: IO ()
    hermitOutput = do
        cleanObjectFiles
        mbDb <- getSandboxDb
        createDirectoryIfMissing True (dropFileName gfile)

        let dbFlags :: String
            dbFlags | Just db <- mbDb
                    = unwords ["-no-user-package-db", "-package-db", db]
                    | otherwise = ""

        withSystemTempFile "Test.hss" $ \ fp h -> do
            mkTestScript h hss

            let cmd :: String
                cmd = unwords $    [ "("
                                   , "cd"
                                   , pathp
                                   , ";"
                                   , ghc
                                   , dbFlags
                                   , hs
                                   , "-w" -- Disable all warnings
                                   ]
                                ++ ghcFlags
                                ++ [ "-fplugin=HERMIT"
                                   , "-fplugin-opt=HERMIT:Main:" ++ fp -- made by mkTestScript
                                   , "-v0"
                                   ]
                                ++ [ "-fplugin-opt=HERMIT:Main:" ++ f | f <- extraFlags]
                                ++ [ ")" ]

            -- Adding a &> dfile redirect in cmd causes the call to GHC to not block
            -- until the compiler is finished (on Linux, not OSX). So we do the Haskell
            -- equivalent here by opening our own file.
            fh <- openFile dfile WriteMode
            -- putStrLn cmd
            (_,_,_,rHermit) <- createProcess $ (shell cmd) { std_out = UseHandle fh, std_err = UseHandle fh }
            _ <- waitForProcess rHermit

            -- Ensure that the golden file exists prior to calling diff
            goldenExists <- doesFileExist gfile
            unless goldenExists $ copyFile dfile gfile