packages feed

main-tester 0.1.0.0 → 0.2.0.0

raw patch · 6 files changed

+141/−13 lines, 6 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Test.Main: ExitFailure :: Int -> ExitCode
+ Test.Main: [prException] :: ProcessResult -> !(Maybe SomeException)
+ Test.Main: withEnv :: [(String, Maybe String)] -> IO a -> IO a
+ Test.Main.Internal: [prException] :: ProcessResult -> !(Maybe SomeException)
- Test.Main: ProcessResult :: !ByteString -> !ByteString -> !ExitCode -> ProcessResult
+ Test.Main: ProcessResult :: !ByteString -> !ByteString -> !ExitCode -> !(Maybe SomeException) -> ProcessResult
- Test.Main: data ExitCode :: *
+ Test.Main: data ExitCode
- Test.Main.Internal: ProcessResult :: !ByteString -> !ByteString -> !ExitCode -> ProcessResult
+ Test.Main.Internal: ProcessResult :: !ByteString -> !ByteString -> !ExitCode -> !(Maybe SomeException) -> ProcessResult

Files

ChangeLog.md view
@@ -1,3 +1,11 @@+# 0.2.0.0++- Breaking change:+    - Add `prException` field to `ProcessResult`.+    - Now `captureProcessResult` catches any exception (including `ExitCode`) thrown by the given action (usually your `main` function).+        - The caught exception is available by `prException` function (except `ExitCode`).+- :new: Add `withEnv` function.+ # 0.1.0.0  - Initial release.
README.md view
@@ -20,3 +20,7 @@ - Can test exit code of your main. - All input and outputs are strict bytestrings except the command line arguments, which is very important when testing with non-UTF8 input and output. - Currently doesn't support suppressing stdout and stderr (Pull request welcome!).++## Usage++See [the document of `Test.Main` module](https://hackage.haskell.org/package/main-tester/docs/Test-Main.html).
main-tester.cabal view
@@ -1,5 +1,5 @@ name:                main-tester-version:             0.1.0.0+version:             0.2.0.0 synopsis:            Capture stdout/stderr/exit code, and replace stdin of your main function. description:         See README.md for detail. homepage:            https://gitlab.com/igrep/main-tester#readme@@ -23,7 +23,16 @@                      , directory   default-language:    Haskell2010 +flag doctest+  description:         Build and run doctest. Turn off to avoid error on some environment.+  default:             False+  manual:              True+ test-suite main-tester-doctest+  if flag(doctest)+    buildable:         True+  else+    buildable:         False   type:                exitcode-stdio-1.0   hs-source-dirs:      test   main-is:             doctest.hs
src/Test/Main.hs view
@@ -5,6 +5,7 @@     captureProcessResult   , ProcessResult(..)   , withStdin+  , withEnv    -- ** Re-export from System.Environment   , withArgs@@ -15,11 +16,12 @@   import qualified Control.Exception as E+import           Control.Monad (mapM, mapM_) import qualified Data.ByteString as B import           GHC.IO.Handle (hDuplicate, hDuplicateTo) import           System.Directory (removeFile, getTemporaryDirectory)-import           System.Environment (withArgs)-import           System.Exit (ExitCode(ExitSuccess))+import           System.Environment (withArgs, setEnv, unsetEnv, lookupEnv)+import           System.Exit (ExitCode(ExitSuccess, ExitFailure)) import           System.IO                    ( Handle                    , SeekMode(AbsoluteSeek)@@ -47,7 +49,7 @@ -- -- >>> let main = putStr "hello" -- >>> captureProcessResult main--- ProcessResult {prStdout = "hello", prStderr = "", prExitCode = ExitSuccess}+-- ProcessResult {prStdout = "hello", prStderr = "", prExitCode = ExitSuccess, prException = Nothing} -- -- If the IO action exit with error message, the exit code of result is 'ExitFailure'. --@@ -55,7 +57,20 @@ -- >>> import System.Exit -- >>> let main = hPutStr stderr "OMG!" >> exitWith (ExitFailure 1) -- >>> captureProcessResult main--- ProcessResult {prStdout = "", prStderr = "OMG!", prExitCode = ExitFailure 1}+-- ProcessResult {prStdout = "", prStderr = "OMG!", prExitCode = ExitFailure 1, prException = Nothing}+--+-- @since 0.2.0.0+-- Since v0.2.0.0, this function catches @SomeException@, not only @ExitCode@+-- to prevent it from losing output when an exception other than @ExitCode@ is thrown.+-- To get the thrown error, use @prException@.+--+-- Note: 'prStderr' doesn't contain the error message of the thrown exception.+-- See the example below.+--+-- >>> import Control.Exception+-- >>> let main = ioError $ userError "OMG!"+-- >>> captureProcessResult main+-- ProcessResult {prStdout = "", prStderr = "", prExitCode = ExitFailure 1, prException = Just user error (OMG!)} captureProcessResult :: IO () -> IO ProcessResult captureProcessResult action = do   tDir <- getTemporaryDirectory@@ -63,8 +78,7 @@     withBinaryTmpFile tDir "test-stderr" $ \(_ePath, eHd) ->       redirectingHandle stdout oHd $         redirectingHandle stderr eHd $ do-          prExitCode <--            either id (const ExitSuccess) <$> E.try action+          (prExitCode, prException) <- captureExitCodeAndException action           prStdout <- readFromHead oHd stdout           prStderr <- readFromHead eHd stderr           return ProcessResult {..}@@ -75,7 +89,17 @@       hSeek tmpH AbsoluteSeek 0       B.hGetContents tmpH +    captureExitCodeAndException act =+      (act >> return (ExitSuccess, Nothing))+        `E.catches` [E.Handler forExitCode, E.Handler forSomeException] +    forExitCode :: ExitCode -> IO (ExitCode, Maybe E.SomeException)+    forExitCode eCode = return (eCode, Nothing)++    forSomeException :: E.SomeException -> IO (ExitCode, Maybe E.SomeException)+    forSomeException ex = return (ExitFailure 1, Just ex)++ withBinaryTmpFile :: FilePath -> String -> ((FilePath, Handle) -> IO a) -> IO a withBinaryTmpFile parent name =   E.bracket@@ -133,3 +157,35 @@         (openBinaryTempFile tDir "test-stdin")         (\(_path, hd) -> hClose hd)         (\(path, hd) -> B.hPut hd bs >> return path)+++-- |+-- Run the given IO action with the specified environment variables set.+-- The environment variables are specified as pairs of @ENV_VAR_NAME@ and @ENV_VAR_VALUE@.+-- If @ENV_VAR_VALUE@ is @Nothing@, the @ENV_VAR_NAME@ is unset with 'unsetEnv'.+--+-- >>> import System.Environment+-- >>> setEnv "ENV_VAR_TO_UNSET"    "value_to_unset"+-- >>> setEnv "ENV_VAR_TO_OVERWRITE" "value_to_overwrite"+-- >>> let main = (print =<< lookupEnv "ENV_VAR_TO_UNSET") >> (print =<< lookupEnv "ENV_VAR_TO_OVERWRITE")+-- >>> withEnv [("ENV_VAR_TO_UNSET", Nothing), ("ENV_VAR_TO_OVERWRITE" , Just "new_value")] main+-- Nothing+-- Just "new_value"+-- >>> main+-- Just "value_to_unset"+-- Just "value_to_overwrite"+withEnv :: [(String, Maybe String)] -> IO a -> IO a+withEnv newVarVals action =+  E.bracket+    (saveReplaces newVarVals)+    replaces+    (const action)++  where+    saveReplaces = mapM $ \varVal@(var, _val) ->+      ((,) <$> pure var <*> lookupEnv var) <* replace varVal++    replaces = mapM_ replace++    replace (var, Just val) = setEnv var val+    replace (var, Nothing)  = unsetEnv var
src/Test/Main/Internal.hs view
@@ -7,6 +7,7 @@ module Test.Main.Internal where  +import qualified Control.Exception as E import qualified Data.ByteString.Char8 as B import           GHC.Generics (Generic) import           System.Exit (ExitCode)@@ -19,15 +20,25 @@     { prStdout :: !B.ByteString     , prStderr :: !B.ByteString     , prExitCode :: !ExitCode-    } deriving (Eq, Show, Generic)+    , prException :: !(Maybe E.SomeException)+    } deriving (Show, Generic) +-- NOTE: SomeException is not Eq! So can't derive!+instance Eq ProcessResult where+  pr1 == pr2 =+    prStdout pr1 == prStdout pr2+      && prStderr pr1 == prStderr pr2+      && prStderr pr1 == prStderr pr2+      && prExitCode pr1 == prExitCode pr2+      && fmap show (prException pr1) == fmap show (prException pr2) + -- | Use to avoid errors in related to new line code in tests. --   Currently I use this function only for this module's test. normalizeNewLines :: ProcessResult -> ProcessResult #if defined(mingw32_HOST_OS) normalizeNewLines ProcessResult {..} =-  ProcessResult (nl prStdout) (nl prStderr) (prExitCode)+  ProcessResult (nl prStdout) (nl prStderr) prExitCode prException   where     nl = B.concat . B.split '\r' #else
test/Spec.hs view
@@ -1,6 +1,7 @@ import           Control.Monad (mapM_) import           Data.ByteString.Char8 (pack)-import           System.Environment (getArgs)+import qualified Control.Exception as E+import           System.Environment (getArgs, setEnv, lookupEnv) import           System.Exit (ExitCode(ExitSuccess, ExitFailure) , exitWith) import           System.IO (stderr, hPutStr) import           Test.Hspec@@ -34,8 +35,8 @@  main :: IO () main = hspec $-  describe "Test.Main" $-    prop "passes stdin and arguments to the program, and capture stdin data, stderr data, and exitCode" $+  describe "Test.Main" $ do+    prop "passes stdin and arguments to the program, and captures stdin data, stderr data, and exit code" $       \(argsA, inDataA, AExitCode eCode) -> do         let args = map getPrintableAsciiString argsA             inData = pack $ getPrintableAsciiString inDataA@@ -47,4 +48,43 @@         actual <- withStdin inData $ withArgs args $ captureProcessResult testedMain          normalizeNewLines actual `shouldBe`-          normalizeNewLines (ProcessResult (pack $ unlines args) inData eCode)+          normalizeNewLines (ProcessResult (pack $ unlines args) inData eCode Nothing)++    prop "captures stdin data, stderr data, exit code, and thrown exception when the program throws an exception" $+      \(inDataA, errDataA, errorMesage) -> do+        let inData = pack $ getPrintableAsciiString inDataA+            errDataS = getPrintableAsciiString errDataA+            errData = pack errDataS+            testedMain = do+              putStr =<< getContents+              hPutStr stderr errDataS+              _ <- fail errorMesage+              putStrLn "*** THIS SHOULD NOT BE PRINTED! ***"++        actual <- withStdin inData $ captureProcessResult testedMain++        normalizeNewLines actual `shouldBe`+          normalizeNewLines+            ( ProcessResult+                inData+                errData+                (ExitFailure 1)+                (Just $ E.SomeException $ userError errorMesage)+            )++    it "overwrites or deletes the specified environment variables temporarily" $ do+      let vu = "value_to_unset"+          vw = "value_to_overwrite"+          nv = "new_value"++      setEnv "ENV_VAR_TO_UNSET"     vu+      setEnv "ENV_VAR_TO_OVERWRITE" vw++      let action =+            (,) <$> lookupEnv "ENV_VAR_TO_UNSET" <*> lookupEnv "ENV_VAR_TO_OVERWRITE"+      resultMutated <- withEnv+        [("ENV_VAR_TO_UNSET", Nothing), ("ENV_VAR_TO_OVERWRITE", Just nv)]+        action++      resultMutated `shouldBe` (Nothing, Just nv)+      action `shouldReturn` (Just vu, Just vw)