packages feed

hs-opentelemetry-instrumentation-tasty-0.1: tests/OpenTelemetry/Instrumentation/Tasty/Tests.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}

module OpenTelemetry.Instrumentation.Tasty.Tests (tests) where

import Control.Concurrent (newEmptyMVar, putMVar, takeMVar)
import Control.Concurrent.Async (async)
import Control.Exception (bracket)
import Data.Functor (void)
import Data.IORef (atomicModifyIORef, newIORef, readIORef)
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Maybe (fromMaybe)
import Data.Set qualified as Set
import Data.Text (Text)
import OpenTelemetry.Instrumentation.Tasty (instrumentTestTree)
import OpenTelemetry.Processor.Span (ShutdownResult (ShutdownSuccess), SpanProcessor, spanProcessorForceFlush, spanProcessorOnEnd, spanProcessorOnStart, spanProcessorShutdown, pattern SpanProcessor)
import OpenTelemetry.Trace (ImmutableSpan (ImmutableSpan, spanName, spanParent), createTracerProvider, defaultSpanArguments, emptyTracerProviderOptions, getGlobalTracerProvider, inSpan, makeTracer, setGlobalTracerProvider, shutdownTracerProvider, tracerOptions)
import OpenTelemetry.Trace.Core (unsafeReadSpan)
import Test.Tasty (DependencyType (AllFinish), TestTree, sequentialTestGroup, testGroup, withResource)
import Test.Tasty.HUnit (testCase, (@?=))
import Test.Tasty.Ingredients (tryIngredients)
import Test.Tasty.Ingredients.Basic (Quiet (Quiet), consoleTestReporter)
import Test.Tasty.Options (setOption)


tests :: TestTree
-- each of these tests sets and tears down the global tracer provider,
-- so they have to run sequentially
tests =
  sequentialTestGroup
    "OpenTelemetry.Instrumentation.Tasty"
    AllFinish
    [ basic
    , simpleNesting
    , branching
    , testWithSpan
    , parallelism
    , resources
    ]


basic :: TestTree
basic = testCase "basic" $ do
  (_, trees) <- spanTrees $ do
    testTree <- instrumentTestTree $ testCase "hello" $ pure ()
    runTests $ testTree
  trees @?= [Leaf "hello"]


simpleNesting :: TestTree
simpleNesting = testCase "nested groups make a simple tree" $ do
  (_, trees) <- spanTrees $ do
    testTree <-
      instrumentTestTree $
        testGroup
          "g1"
          [ testGroup "g2" [testCase "t1" (pure ())]
          ]
    runTests $ testTree
  trees @?= [Branch "g1" [Branch "g2" [Leaf "t1"]]]


branching :: TestTree
branching = testCase "nested groups make a branching tree" $ do
  (_, trees) <- spanTrees $ do
    testTree <-
      instrumentTestTree $
        testGroup
          "g1"
          [ testGroup "g2" [testCase "t1" (pure ()), testCase "t2" (pure ())]
          , testGroup "g3" [testCase "t3" (pure ())]
          ]
    runTests $ testTree
  trees @?= [Branch "g1" [Branch "g2" [Leaf "t1", Leaf "t2"], Branch "g3" [Leaf "t3"]]]


testWithSpan :: TestTree
testWithSpan = testCase "test that has a span itself" $ do
  (_, trees) <- spanTrees $ do
    testTree <- instrumentTestTree $ testCase "hello" $ do
      tp <- getGlobalTracerProvider
      let tracer = makeTracer tp "test" tracerOptions
      inSpan tracer "inner" defaultSpanArguments $ pure ()
    runTests $ testTree
  trees @?= [Branch "hello" [Leaf "inner"]]


parallelism :: TestTree
parallelism = testCase "parallelism works" $ do
  block <- newEmptyMVar
  (_, trees) <- spanTrees $ do
    testTree <-
      instrumentTestTree $
        testGroup
          "g1"
          -- Force t1 to wait for t2 before it can even begin, so we
          -- should definitely not start the span for t1 until then
          [ testGroup
              "g2"
              [ withResource (takeMVar block) (const $ pure ()) $ \_ ->
                  testCase "t1" (pure ())
              ]
          , testGroup "g3" [testCase "t2" (putMVar block ())]
          ]
    runTests $ testTree
  trees @?= [Branch "g1" [Branch "g2" [Leaf "acquire", Leaf "t1", Leaf "release"], Branch "g3" [Leaf "t2"]]]


resources :: TestTree
resources = testCase "spans for resource setup and teardown" $ do
  (_, trees) <- spanTrees $ do
    tp <- getGlobalTracerProvider
    let tracer = makeTracer tp "test" tracerOptions
    let acquire = inSpan tracer "myAcquire" defaultSpanArguments $ pure ()
    let release _ = inSpan tracer "myRelease" defaultSpanArguments $ pure ()
    testTree <- instrumentTestTree $ withResource acquire release $ \_ -> testCase "hello" $ pure ()
    runTests $ testTree
  trees @?= [Leaf "hello", Branch "acquire" [Leaf "myAcquire"], Branch "release" [Leaf "myRelease"]]


data Tree a = Branch a (Set.Set (Tree a)) | Leaf a
  deriving stock (Show, Eq, Ord)


spanTrees :: IO a -> IO (a, Set.Set (Tree Text))
spanTrees act = do
  (processor, readSpans) <- recordingProcessor
  res <- bracket (setup processor) shutdownTracerProvider $ \_ -> do
    act
  spans <- readSpans
  trees <- spansToTrees spans
  pure (res, Set.fromList trees)
  where
    setup processor = do
      tp <- createTracerProvider [processor] emptyTracerProviderOptions
      setGlobalTracerProvider tp
      pure tp


toChildMap :: [ImmutableSpan] -> ([Text], Map Text [Text]) -> IO ([Text], Map Text [Text])
toChildMap [] acc = pure acc
toChildMap (ImmutableSpan {spanParent, spanName} : spans) (accRoots, accChildren) = case spanParent of
  Just parent -> do
    ImmutableSpan {spanName = parentName} <- unsafeReadSpan parent
    let existingChildren = fromMaybe mempty $ Map.lookup parentName accChildren
    toChildMap spans (accRoots, Map.insert parentName (existingChildren ++ [spanName]) accChildren)
  Nothing -> toChildMap spans (spanName : accRoots, accChildren)


toTree :: (Ord a) => Map a [a] -> a -> Tree a
toTree childMap node = case Map.lookup node childMap of
  Nothing -> Leaf node
  Just children ->
    if null children
      then Leaf node
      else Branch node $ Set.fromList $ fmap (toTree childMap) children


spansToTrees :: [ImmutableSpan] -> IO [Tree Text]
spansToTrees spans = do
  (roots, childMap) <- toChildMap spans mempty
  let trees = fmap (toTree childMap) roots
  pure trees


recordingProcessor :: IO (SpanProcessor, IO [ImmutableSpan])
recordingProcessor = do
  spans <- newIORef []

  let processor =
        SpanProcessor
          { spanProcessorOnStart = mempty
          , spanProcessorOnEnd = \spanRef -> do
              immutableSpan <- readIORef spanRef
              atomicModifyIORef spans $ \soFar -> (soFar ++ [immutableSpan], ())
          , spanProcessorShutdown = async $ pure ShutdownSuccess
          , spanProcessorForceFlush = mempty
          }

  setGlobalTracerProvider
    =<< createTracerProvider [processor] emptyTracerProviderOptions

  pure (processor, readIORef spans)


runTests :: TestTree -> IO ()
runTests t = case tryIngredients [consoleTestReporter] (setOption (Quiet True) mempty) t of
  Just act -> void act
  Nothing -> error "no ingredient"