summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Truzjan <pxqr.sta@gmail.com>2013-12-26 02:06:05 +0400
committerSam Truzjan <pxqr.sta@gmail.com>2013-12-26 02:06:05 +0400
commitea5c29d7f91397f7979b6f73052ef30a5cdf030f (patch)
treeb2ca2c858f814f45658cfe95fbe6aa305635a2f7
parent62253eb04e3ad1225d2a87a3c9647c6c092114df (diff)
parent5d545a58787bc1beadc13fa5838a4ad2472c5e88 (diff)
Merge branch 'dev' of https://github.com/DanielG/bittorrent
-rw-r--r--.gitmodules3
-rw-r--r--TODO.org13
-rw-r--r--bittorrent.cabal6
-rw-r--r--res/testfilebin0 -> 8192 bytes
-rw-r--r--res/testfile.torrent1
-rw-r--r--src/Network/BitTorrent/Core/PeerAddr.hs14
-rw-r--r--src/Network/BitTorrent/Exchange/Wire.hs40
m---------sub/hspec0
-rw-r--r--tests/Main.hs286
-rw-r--r--tests/Spec.hs2
10 files changed, 81 insertions, 284 deletions
diff --git a/.gitmodules b/.gitmodules
index d90db85e..21868293 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
4[submodule "sub/krpc"] 4[submodule "sub/krpc"]
5 path = sub/krpc 5 path = sub/krpc
6 url = git://github.com/pxqr/krpc.git 6 url = git://github.com/pxqr/krpc.git
7[submodule "sub/hspec"]
8 path = sub/hspec
9 url = https://github.com/DanielG/hspec.git
diff --git a/TODO.org b/TODO.org
index 7fcb95c3..4c8fc619 100644
--- a/TODO.org
+++ b/TODO.org
@@ -1,5 +1,8 @@
1* DONE version 0.0.0.1 1* TODO Rename Ext,Extension -> Cap
2* TODO torrent linting for indexing 2* TODO Use System.IO instead of Network.recv/send
3* TODO move PeerClient to Data.Torrent.Client 3 The functions in System.IO use GHC/base's built in support for epoll/kqueue
4* TODO Word64 for Progress fields 4 and the like which makes them a whole lot more efficient for file descriptor
5* TODO rename TConnection -> Connection, etc 5 I/O. Network.recv/send on the other hand just use a plain FFI call to
6 recvmsg/sendmsg which will block the whole OS thread.
7
8** TODO Compare performance of the two
diff --git a/bittorrent.cabal b/bittorrent.cabal
index 4cb9fce7..9c7dc2c6 100644
--- a/bittorrent.cabal
+++ b/bittorrent.cabal
@@ -157,8 +157,9 @@ test-suite spec
157 default-extensions: OverloadedStrings 157 default-extensions: OverloadedStrings
158 type: exitcode-stdio-1.0 158 type: exitcode-stdio-1.0
159 hs-source-dirs: tests 159 hs-source-dirs: tests
160 main-is: Spec.hs 160 main-is: Main.hs
161 other-modules: Data.Torrent.BitfieldSpec 161 other-modules: Spec
162 Data.Torrent.BitfieldSpec
162 Data.Torrent.InfoHashSpec 163 Data.Torrent.InfoHashSpec
163 Data.Torrent.LayoutSpec 164 Data.Torrent.LayoutSpec
164 Data.Torrent.MagnetSpec 165 Data.Torrent.MagnetSpec
@@ -202,6 +203,7 @@ test-suite spec
202 , quickcheck-instances 203 , quickcheck-instances
203 , http-types 204 , http-types
204 , bencoding 205 , bencoding
206 , process
205 , bittorrent 207 , bittorrent
206 ghc-options: -Wall -fno-warn-orphans 208 ghc-options: -Wall -fno-warn-orphans
207 209
diff --git a/res/testfile b/res/testfile
new file mode 100644
index 00000000..8e984818
--- /dev/null
+++ b/res/testfile
Binary files differ
diff --git a/res/testfile.torrent b/res/testfile.torrent
new file mode 100644
index 00000000..a03f7780
--- /dev/null
+++ b/res/testfile.torrent
@@ -0,0 +1 @@
d8:announce44:udp://tracker.openbittorrent.com:80/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1387753787e4:infod6:lengthi8192e4:name8:testfile12:piece lengthi262144e6:pieces20:œd•Dú—uÈÔtÝ®aÿöK³2ee \ No newline at end of file
diff --git a/src/Network/BitTorrent/Core/PeerAddr.hs b/src/Network/BitTorrent/Core/PeerAddr.hs
index 63ae04b9..b9d4878e 100644
--- a/src/Network/BitTorrent/Core/PeerAddr.hs
+++ b/src/Network/BitTorrent/Core/PeerAddr.hs
@@ -133,6 +133,20 @@ instance BEncode IPv6 where
133 fromBEncode = ipFromBEncode 133 fromBEncode = ipFromBEncode
134 {-# INLINE fromBEncode #-} 134 {-# INLINE fromBEncode #-}
135 135
136instance Serialize IP where
137 put (IPv4 ip) = put ip
138 put (IPv6 ip) = put ip
139
140 -- | When 'get'ing an IP it must be 'isolate'd to the appropriate number of
141 -- bytes since we have no other way of telling which address type we are
142 -- trying to parse
143 get = do
144 n <- remaining
145 case n of
146 4 -> IPv4 <$> get
147 16 -> IPv6 <$> get
148 _ -> fail "Wrong number of bytes remaining to parse IP"
149
136instance Serialize IPv4 where 150instance Serialize IPv4 where
137 put = putWord32host . toHostAddress 151 put = putWord32host . toHostAddress
138 get = fromHostAddress <$> getWord32host 152 get = fromHostAddress <$> getWord32host
diff --git a/src/Network/BitTorrent/Exchange/Wire.hs b/src/Network/BitTorrent/Exchange/Wire.hs
index 109f6551..c0658961 100644
--- a/src/Network/BitTorrent/Exchange/Wire.hs
+++ b/src/Network/BitTorrent/Exchange/Wire.hs
@@ -432,8 +432,9 @@ data Connection = Connection
432 -- future. 432 -- future.
433 connProtocol :: !ProtocolName 433 connProtocol :: !ProtocolName
434 434
435 -- | A set of enabled extensions. This value used to check if a 435 -- | Set of enabled core extensions, i.e. the pre BEP10 extension
436 -- message is allowed to be sent or received. 436 -- mecahnism. This value is used to check if a message is allowed to be sent
437 -- or received.
437 , connCaps :: !Caps 438 , connCaps :: !Caps
438 439
439 -- | /Both/ peers handshaked with this infohash. A connection can 440 -- | /Both/ peers handshaked with this infohash. A connection can
@@ -450,9 +451,9 @@ data Connection = Connection
450 -- | 451 -- |
451 , connOptions :: !Options 452 , connOptions :: !Options
452 453
453 -- | If @not (allowed ExtExtended connCaps)@ then this set is 454 -- | If @not (allowed ExtExtended connCaps)@ then this set is always
454 -- always empty. Otherwise it has extension protocol 'MessageId' 455 -- empty. Otherwise it has the BEP10 extension protocol mandated mapping of
455 -- map. 456 -- 'MessageId' to the message type for the remote peer.
456 , connExtCaps :: !(IORef ExtendedCaps) 457 , connExtCaps :: !(IORef ExtendedCaps)
457 458
458 -- | Current extended handshake information from the remote peer 459 -- | Current extended handshake information from the remote peer
@@ -632,9 +633,9 @@ rehandshake caps = undefined
632reconnect :: Wire () 633reconnect :: Wire ()
633reconnect = undefined 634reconnect = undefined
634 635
635-- | Initiate 'Wire' connection and handshake with a peer. This 636-- | Initiate 'Wire' connection and handshake with a peer. This function will
636-- function will also do extension protocol handshake if 'ExtExtended' 637-- also do the BEP10 extension protocol handshake if 'ExtExtended' is enabled on
637-- is enabled on both sides. 638-- both sides.
638-- 639--
639-- This function can throw 'WireFailure' exception. 640-- This function can throw 'WireFailure' exception.
640-- 641--
@@ -643,20 +644,19 @@ connectWire hs addr extCaps wire =
643 bracket (connectToPeer addr) close $ \ sock -> do 644 bracket (connectToPeer addr) close $ \ sock -> do
644 hs' <- initiateHandshake sock hs 645 hs' <- initiateHandshake sock hs
645 646
646 unless (def == hsProtocol hs') $ do 647 Prelude.mapM_ (\(t,e) -> unless t $ throwIO $ ProtocolError e) [
647 throwIO $ ProtocolError $ InvalidProtocol (hsProtocol hs') 648 (def == hsProtocol hs'
648 649 , InvalidProtocol $ hsProtocol hs'),
649 unless (hsProtocol hs == hsProtocol hs') $ do 650 (hsProtocol hs == hsProtocol hs'
650 throwIO $ ProtocolError $ UnexpectedProtocol (hsProtocol hs') 651 , UnexpectedProtocol $ hsProtocol hs'),
651 652 (hsInfoHash hs == hsInfoHash hs'
652 unless (hsInfoHash hs == hsInfoHash hs') $ do 653 , UnexpectedTopic $ hsInfoHash hs'),
653 throwIO $ ProtocolError $ UnexpectedTopic (hsInfoHash hs') 654 (hsPeerId hs' == fromMaybe (hsPeerId hs') (peerId addr)
654 655 , UnexpectedPeerId $ hsPeerId hs')
655 unless (hsPeerId hs' == fromMaybe (hsPeerId hs') (peerId addr)) $ do 656 ]
656 throwIO $ ProtocolError $ UnexpectedPeerId (hsPeerId hs')
657 657
658 let caps = hsReserved hs <> hsReserved hs' 658 let caps = hsReserved hs <> hsReserved hs'
659 let wire' = if ExtExtended `allowed` caps 659 wire' = if ExtExtended `allowed` caps
660 then extendedHandshake extCaps >> wire 660 then extendedHandshake extCaps >> wire
661 else wire 661 else wire
662 662
diff --git a/sub/hspec b/sub/hspec
new file mode 160000
Subproject 6b5bf6fe8bc59909c5b7346f6f70fdfe643d115
diff --git a/tests/Main.hs b/tests/Main.hs
index 3c58e2ed..66283339 100644
--- a/tests/Main.hs
+++ b/tests/Main.hs
@@ -1,260 +1,34 @@
1-- | 1module Main where
2-- Copyright : (c) Sam Truzjan 2013 2
3-- License : BSD3 3import Spec
4-- Maintainer : pxqr.sta@gmail.com 4import System.Exit
5-- Stability : experimental 5import System.Environment
6-- Portability : portable 6import System.Process
7-- 7import Control.Exception
8-- Do not add other (than this) test suites without need. Do not use 8import Data.List
9-- linux-specific paths, use 'filepath' and 'directory' machinery.
10--
11{-# LANGUAGE StandaloneDeriving #-}
12{-# LANGUAGE GeneralizedNewtypeDeriving #-}
13{-# LANGUAGE OverloadedStrings #-}
14module Main (main) where
15
16import Control.Applicative hiding (empty)
17import Data.ByteString (ByteString)
18import qualified Data.ByteString as B
19import qualified Data.ByteString.Lazy as Lazy
20import Data.List as L
21import Data.Maybe 9import Data.Maybe
22import Data.Word 10import Data.Functor
23import Data.Serialize as S
24import Data.Text as T
25
26import Network
27import Network.URI
28
29import Test.QuickCheck as QC
30import Test.Framework as Framework (Test, defaultMain)
31import Test.Framework.Providers.QuickCheck2 (testProperty)
32
33import Data.Aeson as JSON
34import Data.BEncode as BE
35import Data.Torrent.Block
36import Data.Torrent.Bitfield as BF
37import Data.Torrent.Metainfo
38import Network.BitTorrent.Exchange.Protocol
39import Network.BitTorrent.Tracker
40import Network.BitTorrent.Tracker.Protocol
41import Network.BitTorrent.Tracker.HTTP
42import Network.BitTorrent.Peer
43
44-- import Debug.Trace
45
46
47data T a = T
48
49
50prop_properBEncode :: Show a => BEncodable a => Eq a => T a -> a -> Bool
51prop_properBEncode _ expected = actual == Right expected
52 where
53 actual = decoded $ Lazy.toStrict $ encoded expected
54
55instance Arbitrary URI where
56 arbitrary = pure $ fromJust
57 $ parseURI "http://exsample.com:80/123365_asd"
58
59instance Arbitrary Text where
60 arbitrary = T.pack <$> arbitrary
61
62{-
63{-----------------------------------------------------------------------
64 MemMap
65-----------------------------------------------------------------------}
66
67tmpdir :: FilePath
68tmpdir = "tmp"
69
70boundaryTest :: Assertion
71boundaryTest = do
72 f <- mallocTo (Fixed.interval 0 1) Fixed.empty
73 f <- mallocTo (Fixed.interval 1 2) f
74 writeElem f 0 (1 :: Word8)
75 writeElem f 1 (2 :: Word8)
76 bs <- readBytes (Fixed.interval 0 2) f
77 "\x1\x2" @=? bs
78
79mmapSingle :: Assertion
80mmapSingle = do
81 f <- mmapTo (tmpdir </> "single.test") (10, 5) 5 Fixed.empty
82 writeBytes (Fixed.interval 5 5) "abcde" f
83 bs <- readBytes (Fixed.interval 5 5) f
84 "abcde" @=? bs
85
86coalesceTest :: Assertion
87coalesceTest = do
88 f <- mmapTo (tmpdir </> "a.test") (0, 1) 10 Fixed.empty
89 f <- mmapTo (tmpdir </> "bc.test") (0, 2) 12 f
90 f <- mmapTo (tmpdir </> "c.test") (0, 1) 13 f
91 writeBytes (Fixed.interval 10 4) "abcd" f
92 bs <- readBytes (Fixed.interval 10 4) f
93 "abcd" @=? bs
94-}
95
96{-----------------------------------------------------------------------
97 Bitfield
98-----------------------------------------------------------------------}
99-- other properties are tested in IntervalSet
100
101prop_completenessRange :: Bitfield -> Bool
102prop_completenessRange bf = 0 <= c && c <= 1
103 where
104 c = completeness bf
105
106prop_minMax :: Bitfield -> Bool
107prop_minMax bf
108 | BF.null bf = True
109 | otherwise = BF.findMin bf <= BF.findMax bf
110
111prop_rarestInRange :: [Bitfield] -> Bool
112prop_rarestInRange xs = case rarest xs of
113 Just r -> 0 <= r
114 && r < totalCount (maximumBy (comparing totalCount) xs)
115 Nothing -> True
116
117{- this one should give pretty good coverage -}
118prop_differenceDeMorgan :: Bitfield -> Bitfield -> Bitfield -> Bool
119prop_differenceDeMorgan a b c =
120 (a `BF.difference` (b `BF.intersection` c))
121 == ((a `BF.difference` b) `BF.union` (a `BF.difference` c))
122 &&
123 (a `BF.difference` (b `BF.union` c))
124 == ((a `BF.difference` b) `BF.intersection` (a `BF.difference` c))
125
126 {-
127 [ -- mem map
128 testCase "boudary" boundaryTest
129 , testCase "single" mmapSingle
130 , testCase "coalesce" coalesceTest
131 ]
132
133instance Arbitrary Bitfield where
134 arbitrary = mkBitfield <$> (succ . min 1000 <$> positive)
135 <*> arbitrary
136
137 testProperty "completeness range" prop_completenessRange
138 , testProperty "rarest in range" prop_rarestInRange
139 , testProperty "min less that max" prop_minMax
140 , testProperty "difference de morgan" prop_differenceDeMorgan
141-}
142{-----------------------------------------------------------------------
143 Handshake
144-----------------------------------------------------------------------}
145
146instance Arbitrary PeerId where
147 arbitrary = azureusStyle <$> pure defaultClientId
148 <*> arbitrary
149 <*> arbitrary
150
151instance Arbitrary InfoHash where
152 arbitrary = (hash . B.pack) <$> arbitrary
153
154instance Arbitrary Handshake where
155 arbitrary = defaultHandshake <$> arbitrary <*> arbitrary
156
157prop_cerealEncoding :: (Serialize a, Eq a) => T a -> [a] -> Bool
158prop_cerealEncoding _ msgs = S.decode (S.encode msgs) == Right msgs
159
160{-----------------------------------------------------------------------
161 Tracker/Scrape
162-----------------------------------------------------------------------}
163
164instance Arbitrary ScrapeInfo where
165 arbitrary = ScrapeInfo <$> arbitrary <*> arbitrary
166 <*> arbitrary <*> arbitrary
167
168-- | Note that in 6 esample we intensionally do not agree with
169-- specification, because taking in account '/' in query parameter
170-- seems to be meaningless. (And thats because other clients do not
171-- chunk uri by parts) Moreover in practice there should be no
172-- difference. (I think so)
173--
174test_scrape_url :: [Framework.Test]
175test_scrape_url = L.zipWith mkTest [1 :: Int ..] (check `L.map` tests)
176 where
177 check (iu, ou) = (parseURI iu >>= (`scrapeURL` [])
178 >>= return . show) == ou
179 tests =
180 [ ( "http://example.com/announce"
181 , Just "http://example.com/scrape")
182 , ( "http://example.com/x/announce"
183 , Just "http://example.com/x/scrape")
184 , ( "http://example.com/announce.php"
185 , Just "http://example.com/scrape.php")
186 , ( "http://example.com/a" , Nothing)
187 , ( "http://example.com/announce?x2%0644"
188 , Just "http://example.com/scrape?x2%0644")
189 , ( "http://example.com/announce?x=2/4"
190 , Just "http://example.com/scrape?x=2/4")
191-- , ("http://example.com/announce?x=2/4" , Nothing) -- by specs
192 , ("http://example.com/x%064announce" , Nothing)
193 ]
194
195 mkTest i = testProperty ("scrape test #" ++ show i)
196
197{-----------------------------------------------------------------------
198 P2P/message
199-----------------------------------------------------------------------}
200
201positive :: Gen Int
202positive = fromIntegral <$> (arbitrary :: Gen Word32)
203
204instance Arbitrary ByteString where
205 arbitrary = B.pack <$> arbitrary
206
207instance Arbitrary Lazy.ByteString where
208 arbitrary = Lazy.pack <$> arbitrary
209
210instance Arbitrary BlockIx where
211 arbitrary = BlockIx <$> positive <*> positive <*> positive
212
213instance Arbitrary Block where
214 arbitrary = Block <$> positive <*> positive <*> arbitrary
215
216instance Arbitrary PortNumber where
217 arbitrary = fromIntegral <$> (arbitrary :: Gen Word16)
218
219instance Arbitrary Message where
220 arbitrary = oneof
221 [ pure KeepAlive
222 , pure Choke
223 , pure Unchoke
224 , pure Interested
225 , pure NotInterested
226 , Have <$> positive
227 , pure $ Bitfield $ BF.haveNone 0
228 , Request <$> arbitrary
229 , Piece <$> arbitrary
230 , Cancel <$> arbitrary
231 , Port <$> arbitrary
232 ]
233-- todo add all messages
234
235prop_messageEncoding :: Message -> Bool
236prop_messageEncoding msg @ (Bitfield bf)
237 = case S.decode (S.encode msg) of
238 Right (Bitfield bf') -> bf == adjustSize (totalCount bf) bf'
239 _ -> False
240prop_messageEncoding msg
241 = S.decode (S.encode msg) == Right msg
242
243{-----------------------------------------------------------------------
244 Main
245-----------------------------------------------------------------------}
246 11
247allTests :: [Framework.Test] 12clients :: [(String, String)]
248allTests = 13clients = [
249 [ -- handshake module 14 ("rtorrent","rtorrent -p 51234-51234 res/testfile.torrent") ]
250 testProperty "handshake encoding" $
251 prop_cerealEncoding (T :: T Handshake)
252 , testProperty "message encoding" prop_messageEncoding
253 ] ++ test_scrape_url
254 ++
255 [ testProperty "scrape bencode" $
256 prop_properBEncode (T :: T ScrapeInfo)
257 ]
258 15
259main :: IO () 16main :: IO ()
260main = defaultMain allTests 17main = do
18 args <- getArgs
19 let cmd' = do
20 cl <- listToMaybe $ reverse
21 $ map (tail . dropWhile (/='='))
22 $ filter (isPrefixOf "--bittorrent-client=") args
23 cmd <- (++) "screen -dm -S bittorrent-testsuite " <$> lookup cl clients
24 return cmd
25 case cmd' of
26 Just cmd -> do _ <- system "screen -S bittorrent-testsuite -X quit"
27 createProcess (shell cmd) >> return ()
28 Nothing -> return ()
29
30 let args' = (filter (not . isPrefixOf "--bittorrent-client=") args)
31 code <- catch (withArgs args' hspecMain >> return ExitSuccess) return
32
33 _ <- system "screen -S bittorrent-testsuite -X quit"
34 exitWith code >> return ()
diff --git a/tests/Spec.hs b/tests/Spec.hs
index 52ef578f..8d2b5139 100644
--- a/tests/Spec.hs
+++ b/tests/Spec.hs
@@ -1 +1 @@
{-# OPTIONS_GHC -F -pgmF hspec-discover #-} \ No newline at end of file {-# OPTIONS_GHC -F -pgmF hspec-discover -optF --hook-main #-}