packages feed

fractal-layer-0.1.0.0: test/Fractal/Layer/InterceptorSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module Fractal.Layer.InterceptorSpec (spec) where

import Test.Hspec
import Data.Typeable (Proxy(..), typeRep)
import Fractal.Layer.Interceptor
import UnliftIO

spec :: Spec
spec = do
  describe "CompositionType" $ do
    it "Sequential show" $
      show Sequential `shouldBe` "Sequential"

    it "Parallel show" $
      show Parallel `shouldBe` "Parallel"

    it "Sequential == Sequential" $
      Sequential `shouldBe` Sequential

    it "Parallel == Parallel" $
      Parallel `shouldBe` Parallel

    it "Sequential /= Parallel" $
      Sequential `shouldNotBe` Parallel

  describe "OperationContext" $ do
    it "show includes all fields" $ do
      let ctx = OperationContext
            { operationName = "test"
            , operationType = Just (typeRep (Proxy @Int))
            , operationMetadata = [("key", "value")]
            }
      let s = show ctx
      s `shouldContain` "test"
      s `shouldContain` "key"
      s `shouldContain` "value"

    it "show with Nothing type" $ do
      let ctx = simpleContext "bare"
      show ctx `shouldContain` "bare"

  describe "nullInterceptor" $ do
    it "all callbacks are no-ops" $ do
      let ni = nullInterceptor :: LayerInterceptor IO
      onResourceAcquire ni (simpleContext "test")
      onResourceAcquireComplete ni "test" 0
      onResourceRelease ni "test"
      onEffectRun ni (simpleContext "test")
      onEffectComplete ni "test" 0
      onServiceCreate ni (simpleContext "test")
      onServiceReuse ni "test" (typeRep (Proxy @Int))
      onCompositionStart ni Sequential
      onCompositionEnd ni Sequential 0

  describe "combineInterceptors" $ do
    it "empty list behaves like nullInterceptor" $ do
      let combined = combineInterceptors ([] :: [LayerInterceptor IO])
      onResourceAcquire combined (simpleContext "test")
      onCompositionStart combined Sequential
      onCompositionEnd combined Parallel 1.0

    it "preserves order of callbacks" $ do
      ref <- newIORef ([] :: [Int])
      let i1 = nullInterceptor { onEffectRun = \_ -> modifyIORef ref (++ [1]) }
      let i2 = nullInterceptor { onEffectRun = \_ -> modifyIORef ref (++ [2]) }
      let i3 = nullInterceptor { onEffectRun = \_ -> modifyIORef ref (++ [3]) }
      let combined = combineInterceptors [i1, i2, i3]
      onEffectRun combined (simpleContext "test")
      readIORef ref >>= (`shouldBe` [1, 2, 3])

    it "combines onCompositionStart callbacks" $ do
      ref <- newIORef ([] :: [String])
      let i1 = nullInterceptor { onCompositionStart = \t -> modifyIORef ref (++ ["i1:" ++ show t]) }
      let i2 = nullInterceptor { onCompositionStart = \t -> modifyIORef ref (++ ["i2:" ++ show t]) }
      let combined = combineInterceptors [i1, i2]

      onCompositionStart combined Sequential
      onCompositionStart combined Parallel

      logs <- readIORef ref
      logs `shouldBe` ["i1:Sequential", "i2:Sequential", "i1:Parallel", "i2:Parallel"]

    it "combines onCompositionEnd callbacks" $ do
      ref <- newIORef ([] :: [String])
      let i1 = nullInterceptor { onCompositionEnd = \t _ -> modifyIORef ref (++ ["i1:" ++ show t]) }
      let i2 = nullInterceptor { onCompositionEnd = \t _ -> modifyIORef ref (++ ["i2:" ++ show t]) }
      let combined = combineInterceptors [i1, i2]

      onCompositionEnd combined Sequential 1.0
      logs <- readIORef ref
      logs `shouldBe` ["i1:Sequential", "i2:Sequential"]

    it "combines all callback types" $ do
      ref <- newIORef ([] :: [String])
      let tracker :: String -> LayerInterceptor IO
          tracker tag = (nullInterceptor :: LayerInterceptor IO)
            { onResourceAcquire = \_ -> modifyIORef ref (++ [tag ++ ":acq"])
            , onResourceAcquireComplete = \_ _ -> modifyIORef ref (++ [tag ++ ":acqd"])
            , onResourceRelease = \_ -> modifyIORef ref (++ [tag ++ ":rel"])
            , onEffectRun = \_ -> modifyIORef ref (++ [tag ++ ":eff"])
            , onEffectComplete = \_ _ -> modifyIORef ref (++ [tag ++ ":done"])
            , onServiceCreate = \_ -> modifyIORef ref (++ [tag ++ ":svc"])
            , onServiceReuse = \_ _ -> modifyIORef ref (++ [tag ++ ":reuse"])
            , onCompositionStart = \_ -> modifyIORef ref (++ [tag ++ ":cstart"])
            , onCompositionEnd = \_ _ -> modifyIORef ref (++ [tag ++ ":cend"])
            }
      let combined = combineInterceptors [tracker "a", tracker "b"]

      onResourceAcquire combined (simpleContext "r")
      onResourceAcquireComplete combined "r" 0
      onResourceRelease combined "r"
      onEffectRun combined (simpleContext "e")
      onEffectComplete combined "e" 0
      onServiceCreate combined (simpleContext "s")
      onServiceReuse combined "s" (typeRep (Proxy @Int))
      onCompositionStart combined Sequential
      onCompositionEnd combined Sequential 0

      logs <- readIORef ref
      logs `shouldBe`
        [ "a:acq", "b:acq"
        , "a:acqd", "b:acqd"
        , "a:rel", "b:rel"
        , "a:eff", "b:eff"
        , "a:done", "b:done"
        , "a:svc", "b:svc"
        , "a:reuse", "b:reuse"
        , "a:cstart", "b:cstart"
        , "a:cend", "b:cend"
        ]

  describe "simpleContext" $ do
    it "creates context with name only" $ do
      let ctx = simpleContext "my-op"
      operationName ctx `shouldBe` "my-op"
      operationType ctx `shouldBe` Nothing
      operationMetadata ctx `shouldBe` []

  describe "withType" $ do
    it "adds type to context" $ do
      let ctx = withType (typeRep (Proxy @Bool)) (simpleContext "op")
      operationType ctx `shouldBe` Just (typeRep (Proxy @Bool))
      operationName ctx `shouldBe` "op"
      operationMetadata ctx `shouldBe` []

  describe "withMetadata" $ do
    it "adds metadata to context" $ do
      let ctx = withMetadata [("k1", "v1"), ("k2", "v2")] (simpleContext "op")
      operationMetadata ctx `shouldBe` [("k1", "v1"), ("k2", "v2")]

    it "appends to existing metadata" $ do
      let ctx0 = OperationContext "op" Nothing [("existing", "data")]
      let ctx1 = withMetadata [("new", "field")] ctx0
      operationMetadata ctx1 `shouldBe` [("existing", "data"), ("new", "field")]

  describe "showList coverage" $ do
    it "showList for CompositionType" $ do
      let s = show [Sequential, Parallel, Sequential]
      s `shouldContain` "Sequential"
      s `shouldContain` "Parallel"

    it "showList for OperationContext" $ do
      let ctx = simpleContext "test"
      let s = show [ctx, ctx]
      s `shouldContain` "test"