mirror of
https://github.com/GaloisInc/cryptol.git
synced 2024-11-30 23:45:23 +03:00
305 lines
9.5 KiB
Haskell
305 lines
9.5 KiB
Haskell
-- |
|
|
-- Module : $Header$
|
|
-- Copyright : (c) 2013-2016 Galois, Inc.
|
|
-- License : BSD3
|
|
-- Maintainer : cryptol@galois.com
|
|
-- Stability : provisional
|
|
-- Portability : portable
|
|
|
|
{-# LANGUAGE CPP #-}
|
|
module Main where
|
|
|
|
import Control.Monad (when,foldM)
|
|
import Data.List (isPrefixOf,partition,nub)
|
|
import Data.Monoid (Endo(..))
|
|
import System.Console.GetOpt
|
|
(getOpt,usageInfo,ArgOrder(..),OptDescr(..),ArgDescr(..))
|
|
import System.Directory
|
|
(getDirectoryContents,doesDirectoryExist,createDirectoryIfMissing
|
|
,canonicalizePath)
|
|
import System.Environment (getArgs,withArgs,getProgName)
|
|
import System.Exit (exitFailure,exitSuccess)
|
|
import System.FilePath
|
|
((</>),(<.>),takeExtension,splitFileName,splitDirectories,pathSeparator
|
|
,isRelative)
|
|
import System.Process
|
|
(createProcess,CreateProcess(..),StdStream(..),proc,waitForProcess
|
|
,readProcessWithExitCode)
|
|
import System.IO
|
|
(IOMode(..),withFile,Handle,hSetBuffering,BufferMode(..))
|
|
import Test.Framework (defaultMain,Test,testGroup)
|
|
import Test.Framework.Providers.HUnit (testCase)
|
|
import Test.HUnit (assertFailure)
|
|
import qualified Control.Exception as X
|
|
import qualified Data.Map as Map
|
|
|
|
import Prelude ()
|
|
import Prelude.Compat
|
|
|
|
#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
|
|
import Text.Regex
|
|
#endif
|
|
|
|
main :: IO ()
|
|
main = do
|
|
opts <- parseOptions
|
|
createDirectoryIfMissing True (optResultDir opts)
|
|
resultsDir <- canonicalizePath (optResultDir opts)
|
|
let opts' = opts { optResultDir = resultsDir }
|
|
files <- findTests (optTests opts')
|
|
withArgs (optOther opts') (defaultMain (generateTests opts' files))
|
|
|
|
|
|
-- Command Line Options --------------------------------------------------------
|
|
|
|
data Options = Options
|
|
{ optCryptol :: String
|
|
, optOther :: [String]
|
|
, optHelp :: Bool
|
|
, optResultDir :: FilePath
|
|
, optTests :: [FilePath]
|
|
, optDiff :: Maybe String
|
|
, optIgnoreExpected :: Bool
|
|
} deriving (Show)
|
|
|
|
defaultOptions :: Options
|
|
defaultOptions = Options
|
|
{ optCryptol = "cryptol-2"
|
|
, optOther = []
|
|
, optHelp = False
|
|
, optResultDir = "output"
|
|
, optTests = []
|
|
, optDiff = Nothing
|
|
, optIgnoreExpected = False
|
|
}
|
|
|
|
setHelp :: Endo Options
|
|
setHelp = Endo (\ opts -> opts { optHelp = True } )
|
|
|
|
setDiff :: String -> Endo Options
|
|
setDiff diff = Endo (\opts -> opts { optDiff = Just diff })
|
|
|
|
setCryptol :: String -> Endo Options
|
|
setCryptol path = Endo (\ opts -> opts { optCryptol = path } )
|
|
|
|
addOther :: String -> Endo Options
|
|
addOther arg = Endo (\ opts -> opts { optOther = optOther opts ++ [arg] } )
|
|
|
|
setResultDir :: String -> Endo Options
|
|
setResultDir path = Endo (\ opts -> opts { optResultDir = path })
|
|
|
|
addTestFile :: String -> Endo Options
|
|
addTestFile path =
|
|
Endo (\ opts -> opts { optTests = path : optTests opts })
|
|
|
|
setIgnoreExpected :: Endo Options
|
|
setIgnoreExpected =
|
|
Endo (\ opts -> opts { optIgnoreExpected = True })
|
|
|
|
options :: [OptDescr (Endo Options)]
|
|
options =
|
|
[ Option "c" ["cryptol"] (ReqArg setCryptol "PATH")
|
|
"the cryptol executable to use"
|
|
, Option "r" ["result-dir"] (ReqArg setResultDir "PATH")
|
|
"the result directory for test runs"
|
|
, Option "p" ["diff-prog"] (ReqArg setDiff "PROG")
|
|
"use this diffing program on failures"
|
|
, Option "T" [] (ReqArg addOther "STRING")
|
|
"add an argument to pass to the test-runner main"
|
|
, Option "i" ["ignore-expected"] (NoArg setIgnoreExpected)
|
|
"ignore expected failures"
|
|
, Option "h" ["help"] (NoArg setHelp)
|
|
"display this message"
|
|
]
|
|
|
|
parseOptions :: IO Options
|
|
parseOptions = do
|
|
args <- getArgs
|
|
case getOpt (ReturnInOrder addTestFile) options args of
|
|
|
|
(es,_,[]) -> do
|
|
let opts = appEndo (mconcat es) defaultOptions
|
|
when (optHelp opts) $ do
|
|
displayUsage []
|
|
exitSuccess
|
|
|
|
-- canonicalize the path to cryptol, if it's relative
|
|
cryptol' <- if pathSeparator `elem` optCryptol opts
|
|
&& isRelative (optCryptol opts)
|
|
then canonicalizePath (optCryptol opts)
|
|
else return (optCryptol opts)
|
|
|
|
return opts
|
|
{ optCryptol = cryptol'
|
|
, optTests = reverse (optTests opts)
|
|
}
|
|
|
|
(_,_,errs) -> do
|
|
displayUsage errs
|
|
exitFailure
|
|
|
|
displayUsage :: [String] -> IO ()
|
|
displayUsage errs = do
|
|
prog <- getProgName
|
|
let banner = unlines (errs ++ ["Usage: " ++ prog ++ " [OPTIONS] [FILES]"])
|
|
putStrLn (usageInfo banner options)
|
|
|
|
|
|
-- Test Generation -------------------------------------------------------------
|
|
|
|
-- | Write the output of a run of cryptol-2 to this handle. Stdin and stderr
|
|
-- will both be given the handle provided.
|
|
cryptol2 :: Options -> Handle -> FilePath -> [String] -> IO ()
|
|
cryptol2 opts hout path args = do
|
|
(_, _, _, ph) <- createProcess (proc (optCryptol opts) args)
|
|
{ cwd = Just path
|
|
, std_out = UseHandle hout
|
|
, std_in = Inherit
|
|
, std_err = UseHandle hout
|
|
}
|
|
_ <- waitForProcess ph
|
|
return ()
|
|
|
|
generateTests :: Options -> TestFiles -> [Test]
|
|
generateTests opts = loop ""
|
|
where
|
|
loop dir (TestFiles m fs) = as ++ grouped
|
|
where
|
|
as = map (generateAssertion opts dir) fs
|
|
grouped = [ testGroup path (loop (dir </> path) t)
|
|
| (path,t) <- Map.toList m ]
|
|
|
|
|
|
generateAssertion :: Options -> FilePath -> FilePath -> Test
|
|
generateAssertion opts dir file = testCase file $ do
|
|
|
|
createDirectoryIfMissing True resultDir
|
|
|
|
withFile resultOut WriteMode $ \ hout ->
|
|
do hSetBuffering hout NoBuffering
|
|
cryptol2 opts hout dir ["-b",file]
|
|
|
|
out <- fixPaths <$> readFile resultOut
|
|
expected <- readFile goldFile
|
|
mbKnown <- X.try (readFile knownFailureFile)
|
|
checkOutput mbKnown expected out
|
|
where
|
|
knownFailureFile = dir </> file <.> "fails"
|
|
goldFile = dir </> file <.> "stdout"
|
|
resultOut = resultDir </> file <.> "stdout"
|
|
resultDir = optResultDir opts </> dir
|
|
checkOutput mbKnown expected out
|
|
| expected == out =
|
|
case mbKnown of
|
|
Left _ -> return ()
|
|
Right _ -> assertFailure $
|
|
"Test completed successfully. Please remove " ++ knownFailureFile
|
|
| otherwise =
|
|
case mbKnown of
|
|
|
|
Left (X.SomeException {})
|
|
| Just prog <- optDiff opts ->
|
|
do goldFile' <- canonicalizePath goldFile
|
|
assertFailure (unwords [ prog, goldFile', "\\\n ", resultOut ])
|
|
|
|
| otherwise ->
|
|
do goldFile' <- canonicalizePath goldFile
|
|
(_,diffOut,_) <- readProcessWithExitCode "diff" [ goldFile', resultOut ] ""
|
|
assertFailure diffOut
|
|
|
|
Right fail_msg | optIgnoreExpected opts -> return ()
|
|
| otherwise -> assertFailure fail_msg
|
|
|
|
-- Test Discovery --------------------------------------------------------------
|
|
|
|
findTests :: [FilePath] -> IO TestFiles
|
|
findTests = foldM step mempty
|
|
where
|
|
step tests path = do
|
|
tests' <- evalStrategy path
|
|
return (tests `mappend` tests')
|
|
|
|
evalStrategy path =
|
|
do isDir <- doesDirectoryExist path
|
|
if isDir
|
|
then testDiscovery path
|
|
else let (dir,file) = splitFileName path
|
|
dirs = splitDirectories dir
|
|
insert d t = TestFiles (Map.singleton d t) []
|
|
in return $! foldr insert (TestFiles Map.empty [file]) dirs
|
|
|
|
|
|
-- | Files that end in .icry are cryptol test cases.
|
|
isTestCase :: FilePath -> Bool
|
|
isTestCase path = takeExtension path == ".icry"
|
|
|
|
-- | Directory structure of the discovered tests. Each entry in the map
|
|
-- represents a single folder, with the top-level list representing tests
|
|
-- inside the base directory.
|
|
data TestFiles = TestFiles (Map.Map FilePath TestFiles) [FilePath]
|
|
deriving (Show)
|
|
|
|
instance Monoid TestFiles where
|
|
mempty = TestFiles Map.empty []
|
|
mappend (TestFiles lt lf) (TestFiles rt rf) = TestFiles mt mf
|
|
where
|
|
mt = Map.unionWith mappend lt rt
|
|
mf = nub (lf ++ rf)
|
|
|
|
nullTests :: TestFiles -> Bool
|
|
nullTests (TestFiles m as) = null as && Map.null m
|
|
|
|
-- | Find test cases to run.
|
|
testDiscovery :: FilePath -> IO TestFiles
|
|
testDiscovery from = do
|
|
subTests <- process from =<< getDirectoryContents from
|
|
let insert d t = TestFiles (Map.singleton d t) []
|
|
return (foldr insert subTests (splitDirectories from))
|
|
|
|
where
|
|
process base contents = do
|
|
let (tests,files) = partition isTestCase
|
|
$ filter (not . isDotFile) contents
|
|
subdirs <- mapM (resolve base) files
|
|
let subTests = Map.fromList [ (p,m) | (p,m) <- subdirs
|
|
, not (nullTests m) ]
|
|
|
|
return (TestFiles subTests tests)
|
|
|
|
loop base = do
|
|
isDir <- doesDirectoryExist base
|
|
if isDir
|
|
then do subTests <- process base =<< getDirectoryContents base
|
|
return (TestFiles (Map.singleton base subTests) [])
|
|
else return mempty
|
|
|
|
resolve base p = do
|
|
let path = base </> p
|
|
tests <- loop path
|
|
return (p,tests)
|
|
|
|
|
|
-- Utilities -------------------------------------------------------------------
|
|
|
|
-- | Screen out dotfiles.
|
|
isDotFile :: FilePath -> Bool
|
|
isDotFile path = "." `isPrefixOf` path
|
|
|
|
-- | Normalize to unix-style paths with forward slashes when on
|
|
-- Windows. This is pretty crude; it just looks for any non-whitespace
|
|
-- strings that contain a blackslash and end in @.cry@ or @.icry@.
|
|
fixPaths :: String -> String
|
|
#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
|
|
fixPaths str = go str
|
|
where
|
|
go str
|
|
| Just (pre, match, post, _) <- matchCryFile str
|
|
= pre ++ (subst match) ++ go post
|
|
| otherwise
|
|
= str
|
|
subst match = subRegex (mkRegex "\\\\") match "/"
|
|
matchCryFile = matchRegexAll (mkRegex "\\\\([^[:space:]]*\\.i?cry)")
|
|
#else
|
|
fixPaths = id
|
|
#endif
|