packages feed

main-tester (empty) → 0.1.0.0

raw patch · 9 files changed

+319/−0 lines, 9 filesdep +QuickCheckdep +basedep +bytestringsetup-changed

Dependencies added: QuickCheck, base, bytestring, directory, doctest, hspec, hspec-core, main-tester, text

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+# 0.1.0.0++- Initial release.
+ LICENSE view
@@ -0,0 +1,13 @@+Copyright Yuji Yamamoto (c) 2018++Licensed under the Apache License, Version 2.0 (the "License");+you may not use this file except in compliance with the License.+You may obtain a copy of the License at++    http://www.apache.org/licenses/LICENSE-2.0++Unless required by applicable law or agreed to in writing, software+distributed under the License is distributed on an "AS IS" BASIS,+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+See the License for the specific language governing permissions and+limitations under the License.
+ README.md view
@@ -0,0 +1,22 @@+# main-tester++Capture stdout/stderr/exit code, and replace stdin of your main function.++## Why?++Against the best practice, I often prefer large, end-to-end (E2E) tests.  +Because:++- E2E tests can directly test your users' requirement.+- E2E tests can detect bugs caused by misassumption of external components+    - Which happenes relatively more often than the others, according to my experience.+- I often write small programs where E2E tests are sufficient.++This library main-tester provides utility functions for E2E testing of your CLI applications.  +With main-tester, You can test your apps with arbitrary stdin data, check their output and exit code.++## Comparison with [silently](https://hackage.haskell.org/package/silently) and [System.IO.Fake](https://hackage.haskell.org/package/imperative-edsl-0.7.1/docs/System-IO-Fake.html)++- 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!).
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ main-tester.cabal view
@@ -0,0 +1,53 @@+name:                main-tester+version:             0.1.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+license:             Apache-2.0+license-file:        LICENSE+author:              Yuji Yamamoto+maintainer:          whosekiteneverfly@gmail.com+copyright:           2018 Yuji Yamamoto+category:            System, Testing+build-type:          Simple+extra-source-files:  README.md+                   , ChangeLog.md+cabal-version:       >=1.10++library+  hs-source-dirs:      src+  exposed-modules:     Test.Main+                     , Test.Main.Internal+  build-depends:       base >= 4.7 && < 5+                     , bytestring+                     , directory+  default-language:    Haskell2010++test-suite main-tester-doctest+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             doctest.hs+  build-depends:       base+                     , doctest+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N -Wall+  default-language:    Haskell2010+  default-extensions:  OverloadedStrings++test-suite main-tester-test+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             Spec.hs+  build-depends:       base+                     , main-tester+                     , bytestring+                     , hspec+                     , hspec-core+                     , QuickCheck+                     , text+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N -Wall+  default-language:    Haskell2010+  default-extensions:  OverloadedStrings++source-repository head+  type:     git+  location: https://gitlab.com/igrep/main-tester
+ src/Test/Main.hs view
@@ -0,0 +1,135 @@+{-# LANGUAGE RecordWildCards #-}++module Test.Main+  ( -- * Utilities for testing your main function+    captureProcessResult+  , ProcessResult(..)+  , withStdin++  -- ** Re-export from System.Environment+  , withArgs++  -- ** Re-export from System.Exit+  , ExitCode(..)+  ) where+++import qualified Control.Exception as E+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.IO+                   ( Handle+                   , SeekMode(AbsoluteSeek)+                   , stdin+                   , stderr+                   , stdout+                   , hClose+                   , hFlush+                   , hSetBinaryMode+                   , hGetBuffering+                   , hSetBuffering+                   , hGetEncoding+                   , hSetEncoding+                   , hSeek+                   , openBinaryTempFile+                   , withBinaryFile+                   , IOMode(ReadMode)+                   )++import           Test.Main.Internal (ProcessResult(..))+++-- |+-- Capture stdout, stderr, and exit code of the given IO action.+--+-- >>> let main = putStr "hello"+-- >>> captureProcessResult main+-- ProcessResult {prStdout = "hello", prStderr = "", prExitCode = ExitSuccess}+--+-- If the IO action exit with error message, the exit code of result is 'ExitFailure'.+--+-- >>> import System.IO+-- >>> import System.Exit+-- >>> let main = hPutStr stderr "OMG!" >> exitWith (ExitFailure 1)+-- >>> captureProcessResult main+-- ProcessResult {prStdout = "", prStderr = "OMG!", prExitCode = ExitFailure 1}+captureProcessResult :: IO () -> IO ProcessResult+captureProcessResult action = do+  tDir <- getTemporaryDirectory+  withBinaryTmpFile tDir "test-stdout" $ \(_oPath, oHd) ->+    withBinaryTmpFile tDir "test-stderr" $ \(_ePath, eHd) ->+      redirectingHandle stdout oHd $+        redirectingHandle stderr eHd $ do+          prExitCode <-+            either id (const ExitSuccess) <$> E.try action+          prStdout <- readFromHead oHd stdout+          prStderr <- readFromHead eHd stderr+          return ProcessResult {..}++  where+    readFromHead tmpH stdH = do+      hFlush stdH+      hSeek tmpH AbsoluteSeek 0+      B.hGetContents tmpH+++withBinaryTmpFile :: FilePath -> String -> ((FilePath, Handle) -> IO a) -> IO a+withBinaryTmpFile parent name =+  E.bracket+    (openBinaryTempFile parent name)+    (\(path, hd) -> do+      hClose hd+      removeFile path `E.catch` doNothing+    )++  where+    doNothing :: IOError-> IO ()+    doNothing _ = return ()+++redirectingHandle :: Handle -> Handle -> IO r -> IO r+redirectingHandle from to action = do+  saveEnc <- hGetEncoding from+  saveBuf <- hGetBuffering from+  let redirect = do+        save <- hDuplicate from+        hDuplicateTo to from+        setEnc to+        return save+      restore save = do+        hDuplicateTo save from+        setEnc from+        hSetBuffering from saveBuf++      setEnc h =+        maybe (hSetBinaryMode h True) (hSetEncoding h) saveEnc+  E.bracket redirect restore (const action)+++-- |+-- Pass the ByteString to stdin of the given IO action.+--+-- >>> import Data.ByteString.Char8 ()+-- >>> :set -XOverloadedStrings+-- >>> let main = putStrLn . reverse =<< getLine+-- >>> withStdin "abcde" main+-- edcba+withStdin :: B.ByteString -> IO a -> IO a+withStdin bs action =+  E.bracket+    prepareInputFile+    removeFile+    (\inPath ->+      withBinaryFile inPath ReadMode $ \tmpHd ->+        redirectingHandle stdin tmpHd action+    )+  where+    prepareInputFile = do+      tDir <- getTemporaryDirectory+      E.bracket+        (openBinaryTempFile tDir "test-stdin")+        (\(_path, hd) -> hClose hd)+        (\(path, hd) -> B.hPut hd bs >> return path)
+ src/Test/Main/Internal.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveGeneric #-}+#if defined(mingw32_HOST_OS)+{-# LANGUAGE RecordWildCards #-}+#endif++module Test.Main.Internal where+++import qualified Data.ByteString.Char8 as B+import           GHC.Generics (Generic)+import           System.Exit (ExitCode)++++-- | Used for the result of 'Test.Main.captureProcessResult'.+data ProcessResult =+  ProcessResult+    { prStdout :: !B.ByteString+    , prStderr :: !B.ByteString+    , prExitCode :: !ExitCode+    } deriving (Eq, Show, Generic)+++-- | 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)+  where+    nl = B.concat . B.split '\r'+#else+normalizeNewLines = id+#endif
+ test/Spec.hs view
@@ -0,0 +1,50 @@+import           Control.Monad (mapM_)+import           Data.ByteString.Char8 (pack)+import           System.Environment (getArgs)+import           System.Exit (ExitCode(ExitSuccess, ExitFailure) , exitWith)+import           System.IO (stderr, hPutStr)+import           Test.Hspec+import           Test.Hspec.QuickCheck+import           Test.QuickCheck+                   ( Arbitrary+                   , arbitrary+                   , choose+                   , oneof+                   , listOf+                   )++import           Test.Main+import           Test.Main.Internal+++newtype AExitCode = AExitCode ExitCode deriving Show++instance Arbitrary AExitCode where+  arbitrary =+    AExitCode <$> oneof [pure ExitSuccess, ExitFailure <$> choose (1, maxBound)]+++newtype PrintableAsciiString =+  PrintableAsciiString { getPrintableAsciiString :: String } deriving Show++-- https://en.wikipedia.org/wiki/ASCII#Character_set+instance Arbitrary PrintableAsciiString where+  arbitrary = PrintableAsciiString <$> listOf (choose (' ', '~'))+++main :: IO ()+main = hspec $+  describe "Test.Main" $+    prop "passes stdin and arguments to the program, and capture stdin data, stderr data, and exitCode" $+      \(argsA, inDataA, AExitCode eCode) -> do+        let args = map getPrintableAsciiString argsA+            inData = pack $ getPrintableAsciiString inDataA+            testedMain = do+              mapM_ putStrLn =<< getArgs+              hPutStr stderr =<< getContents+              exitWith eCode++        actual <- withStdin inData $ withArgs args $ captureProcessResult testedMain++        normalizeNewLines actual `shouldBe`+          normalizeNewLines (ProcessResult (pack $ unlines args) inData eCode)
+ test/doctest.hs view
@@ -0,0 +1,6 @@+module Main where++import Test.DocTest++main :: IO ()+main = doctest ["src/Test/Main.hs"]