packages feed

shibuya-metrics-0.11.0.0: test/Shibuya/Metrics/OutboundQueueSpec.hs

module Shibuya.Metrics.OutboundQueueSpec (spec) where

import Control.Monad (forM_)
import Data.ByteString qualified as BS
import Shibuya.Metrics.Outbound qualified as Q
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)

spec :: Spec
spec = describe "bounded outbound queue" $ do
  it "preserves frame and byte bounds and exact counters over exhaustive transition traces" $ do
    let actions =
          [ Add Q.Required 1,
            Add Q.Required 7,
            Add Q.Update 3,
            Add Q.Update 12,
            Add Q.Ephemeral 2,
            Pop,
            Discard
          ]
        step q action = case action of
          Add keep size -> snd $ Q.enqueue (Q.Limits 3 10) [value keep size] q
          Pop -> maybe q snd $ Q.pop q
          Discard -> Q.discardUpdates q
        walk depth q = do
          Q.queuedFrames q `shouldBe` length (Q.entries q)
          Q.queuedBytes q `shouldBe` sum (map (BS.length . Q.payload) $ Q.entries q)
          Q.queuedFrames q `shouldSatisfy` (<= 3)
          Q.queuedBytes q `shouldSatisfy` (<= 10)
          if depth == (0 :: Int) then pure () else forM_ actions (walk (depth - 1) . step q)
    walk 5 Q.empty

  it "evicts updates oldest-first while retaining required order" $ do
    let a = value Q.Update 1
        b = value Q.Required 2
        c = value Q.Update 3
        (_, q) = Q.enqueue (Q.Limits 3 10) [a, b, c] Q.empty
        (outcome, next) = Q.enqueue (Q.Limits 3 10) [value Q.Required 4] q
    outcome `shouldBe` Q.DroppedUpdates
    map (BS.length . Q.payload) (Q.entries next) `shouldBe` [2, 3, 4]

  it "refuses required saturation and oversized snapshots without dropping required frames" $ do
    let (_, q) = Q.enqueue (Q.Limits 2 10) [value Q.Required 4, value Q.Required 4] Q.empty
    Q.enqueue (Q.Limits 2 10) [value Q.Required 1] q `shouldBe` (Q.MustClose, q)
    Q.enqueue (Q.Limits 2 10) [value Q.Required 11] Q.empty `shouldBe` (Q.MustClose, Q.empty)

  it "admits recovery batches wholly and consecutively after earlier required frames" $ do
    let (_, q) = Q.enqueue (Q.Limits 3 10) [value Q.Required 1, value Q.Update 3] Q.empty
        batch = [value Q.Required 4, value Q.Required 5]
        (result, next) = Q.enqueue (Q.Limits 3 10) batch (Q.discardUpdates q)
    result `shouldBe` Q.Enqueued
    Q.entries next `shouldBe` value Q.Required 1 : batch
    Q.enqueue (Q.Limits 2 10) batch q `shouldBe` (Q.MustClose, q)

  it "marks rejected oversized updates for recovery, but not lost pongs" $ do
    fst (Q.enqueue (Q.Limits 2 10) [value Q.Update 11] Q.empty) `shouldBe` Q.DroppedUpdates
    fst (Q.enqueue (Q.Limits 2 10) [value Q.Ephemeral 11] Q.empty) `shouldBe` Q.Enqueued

  it "removes unsent old-generation updates without deleting required snapshots" $ do
    let snapshot = Q.frame 2 Q.Required "snapshot"
        update = Q.frame 1 Q.Update "old"
        (_, q) = Q.enqueue (Q.Limits 2 30) [update, snapshot] Q.empty
    Q.entries (Q.discardUpdates q) `shouldBe` [snapshot]

value :: Q.Retention -> Int -> Q.Frame
value keep size = Q.frame 0 keep (BS.replicate size 97)

data Action = Add Q.Retention Int | Pop | Discard