packages feed

miss-0: tests/TestUtil.hs

{-# LANGUAGE ImplicitParams      #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}

{-# OPTIONS_GHC -fno-warn-orphans #-}

module TestUtil
  ( module Test.Tasty
  , module Test.Tasty.HUnit
  , module Test.Tasty.TestVector
  , module Test.Tasty.QuickCheck
  , testContact, testDate, testCommit, testTag
  , notTheEmptyTreeSha
  , testInDir, testPropInDir, testWithRepo, testRepoProp, testBottom
  , expectThrow
  , ascii2bytestring
  , shouldReturn, shouldNotChange
  , shouldSatisfy, shouldNotSatisfy
  -- Special types to help generate property tests.
  , ArbMode(..)
  , ArbDate(..)
  , zeroSha
  ) where

import qualified Control.Monad.Catch as E
import           Control.Monad.IO.Class
import           Data.Bifunctor
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import           Data.Char (ord)
import qualified Data.Map as Map
import           Data.Word
import           System.Directory
import           System.IO.Temp
import           Test.ChasingBottoms (isBottom)
import           Test.Tasty
import           Test.Tasty.HUnit
import           Test.Tasty.TestVector
import           Test.Tasty.QuickCheck
import           Text.Printf

import Data.Git.Formats
import Data.Git.Hash
import Data.Git.Monad
import Data.Git.Object
import Data.Git.Types

zeroSha :: Sha1
zeroSha = fromHex "0000000000000000000000000000000000000000"

instance Arbitrary Sha1 where
  arbitrary = Sha1 . BS.pack <$> vector 20
  -- There are no valid shrinkings for a SHA
  shrink _ = []

instance Arbitrary Sha1Hex where
  arbitrary =
    Sha1Hex . BS.pack <$> vectorOf 40 (elements $ map (fromIntegral.ord) "0123456789abcdef")
  -- There are no valid shrinkings for a SHA
  shrink _ = []

instance Arbitrary BS.ByteString where
  arbitrary = BS.pack <$> arbitrary
  -- Shrink to nothing to see if the ByteString actually mattered for the error.
  -- Further shrinks are probably uninteresting since we don't expect the bytestring
  -- structure to matter.
  shrink _ = [mempty]

instance Arbitrary BL.ByteString where
  arbitrary = BL.pack <$> arbitrary
  -- Shrink to nothing to see if the ByteString actually mattered for the error.
  -- Further shrinks are probably uninteresting since we don't expect the bytestring
  -- structure to matter.
  shrink _ = [mempty]

newtype ArbMode =
    AM { unAM :: Mode }
  deriving (Eq, Ord, Show)

instance Arbitrary ArbMode where
  arbitrary = AM <$> elements [BlobMode, ExecMode, LinkMode]
  -- It is what it is
  shrink _ = []

newtype ArbDate =
    AD { unAD :: Date }
  deriving (Eq, Ord, Show)

instance Arbitrary ArbDate where
  arbitrary = do
    -- Git only seems to handle times since the Unix epoc.
    p <- arbitrary::Gen Word
    z <- elements . map ascii2bytestring $ [ "+0000", "-0500", "-0400", "+0930", "UTC" ]
    return (AD (fromIntegral p, z))

instance Arbitrary SafeString where
  arbitrary =
      frequency
      [ (1, pure "")
      , (20, do
           f <- suchThat arbitrary (`BS.notElem` (ascii2bytestring " .,:;<>\"'"))
           m <- listOf1 (suchThat arbitrary (`BS.notElem` (ascii2bytestring "<>\x0a\x00")))
           l <- suchThat arbitrary (`BS.notElem` (ascii2bytestring " .,:;<>\"'"))
           let b = BS.pack $ f:m++[l]
           maybe (fail $ "SafeString Arbitrary doesn't match definition") pure .
                 safeString $ b)
      ]

instance Arbitrary Contact where
  arbitrary = Contact <$> arbitrary <*> arbitrary

instance Arbitrary Blob where
  arbitrary = Blob <$> arbitrary
  -- A blob is an atomic object
  shrink _ = []

instance Arbitrary PathComponent where
  arbitrary =
    (pathComponent . BS.pack) <$> listOf1 (suchThat arbitrary (`BS.notElem` "\x00/")) >>=
    maybe (fail "PathComponent Arbitrary doesn't match definition") pure

instance Arbitrary LfFree where
  arbitrary =
    (lfFree . BS.pack) <$> listOf1 (suchThat arbitrary (`BS.notElem` "\x0a")) >>=
    maybe (fail "LfFree Arbitrary doesn't match definition") pure

instance Arbitrary TreeEntry where
  arbitrary = Entry <$> arbitrary <*> (unAM <$> arbitrary)

instance Arbitrary Tree where
  arbitrary = (Tree . Map.fromList) <$> listOf arbitrary

instance Arbitrary ObjectType where
  arbitrary = elements [BlobType, TreeType, CommitType, TagType]

instance Arbitrary Tag where
  arbitrary =
    Tag <$> arbitrary  <*> arbitrary <*> arbitrary <*> (second unAD <$> arbitrary) <*> arbitrary

instance Arbitrary Commit where
  arbitrary =
    Commit <$> arbitrary
           <*> listOf arbitrary
           <*> (second unAD <$> arbitrary)
           <*> (second unAD <$> arbitrary)
           <*> arbitrary

instance Arbitrary Object where
  arbitrary = oneof
              [ BlobObj   <$> arbitrary
              , TreeObj   <$> arbitrary
              , CommitObj <$> arbitrary
              , TagObj    <$> arbitrary
              ]

testContact :: Contact
testContact = makeContact "Bob McTesterton" "bob@mctesterton.gov"

testDate :: Date
testDate = (1467644623, "+0000")

testCommit :: Commit
testCommit = Commit { commitTree=emptyTreeSha
                    , commitParents=[]
                    , commitAuthor=(testContact, testDate)
                    , commitCommitter=(testContact, testDate)
                    , commitMessage="msg" }

testTag :: Tag
testTag = Tag emptyTreeSha TreeType "the-tag" (testContact, testDate) "the message"

escapeFileTestName :: String -> String
escapeFileTestName = map (\case {' ' -> '_'; a -> a })

notTheEmptyTreeSha :: Sha1
notTheEmptyTreeSha = sha1 $ Blob "a thing which isn't the empty tree"

expectThrow :: (MonadIO m, E.MonadCatch m) => m () -> m ()
expectThrow act =
    E.catch (act >> liftIO (assertFailure "Didn't throw"))
            (\(_::E.SomeException) -> return ())

-- | Creates a temporary directory, cdsinto it, and runs the test
--   Ifthe tests fails, save and report the directory otherwise cleans up.
testInDir :: TestName -> IO () -> TestTree
testInDir name act =
  testCase name $ getCanonicalTemporaryDirectory >>= \stmpdir -> do
    tdir <- createTempDirectory stmpdir (printf "miss-test-%s" (escapeFileTestName name))
    E.handle (\(e::E.SomeException) -> do
               printf "\nTest working directory saved at %s\n" tdir
               E.throwM e) $
          withCurrentDirectory tdir $ act
    removeDirectoryRecursive tdir

testPropInDir :: (Show a, Arbitrary a, Testable prop) => TestName -> (a -> IO prop) -> TestTree
testPropInDir name prop =
  testProperty name $ \a -> ioProperty $ getCanonicalTemporaryDirectory >>= \stmpdir -> do
    tdir <- createTempDirectory stmpdir (printf "miss-test-%s" (escapeFileTestName name))
    r <- E.handle (\(e::E.SomeException) -> do
               printf "\nTest working directory saved at %s\n" tdir
               E.throwM e) $
          withCurrentDirectory tdir $ prop a
    removeDirectoryRecursive tdir
    return r

testRepoProp :: (Show a, Arbitrary a, Testable prop) => TestName -> (a -> Git prop) -> TestTree
testRepoProp name prop = testPropInDir name (\a -> initRepo Nothing >> runGit ".git" (prop a))

-- | Test that something did fail when it should
testBottom :: TestName -> a -> TestTree
testBottom name = testCase name . assertBool "Not bottom!" . isBottom


-- | Create a test repository in a temp directory rooted at the given path, run the given action,
--   and then delete the repository if no exception is thrown.
testWithRepo :: TestName -> Git () -> TestTree
testWithRepo name git = testInDir name (initRepo Nothing >> runGit ".git" git)

ascii2word8 :: String -> [Word8]
ascii2word8 = map (fromIntegral.ord)

ascii2bytestring :: String -> BS.ByteString
ascii2bytestring = BS.pack . ascii2word8

infix 1 `shouldReturn`, `shouldNotChange`, `shouldSatisfy`, `shouldNotSatisfy`

-- |
-- @action \`shouldReturn\` expected@ sets the expectation that @action@
-- returns @expected@.
shouldReturn :: (HasCallStack, Show a, Eq a, MonadIO m) => m a -> a -> m ()
action `shouldReturn` expected = action >>= liftIO . (@?= expected)

shouldNotChange :: (HasCallStack, Eq a, Show a, MonadIO m) => (a -> m a) -> a -> m ()
shouldNotChange f a = f a `shouldReturn` a

-- |
-- @v \`shouldSatisfy\` p@ sets the expectation that @p v@ is @True@.
shouldSatisfy :: (HasCallStack, Show a, MonadIO m) => a -> (a -> Bool) -> m ()
shouldSatisfy v p = liftIO $ (p v) @?= True

-- |
-- @v \`shouldSatisfy\` p@ sets the expectation that @p v@ is @True@.
shouldNotSatisfy :: (HasCallStack, Show a, MonadIO m) => a -> (a -> Bool) -> m ()
shouldNotSatisfy v p = liftIO $ (p v) @?= False