Refactor error types returned by the library

This commit is contained in:
Aditya Manthramurthy 2017-01-31 13:05:13 +05:30
parent 5f1ee7fc67
commit f26fbc82a7
7 changed files with 118 additions and 90 deletions

View File

@ -41,6 +41,7 @@ library
, cryptonite , cryptonite
, cryptonite-conduit , cryptonite-conduit
, data-default , data-default
, exceptions
, filepath , filepath
, http-client , http-client
, http-conduit , http-conduit
@ -83,6 +84,7 @@ test-suite minio-hs-test
, cryptonite , cryptonite
, cryptonite-conduit , cryptonite-conduit
, data-default , data-default
, exceptions
, filepath , filepath
, http-client , http-client
, http-conduit , http-conduit
@ -110,6 +112,7 @@ test-suite minio-hs-test
, NoImplicitPrelude , NoImplicitPrelude
, MultiParamTypeClasses , MultiParamTypeClasses
, MultiWayIf , MultiWayIf
, ScopedTypeVariables
, RankNTypes , RankNTypes
, TupleSections , TupleSections
, TypeFamilies , TypeFamilies

View File

@ -2,6 +2,7 @@
module Network.Minio.Data where module Network.Minio.Data where
import Control.Monad.Base import Control.Monad.Base
import qualified Control.Monad.Catch as MC
import Control.Monad.Trans.Control import Control.Monad.Trans.Control
import Control.Monad.Trans.Resource import Control.Monad.Trans.Resource
import qualified Data.ByteString as B import qualified Data.ByteString as B
@ -149,25 +150,8 @@ getPathFromRI ri = B.concat $ parts
getRegionFromRI :: RequestInfo -> Text getRegionFromRI :: RequestInfo -> Text
getRegionFromRI ri = maybe "us-east-1" identity (riRegion ri) getRegionFromRI ri = maybe "us-east-1" identity (riRegion ri)
-- | Various validation errors
data MErrV = MErrVSinglePUTSizeExceeded Int64
| MErrVPutSizeExceeded Int64
| MErrVETagHeaderNotFound
deriving (Show)
-- |
-- Minio Error data type for various errors/exceptions caught and
-- returned.
data MinioErr = MErrMsg ByteString -- generic
| MErrHttp HttpException -- http exceptions
| MErrXml ByteString -- XML parsing/generation errors
| MErrService ByteString -- error response from service
| MErrValidation MErrV -- client-side validation errors
| MErrIO IOException -- exceptions while working with files
deriving (Show)
newtype Minio a = Minio { newtype Minio a = Minio {
unMinio :: ReaderT MinioConn (ExceptT MinioErr (ResourceT IO)) a unMinio :: ReaderT MinioConn (ResourceT IO) a
} }
deriving ( deriving (
Functor Functor
@ -175,14 +159,14 @@ newtype Minio a = Minio {
, Monad , Monad
, MonadIO , MonadIO
, MonadReader MinioConn , MonadReader MinioConn
, MonadError MinioErr
, MonadThrow , MonadThrow
, MC.MonadCatch
, MonadBase IO , MonadBase IO
, MonadResource , MonadResource
) )
instance MonadBaseControl IO Minio where instance MonadBaseControl IO Minio where
type StM Minio a = Either MinioErr a type StM Minio a = a
liftBaseWith f = Minio $ liftBaseWith $ \q -> f (q . unMinio) liftBaseWith f = Minio $ liftBaseWith $ \q -> f (q . unMinio)
restoreM = Minio . restoreM restoreM = Minio . restoreM
@ -203,7 +187,42 @@ connect ci = do
runMinio :: ConnectInfo -> Minio a -> ResourceT IO (Either MinioErr a) runMinio :: ConnectInfo -> Minio a -> ResourceT IO (Either MinioErr a)
runMinio ci m = do runMinio ci m = do
conn <- liftIO $ connect ci conn <- liftIO $ connect ci
runExceptT . flip runReaderT conn . unMinio $ m flip runReaderT conn . unMinio $
(m >>= (return . Right))
`MC.catch` handlerME
`MC.catch` handlerHE
`MC.catch` handlerFE
where
handlerME = return . Left . ME
handlerHE = return . Left . MEHttp
handlerFE = return . Left . MEFile
s3Name :: Text -> Name s3Name :: Text -> Name
s3Name s = Name s (Just "http://s3.amazonaws.com/doc/2006-03-01/") Nothing s3Name s = Name s (Just "http://s3.amazonaws.com/doc/2006-03-01/") Nothing
---------------------------------
-- Errors
---------------------------------
-- | Various validation errors
data MErrV = MErrVSinglePUTSizeExceeded Int64
| MErrVPutSizeExceeded Int64
| MErrVETagHeaderNotFound
deriving (Show, Eq)
-- | Errors thrown by the library
data MinioErr = ME MError
| MEHttp HttpException
| MEFile IOException
deriving (Show)
instance Exception MinioErr
-- | Library internal errors
data MError = XMLParseError Text
| ResponseError (NC.Response LByteString)
| ValidationError MErrV
deriving (Show, Eq)
instance Exception MError

View File

@ -66,7 +66,7 @@ putObject b o (ODFile fp sizeMay) = do
etag <- putObjectSingle b o [] h 0 size etag <- putObjectSingle b o [] h 0 size
R.release rKey R.release rKey
return etag return etag
| size > maxObjectSize -> throwError $ MErrValidation $ | size > maxObjectSize -> R.throwM $ ValidationError $
MErrVPutSizeExceeded size MErrVPutSizeExceeded size
| isSeekable -> parallelMultipartUpload b o fp size | isSeekable -> parallelMultipartUpload b o fp size
| otherwise -> sequentialMultipartUpload b o (Just size) $ | otherwise -> sequentialMultipartUpload b o (Just size) $

View File

@ -35,6 +35,7 @@ module Network.Minio.S3API
) where ) where
import Control.Monad.Trans.Resource
import qualified Data.Conduit as C import qualified Data.Conduit as C
import Data.Default (def) import Data.Default (def)
import qualified Network.HTTP.Conduit as NC import qualified Network.HTTP.Conduit as NC
@ -96,7 +97,7 @@ putObjectSingle :: Bucket -> Object -> [HT.Header] -> Handle -> Int64
putObjectSingle bucket object headers h offset size = do putObjectSingle bucket object headers h offset size = do
-- check length is within single PUT object size. -- check length is within single PUT object size.
when (size > maxSinglePutObjectSizeBytes) $ when (size > maxSinglePutObjectSizeBytes) $
throwError $ MErrValidation $ MErrVSinglePUTSizeExceeded size throwM $ ValidationError $ MErrVSinglePUTSizeExceeded size
-- content-length header is automatically set by library. -- content-length header is automatically set by library.
resp <- executeRequest $ resp <- executeRequest $
@ -110,7 +111,7 @@ putObjectSingle bucket object headers h offset size = do
let rheaders = NC.responseHeaders resp let rheaders = NC.responseHeaders resp
etag = getETagHeader rheaders etag = getETagHeader rheaders
maybe maybe
(throwError $ MErrValidation MErrVETagHeaderNotFound) (throwM $ ValidationError MErrVETagHeaderNotFound)
return etag return etag
@ -176,7 +177,7 @@ putObjectPart bucket object uploadId partNumber headers payload = do
let rheaders = NC.responseHeaders resp let rheaders = NC.responseHeaders resp
etag = getETagHeader rheaders etag = getETagHeader rheaders
maybe maybe
(throwError $ MErrValidation MErrVETagHeaderNotFound) (throwM $ ValidationError MErrVETagHeaderNotFound)
(return . PartInfo partNumber) etag (return . PartInfo partNumber) etag
where where
params = [ params = [

View File

@ -3,9 +3,10 @@ module Network.Minio.Utils where
import qualified Control.Concurrent.Async.Lifted as A import qualified Control.Concurrent.Async.Lifted as A
import qualified Control.Concurrent.QSem as Q import qualified Control.Concurrent.QSem as Q
import qualified Control.Exception.Lifted as ExL import qualified Control.Exception.Lifted as ExL
import Control.Monad.Catch (throwM)
import qualified Control.Monad.Catch as MC
import Control.Monad.Trans.Control (liftBaseOp_, StM) import Control.Monad.Trans.Control (liftBaseOp_, StM)
import qualified Control.Monad.Trans.Resource as R import qualified Control.Monad.Trans.Resource as R
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Conduit as C import qualified Data.Conduit as C
import Data.Text.Encoding.Error (lenientDecode) import Data.Text.Encoding.Error (lenientDecode)
import qualified Network.HTTP.Client as NClient import qualified Network.HTTP.Client as NClient
@ -18,26 +19,27 @@ import Lib.Prelude
import Network.Minio.Data import Network.Minio.Data
allocateReadFile :: (R.MonadResource m, MonadError MinioErr m) allocateReadFile :: (R.MonadResource m, R.MonadResourceBase m)
=> FilePath -> m (R.ReleaseKey, Handle) => FilePath -> m (R.ReleaseKey, Handle)
allocateReadFile fp = do allocateReadFile fp = do
(rk, hdlE) <- R.allocate (openReadFile fp) cleanup (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
either (throwError . MErrIO) (return . (rk,)) hdlE either (MC.throwM . MEFile) (return . (rk,)) hdlE
where where
openReadFile f = ExL.try $ IO.openBinaryFile f IO.ReadMode openReadFile f = ExL.try $ IO.openBinaryFile f IO.ReadMode
cleanup = either (const $ return ()) IO.hClose cleanup = either (const $ return ()) IO.hClose
getFileSize :: (R.MonadResourceBase m, R.MonadResource m, MonadError MinioErr m) getFileSize :: (R.MonadResourceBase m, R.MonadResource m)
=> Handle -> m (Either IOException Int64) => Handle -> m (Either IOException Int64)
getFileSize h = ExL.try $ liftIO $ fromIntegral <$> IO.hFileSize h getFileSize h = ExL.try $ liftIO $ fromIntegral <$> IO.hFileSize h
isFileSeekable :: (R.MonadResource m, MonadError MinioErr m) isFileSeekable :: (R.MonadResource m, R.MonadResourceBase m)
=> FilePath -> m Bool => FilePath -> m Bool
isFileSeekable fp = do isFileSeekable fp = do
(rKey, h) <- allocateReadFile fp (rKey, h) <- allocateReadFile fp
isSeekable <- liftIO $ IO.hIsSeekable h resE <- liftIO $ try $ IO.hIsSeekable h
R.release rKey R.release rKey
return isSeekable either (MC.throwM . MEFile) return resE
lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString lookupHeader :: HT.HeaderName -> [HT.Header] -> Maybe ByteString
lookupHeader hdr = headMay . map snd . filter (\(h, _) -> h == hdr) lookupHeader hdr = headMay . map snd . filter (\(h, _) -> h == hdr)
@ -52,30 +54,34 @@ isSuccessStatus :: HT.Status -> Bool
isSuccessStatus sts = let s = HT.statusCode sts isSuccessStatus sts = let s = HT.statusCode sts
in (s >= 200 && s < 300) in (s >= 200 && s < 300)
checkEither :: (Monad m, MonadError MinioErr m) httpLbs :: (R.MonadThrow m, MonadIO m)
=> (e -> MinioErr) -> (Either e a) -> m a
checkEither f = either (throwError . f) return
httpLbs :: (MonadError MinioErr m, MonadIO m)
=> NC.Request -> NC.Manager => NC.Request -> NC.Manager
-> m (NC.Response LByteString) -> m (NC.Response LByteString)
httpLbs req mgr = do httpLbs req mgr = do
respE <- liftIO $ try $ NClient.httpLbs req mgr respE <- liftIO $ tryHttpEx $ (NClient.httpLbs req mgr)
resp <- checkEither MErrHttp respE resp <- either throwM return respE
unless (isSuccessStatus $ NC.responseStatus resp) $ unless (isSuccessStatus $ NC.responseStatus resp) $
throwError $ MErrService $ LBS.toStrict $ NC.responseBody resp throwM $ ResponseError resp
return resp return resp
where
tryHttpEx :: (IO (NC.Response LByteString))
-> IO (Either NC.HttpException (NC.Response LByteString))
tryHttpEx = try
http :: (MonadError MinioErr m, R.MonadResourceBase m, R.MonadResource m) http :: (R.MonadResourceBase m, R.MonadResource m)
=> NC.Request -> NC.Manager => NC.Request -> NC.Manager
-> m (Response (C.ResumableSource m ByteString)) -> m (Response (C.ResumableSource m ByteString))
http req mgr = do http req mgr = do
respE <- ExL.try $ NC.http req mgr respE <- tryHttpEx $ NC.http req mgr
resp <- checkEither MErrHttp respE resp <- either throwM return respE
unless (isSuccessStatus $ NC.responseStatus resp) $ do unless (isSuccessStatus $ NC.responseStatus resp) $ do
lbsResp <- NC.lbsResponse resp lbsResp <- NC.lbsResponse resp
throwError $ MErrService $ LBS.toStrict $ NC.responseBody lbsResp throwM $ ResponseError lbsResp
return resp return resp
where
tryHttpEx :: (R.MonadResourceBase m) => (m a)
-> m (Either NC.HttpException a)
tryHttpEx = ExL.try
-- like mapConcurrently but with a limited number of concurrent -- like mapConcurrently but with a limited number of concurrent
-- threads. -- threads.

View File

@ -8,6 +8,7 @@ module Network.Minio.XmlParser
, parseListPartsResponse , parseListPartsResponse
) where ) where
import Control.Monad.Trans.Resource
import Data.List (zip3, zip4) import Data.List (zip3, zip4)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Read (decimal) import Data.Text.Read (decimal)
@ -32,24 +33,24 @@ s3TimeFormat :: [Char]
s3TimeFormat = iso8601DateFormat $ Just "%T%QZ" s3TimeFormat = iso8601DateFormat $ Just "%T%QZ"
-- | Parse time strings from XML -- | Parse time strings from XML
parseS3XMLTime :: (MonadError MinioErr m) => Text -> m UTCTime parseS3XMLTime :: (MonadThrow m) => Text -> m UTCTime
parseS3XMLTime = either (throwError . MErrXml) return parseS3XMLTime = either (throwM . XMLParseError) return
. parseTimeM True defaultTimeLocale s3TimeFormat . parseTimeM True defaultTimeLocale s3TimeFormat
. T.unpack . T.unpack
parseDecimals :: (MonadError MinioErr m, Integral a) => [Text] -> m [a] parseDecimals :: (MonadThrow m, Integral a) => [Text] -> m [a]
parseDecimals numStr = forM numStr $ \str -> parseDecimals numStr = forM numStr $ \str ->
either (throwError . MErrXml . show) return $ fst <$> decimal str either (throwM . XMLParseError . show) return $ fst <$> decimal str
s3Elem :: Text -> Axis s3Elem :: Text -> Axis
s3Elem = element . s3Name s3Elem = element . s3Name
parseRoot :: (MonadError MinioErr m) => LByteString -> m Cursor parseRoot :: (MonadThrow m) => LByteString -> m Cursor
parseRoot = either (throwError . MErrXml . show) (return . fromDocument) parseRoot = either (throwM . XMLParseError . show) (return . fromDocument)
. parseLBS def . parseLBS def
-- | Parse the response XML of a list buckets call. -- | Parse the response XML of a list buckets call.
parseListBuckets :: (MonadError MinioErr m) => LByteString -> m [BucketInfo] parseListBuckets :: (MonadThrow m) => LByteString -> m [BucketInfo]
parseListBuckets xmldata = do parseListBuckets xmldata = do
r <- parseRoot xmldata r <- parseRoot xmldata
let let
@ -60,27 +61,27 @@ parseListBuckets xmldata = do
return $ map (\(n, t) -> BucketInfo n t) $ zip names times return $ map (\(n, t) -> BucketInfo n t) $ zip names times
-- | Parse the response XML of a location request. -- | Parse the response XML of a location request.
parseLocation :: (MonadError MinioErr m) => LByteString -> m Region parseLocation :: (MonadThrow m) => LByteString -> m Region
parseLocation xmldata = do parseLocation xmldata = do
r <- parseRoot xmldata r <- parseRoot xmldata
return $ T.concat $ r $/ content return $ T.concat $ r $/ content
-- | Parse the response XML of an newMultipartUpload call. -- | Parse the response XML of an newMultipartUpload call.
parseNewMultipartUpload :: (MonadError MinioErr m) parseNewMultipartUpload :: (MonadThrow m)
=> LByteString -> m UploadId => LByteString -> m UploadId
parseNewMultipartUpload xmldata = do parseNewMultipartUpload xmldata = do
r <- parseRoot xmldata r <- parseRoot xmldata
return $ T.concat $ r $// element (s3Name "UploadId") &/ content return $ T.concat $ r $// element (s3Name "UploadId") &/ content
-- | Parse the response XML of completeMultipartUpload call. -- | Parse the response XML of completeMultipartUpload call.
parseCompleteMultipartUploadResponse :: (MonadError MinioErr m) parseCompleteMultipartUploadResponse :: (MonadThrow m)
=> LByteString -> m ETag => LByteString -> m ETag
parseCompleteMultipartUploadResponse xmldata = do parseCompleteMultipartUploadResponse xmldata = do
r <- parseRoot xmldata r <- parseRoot xmldata
return $ T.concat $ r $// s3Elem "ETag" &/ content return $ T.concat $ r $// s3Elem "ETag" &/ content
-- | Parse the response XML of a list objects call. -- | Parse the response XML of a list objects call.
parseListObjectsResponse :: (MonadError MinioErr m) parseListObjectsResponse :: (MonadThrow m)
=> LByteString -> m ListObjectsResult => LByteString -> m ListObjectsResult
parseListObjectsResponse xmldata = do parseListObjectsResponse xmldata = do
r <- parseRoot xmldata r <- parseRoot xmldata
@ -106,7 +107,7 @@ parseListObjectsResponse xmldata = do
return $ ListObjectsResult hasMore nextToken objects prefixes return $ ListObjectsResult hasMore nextToken objects prefixes
-- | Parse the response XML of a list incomplete multipart upload call. -- | Parse the response XML of a list incomplete multipart upload call.
parseListUploadsResponse :: (MonadError MinioErr m) parseListUploadsResponse :: (MonadThrow m)
=> LByteString -> m ListUploadsResult => LByteString -> m ListUploadsResult
parseListUploadsResponse xmldata = do parseListUploadsResponse xmldata = do
r <- parseRoot xmldata r <- parseRoot xmldata
@ -126,7 +127,7 @@ parseListUploadsResponse xmldata = do
return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes
parseListPartsResponse :: (MonadError MinioErr m) parseListPartsResponse :: (MonadThrow m)
=> LByteString -> m ListPartsResult => LByteString -> m ListPartsResult
parseListPartsResponse xmldata = do parseListPartsResponse xmldata = do
r <- parseRoot xmldata r <- parseRoot xmldata

View File

@ -3,9 +3,10 @@ module Network.Minio.XmlParser.Test
xmlParserTests xmlParserTests
) where ) where
import qualified Control.Monad.Catch as MC
import Data.Time (fromGregorian, UTCTime(..))
import Test.Tasty import Test.Tasty
import Test.Tasty.HUnit import Test.Tasty.HUnit
import Data.Time (fromGregorian, UTCTime(..))
import Lib.Prelude import Lib.Prelude
@ -22,19 +23,26 @@ xmlParserTests = testGroup "XML Parser Tests"
, testCase "Test parseListPartsResponse" testParseListPartsResponse , testCase "Test parseListPartsResponse" testParseListPartsResponse
] ]
tryMError :: (MC.MonadCatch m) => m a -> m (Either MError a)
tryMError act = MC.try act
assertMError :: MError -> Assertion
assertMError e = assertFailure $ "Failed due to exception => " ++ show e
eitherMError :: Either MError a -> (a -> Assertion) -> Assertion
eitherMError (Left e) _ = assertMError e
eitherMError (Right a) f = f a
testParseLocation :: Assertion testParseLocation :: Assertion
testParseLocation = do testParseLocation = do
-- 1. Test parsing of an invalid location constraint xml. -- 1. Test parsing of an invalid location constraint xml.
parsedLocationE <- runExceptT $ parseLocation "ClearlyInvalidXml" parseResE <- tryMError $ parseLocation "ClearlyInvalidXml"
case parsedLocationE of when (isRight parseResE) $
Right _ -> assertFailure $ "Parsing should have failed => " ++ show parsedLocationE assertFailure $ "Parsing should have failed => " ++ show parseResE
Left _ -> return ()
forM_ cases $ \(xmldata, expectedLocation) -> do forM_ cases $ \(xmldata, expectedLocation) -> do
parsedLocationE1 <- runExceptT $ parseLocation xmldata parseLocE <- tryMError $ parseLocation xmldata
case parsedLocationE1 of either assertMError (@?= expectedLocation) parseLocE
Right parsedLocation -> parsedLocation @?= expectedLocation
_ -> assertFailure $ "Parsing failed => " ++ show parsedLocationE1
where where
cases = [ cases = [
-- 2. Test parsing of a valid location xml. -- 2. Test parsing of a valid location xml.
@ -53,10 +61,8 @@ testParseLocation = do
testParseNewMultipartUpload :: Assertion testParseNewMultipartUpload :: Assertion
testParseNewMultipartUpload = do testParseNewMultipartUpload = do
forM_ cases $ \(xmldata, expectedUploadId) -> do forM_ cases $ \(xmldata, expectedUploadId) -> do
parsedUploadIdE <- runExceptT $ parseNewMultipartUpload xmldata parsedUploadIdE <- tryMError $ parseNewMultipartUpload xmldata
case parsedUploadIdE of eitherMError parsedUploadIdE (@?= expectedUploadId)
Right upId -> upId @?= expectedUploadId
_ -> assertFailure $ "Parsing failed => " ++ show parsedUploadIdE
where where
cases = [ cases = [
("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
@ -100,10 +106,8 @@ testParseListObjectsResult = do
object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234
modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12 modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
parsedListObjectsResult <- runExceptT $ parseListObjectsResponse xmldata parsedListObjectsResult <- tryMError $ parseListObjectsResponse xmldata
case parsedListObjectsResult of eitherMError parsedListObjectsResult (@?= expectedListResult)
Right listObjectsResult -> listObjectsResult @?= expectedListResult
_ -> assertFailure $ "Parsing failed => " ++ show parsedListObjectsResult
testParseListIncompleteUploads :: Assertion testParseListIncompleteUploads :: Assertion
testParseListIncompleteUploads = do testParseListIncompleteUploads = do
@ -144,10 +148,8 @@ testParseListIncompleteUploads = do
initTime = UTCTime (fromGregorian 2010 11 26) 69857 initTime = UTCTime (fromGregorian 2010 11 26) 69857
prefixes = ["photos/", "videos/"] prefixes = ["photos/", "videos/"]
parsedListUploadsResult <- runExceptT $ parseListUploadsResponse xmldata parsedListUploadsResult <- tryMError $ parseListUploadsResponse xmldata
case parsedListUploadsResult of eitherMError parsedListUploadsResult (@?= expectedListResult)
Right listUploadsResult -> listUploadsResult @?= expectedListResult
_ -> assertFailure $ "Parsing failed => " ++ show parsedListUploadsResult
testParseCompleteMultipartUploadResponse :: Assertion testParseCompleteMultipartUploadResponse :: Assertion
@ -163,9 +165,7 @@ testParseCompleteMultipartUploadResponse = do
expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\"" expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
parsedETagE <- runExceptT $ parseCompleteMultipartUploadResponse xmldata parsedETagE <- runExceptT $ parseCompleteMultipartUploadResponse xmldata
case parsedETagE of eitherMError parsedETagE (@?= expectedETag)
Right actualETag -> actualETag @?= expectedETag
_ -> assertFailure $ "Parsing failed => " ++ show parsedETagE
testParseListPartsResponse :: Assertion testParseListPartsResponse :: Assertion
testParseListPartsResponse = do testParseListPartsResponse = do
@ -209,6 +209,4 @@ testParseListPartsResponse = do
modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10 modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
parsedListPartsResult <- runExceptT $ parseListPartsResponse xmldata parsedListPartsResult <- runExceptT $ parseListPartsResponse xmldata
case parsedListPartsResult of eitherMError parsedListPartsResult (@?= expectedListResult)
Right listPartsResult -> listPartsResult @?= expectedListResult
_ -> assertFailure $ "Parsing failed => " ++ show parsedListPartsResult