packages feed

haskoin-node-2.4.0: test/unit/Haskoin/Node/PeerMgr/AddressesSpec.hs

module Haskoin.Node.PeerMgr.AddressesSpec (spec) where

import Data.Maybe (isJust, isNothing)
import Data.Time.Calendar (fromGregorian)
import Data.Time.Clock (NominalDiffTime, UTCTime (..), addUTCTime)
import Haskoin.Node.PeerMgr.Addresses
import Test.Hspec
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck

t0 :: UTCTime
t0 = UTCTime (fromGregorian 2026 1 1) 0

later :: NominalDiffTime -> UTCTime
later d = d `addUTCTime` t0

book :: [Int] -> AddressBook Int
book = foldl' (flip (addAddress Discovered)) emptyBook

-- | Take an address that must be available.
taken :: UTCTime -> AddressBook Int -> (Int, AddressBook Int)
taken now b = case takeAddress now b of
  Just x -> x
  Nothing -> error "no address to take"

-- | Take an address, end its connection, and return the new book.
cycleWith :: Outcome -> Int -> AddressBook Int -> AddressBook Int
cycleWith outcome a b = case takeAddress t0 b of
  Just (a', b') | a == a' -> finishAddress t0 outcome a b'
  _ -> error "address not taken"

-- | When an address can be tried again, if it is waiting.
retryAt :: Int -> AddressBook Int -> Maybe UTCTime
retryAt a b = case addressState a b of
  Just (Waiting t _) -> Just t
  _ -> Nothing

-- | Transitions applied to a few addresses at increasing times.
data Op
  = Add Source Int
  | Take NominalDiffTime
  | Give Int
  | Connected Int
  | Finish NominalDiffTime Outcome Int
  deriving stock (Show)

instance Arbitrary Op where
  arbitrary =
    oneof
      [ Add <$> elements [Configured, Discovered] <*> addr,
        Take <$> time,
        Give <$> addr,
        Connected <$> addr,
        Finish <$> time <*> elements [Disconnected, Failed, Misbehaved] <*> addr
      ]
    where
      addr = chooseInt (1, 4)
      time = fromIntegral <$> chooseInt (0, 100000)

apply :: AddressBook Int -> Op -> AddressBook Int
apply b = \case
  Add src a -> addAddress src a b
  Take d -> maybe b snd (takeAddress (later d) b)
  Give a -> returnAddress a b
  Connected a -> markConnected a b
  Finish d o a -> finishAddress (later d) o a b

spec :: Spec
spec = describe "address book" $ do
  it "hands out each address once" $ do
    let (a, b) = taken t0 (book [1])
    a `shouldBe` 1
    addressState 1 b `shouldBe` Just InUse
    takeAddress t0 b `shouldSatisfy` isNothing

  it "takes back an address that was not used" $ do
    let (_, b) = taken t0 (book [1])
    addressState 1 (returnAddress 1 b) `shouldBe` Just Ready

  it "waits briefly later a normal disconnection" $ do
    let b = cycleWith Disconnected 1 (book [1])
    retryAt 1 b `shouldBe` Just (later retryDelay)
    takeAddress t0 b `shouldSatisfy` isNothing
    takeAddress (later retryDelay) b `shouldSatisfy` isJust

  it "doubles the wait later each failure, up to a limit" $ do
    let waits = map (retryAt 1) (drop 1 (iterate (cycleFailed 1) (book [1])))
        cycleFailed a b = finishAddress t0 Failed a (snd (taken (later 1000000) b))
    take 4 waits `shouldBe` map (Just . later) [2, 4, 8, 16]
    waits !! 20 `shouldBe` Just (later maxBackoff)

  it "forgets failures once the peer comes online" $ do
    let failed = cycleWith Failed 1 (book [1])
        (_, b) = taken (later 1000) failed
        b' = finishAddress t0 Failed 1 (markConnected 1 b)
    retryAt 1 b' `shouldBe` Just (later 2)

  it "bans a discovered address that misbehaved" $
    retryAt 1 (cycleWith Misbehaved 1 (book [1])) `shouldBe` Just (later banTime)

  it "never bans a configured address" $ do
    let b = addAddress Configured 1 (book [1])
    retryAt 1 (cycleWith Misbehaved 1 b) `shouldBe` Just (later maxBackoff)

  it "keeps a bounded number of discovered addresses" $ do
    let b = book [1 .. maxDiscovered + 10]
        b' = addAddress Configured (maxDiscovered + 20) b
    length (knownAddresses b) `shouldBe` maxDiscovered
    length (knownAddresses b') `shouldBe` maxDiscovered + 1

  describe "loading" $ do
    it "loads when the book is empty" $
      needsReload t0 (emptyBook :: AddressBook Int) `shouldBe` True

    it "does not load while an address can be tried" $
      needsReload t0 (book [1]) `shouldBe` False

    it "does not load again too soon" $ do
      -- banned, so that nothing can be tried for a long time
      let b = markLoaded t0 (cycleWith Misbehaved 1 (book [1]))
      needsReload (later 1) b `shouldBe` False
      needsReload (later reloadInterval) b `shouldBe` True

  prop "only hands out addresses that can be tried" $ \(ops :: [Op]) (d :: Int) ->
    let b = foldl' apply emptyBook ops
        now = later (fromIntegral (abs d))
     in case takeAddress now b of
          Nothing -> property True
          Just (a, b') ->
            let ok = case addressState a b of
                  Just Ready -> True
                  Just (Waiting t _) -> t <= now
                  _ -> False
             in ok .&&. addressState a b' === Just InUse

  prop "never forgets an address" $ \(ops :: [Op]) ->
    let b = foldl' apply emptyBook ops
        added = [a | Add _ a <- ops]
     in all (\a -> isJust (addressState a b)) added