packages feed

ghc-typelits-natnormalise-0.9.7: tests/ShouldError/Tasty.hs

{-# LANGUAGE CPP #-}

module ShouldError.Tasty where

import Data.List (isInfixOf)
import Data.Maybe (fromMaybe)
import System.Environment (lookupEnv)
import System.Exit
import System.IO
import System.IO.Temp
import System.Process
import System.Timeout (timeout)
import Test.Tasty.HUnit

-- | Compile a Haskell code snippet with the plugin enabled, with an optional
-- timeout in seconds. Returns 'Nothing' if the timeout expired, otherwise the
-- exit code and stderr output of GHC.
runGhc :: Maybe Int -> String -> IO (Maybe (ExitCode, String))
runGhc timeLimit source = do
  -- XXX: This will pick the wrong GHC if the HC environment variable (as seen on CI)
  --      isn't set and the test suite is compiled with a GHC compiler other than the
  --      system's default.
  hc <- fromMaybe "ghc" <$> lookupEnv "HC"
  withSystemTempFile "ShouldError.hs" $ \tempFile tempHandle -> do
    hPutStr tempHandle source
    hClose tempHandle
    let compile = readProcessWithExitCode hc
          [ "-XCPP"
          , "-XAllowAmbiguousTypes"
          , "-XConstraintKinds"
          , "-XDataKinds"
          , "-XFlexibleContexts"
          , "-XGADTs"
          , "-XScopedTypeVariables"
          , "-XStandaloneDeriving"
          , "-XTypeApplications"
          , "-XTypeFamilies"
          , "-XTypeOperators"
          , "-XUndecidableInstances"
          , "-XNoStarIsType"
          , "-fno-code"
          , "-fplugin", "GHC.TypeLits.Normalise"
          , tempFile
          ] ""
    result <- case timeLimit of
      Nothing -> Just <$> compile
      -- 'timeout' interrupts 'readProcessWithExitCode' with an asynchronous
      -- exception, upon which it kills the GHC process before returning.
      Just seconds -> timeout (seconds * 1000000) compile
    return (fmap (\(exitCode, _, stderrOutput) -> (exitCode, stderrOutput)) result)

-- | Assert that a Haskell code snippet compiles successfully within the given
-- number of seconds
assertCompileSuccessWithin :: Int -> String -> Assertion
assertCompileSuccessWithin seconds source = do
  result <- runGhc (Just seconds) source
  case result of
    Nothing -> assertFailure $
      "Compilation did not finish within " ++ show seconds ++ " seconds"
    Just (ExitFailure _, stderrOutput) -> assertFailure $
      "Expected compilation to succeed but it failed:\n" ++ stderrOutput
    Just (ExitSuccess, _) -> return ()

-- | Assert that a Haskell code snippet fails to compile with expected error messages
assertCompileError :: String -> [String] -> Assertion
assertCompileError source expectedErrors = do
  Just (exitCode, stderrOutput) <- runGhc Nothing source
  case exitCode of
    ExitSuccess -> assertFailure "Expected compilation to fail but it succeeded"
    ExitFailure _ ->
      let cleanedStderr = removeProblemChars stderrOutput
          cleanedExpected = map removeProblemChars expectedErrors
      in if all (`isInfixOf` cleanedStderr) cleanedExpected
         then return ()
         else assertFailure $ "Error message mismatch:\n" ++
                             "Expected substrings: " ++ show expectedErrors ++ "\n" ++
                             "Actual output:\n" ++ stderrOutput

-- | Remove problematic characters that vary depending on locale
-- The kind and amount of quotes in GHC error messages changes depending on
-- whether or not our locale supports unicode.
removeProblemChars :: String -> String
removeProblemChars = filter (`notElem` problemChars)
  where problemChars = "‘’`'"