1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE ScopedTypeVariables #-}
4module RunSpec (main, spec) where
6import Control.Concurrent (forkIO, killThread, threadDelay)
7import Control.Monad (forM_)
8import Control.Monad.IO.Class (MonadIO, liftIO)
9import Data.ByteString (ByteString, hPutStr, hGetSome)
10import qualified Data.ByteString as S
11import qualified Data.ByteString.Lazy as L
12import Data.Conduit (($$))
13import qualified Data.Conduit.List
14import qualified Data.IORef as I
15import Network (connectTo, PortID (PortNumber))
16import Network.HTTP.Types
18import Network.Wai.Handler.Warp
19import System.IO (hFlush, hClose)
20import System.IO.Unsafe (unsafePerformIO)
22import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar)
23import Control.Exception (bracket, try, IOException, onException)
24import Data.Conduit.Network (bindPort)
25import Network.Socket (sClose)
30type Counter = I.IORef (Either String Int)
31type CounterApplication = Counter -> Application
33incr :: MonadIO m => Counter -> m ()
34incr icount = liftIO $ I.atomicModifyIORef icount $ \ecount ->
37 Right i -> Right $ i + 1), ())
39err :: (MonadIO m, Show a) => Counter -> a -> m ()
40err icount msg = liftIO $ I.writeIORef icount $ Left $ show msg
42readBody :: CounterApplication
43readBody icount req = do
44 body <- requestBody req $$ Data.Conduit.List.consume
47 | pathInfo req == ["hello"] && L.fromChunks body /= "Hello"
48 -> err icount ("Invalid hello" :: String, body)
49 | requestMethod req == "GET" && L.fromChunks body /= ""
50 -> err icount ("Invalid GET" :: String, body)
51 | not $ requestMethod req `elem` ["GET", "POST"]
52 -> err icount ("Invalid request method (readBody)" :: String, requestMethod req)
53 | otherwise -> incr icount
54 return $ responseLBS status200 [] "Read the body"
56ignoreBody :: CounterApplication
57ignoreBody icount req = do
58 if (requestMethod req `elem` ["GET", "POST"])
60 else err icount ("Invalid request method" :: String, requestMethod req)
61 return $ responseLBS status200 [] "Ignored the body"
63doubleConnect :: CounterApplication
64doubleConnect icount req = do
65 _ <- requestBody req $$ Data.Conduit.List.consume
66 _ <- requestBody req $$ Data.Conduit.List.consume
68 return $ responseLBS status200 [] "double connect"
70nextPort :: I.IORef Int
71nextPort = unsafePerformIO $ I.newIORef 5000
75 port <- I.atomicModifyIORef nextPort $ \p -> (p + 1, p)
76 esocket <- try $ bindPort port HostIPv4
78 Left (_ :: IOException) -> getPort
83withApp :: Settings -> Application -> (Int -> IO a) -> IO a
84withApp settings app f = do
88 (forkIO $ runSettings settings
90 , settingsBeforeMainLoop = putMVar baton ()
91 } app `onException` putMVar baton ())
93 (const $ takeMVar baton >> f port)
95runTest :: Int -- ^ expected number of requests
97 -> [ByteString] -- ^ chunks to send
99runTest expected app chunks = do
100 ref <- I.newIORef (Right 0)
101 withApp defaultSettings (app ref) $ \port -> do
102 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
103 forM_ chunks $ \chunk -> hPutStr handle chunk >> hFlush handle
104 _ <- hGetSome handle 4096
106 res <- I.readIORef ref
109 Right i -> i `shouldBe` expected
111dummyApp :: Application
112dummyApp _ = return $ responseLBS status200 [] "foo"
114runTerminateTest :: InvalidRequest
117runTerminateTest expected input = do
118 ref <- I.newIORef Nothing
119 withApp defaultSettings
120 { settingsOnException = \e -> I.writeIORef ref $ Just e
121 } dummyApp $ \port -> do
122 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
127 res <- I.readIORef ref
128 show res `shouldBe` show (Just expected)
130singleGet :: ByteString
131singleGet = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"
133singlePostHello :: ByteString
134singlePostHello = "POST /hello HTTP/1.1\r\nHost: localhost\r\nContent-length: 5\r\n\r\nHello"
138 describe "non-pipelining" $ do
139 it "no body, read" $ runTest 5 readBody $ replicate 5 singleGet
140 it "no body, ignore" $ runTest 5 ignoreBody $ replicate 5 singleGet
141 it "has body, read" $ runTest 2 readBody
145 it "has body, ignore" $ runTest 2 ignoreBody
149 describe "pipelining" $ do
150 it "no body, read" $ runTest 5 readBody [S.concat $ replicate 5 singleGet]
151 it "no body, ignore" $ runTest 5 ignoreBody [S.concat $ replicate 5 singleGet]
152 it "has body, read" $ runTest 2 readBody $ return $ S.concat
156 it "has body, ignore" $ runTest 2 ignoreBody $ return $ S.concat
160 describe "no hanging" $ do
161 it "has body, read" $ runTest 1 readBody $ map S.singleton $ S.unpack singlePostHello
162 it "double connect" $ runTest 1 doubleConnect [singlePostHello]
164 describe "connection termination" $ do
165-- it "ConnectionClosedByPeer" $ runTerminateTest ConnectionClosedByPeer "GET / HTTP/1.1\r\ncontent-length: 10\r\n\r\nhello"
166 it "IncompleteHeaders" $ runTerminateTest IncompleteHeaders "GET / HTTP/1.1\r\ncontent-length: 10\r\n"
168 describe "special input" $ do
169 it "multiline headers" $ do
170 iheaders <- I.newIORef []
172 liftIO $ I.writeIORef iheaders $ requestHeaders req
173 return $ responseLBS status200 [] ""
174 withApp defaultSettings app $ \port -> do
175 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
177 [ "GET / HTTP/1.1\r\nfoo: bar\r\n baz\r\n\tbin\r\n\r\n"
183 headers <- I.readIORef iheaders
185 [ ("foo", "bar baz\tbin")
187 it "no space between colon and value" $ do
188 iheaders <- I.newIORef []
190 liftIO $ I.writeIORef iheaders $ requestHeaders req
191 return $ responseLBS status200 [] ""
192 withApp defaultSettings app $ \port -> do
193 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
195 [ "GET / HTTP/1.1\r\nfoo:bar\r\n\r\n"
201 headers <- I.readIORef iheaders
205 it "extra spaces in first line" $ do
206 iheaders <- I.newIORef []
208 liftIO $ I.writeIORef iheaders $ requestHeaders req
209 return $ responseLBS status200 [] ""
210 withApp defaultSettings app $ \port -> do
211 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
213 [ "GET / HTTP/1.1\r\nfoo: bar\r\n\r\n"
219 headers <- I.readIORef iheaders
223 it "spaces in http version" $ do
224 iversion <- I.newIORef $ error "Version not parsed"
226 liftIO $ I.writeIORef iversion $ httpVersion req
227 return $ responseLBS status200 [] ""
228 withApp defaultSettings app $ \port -> do
229 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
231 [ "GET / HTTP\t/ 1 . 1 \r\nfoo: bar\r\n\r\n"
237 version <- I.readIORef iversion
238 version `shouldBe` http11
240 describe "chunked bodies" $ do
242 ifront <- I.newIORef id
244 bss <- requestBody req $$ Data.Conduit.List.consume
245 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
246 return $ responseLBS status200 [] ""
247 withApp defaultSettings app $ \port -> do
248 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
250 [ "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
251 , "c\r\nHello World\n\r\n3\r\nBye\r\n0\r\n\r\n"
252 , "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
253 , "b\r\nHello World\r\n0\r\n\r\n"
259 front <- I.readIORef ifront
264 it "lots of chunks" $ do
265 ifront <- I.newIORef id
267 bss <- requestBody req $$ Data.Conduit.List.consume
268 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
269 return $ responseLBS status200 [] ""
270 withApp defaultSettings app $ \port -> do
271 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
272 let input = concat $ replicate 2 $
273 ["POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"] ++
274 (replicate 50 "5\r\n12345\r\n") ++
276 mapM_ (\bs -> hPutStr handle bs >> hFlush handle) input
279 front <- I.readIORef ifront
280 front [] `shouldBe` replicate 2 (S.concat $ replicate 50 "12345")
282 ifront <- I.newIORef id
284 bss <- requestBody req $$ Data.Conduit.List.consume
285 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
286 return $ responseLBS status200 [] ""
287 withApp defaultSettings app $ \port -> do
288 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
290 [ "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
291 , "c\r\nHello World\n\r\n3\r\nBye\r\n0\r\n"
292 , "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
293 , "b\r\nHello World\r\n0\r\n\r\n"
295 mapM_ (\bs -> hPutStr handle bs >> hFlush handle) $ map S.singleton $ S.unpack input
298 front <- I.readIORef ifront
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list