packages feed

haskoin-store-1.12.0: test/Haskoin/Store/Web/PiecesSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Haskoin.Store.Web.PiecesSpec (spec) where

import Control.Exception (ErrorCall (..), throwIO, try)
import Data.Aeson (encode, toEncoding)
import Data.ByteString.Builder (Builder, toLazyByteString)
import Data.ByteString.Lazy qualified as L
import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
import Haskoin.Store.Web.Pieces
import Network.Wai.Internal (Response (..))
import Test.Hspec hiding (around)

spec :: Spec
spec = describe "a response in pieces" $ do
  it "has the bytes of its pieces, in order" $ do
    answer [] `shouldReturn` ("", True)
    answer [return "a"] `shouldReturn` ("a", True)
    answer (map return ["a", "", "bc", "d"]) `shouldReturn` ("abcd", True)
  it "has the bytes of a list encoded at once, for pieces of its items" $ do
    let lists = [[], [[]], [[1]], [[1, 2], [3]], [[], [1], [], [2, 3], []], [[1 .. 100], [101 .. 150]]]
        listed :: [[Int]] -> IO (L.ByteString, Bool)
        listed l = listPieces toEncoding (map return l) >>= answer
    mapM_ (\l -> listed l `shouldReturn` (encode (concat l), True)) lists
  it "cuts keys into pieces of the size, and the last of what is left" $ do
    read' <- newIORef []
    let fetch ks = modifyIORef' read' (<> [ks]) >> return ks
    pieces <- listPieces toEncoding (pieced 3 fetch [1 .. 8 :: Int])
    answer pieces `shouldReturn` (encode [1 .. 8 :: Int], True)
    readIORef read' `shouldReturn` [[1, 2, 3], [4, 5, 6], [7, 8]]
  it "puts bytes around pieces without a piece more" $ do
    let wrapped ps = around "<" ">" (map return ps)
    let cases = [[], ["a"], ["a", "b"], ["a", "b", "c"]]
    mapM (fmap (toLazyByteString . mconcat) . sequence . wrapped) cases
      `shouldReturn` ["<>", "<a>", "<ab>", "<abc>"]
    map (length . wrapped) cases `shouldBe` [1, 1, 2, 3]
  it "throws what the first piece throws, before there is a response" $ do
    ended <- newIORef False
    r <- try (piecesResponse [] (mark ended) [broken, return "b"])
    either (\(ErrorCall e) -> e) (const "") r `shouldBe` "gone"
  it "ends short, and says that it ended, when a later piece throws" $ do
    ended <- newIORef False
    res <- piecesResponse [] (mark ended) [return "[1", broken]
    written <- newIORef mempty
    r <- try (body res (\b -> modifyIORef' written (<> b)) (return ()))
    either (\(ErrorCall e) -> e) (const "") r `shouldBe` "gone"
    (toLazyByteString <$> readIORef written) `shouldReturn` "[1"
    readIORef ended `shouldReturn` True
  it "reads one piece ahead of the one that it writes, and no further" $ do
    events <- newIORef []
    let note e = modifyIORef' events (<> [e])
        piece n = note ("read " <> show n) >> return mempty
    res <- piecesResponse [] (return ()) (map piece [1 .. 4 :: Int])
    body res (const (return ())) (note "flush")
    -- the next piece is read while one is written, so which of the two
    -- comes first is not fixed; what is fixed is that by the flush of a
    -- piece no piece past the next one has been asked for
    seen <- readIORef events
    filter (/= "flush") seen `shouldBe` ["read 1", "read 2", "read 3", "read 4"]
    let readsBy k = length (filter (/= "flush") (before k seen))
        before k = map fst . takeWhile ((< k) . snd) . (zip <*> flushes)
        flushes = scanl (\n e -> if e == "flush" then n + 1 else n) (0 :: Int)
    map readsBy [1, 2, 3] `shouldSatisfy` and . zipWith (>=) [2, 3, 4]
  where
    broken :: IO Builder
    broken = throwIO (ErrorCall "gone")
    mark :: IORef Bool -> IO ()
    mark ended = modifyIORef' ended (const True)
    body (ResponseStream _ _ run) = run
    body _ = \_ _ -> expectationFailure "not a stream"
    -- the bytes of the response, and whether it said that it ended
    answer :: [IO Builder] -> IO (L.ByteString, Bool)
    answer pieces = do
      ended <- newIORef False
      res <- piecesResponse [] (mark ended) pieces
      written <- newIORef mempty
      body res (\b -> modifyIORef' written (<> b)) (return ())
      (,) . toLazyByteString <$> readIORef written <*> readIORef ended