packages feed

launchdarkly-server-sdk-4.6.1: test/Spec/Events.hs

module Spec.Events (allTests) where

import Control.Concurrent (forkIO)
import Control.Concurrent.MVar (newEmptyMVar, putMVar, readMVar, takeMVar)
import Control.Monad (forM, forM_, replicateM_)
import Data.Aeson (Value (..))
import Data.Function ((&))
import Data.Generics.Product (getField)
import GHC.Natural (Natural)
import System.Timeout (timeout)
import Test.HUnit

import LaunchDarkly.AesonCompat (objectValues)
import LaunchDarkly.Server.Config (configSetSendEvents, makeConfig)
import LaunchDarkly.Server.Config.Internal (Config)
import LaunchDarkly.Server.Context (makeContext)
import LaunchDarkly.Server.Details (EvaluationReason (EvaluationReasonOff))
import LaunchDarkly.Server.Events

enabledConfig :: Config
enabledConfig = makeConfig "sdk-key"

disabledConfig :: Config
disabledConfig = makeConfig "sdk-key" & configSetSendEvents False

evalEvent :: EvalEvent
evalEvent = newUnknownFlagEvent "flag" (Bool True) EvaluationReasonOff (makeContext "user-key" "user")

summaryEvents :: EventState -> IO [SummaryEvent]
summaryEvents state = do
    events <- readMVar $ getField @"events" state
    pure [summary | EventTypeSummary summary <- events]

totalCount :: SummaryEvent -> Natural
totalCount summary =
    sum
        [ getField @"count" counter
        | flag <- objectValues $ getField @"features" summary
        , counter <- objectValues $ getField @"counters" flag
        ]

-- Test that a flush with no evaluations does not queue a summary event.
testNoSummaryWithoutEvaluations :: Test
testNoSummaryWithoutEvaluations = TestCase $ do
    state <- makeEventState enabledConfig
    processSummary enabledConfig state
    summaries <- summaryEvents state
    assertEqual "summary event count" 0 (length summaries)

-- Test that evaluations with events disabled do not keep summary data.
-- The flush uses an enabled config to show any data that is kept.
testDisabledEventsKeepNoSummary :: Test
testDisabledEventsKeepNoSummary = TestCase $ do
    state <- makeEventState disabledConfig
    replicateM_ 100 $ processEvalEvents disabledConfig state (makeContext "user-key" "user") False [evalEvent] False
    processSummary enabledConfig state
    summaries <- summaryEvents state
    assertEqual "summary event count" 0 (length summaries)

-- Test that the start date is the earliest evaluation time, not the first one recorded.
testStartDateIsEarliestEvaluation :: Test
testStartDateIsEarliestEvaluation = TestCase $ do
    state <- makeEventState enabledConfig
    runSummary 200 state evalEvent False
    runSummary 100 state evalEvent False
    runSummary 300 state evalEvent False
    processSummary enabledConfig state
    summaries <- summaryEvents state
    case summaries of
        [summary] -> do
            assertEqual "startDate" 100 (getField @"startDate" summary)
            assertEqual "total count" 3 (totalCount summary)
        _ -> assertFailure $ "expected one summary event, got " ++ show (length summaries)

-- Test that a flush resets the summary, so the next window counts only new evaluations.
testFlushStartsNewWindow :: Test
testFlushStartsNewWindow = TestCase $ do
    state <- makeEventState enabledConfig
    runSummary 100 state evalEvent False
    runSummary 100 state evalEvent False
    processSummary enabledConfig state
    runSummary 500 state evalEvent False
    processSummary enabledConfig state
    -- The events queue holds the newest event first.
    summaries <- summaryEvents state
    case summaries of
        [second, first] -> do
            assertEqual "first startDate" 100 (getField @"startDate" first)
            assertEqual "first total count" 2 (totalCount first)
            assertEqual "second startDate" 500 (getField @"startDate" second)
            assertEqual "second total count" 1 (totalCount second)
        _ -> assertFailure $ "expected two summary events, got " ++ show (length summaries)

-- Test that concurrent evaluations finish and that the summary counts every evaluation.
testConcurrentEvaluationsAreCounted :: Test
testConcurrentEvaluationsAreCounted = TestCase $ do
    state <- makeEventState enabledConfig
    let threadCount = 32 :: Int
        callsPerThread = 1000 :: Int
    result <- timeout (30 * 1000000) $ do
        done <- forM [1 .. threadCount] $ \thread -> do
            finished <- newEmptyMVar
            _ <- forkIO $ do
                forM_ [1 .. callsPerThread] $ \call ->
                    runSummary (fromIntegral $ 1000 + thread + call) state evalEvent False
                putMVar finished ()
            pure finished
        mapM_ takeMVar done
    assertEqual "all threads finished" (Just ()) result
    processSummary enabledConfig state
    summaries <- summaryEvents state
    case summaries of
        [summary] -> do
            assertEqual "startDate" 1002 (getField @"startDate" summary)
            assertEqual "total count" (fromIntegral $ threadCount * callsPerThread) (totalCount summary)
        _ -> assertFailure $ "expected one summary event, got " ++ show (length summaries)

allTests :: Test
allTests =
    TestList
        [ testNoSummaryWithoutEvaluations
        , testDisabledEventsKeepNoSummary
        , testStartDateIsEarliestEvaluation
        , testFlushStartsNewWindow
        , testConcurrentEvaluationsAreCounted
        ]