{- | Helpers for asserting certain things for programs, using HUnit.

All of the assertions in this module throw HUnit exceptions on failure
using `assertFailure`.
-}

module Test.Proctest.Assertions (
  -- * Starting programs
    runAssert
  , assertExited
  , _PROCTEST_POLL_TIMEOUT
  , assertExitedTimeout

) where


import Control.Monad
import Test.Proctest
import Test.HUnit


-- | Performs the monadic action on the contents of the `Just`, if any.
onJust :: (Monad m) => (a -> m ()) -> Maybe a -> m ()
onJust = maybe (return ())


-- | Runs the given program with `run` and asserts that it is still running
-- after the given timeout.
--
-- Don't choose the timeout too high as this function will block for it.
--
-- If the timeout is exceeded, a HUnit `assertFailure` exception is thrown,
-- showing the command line to be invoked, the exit code, and the standard
-- error output of the program.
runAssert :: Timeout -> FilePath -> [String] -> IO (Handle, Handle, Handle, ProcessHandle)
runAssert timeout cmd args = do
  r@(_, _, hErr, p) <- run cmd args
  sleep timeout
  getProcessExitCode p >>= (onJust $ \ec -> do
    -- The lazy IO here is OK, as we immediately show the String afterwards.
    err <- hGetContents hErr
    assertFailure $ "The program '" ++ program ++ "' after being started immediately "
                    ++ "exited with exit code " ++ show ec ++ ".\n"
                    ++ "--- stderr: ---\n" ++ err ++ "\n--- End of stderr ---")
  return r
  where
    program = cmd ++ concatMap (" " ++) args


-- | Asserts that the given process has shut down.
--
-- You might need to `sleep` before to give the process time to exit.
-- It is usually better to use `assertExitedTimeout` in those cases.
--
-- If the process is still running, a HUnit `assertFailure` exception is thrown.
assertExited :: ProcessHandle -> IO ()
assertExited p = do
  mE <- getProcessExitCode p
  when (mE == Nothing) $ assertFailure "The process is still running"


-- | How often to poll in waiting functions with maximum timeout.
_PROCTEST_POLL_TIMEOUT :: Timeout
_PROCTEST_POLL_TIMEOUT = mkTimeoutMs (1 :: Int)


-- | HUnit's `assertFailure` currently does not allow returning any type
-- like normal throw functions. This is a workaround.
--
-- Usage:
--
-- >fixHunitFailure $ assertFailure "boo!"
--
-- TODO Remove this in case it gets fixed in HUnit.
fixHunitFailure :: IO () -> IO a
fixHunitFailure = fmap . const $ error "Test.Proctest.Assertions: Executing after assertFailure, cannot happen!"


-- | Asserts that the given process has shut down in *at most* the given timeout.
--
-- Periodically polling with `_PROCTEST_POLL_TIMEOUT`,
-- returns as soon as the application has terminated or the timeout is exceeded.
--
-- Use this to write faster tests than with manual `sleep`ing:
-- For most tests, the application will actually finish way before the timeout.
--
-- If the process is still running, a HUnit `assertFailure` exception is thrown.
assertExitedTimeout :: Timeout -> ProcessHandle -> IO ExitCode
assertExitedTimeout timeout p =
  -- withTimeout timeout loop >>= maybe failure return
  -- TODO Fix HUnit.
  withTimeout timeout loop >>= maybe (fixHunitFailure failure) return
  where
    failure = assertFailure "The process is still running"
    loop = getProcessExitCode p >>= maybe loop return