PyF-0.8.0.1: test/SpecFail.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ExtendedDefaultRules #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DataKinds #-}
import Test.Hspec
import Test.HUnit.Lang
import System.Process (readProcessWithExitCode)
import System.Exit
import qualified Data.Text as Text
import System.IO.Temp
import qualified System.IO as IO
import Data.Hashable
import System.FilePath
import Control.Exception
import Control.DeepSeq
-- * Check compilation with external GHC (this is usefull to test compilation failure)
data CompilationStatus
= CompileError String -- ^ Fails during compilation (with error)
| RuntimeError String
| Ok String
deriving (Show, Eq)
makeTemplate :: String -> String
makeTemplate s = "{-# LANGUAGE QuasiQuotes, ExtendedDefaultRules, TypeApplications #-}\nimport PyF\ntruncate' = truncate @Float @Int\nhello = \"hello\"\nnumber = 3.14 :: Float\nmain :: IO ()\nmain = putStrLn [fmt|" ++ s ++ "|]\n"
{- | Compile a formatting string
>>> checkCompile fileContent
CompileError "Bla bla bla, Floating cannot be formatted as hexa (`x`)
-}
checkCompile :: HasCallStack => String -> IO CompilationStatus
checkCompile content = withSystemTempFile "PyFTest.hs" $ \path fd -> do
IO.hPutStr fd content
IO.hFlush fd
(ecode, _stdout, stderr) <- readProcessWithExitCode "ghc" [path,
-- Include all PyF files
"-isrc",
-- Disable the usage of the annoying .ghc environment file
"-package-env", "-",
-- Tests use a filename in a temporary directory which may have a long filename which triggers
-- line wrapping, reducing the reproducibility of error message
-- By setting the column size to a high value, we ensure reproducible error messages
"-dppr-cols=10000000000000"
] ""
case ecode of
ExitFailure _ -> pure (CompileError (sanitize path stderr))
ExitSuccess -> do
(ecode', stdout', stderr') <- readProcessWithExitCode (take (length path - 3) path) [] ""
case ecode' of
ExitFailure _ -> pure (RuntimeError stderr')
ExitSuccess -> pure (Ok stdout')
-- sanitize a compilation result by removing variables strings such as
-- temporary files name
sanitize :: FilePath -> String -> String
sanitize path s =
-- strip the filename
let
t = Text.pack s
in Text.unpack (Text.replace (Text.pack path) (Text.pack "INITIALPATH") t)
golden :: HasCallStack => String -> String -> IO ()
golden name output = do
let
goldenFile = "test/golden" </> (name <> ".golden")
actualFile = "test/golden" </> (name <> ".actual")
-- It can fail if the golden file does not exists
goldenContentE :: Either SomeException String <- try $ readFile goldenFile
let
-- if no golden file, the golden file is the content
goldenContent = case goldenContentE of
Right e -> e
Left _ -> output
-- Flush lazy IO
_ <- evaluate (force goldenContent)
if output /= goldenContent
then do
writeFile actualFile output
(_, diffOutput, _) <- readProcessWithExitCode "diff" [goldenFile, actualFile] ""
putStrLn diffOutput
-- Update golden file
writeFile goldenFile output
assertFailure diffOutput
else do
writeFile goldenFile output
-- if the compilation fails, runs a golden test on compilation output
-- else, fails the test
fileFailCompile :: HasCallStack => FilePath -> Spec
fileFailCompile path = do
fileContent <- runIO $ readFile path
-- I'm using the hash of the path, considering that the file content can evolve
failCompileContent (hash path) path fileContent
failCompile :: HasCallStack => String -> Spec
failCompile s = failCompileContent (hash s) s (makeTemplate s)
failCompileContent :: HasCallStack => Int -> String -> String -> Spec
failCompileContent h caption fileContent = do
before (checkCompile fileContent) $ it (show caption) $ \res -> case res of
CompileError output -> golden (show h) output
_ -> assertFailure (show $ ".golden/" <> show h <> "\n" <>show res)
main :: IO ()
main = hspec spec
spec :: Spec
spec = do
describe "error reporting" $ do
describe "string" $ do
describe "integral / fractional qualifiers" $ do
failCompile "{hello:f}"
failCompile "{hello:d}"
failCompile "{hello:e}"
failCompile "{hello:b}"
failCompile "{hello:E}"
failCompile "{hello:G}"
failCompile "{hello:g}"
failCompile "{hello:%}"
failCompile "{hello:x}"
failCompile "{hello:X}"
failCompile "{hello:o}"
describe "padding center" $ do
failCompile "{hello:=100s}"
failCompile "{hello:=100}"
describe "grouping" $ do
failCompile "{hello:_s}"
failCompile "{hello:,s}"
describe "sign" $ do
failCompile "{hello:+s}"
failCompile "{hello: s}"
failCompile "{hello:-s}"
describe "number" $ do
failCompile "{truncate' number:f}"
failCompile "{truncate' number:g}"
failCompile "{truncate' number:G}"
failCompile "{truncate' number:e}"
failCompile "{truncate' number:E}"
failCompile "{truncate' number:%}"
failCompile "{truncate number:s}"
describe "number with precision" $ do
failCompile "{truncate number:.3d}"
failCompile "{truncate number:.3o}"
failCompile "{truncate number:.3b}"
failCompile "{truncate number:.3x}"
describe "floats" $ do
failCompile "{number:o}"
failCompile "{number:b}"
failCompile "{number:x}"
failCompile "{number:X}"
failCompile "{number:d}"
failCompile "{number:s}"
-- XXX: this are not failing for now, it should be fixed
xdescribe "not specified" $ do
failCompile "{truncate number:.3}"
failCompile "{hello:#}"
failCompile "{hello:+}"
failCompile "{hello: }"
failCompile "{hello:-}"
failCompile "{hello:_}"
failCompile "{hello:,}"
describe "multiples lines" $ do
failCompile "hello\n\n\n{pi:l}"
describe "on haskell expression parsing" $ do
describe "single line" $ do
failCompile "{1 + - / lalalal}"
describe "multiples lines" $ do
failCompile "hello\n {\nlet a = 5\n b = 10\nin 1 + - / lalalal}"
describe "non-doubled delimiters" $ do
failCompile "hello } world"
failCompile "hello { world"
describe "fail is not enabled extension" $ do
failCompile "{0b0001}"
describe "lexical errors" $ do
failCompile "foo\\Pbar"
describe "fileFailures" $ do
mapM_ fileFailCompile [
"test/failureCases/bug18.hs"
]
describe "Wrong type" $ do
failCompile "{True:s}"
failCompile "{True}"
failCompile "{True:f}"
failCompile "{True:d}"