1{-# LANGUAGE OverloadedStrings #-}
3module RunSpec (main, spec) where
5import Control.Concurrent (forkIO, killThread, threadDelay)
6import Control.Monad (forM_)
7import Control.Monad.IO.Class (MonadIO, liftIO)
8import Data.ByteString (ByteString, hPutStr, hGetSome)
9import qualified Data.ByteString as S
10import qualified Data.ByteString.Lazy as L
11import Data.Conduit (($$))
12import qualified Data.Conduit.List
13import qualified Data.IORef as I
14import Network (connectTo, PortID (PortNumber))
15import Network.HTTP.Types
17import Network.Wai.Handler.Warp
18import System.IO (hFlush, hClose)
19import System.IO.Unsafe (unsafePerformIO)
25type Counter = I.IORef (Either String Int)
26type CounterApplication = Counter -> Application
28incr :: MonadIO m => Counter -> m ()
29incr icount = liftIO $ I.atomicModifyIORef icount $ \ecount ->
32 Right i -> Right $ i + 1), ())
34err :: (MonadIO m, Show a) => Counter -> a -> m ()
35err icount msg = liftIO $ I.writeIORef icount $ Left $ show msg
37readBody :: CounterApplication
38readBody icount req = do
39 body <- requestBody req $$ Data.Conduit.List.consume
42 | pathInfo req == ["hello"] && L.fromChunks body /= "Hello"
43 -> err icount ("Invalid hello" :: String, body)
44 | requestMethod req == "GET" && L.fromChunks body /= ""
45 -> err icount ("Invalid GET" :: String, body)
46 | not $ requestMethod req `elem` ["GET", "POST"]
47 -> err icount ("Invalid request method (readBody)" :: String, requestMethod req)
48 | otherwise -> incr icount
49 return $ responseLBS status200 [] "Read the body"
51ignoreBody :: CounterApplication
52ignoreBody icount req = do
53 if (requestMethod req `elem` ["GET", "POST"])
55 else err icount ("Invalid request method" :: String, requestMethod req)
56 return $ responseLBS status200 [] "Ignored the body"
58doubleConnect :: CounterApplication
59doubleConnect icount req = do
60 _ <- requestBody req $$ Data.Conduit.List.consume
61 _ <- requestBody req $$ Data.Conduit.List.consume
63 return $ responseLBS status200 [] "double connect"
65nextPort :: I.IORef Int
66nextPort = unsafePerformIO $ I.newIORef 5000
69getPort = I.atomicModifyIORef nextPort $ \p -> (p + 1, p)
71runTest :: Int -- ^ expected number of requests
73 -> [ByteString] -- ^ chunks to send
75runTest expected app chunks = do
77 ref <- I.newIORef (Right 0)
78 tid <- forkIO $ run port $ app ref
80 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
81 forM_ chunks $ \chunk -> hPutStr handle chunk >> hFlush handle
82 _ <- hGetSome handle 4096
85 res <- I.readIORef ref
88 Right i -> i `shouldBe` expected
90dummyApp :: Application
91dummyApp _ = return $ responseLBS status200 [] "foo"
93runTerminateTest :: InvalidRequest
96runTerminateTest expected input = do
98 ref <- I.newIORef Nothing
99 tid <- forkIO $ runSettings defaultSettings
100 { settingsOnException = \e -> I.writeIORef ref $ Just e
101 , settingsPort = port
104 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
110 res <- I.readIORef ref
111 show res `shouldBe` show (Just expected)
113singleGet :: ByteString
114singleGet = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"
116singlePostHello :: ByteString
117singlePostHello = "POST /hello HTTP/1.1\r\nHost: localhost\r\nContent-length: 5\r\n\r\nHello"
121 describe "non-pipelining" $ do
122 it "no body, read" $ runTest 5 readBody $ replicate 5 singleGet
123 it "no body, ignore" $ runTest 5 ignoreBody $ replicate 5 singleGet
124 it "has body, read" $ runTest 2 readBody
128 it "has body, ignore" $ runTest 2 ignoreBody
132 describe "pipelining" $ do
133 it "no body, read" $ runTest 5 readBody [S.concat $ replicate 5 singleGet]
134 it "no body, ignore" $ runTest 5 ignoreBody [S.concat $ replicate 5 singleGet]
135 it "has body, read" $ runTest 2 readBody $ return $ S.concat
139 it "has body, ignore" $ runTest 2 ignoreBody $ return $ S.concat
143 describe "no hanging" $ do
144 it "has body, read" $ runTest 1 readBody $ map S.singleton $ S.unpack singlePostHello
145 it "double connect" $ runTest 1 doubleConnect [singlePostHello]
147 describe "connection termination" $ do
148-- it "ConnectionClosedByPeer" $ runTerminateTest ConnectionClosedByPeer "GET / HTTP/1.1\r\ncontent-length: 10\r\n\r\nhello"
149 it "IncompleteHeaders" $ runTerminateTest IncompleteHeaders "GET / HTTP/1.1\r\ncontent-length: 10\r\n"
151 describe "special input" $ do
152 it "multiline headers" $ do
153 iheaders <- I.newIORef []
155 tid <- forkIO $ run port $ \req -> do
156 liftIO $ I.writeIORef iheaders $ requestHeaders req
157 return $ responseLBS status200 [] ""
159 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
161 [ "GET / HTTP/1.1\r\nfoo: bar\r\n baz\r\n\tbin\r\n\r\n"
168 headers <- I.readIORef iheaders
170 [ ("foo", "bar baz\tbin")
172 it "no space between colon and value" $ do
173 iheaders <- I.newIORef []
175 tid <- forkIO $ run port $ \req -> do
176 liftIO $ I.writeIORef iheaders $ requestHeaders req
177 return $ responseLBS status200 [] ""
179 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
181 [ "GET / HTTP/1.1\r\nfoo:bar\r\n\r\n"
188 headers <- I.readIORef iheaders
192 it "extra spaces in first line" $ do
193 iheaders <- I.newIORef []
195 tid <- forkIO $ run port $ \req -> do
196 liftIO $ I.writeIORef iheaders $ requestHeaders req
197 return $ responseLBS status200 [] ""
199 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
201 [ "GET / HTTP/1.1\r\nfoo: bar\r\n\r\n"
208 headers <- I.readIORef iheaders
212 it "spaces in http version" $ do
213 iversion <- I.newIORef $ error "Version not parsed"
215 tid <- forkIO $ run port $ \req -> do
216 liftIO $ I.writeIORef iversion $ httpVersion req
217 return $ responseLBS status200 [] ""
219 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
221 [ "GET / HTTP\t/ 1 . 1 \r\nfoo: bar\r\n\r\n"
228 version <- I.readIORef iversion
229 version `shouldBe` http11
231 describe "chunked bodies" $ do
233 ifront <- I.newIORef id
235 tid <- forkIO $ run port $ \req -> do
236 bss <- requestBody req $$ Data.Conduit.List.consume
237 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
238 return $ responseLBS status200 [] ""
240 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
242 [ "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
243 , "c\r\nHello World\n\r\n3\r\nBye\r\n0\r\n\r\n"
244 , "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
245 , "b\r\nHello World\r\n0\r\n\r\n"
252 front <- I.readIORef ifront
257 it "lots of chunks" $ do
258 ifront <- I.newIORef id
260 tid <- forkIO $ run port $ \req -> do
261 bss <- requestBody req $$ Data.Conduit.List.consume
262 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
263 return $ responseLBS status200 [] ""
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
274 front <- I.readIORef ifront
275 front [] `shouldBe` replicate 2 (S.concat $ replicate 50 "12345")
277 ifront <- I.newIORef id
279 tid <- forkIO $ run port $ \req -> do
280 bss <- requestBody req $$ Data.Conduit.List.consume
281 liftIO $ I.atomicModifyIORef ifront $ \front -> (front . (S.concat bss:), ())
282 return $ responseLBS status200 [] ""
284 handle <- connectTo "127.0.0.1" $ PortNumber $ fromIntegral port
286 [ "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
287 , "c\r\nHello World\n\r\n3\r\nBye\r\n0\r\n"
288 , "POST / HTTP/1.1\r\nTransfer-Encoding: Chunked\r\n\r\n"
289 , "b\r\nHello World\r\n0\r\n\r\n"
291 mapM_ (\bs -> hPutStr handle bs >> hFlush handle) $ map S.singleton $ S.unpack input
295 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