packages feed

effectful-tracing-0.1.0.0: test/Effectful/Tracing/PrettyPrintLeakSpec.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : Effectful.Tracing.PrettyPrintLeakSpec
-- Description : The pretty-print interpreter's in-flight buffer drains to empty.
--
-- The pretty-print interpreter buffers the spans of each in-flight trace in a
-- @'TVar' ('Map' 'TraceId' ['Span'])@ and flushes a trace (rendering it and
-- deleting its entry) the moment its root span closes. If a finished trace were
-- ever left behind, that map would grow without bound over the lifetime of a
-- long-running process: a slow memory leak.
--
-- This test drives a program through the buffer-observing seam
-- ('runTracerPrettyWith') and checks two things: while a root span is still open
-- its already-closed children are held in the buffer (so the buffering is real,
-- not a no-op), and once every root has closed the buffer is empty again (so
-- nothing is retained). It also confirms every trace was actually rendered.
module Effectful.Tracing.PrettyPrintLeakSpec
  ( tests
  ) where

import Control.Concurrent.STM (TVar, newTVarIO, readTVarIO)
import Control.Monad.IO.Class (liftIO)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Data.ByteString.Lazy qualified as BL

import System.IO (hClose)
import System.IO.Temp (withSystemTempFile)

import Effectful (Eff, IOE, runEff)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))

import Effectful.Tracing (Tracer, withSpan)
import Effectful.Tracing.Internal.Ids (TraceId)
import Effectful.Tracing.Internal.Types (Span)
import Effectful.Tracing.Interpreter.PrettyPrint
  ( defaultPrettyPrintConfig
  , runTracerPrettyWith
  )

tests :: TestTree
tests =
  testGroup
    "Pretty-print interpreter buffer"
    [ testCase "the in-flight buffer accumulates and then drains to empty" $ do
        (mid, finalSize, headerCount) <- runWithBuffer
        -- While "root-1" is open, its two already-closed children are buffered
        -- under one trace id, so the buffer holds one trace of two spans.
        mid @?= (1, 2)
        -- Once every root has closed, the buffer retains nothing.
        finalSize @?= 0
        -- All three independent root traces were flushed (rendered) exactly once.
        headerCount @?= 3
    ]

-- | Run a program with three independent root traces through the
-- buffer-observing interpreter seam, returning: the buffer snapshot taken while
-- the first root is still open (number of in-flight traces, total spans
-- buffered across them), the buffer size after the whole program finishes, and
-- the number of rendered trace headers in the output.
runWithBuffer :: IO ((Int, Int), Int, Int)
runWithBuffer =
  withSystemTempFile "pretty-leak.txt" $ \path h -> do
    traces <- newTVarIO Map.empty
    mid <- runEff (runTracerPrettyWith traces (defaultPrettyPrintConfig h) (program traces))
    finalSize <- Map.size <$> readTVarIO traces
    hClose h
    rendered <- TE.decodeUtf8 . BL.toStrict <$> BL.readFile path
    let headerCount = length (filter ("trace " `T.isPrefixOf`) (T.lines rendered))
    pure (mid, finalSize, headerCount)

-- | Three root traces. The first opens two children that close (and so are
-- buffered) before the root does; in that window we snapshot the buffer. The
-- next two roots simply exercise another insert/delete cycle each.
program :: TVar (Map TraceId [Span]) -> Eff '[Tracer, IOE] (Int, Int)
program traces = do
  mid <- withSpan "root-1" $ do
    withSpan "child-a" (pure ())
    withSpan "child-b" (pure ())
    -- Both children have closed and been buffered under root-1's trace id;
    -- root-1 is still open, so nothing has been flushed. Snapshot the buffer.
    snapshot <- liftIO (readTVarIO traces)
    pure (Map.size snapshot, sum (map length (Map.elems snapshot)))
  withSpan "root-2" (withSpan "only-child" (pure ()))
  withSpan "root-3" (pure ())
  pure mid