2{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE ScopedTypeVariables #-}
5module RunSpec (main, spec, withApp) where
7import Control.Concurrent (forkIO, killThread, threadDelay)
8import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar)
9import qualified Control.Exception as E
10import Control.Exception.Lifted (bracket, try, IOException, onException)
11import Control.Monad (forM_, replicateM_, unless)
12import Control.Monad.IO.Class (MonadIO, liftIO)
13import Data.ByteString (ByteString, hPutStr, hGetSome)
14import qualified Data.ByteString as S
15import Data.ByteString.Builder (byteString)
16import qualified Data.ByteString.Char8 as S8
17import qualified Data.ByteString.Lazy as L
18import qualified Data.IORef as I
19import Data.Streaming.Network (bindPortTCP, getSocketTCP, safeRecv)
20import Network (connectTo, PortID (PortNumber))
21import Network.HTTP.Types
22import Network.Socket (sClose)
23import Network.Socket.ByteString (sendAll)
25import Network.Wai.Handler.Warp
26import System.IO (hFlush, hClose)
27import System.IO.Unsafe (unsafePerformIO)
28import System.Timeout (timeout)
36type Counter = I.IORef (Either String Int)
37type CounterApplication = Counter -> Application
39incr :: MonadIO m => Counter -> m ()
40incr icount = liftIO $ I.atomicModifyIORef icount $ \ecount ->
43 Right i -> Right $ i + 1), ())
45err :: (MonadIO m, Show a) => Counter -> a -> m ()
46err icount msg = liftIO $ I.writeIORef icount $ Left $ show msg
48readBody :: CounterApplication
49readBody icount req f = do
50 body <- consumeBody $ requestBody req
53 | pathInfo req == ["hello"] && L.fromChunks body /= "Hello"
54 -> err icount ("Invalid hello" :: String, body)
55 | requestMethod req == "GET" && L.fromChunks body /= ""
56 -> err icount ("Invalid GET" :: String, body)
57 | not $ requestMethod req `elem` ["GET", "POST"]
58 -> err icount ("Invalid request method (readBody)" :: String, requestMethod req)
59 | otherwise -> incr icount
60 f $ responseLBS status200 [] "Read the body"
62ignoreBody :: CounterApplication
63ignoreBody icount req f = do
64 if (requestMethod req `elem` ["GET", "POST"])
66 else err icount ("Invalid request method" :: String, requestMethod req)
67 f $ responseLBS status200 [] "Ignored the body"
69doubleConnect :: CounterApplication
70doubleConnect icount req f = do
71 _ <- consumeBody $ requestBody req
72 _ <- consumeBody $ requestBody req
74 f $ responseLBS status200 [] "double connect"
76nextPort :: I.IORef Int
77nextPort = unsafePerformIO $ I.newIORef 5000
78{-# NOINLINE nextPort #-}
82 port <- I.atomicModifyIORef nextPort $ \p -> (p + 1, p)
83 esocket <- try $ bindPortTCP port "127.0.0.1"
85 Left (_ :: IOException) -> RunSpec.getPort
90withApp :: Settings -> Application -> (Int -> IO a) -> IO a
91withApp settings app f = do
92 port <- RunSpec.getPort
94 let settings' = setPort port
96 $ setBeforeMainLoop (putMVar baton ())
99 (forkIO $ runSettings settings' app `onException` putMVar baton ())
101 (const $ takeMVar baton >> f port)
103runTest :: Int -- ^ expected number of requests
104 -> CounterApplication
105 -> [ByteString] -- ^ chunks to send
107runTest expected app chunks = do
108 ref <- I.newIORef (Right 0)
109 withApp defaultSettings (app ref) $ \port -> do
110 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
111 forM_ chunks $ \chunk -> hPutStr handle chunk >> hFlush handle
112 _ <- timeout 100000 $ replicateM_ expected $ hGetSome handle 4096
113 res <- I.readIORef ref
116 Right i -> i `shouldBe` expected
118dummyApp :: Application
119dummyApp _ f = f $ responseLBS status200 [] "foo"
121runTerminateTest :: InvalidRequest
124runTerminateTest expected input = do
125 ref <- I.newIORef Nothing
126 let onExc _ = I.writeIORef ref . Just
127 withApp (setOnException onExc defaultSettings) dummyApp $ \port -> do
128 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
133 res <- I.readIORef ref
134 show res `shouldBe` show (Just expected)
136singleGet :: ByteString
137singleGet = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"
139singlePostHello :: ByteString
140singlePostHello = "POST /hello HTTP/1.1\r\nHost: localhost\r\nContent-length: 5\r\n\r\nHello"
142singleChunkedPostHello :: [ByteString]
143singleChunkedPostHello =
144 [ "POST /hello HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n"
145 , "5\r\nHello\r\n0\r\n"
150 describe "non-pipelining" $ do
151 it "no body, read" $ runTest 5 readBody $ replicate 5 singleGet
152 it "no body, ignore" $ runTest 5 ignoreBody $ replicate 5 singleGet
153 it "has body, read" $ runTest 2 readBody
157 it "has body, ignore" $ runTest 2 ignoreBody
161 it "chunked body, read" $ runTest 2 readBody $ concat
162 [ singleChunkedPostHello
165 it "chunked body, ignore" $ runTest 2 ignoreBody $ concat
166 [ singleChunkedPostHello
169 describe "pipelining" $ do
170 it "no body, read" $ runTest 5 readBody [S.concat $ replicate 5 singleGet]
171 it "no body, ignore" $ runTest 5 ignoreBody [S.concat $ replicate 5 singleGet]
172 it "has body, read" $ runTest 2 readBody $ return $ S.concat
176 it "has body, ignore" $ runTest 2 ignoreBody $ return $ S.concat
180 it "chunked body, read" $ runTest 2 readBody $ return $ S.concat
181 [ S.concat singleChunkedPostHello
184 it "chunked body, ignore" $ runTest 2 ignoreBody $ return $ S.concat
185 [ S.concat singleChunkedPostHello
188 describe "no hanging" $ do
189 it "has body, read" $ runTest 1 readBody $ map S.singleton $ S.unpack singlePostHello
190 it "double connect" $ runTest 1 doubleConnect [singlePostHello]
192 describe "connection termination" $ do
193-- it "ConnectionClosedByPeer" $ runTerminateTest ConnectionClosedByPeer "GET / HTTP/1.1\r\ncontent-length: 10\r\n\r\nhello"
194 it "IncompleteHeaders" $ runTerminateTest IncompleteHeaders "GET / HTTP/1.1\r\ncontent-length: 10\r\n"
196 describe "special input" $ do
197 it "multiline headers" $ do
198 iheaders <- I.newIORef []
200 liftIO $ I.writeIORef iheaders $ requestHeaders req
201 f $ responseLBS status200 [] ""
202 withApp defaultSettings app $ \port -> do
203 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
205 [ "GET / HTTP/1.1\r\nfoo: bar\r\n baz\r\n\tbin\r\n\r\n"
211 headers <- I.readIORef iheaders
213 [ ("foo", "bar baz\tbin")
215 it "no space between colon and value" $ do
216 iheaders <- I.newIORef []
218 liftIO $ I.writeIORef iheaders $ requestHeaders req
219 f $ responseLBS status200 [] ""
220 withApp defaultSettings app $ \port -> do
221 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
223 [ "GET / HTTP/1.1\r\nfoo:bar\r\n\r\n"
229 headers <- I.readIORef iheaders
234 describe "chunked bodies" $ do
236 ifront <- I.newIORef id
238 bss <- consumeBody $ requestBody req
239 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
240 f $ responseLBS status200 [] ""
241 withApp defaultSettings app $ \port -> do
242 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
244 [ "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
245 , "c\r\nHello World\n\r\n3\r\nBye\r\n0\r\n\r\n"
246 , "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
247 , "b\r\nHello World\r\n0\r\n\r\n"
253 front <- I.readIORef ifront
258 it "lots of chunks" $ do
259 ifront <- I.newIORef id
261 bss <- consumeBody $ requestBody req
262 I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
263 f $ responseLBS status200 [] ""
264 withApp defaultSettings app $ \port -> do
265 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
266 let input = concat $ replicate 2 $
267 ["POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"] ++
268 (replicate 50 "5\r\n12345\r\n") ++
270 mapM_ (\bs -> hPutStr handle bs >> hFlush handle) input
272 threadDelay 100000 -- FIXME why does this delay need to be so high?
273 front <- I.readIORef ifront
274 front [] `shouldBe` replicate 2 (S.concat $ replicate 50 "12345")
276 ifront <- I.newIORef id
278 bss <- consumeBody $ requestBody req
279 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
280 f $ responseLBS status200 [] ""
281 withApp defaultSettings app $ \port -> do
282 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
284 [ "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
285 , "c\r\nHello World\n\r\n3\r\nBye\r\n0\r\n"
286 , "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
287 , "b\r\nHello World\r\n0\r\n\r\n"
289 mapM_ (\bs -> hPutStr handle bs >> hFlush handle) $ map S.singleton $ S.unpack input
292 front <- I.readIORef ifront
297 it "timeout in request body" $ do
298 ifront <- I.newIORef id
300 bss <- (consumeBody $ requestBody req) `onException`
301 liftIO (I.atomicModifyIORef ifront (\front -> (front . ("consume interrupted":), ())))
302 liftIO $ threadDelay 4000000 `E.catch` \e -> do
303 I.atomicModifyIORef ifront (\front ->
304 ( front . ((S8.pack $ "threadDelay interrupted: " ++ show e):)
306 E.throwIO (e :: E.SomeException)
307 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
308 f $ responseLBS status200 [] ""
309 withApp (setTimeout 1 defaultSettings) app $ \port -> do
310 let bs1 = S.replicate 2048 88
311 bs2 = "This is short"
312 bs = S.append bs1 bs2
313 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
314 hPutStr handle "POST / HTTP/1.1\r\n"
315 hPutStr handle "content-length: "
316 hPutStr handle $ S8.pack $ show $ S.length bs
317 hPutStr handle "\r\n\r\n"
324 front <- I.readIORef ifront
325 S.concat (front []) `shouldBe` bs
326 describe "raw body" $ do
329 let backup = responseLBS status200 [] "Not raw"
330 f $ flip responseRaw backup $ \src sink -> do
333 unless (S.null bs) $ do
337 doubleBS = S.concatMap $ \w -> S.pack [w, w]
338 withApp defaultSettings app $ \port -> do
339 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
340 hPutStr handle "POST / HTTP/1.1\r\n\r\n12345"
342 timeout 100000 (S.hGet handle 10) >>= (`shouldBe` Just "1122334455")
343 hPutStr handle "67890"
345 timeout 100000 (S.hGet handle 10) >>= (`shouldBe` Just "6677889900")
347 it "only one date and server header" $ do
348 let app _ f = f $ responseLBS status200
349 [ ("server", "server")
352 withApp defaultSettings app $ \port -> do
353 res <- sendGET $ "http://127.0.0.1:" ++ show port
354 getHeaderValue HdrServer res `shouldBe` Just "server"
355 getHeaderValue HdrDate res `shouldBe` Just "date"
357 it "streaming echo #249" $ do
358 let app req f = f $ responseStream status200 [] $ \write _ -> do
360 bs <- requestBody req
361 unless (S.null bs) $ do
362 write $ byteString bs
365 withApp defaultSettings app $ \port -> do
366 (socket, _addr) <- getSocketTCP "127.0.0.1" port
367 sendAll socket "POST / HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n"
369 sendAll socket "5\r\nhello\r\n0\r\n\r\n"
370 bs <- safeRecv socket 4096
371 S.takeWhile (/= 13) bs `shouldBe` "HTTP/1.1 200 OK"
373 it "streaming response with length" $ do
374 let app _ f = f $ responseStream status200 [("content-length", "20")] $ \write _ -> do
375 replicateM_ 4 $ write $ byteString "Hello"
376 withApp defaultSettings app $ \port -> do
377 res <- sendGET $ "http://127.0.0.1:" ++ show port
378 rspBody res `shouldBe` "HelloHelloHelloHello"
380 describe "head requests" $ do
381 let fp = "test/head-response"
383 f $ case pathInfo req of
384 ["builder"] -> responseBuilder status200 [] $ error "should never be evaluated"
385 ["streaming"] -> responseStream status200 [] $ \write _ ->
386 write $ error "should never be evaluated"
387 ["file"] -> responseFile status200 [] fp Nothing
388 _ -> error "invalid path"
389 it "builder" $ withApp defaultSettings app $ \port -> do
390 res <- sendHEAD $ concat ["http://127.0.0.1:", show port, "/builder"]
391 rspBody res `shouldBe` ""
392 it "streaming" $ withApp defaultSettings app $ \port -> do
393 res <- sendHEAD $ concat ["http://127.0.0.1:", show port, "/streaming"]
394 rspBody res `shouldBe` ""
395 it "file, no range" $ withApp defaultSettings app $ \port -> do
397 res <- sendHEAD $ concat ["http://127.0.0.1:", show port, "/file"]
398 getHeaderValue HdrContentLength res `shouldBe` Just (show $ S.length bs)
399 it "file, with range" $ withApp defaultSettings app $ \port -> do
401 (concat ["http://127.0.0.1:", show port, "/file"])
402 [mkHeader HdrRange "bytes=0-1"]
403 getHeaderValue HdrContentLength res `shouldBe` Just "2"
405consumeBody :: IO ByteString -> IO [ByteString]
412 then return $ front []
413 else loop $ front . (bs:)
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