Switch to SIO

This commit is contained in:
Michael Snoyman 2018-01-15 17:38:36 +02:00
parent a210ce59d7
commit dff7f2372e
No known key found for this signature in database
GPG Key ID: A048E8C057E86876

View File

@ -6,6 +6,7 @@
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ImplicitParams #-} {-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-| {-|
Yesod.Test is a pragmatic framework for testing web applications built Yesod.Test is a pragmatic framework for testing web applications built
@ -63,6 +64,7 @@ module Yesod.Test
, addFile , addFile
, setRequestBody , setRequestBody
, RequestBuilder , RequestBuilder
, SIO
, setUrl , setUrl
, clickOn , clickOn
@ -135,7 +137,7 @@ import qualified Network.Socket.Internal as Sock
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import Network.Wai import Network.Wai
import Network.Wai.Test hiding (assertHeader, assertNoHeader, request) import Network.Wai.Test hiding (assertHeader, assertNoHeader, request)
import Control.Monad.Trans.Reader import Control.Monad.Trans.Reader (ReaderT (..))
import Data.IORef import Data.IORef
import Control.Monad.IO.Class import Control.Monad.IO.Class
import System.IO import System.IO
@ -181,7 +183,7 @@ data YesodExampleData site = YesodExampleData
-- | A single test case, to be run with 'yit'. -- | A single test case, to be run with 'yit'.
-- --
-- Since 1.2.0 -- Since 1.2.0
type YesodExample site = ReaderT (IORef (YesodExampleData site)) IO type YesodExample site = SIO (YesodExampleData site)
-- | Mapping from cookie name to value. -- | Mapping from cookie name to value.
-- --
@ -204,13 +206,13 @@ data YesodSpecTree site
-- --
-- Since 1.2.0 -- Since 1.2.0
getTestYesod :: YesodExample site site getTestYesod :: YesodExample site site
getTestYesod = fmap yedSite rsget getTestYesod = fmap yedSite getSIO
-- | Get the most recently provided response value, if available. -- | Get the most recently provided response value, if available.
-- --
-- Since 1.2.0 -- Since 1.2.0
getResponse :: YesodExample site (Maybe SResponse) getResponse :: YesodExample site (Maybe SResponse)
getResponse = fmap yedResponse rsget getResponse = fmap yedResponse getSIO
data RequestBuilderData site = RequestBuilderData data RequestBuilderData site = RequestBuilderData
{ rbdPostData :: RBDPostData { rbdPostData :: RBDPostData
@ -233,7 +235,7 @@ data RequestPart
-- | The 'RequestBuilder' state monad constructs a URL encoded string of arguments -- | The 'RequestBuilder' state monad constructs a URL encoded string of arguments
-- to send with your requests. Some of the functions that run on it use the current -- to send with your requests. Some of the functions that run on it use the current
-- response to analyze the forms that the server is expecting to receive. -- response to analyze the forms that the server is expecting to receive.
type RequestBuilder site = ReaderT (IORef (RequestBuilderData site)) IO type RequestBuilder site = SIO (RequestBuilderData site)
-- | Start describing a Tests suite keeping cookies and a reference to the tested 'Application' -- | Start describing a Tests suite keeping cookies and a reference to the tested 'Application'
-- and 'ConnectionPool' -- and 'ConnectionPool'
@ -250,7 +252,7 @@ yesodSpec site yspecs =
unYesod (YesodSpecGroup x y) = Hspec.specGroup x $ map unYesod y unYesod (YesodSpecGroup x y) = Hspec.specGroup x $ map unYesod y
unYesod (YesodSpecItem x y) = Hspec.specItem x $ do unYesod (YesodSpecItem x y) = Hspec.specItem x $ do
app <- toWaiAppPlain site app <- toWaiAppPlain site
rsevalStateT y YesodExampleData evalSIO y YesodExampleData
{ yedApp = app { yedApp = app
, yedSite = site , yedSite = site
, yedCookies = M.empty , yedCookies = M.empty
@ -270,7 +272,7 @@ yesodSpecWithSiteGenerator getSiteAction yspecs =
unYesod getSiteAction' (YesodSpecItem x y) = Hspec.specItem x $ do unYesod getSiteAction' (YesodSpecItem x y) = Hspec.specItem x $ do
site <- getSiteAction' site <- getSiteAction'
app <- toWaiAppPlain site app <- toWaiAppPlain site
rsevalStateT y YesodExampleData evalSIO y YesodExampleData
{ yedApp = app { yedApp = app
, yedSite = site , yedSite = site
, yedCookies = M.empty , yedCookies = M.empty
@ -291,7 +293,7 @@ yesodSpecApp site getApp yspecs =
unYesod (YesodSpecGroup x y) = Hspec.specGroup x $ map unYesod y unYesod (YesodSpecGroup x y) = Hspec.specGroup x $ map unYesod y
unYesod (YesodSpecItem x y) = Hspec.specItem x $ do unYesod (YesodSpecItem x y) = Hspec.specItem x $ do
app <- getApp app <- getApp
rsevalStateT y YesodExampleData evalSIO y YesodExampleData
{ yedApp = app { yedApp = app
, yedSite = site , yedSite = site
, yedCookies = M.empty , yedCookies = M.empty
@ -304,12 +306,11 @@ yit label example = tell [YesodSpecItem label example]
-- Performs a given action using the last response. Use this to create -- Performs a given action using the last response. Use this to create
-- response-level assertions -- response-level assertions
withResponse' :: MonadIO m withResponse' :: (state -> Maybe SResponse)
=> (state -> Maybe SResponse)
-> [T.Text] -> [T.Text]
-> (SResponse -> ReaderT (IORef state) m a) -> (SResponse -> SIO state a)
-> ReaderT (IORef state) m a -> SIO state a
withResponse' getter errTrace f = maybe err f . getter =<< rsget withResponse' getter errTrace f = maybe err f . getter =<< getSIO
where err = failure msg where err = failure msg
msg = if null errTrace msg = if null errTrace
then "There was no response, you should make a request." then "There was no response, you should make a request."
@ -328,11 +329,10 @@ parseHTML :: HtmlLBS -> Cursor
parseHTML html = fromDocument $ HD.parseLBS html parseHTML html = fromDocument $ HD.parseLBS html
-- | Query the last response using CSS selectors, returns a list of matched fragments -- | Query the last response using CSS selectors, returns a list of matched fragments
htmlQuery' :: MonadIO m htmlQuery' :: (state -> Maybe SResponse)
=> (state -> Maybe SResponse)
-> [T.Text] -> [T.Text]
-> Query -> Query
-> ReaderT (IORef state) m [HtmlLBS] -> SIO state [HtmlLBS]
htmlQuery' getter errTrace query = withResponse' getter ("Tried to invoke htmlQuery' in order to read HTML of a previous response." : errTrace) $ \ res -> htmlQuery' getter errTrace query = withResponse' getter ("Tried to invoke htmlQuery' in order to read HTML of a previous response." : errTrace) $ \ res ->
case findBySelector (simpleBody res) query of case findBySelector (simpleBody res) query of
Left err -> failure $ query <> " did not parse: " <> T.pack (show err) Left err -> failure $ query <> " did not parse: " <> T.pack (show err)
@ -497,14 +497,14 @@ printMatches query = do
-- | Add a parameter with the given name and value to the request body. -- | Add a parameter with the given name and value to the request body.
addPostParam :: T.Text -> T.Text -> RequestBuilder site () addPostParam :: T.Text -> T.Text -> RequestBuilder site ()
addPostParam name value = addPostParam name value =
rsmodify $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd)) } modifySIO $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd)) }
where addPostData (BinaryPostData _) = error "Trying to add post param to binary content." where addPostData (BinaryPostData _) = error "Trying to add post param to binary content."
addPostData (MultipleItemsPostData posts) = addPostData (MultipleItemsPostData posts) =
MultipleItemsPostData $ ReqKvPart name value : posts MultipleItemsPostData $ ReqKvPart name value : posts
-- | Add a parameter with the given name and value to the query string. -- | Add a parameter with the given name and value to the query string.
addGetParam :: T.Text -> T.Text -> RequestBuilder site () addGetParam :: T.Text -> T.Text -> RequestBuilder site ()
addGetParam name value = rsmodify $ \rbd -> rbd addGetParam name value = modifySIO $ \rbd -> rbd
{ rbdGets = (TE.encodeUtf8 name, Just $ TE.encodeUtf8 value) { rbdGets = (TE.encodeUtf8 name, Just $ TE.encodeUtf8 value)
: rbdGets rbd : rbdGets rbd
} }
@ -523,7 +523,7 @@ addFile :: T.Text -- ^ The parameter name for the file.
-> RequestBuilder site () -> RequestBuilder site ()
addFile name path mimetype = do addFile name path mimetype = do
contents <- liftIO $ BSL8.readFile path contents <- liftIO $ BSL8.readFile path
rsmodify $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd) contents) } modifySIO $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd) contents) }
where addPostData (BinaryPostData _) _ = error "Trying to add file after setting binary content." where addPostData (BinaryPostData _) _ = error "Trying to add file after setting binary content."
addPostData (MultipleItemsPostData posts) contents = addPostData (MultipleItemsPostData posts) contents =
MultipleItemsPostData $ ReqFilePart name path contents mimetype : posts MultipleItemsPostData $ ReqFilePart name path contents mimetype : posts
@ -532,7 +532,7 @@ addFile name path mimetype = do
-- This looks up the name of a field based on the contents of the label pointing to it. -- This looks up the name of a field based on the contents of the label pointing to it.
genericNameFromLabel :: (T.Text -> T.Text -> Bool) -> T.Text -> RequestBuilder site T.Text genericNameFromLabel :: (T.Text -> T.Text -> Bool) -> T.Text -> RequestBuilder site T.Text
genericNameFromLabel match label = do genericNameFromLabel match label = do
mres <- fmap rbdResponse rsget mres <- fmap rbdResponse getSIO
res <- res <-
case mres of case mres of
Nothing -> failure "genericNameFromLabel: No response available" Nothing -> failure "genericNameFromLabel: No response available"
@ -799,7 +799,7 @@ addTokenFromCookieNamedToHeaderNamed cookieName headerName = do
-- Since 1.4.3.2 -- Since 1.4.3.2
getRequestCookies :: RequestBuilder site Cookies getRequestCookies :: RequestBuilder site Cookies
getRequestCookies = do getRequestCookies = do
requestBuilderData <- rsget requestBuilderData <- getSIO
headers <- case simpleHeaders Control.Applicative.<$> rbdResponse requestBuilderData of headers <- case simpleHeaders Control.Applicative.<$> rbdResponse requestBuilderData of
Just h -> return h Just h -> return h
Nothing -> failure "getRequestCookies: No request has been made yet; the cookies can't be looked up." Nothing -> failure "getRequestCookies: No request has been made yet; the cookies can't be looked up."
@ -907,7 +907,7 @@ getLocation = do
-- > request $ do -- > request $ do
-- > setMethod methodPut -- > setMethod methodPut
setMethod :: H.Method -> RequestBuilder site () setMethod :: H.Method -> RequestBuilder site ()
setMethod m = rsmodify $ \rbd -> rbd { rbdMethod = m } setMethod m = modifySIO $ \rbd -> rbd { rbdMethod = m }
-- | Sets the URL used by the request. -- | Sets the URL used by the request.
-- --
@ -922,7 +922,7 @@ setUrl :: (Yesod site, RedirectUrl site url)
=> url => url
-> RequestBuilder site () -> RequestBuilder site ()
setUrl url' = do setUrl url' = do
site <- fmap rbdSite rsget site <- fmap rbdSite getSIO
eurl <- Yesod.Core.Unsafe.runFakeHandler eurl <- Yesod.Core.Unsafe.runFakeHandler
M.empty M.empty
(const $ error "Yesod.Test: No logger available") (const $ error "Yesod.Test: No logger available")
@ -930,7 +930,7 @@ setUrl url' = do
(toTextUrl url') (toTextUrl url')
url <- either (error . show) return eurl url <- either (error . show) return eurl
let (urlPath, urlQuery) = T.break (== '?') url let (urlPath, urlQuery) = T.break (== '?') url
rsmodify $ \rbd -> rbd modifySIO $ \rbd -> rbd
{ rbdPath = { rbdPath =
case DL.filter (/="") $ H.decodePathSegments $ TE.encodeUtf8 urlPath of case DL.filter (/="") $ H.decodePathSegments $ TE.encodeUtf8 urlPath of
("http:":_:rest) -> rest ("http:":_:rest) -> rest
@ -969,7 +969,7 @@ clickOn query = do
-- > request $ do -- > request $ do
-- > setRequestBody $ encode $ object ["age" .= (1 :: Integer)] -- > setRequestBody $ encode $ object ["age" .= (1 :: Integer)]
setRequestBody :: BSL8.ByteString -> RequestBuilder site () setRequestBody :: BSL8.ByteString -> RequestBuilder site ()
setRequestBody body = rsmodify $ \rbd -> rbd { rbdPostData = BinaryPostData body } setRequestBody body = modifySIO $ \rbd -> rbd { rbdPostData = BinaryPostData body }
-- | Adds the given header to the request; see "Network.HTTP.Types.Header" for creating 'Header's. -- | Adds the given header to the request; see "Network.HTTP.Types.Header" for creating 'Header's.
-- --
@ -979,7 +979,7 @@ setRequestBody body = rsmodify $ \rbd -> rbd { rbdPostData = BinaryPostData body
-- > request $ do -- > request $ do
-- > addRequestHeader (hUserAgent, "Chrome/41.0.2228.0") -- > addRequestHeader (hUserAgent, "Chrome/41.0.2228.0")
addRequestHeader :: H.Header -> RequestBuilder site () addRequestHeader :: H.Header -> RequestBuilder site ()
addRequestHeader header = rsmodify $ \rbd -> rbd addRequestHeader header = modifySIO $ \rbd -> rbd
{ rbdHeaders = header : rbdHeaders rbd { rbdHeaders = header : rbdHeaders rbd
} }
@ -999,9 +999,9 @@ addRequestHeader header = rsmodify $ \rbd -> rbd
request :: RequestBuilder site () request :: RequestBuilder site ()
-> YesodExample site () -> YesodExample site ()
request reqBuilder = do request reqBuilder = do
YesodExampleData app site oldCookies mRes <- rsget YesodExampleData app site oldCookies mRes <- getSIO
RequestBuilderData {..} <- liftIO $ rsexecStateT reqBuilder RequestBuilderData RequestBuilderData {..} <- liftIO $ execSIO reqBuilder RequestBuilderData
{ rbdPostData = MultipleItemsPostData [] { rbdPostData = MultipleItemsPostData []
, rbdResponse = mRes , rbdResponse = mRes
, rbdMethod = "GET" , rbdMethod = "GET"
@ -1041,7 +1041,7 @@ request reqBuilder = do
}) app }) app
let newCookies = parseSetCookies $ simpleHeaders response let newCookies = parseSetCookies $ simpleHeaders response
cookies' = M.fromList [(Cookie.setCookieName c, c) | c <- newCookies] `M.union` cookies cookies' = M.fromList [(Cookie.setCookieName c, c) | c <- newCookies] `M.union` cookies
rsput $ YesodExampleData app site cookies' (Just response) putSIO $ YesodExampleData app site cookies' (Just response)
where where
isFile (ReqFilePart _ _ _ _) = True isFile (ReqFilePart _ _ _ _) = True
isFile _ = False isFile _ = False
@ -1145,14 +1145,14 @@ testApp :: site -> Middleware -> TestApp site
testApp site middleware = (site, middleware) testApp site middleware = (site, middleware)
type YSpec site = Hspec.SpecWith (TestApp site) type YSpec site = Hspec.SpecWith (TestApp site)
instance YesodDispatch site => Hspec.Example (ReaderT (IORef (YesodExampleData site)) IO a) where instance YesodDispatch site => Hspec.Example (SIO (YesodExampleData site) a) where
type Arg (ReaderT (IORef (YesodExampleData site)) IO a) = TestApp site type Arg (SIO (YesodExampleData site) a) = TestApp site
evaluateExample example params action = evaluateExample example params action =
Hspec.evaluateExample Hspec.evaluateExample
(action $ \(site, middleware) -> do (action $ \(site, middleware) -> do
app <- toWaiAppPlain site app <- toWaiAppPlain site
_ <- rsevalStateT example YesodExampleData _ <- evalSIO example YesodExampleData
{ yedApp = middleware app { yedApp = middleware app
, yedSite = site , yedSite = site
, yedCookies = M.empty , yedCookies = M.empty
@ -1162,28 +1162,26 @@ instance YesodDispatch site => Hspec.Example (ReaderT (IORef (YesodExampleData s
params params
($ ()) ($ ())
rsget :: MonadIO m => ReaderT (IORef s) m s -- | State + IO
rsget = ReaderT $ liftIO . readIORef --
-- @since 1.6.0
newtype SIO s a = SIO (ReaderT (IORef s) IO a)
deriving (Functor, Applicative, Monad, MonadIO, MonadThrow, MonadUnliftIO)
rsput :: MonadIO m => s -> ReaderT (IORef s) m () getSIO :: SIO s s
rsput s = ReaderT $ \ref -> liftIO $ writeIORef ref $! s getSIO = SIO $ ReaderT readIORef
rsmodify :: MonadIO m => (s -> s) -> ReaderT (IORef s) m () putSIO :: s -> SIO s ()
rsmodify f = ReaderT $ \ref -> liftIO $ modifyIORef' ref f putSIO s = SIO $ ReaderT $ \ref -> writeIORef ref $! s
rsevalStateT modifySIO :: (s -> s) -> SIO s ()
:: MonadIO m modifySIO f = SIO $ ReaderT $ \ref -> modifyIORef' ref f
=> ReaderT (IORef s) m a
-> s
-> m a
rsevalStateT (ReaderT f) s = liftIO (newIORef s) >>= f
rsexecStateT evalSIO :: SIO s a -> s -> IO a
:: MonadIO m evalSIO (SIO (ReaderT f)) s = newIORef s >>= f
=> ReaderT (IORef s) m ()
-> s execSIO :: SIO s () -> s -> IO s
-> m s execSIO (SIO (ReaderT f)) s = do
rsexecStateT (ReaderT f) s = do ref <- newIORef s
ref <- liftIO $ newIORef s
f ref f ref
liftIO $ readIORef ref readIORef ref