packages feed

freckle-http-0.4.1.0: tests/Freckle/App/Http/Cache/InProcessSpec.hs

module Freckle.App.Http.Cache.InProcessSpec
  ( spec
  ) where

import Prelude

import Data.IORef (newIORef, readIORef, writeIORef)
import Data.Time (UTCTime, addUTCTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Freckle.App.Http.Cache.InProcess
  ( InProcessHttpCacheSettings (..)
  , Reap (..)
  , cacheDelete
  , cacheGet
  , cacheGetReap
  , cacheReap
  , cacheSet
  , defaultSettings
  , newInProcessHttpCache
  )
import Test.Hspec

spec :: Spec
spec = describe "InProcessHttpCache" $ do
  it "misses on an empty cache" $ do
    c <- newInProcessHttpCache defaultSettings {maxBytes = 1024}
    mv <- cacheGet c "a"
    mv `shouldBe` Nothing

  it "returns a value after it is set" $ do
    c <- newInProcessHttpCache defaultSettings {maxBytes = 1024}
    cacheSet c "a" "hello" 300
    mv <- cacheGet c "a"
    mv `shouldBe` Just "hello"

  it "no longer returns a value once evicted" $ do
    c <- newInProcessHttpCache defaultSettings {maxBytes = 1024}
    cacheSet c "a" "hello" 300
    cacheDelete c "a"
    mv <- cacheGet c "a"
    mv `shouldBe` Nothing

  it "no longer returns a value once its own ttl has elapsed" $ do
    c <- newInProcessHttpCache defaultSettings {maxBytes = 1024}
    cacheSet c "a" "hello" 0
    mv <- cacheGet c "a"
    mv `shouldBe` Nothing

  it "does not check any other key's ttl when getting one key" $ do
    c <- newInProcessHttpCache defaultSettings {maxBytes = 1024}
    cacheSet c "a" "hello" 0 -- already expired
    cacheSet c "b" "world" 300 -- fresh, unrelated
    _ <- cacheGet c "b" -- looks up "b" only
    mv <- cacheGetReap NoReap c "a" -- skip get's own check, to see whether "b"'s get touched "a"
    mv `shouldBe` Just "hello" -- untouched; still there, even though already expired

  it "leaves an entry that has expired since it was set until something reaps it" $ do
    clockRef <- newIORef epoch
    c <-
      newInProcessHttpCache
        defaultSettings {maxBytes = 1024, clock = readIORef clockRef}
    cacheSet c "a" "hello" 1 -- expires 1 second after epoch
    writeIORef clockRef (addUTCTime 2 epoch) -- simulate 2 seconds passing, no delay needed
    stillThere <- cacheGetReap NoReap c "a" -- skip get's own check, so its lingering presence shows
    stillThere `shouldBe` Just "hello"
    cacheReap c
    afterReap <- cacheGetReap NoReap c "a" -- skip get's own check again, to isolate cacheReap's effect
    afterReap `shouldBe` Nothing

  it "evicts the least-recently-used entry once over budget" $ do
    c <- newInProcessHttpCache defaultSettings {maxBytes = 10}
    cacheSet c "a" "aaaaa" 300 -- total: 5
    cacheSet c "b" "bbbbb" 300 -- total: 10
    cacheSet c "c" "ccccc" 300 -- total: 15, over budget; evicts "a"
    va <- cacheGet c "a"
    vb <- cacheGet c "b"
    vc <- cacheGet c "c"
    va `shouldBe` Nothing
    vb `shouldBe` Just "bbbbb"
    vc `shouldBe` Just "ccccc"

  it "treats a 'get' as refreshing an entry's recency" $ do
    c <- newInProcessHttpCache defaultSettings {maxBytes = 10}
    cacheSet c "a" "aaaaa" 300 -- total: 5
    cacheSet c "b" "bbbbb" 300 -- total: 10
    _ <- cacheGet c "a" -- "a" is now more-recently-used than "b"
    cacheSet c "c" "ccccc" 300 -- total: 15, over budget; evicts "b"
    va <- cacheGet c "a"
    vb <- cacheGet c "b"
    vc <- cacheGet c "c"
    va `shouldBe` Just "aaaaa"
    vb `shouldBe` Nothing
    vc `shouldBe` Just "ccccc"

  it "does not cache a single response larger than the whole budget" $ do
    c <- newInProcessHttpCache defaultSettings {maxBytes = 10}
    cacheSet c "a" "this value is way over budget" 300
    mv <- cacheGet c "a"
    mv `shouldBe` Nothing

  it "evicts an already-expired entry ahead of the least-recently-used one" $ do
    c <- newInProcessHttpCache defaultSettings {maxBytes = 10}
    cacheSet c "a" "aaaaa" 300 -- total: 5, fresh, least-recently-touched
    cacheSet c "b" "bbbbb" 0 -- total: 10, already expired, but more-recently-touched than "a"
    cacheSet c "c" "ccccc" 300 -- total: 15, over budget; evicts "b" (expired), not "a" (LRU tail)
    va <- cacheGetReap NoReap c "a"
    vb <- cacheGetReap NoReap c "b" -- skip get's own check, to isolate the set-time eviction
    vc <- cacheGetReap NoReap c "c"
    va `shouldBe` Just "aaaaa"
    vb `shouldBe` Nothing
    vc `shouldBe` Just "ccccc"

  it "falls back to least-recently-used once no expired entries remain" $ do
    c <- newInProcessHttpCache defaultSettings {maxBytes = 10}
    cacheSet c "a" "aaaaa" 0 -- total: 5, already expired
    cacheSet c "b" "bbbbb" 300 -- total: 10, fresh
    cacheSet c "c" "ccccc" 300 -- total: 15, over budget; evicts "a" (expired)
    cacheSet c "d" "ddddd" 300 -- total: 15, over budget; no expired entries left, evicts "b" (LRU)
    va <- cacheGet c "a"
    vb <- cacheGet c "b"
    vc <- cacheGet c "c"
    vd <- cacheGet c "d"
    va `shouldBe` Nothing
    vb `shouldBe` Nothing
    vc `shouldBe` Just "ccccc"
    vd `shouldBe` Just "ddddd"

epoch :: UTCTime
epoch = posixSecondsToUTCTime 0