packages feed

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

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

-- |
-- Module      : Effectful.Tracing.TestingSpec
-- Description : Tests for the public testing helpers.
--
-- Drives a small traced computation through the re-exported in-memory
-- interpreter and checks that the matchers and finders in
-- "Effectful.Tracing.Testing" report the captured tree faithfully: span lookup
-- (single and repeated names), parent/child and descendant structure, root
-- detection, and the attribute / status / event / kind predicates.
module Effectful.Tracing.TestingSpec
  ( tests
  ) where

import Data.List (sort)
import Data.Maybe (isJust)
import Data.Text (Text)

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

import Effectful.Tracing
  ( SpanArguments (kind)
  , SpanKind (Client, Internal)
  , SpanStatus (Ok, Unset)
  , Tracer
  , addAttribute
  , addEvent
  , defaultSpanArguments
  , setStatus
  , withSpan
  , withSpan'
  )
import Effectful.Tracing.Attribute (AttributeValue (AttrText))
import Effectful.Tracing.Internal.Types (Span, spanName)
import Effectful.Tracing.Testing
  ( childrenOf
  , descendantsOf
  , findSpan
  , findSpans
  , hasAttribute
  , hasAttributeValue
  , hasEvent
  , hasKind
  , hasStatus
  , isChildOf
  , isRoot
  , lookupAttribute
  , lookupEvent
  , newCapturedSpans
  , readCapturedSpans
  , rootSpans
  , runTracerInMemory
  )

tests :: TestTree
tests =
  testGroup
    "Testing helpers"
    [ testCase "findSpan / findSpans locate spans by name" $ do
        spans <- captured
        fmap spanName (findSpan "root" spans) @?= Just "root"
        findSpan "absent" spans @?= Nothing
        -- "leaf" is opened twice, so findSpans returns both occurrences.
        length (findSpans "leaf" spans) @?= 2
        findSpans "absent" spans @?= []
    , testCase "rootSpans and isRoot agree on the single root" $ do
        spans <- captured
        fmap spanName (rootSpans spans) @?= ["root"]
        assertBool "root isRoot" (all isRoot (findSpan "root" spans))
        assertBool "db is not a root" (not (any isRoot (findSpan "db" spans)))
    , testCase "childrenOf returns direct children only" $ do
        spans <- captured
        case findSpan "root" spans of
          Just root -> sort (map spanName (childrenOf root spans)) @?= ["api", "leaf", "leaf"]
          Nothing -> assertBool "root present" False
    , testCase "descendantsOf returns the whole subtree" $ do
        spans <- captured
        case findSpan "root" spans of
          Just root -> sort (map spanName (descendantsOf root spans)) @?= ["api", "db", "leaf", "leaf"]
          Nothing -> assertBool "root present" False
    , testCase "isChildOf reflects the recorded parent" $ do
        spans <- captured
        case (findSpan "db" spans, findSpan "api" spans, findSpan "root" spans) of
          (Just db, Just api, Just root) -> do
            assertBool "db is a child of api" (db `isChildOf` api)
            assertBool "db is not a child of root" (not (db `isChildOf` root))
          _ -> assertBool "db, api, root present" False
    , testCase "attribute matchers read the captured attributes" $ do
        spans <- captured
        case findSpan "root" spans of
          Just root -> do
            lookupAttribute "service" root @?= Just (AttrText "checkout")
            lookupAttribute "absent" root @?= Nothing
            hasAttribute "service" root @?= True
            hasAttribute "absent" root @?= False
            hasAttributeValue "service" (AttrText "checkout") root @?= True
            hasAttributeValue "service" (AttrText "other") root @?= False
          Nothing -> assertBool "root present" False
    , testCase "status, event, and kind matchers" $ do
        spans <- captured
        case (findSpan "root" spans, findSpan "api" spans) of
          (Just root, Just api) -> do
            hasStatus Ok root @?= True
            hasStatus Unset api @?= True
            hasEvent "ready" root @?= True
            hasEvent "absent" root @?= False
            isJust (lookupEvent "ready" root) @?= True
            hasKind Client api @?= True
            hasKind Internal root @?= True
          _ -> assertBool "root and api present" False
    ]

-- | A small traced tree: a root with a service attribute, a "ready" event, and
-- an Ok status; a client-kind "api" child holding a "db" grandchild; and two
-- sibling "leaf" spans sharing a name.
program :: Tracer :> es => Eff es ()
program = withSpan "root" $ do
  addAttribute "service" ("checkout" :: Text)
  addEvent "ready" []
  setStatus Ok
  withSpan' "api" defaultSpanArguments {kind = Client} $
    withSpan "db" (pure ())
  withSpan "leaf" (pure ())
  withSpan "leaf" (pure ())

-- | Run 'program' through the in-memory interpreter and return the captured
-- spans.
captured :: IO [Span]
captured = runEff $ do
  buffer <- newCapturedSpans
  runTracerInMemory buffer program
  readCapturedSpans buffer