keiki-0.1.0.0: test/Keiki/NoThunksSpec.hs
{-# LANGUAGE DerivingVia #-}
-- 'Vertex' is a user-defined nullary enum in
-- "Keiki.Fixtures.UserRegistration"; we add an orphan
-- @NoThunks Vertex@ here purely for the canonical-log assertion.
-- This mirrors the pattern a real embedder would use in their own
-- module: derive via 'OnlyCheckWhnf' for small enum-like vertices.
{-# OPTIONS_GHC -Wno-orphans #-}
module Keiki.NoThunksSpec (spec) where
import Control.DeepSeq (force)
import Data.Maybe (isNothing)
import Data.Proxy (Proxy (..))
import Data.Time (UTCTime (..), fromGregorian, secondsToDiffTime)
import Keiki.Core (RegFile (..), reconstitute)
import Keiki.Fixtures.UserRegistration
( AccountConfirmedData (..),
AccountDeletedData (..),
ConfirmationEmailSentData (..),
ConfirmationResentData (..),
RegistrationStartedData (..),
UserEvent (..),
Vertex,
userReg,
)
import Keiki.NoThunks ()
import NoThunks.Class (NoThunks, OnlyCheckWhnf (..), noThunks)
import Test.Hspec
deriving via OnlyCheckWhnf Vertex instance NoThunks Vertex
-- | A trivial UTC-time fixture, mirroring the one in
-- 'Keiki.Fixtures.UserRegistrationSpec', except deep-forced.
--
-- 'Data.Time.Clock.UTCTime'\'s fields are *lazy* — bang-binding the
-- whole value reaches WHNF (the outer constructor) but leaves
-- @utctDay@ and @utctDayTime@ as thunks. The bundled @nothunks@
-- instance derives via 'NoThunks.Class.InspectHeap' and walks the
-- entire heap closure, so it would report those internal thunks as
-- (false-positive) leaks. 'Control.DeepSeq.force' uses the @time@
-- package's 'Control.DeepSeq.NFData' instance to evaluate the
-- whole structure to NF, dispelling the false positive.
t :: Integer -> UTCTime
t s = force (UTCTime (fromGregorian 2026 5 1) (secondsToDiffTime s))
-- | The synthesis §4 canonical event log. Reproduced inline (rather
-- than imported) because the upstream definition lives in another
-- spec module's local scope; coupling test modules to share a
-- five-line literal would cost more than it saves.
canonicalLog :: [UserEvent]
canonicalLog =
let !t0 = t 0
!t100 = t 100
!t200 = t 200
!t300 = t 300
in [ RegistrationStarted (RegistrationStartedData "alice@x" "Z9F4" t0),
ConfirmationEmailSent (ConfirmationEmailSentData "alice@x"),
ConfirmationResent (ConfirmationResentData "alice@x" "K2P7" t100),
AccountConfirmed (AccountConfirmedData "alice@x" "K2P7" t200),
AccountDeleted (AccountDeletedData "alice@x" t300)
]
-- A genuine thunk for the sanity check. Using @(1 + 1) :: Int@ as a
-- fixture is unreliable: GHC at -O1 may constant-fold it before the
-- thunk is ever stored. NOINLINE on a top-level binding keeps the
-- reference opaque to the optimizer.
{-# NOINLINE leakySlotValue #-}
leakySlotValue :: Int
leakySlotValue =
error
"leakySlotValue should not be forced; the spec only inspects \
\the RegFile spine for thunk presence"
spec :: Spec
spec = describe "NoThunks instances" $ do
it "RNil contains no thunks" $ do
result <- noThunks [] RNil
isNothing result `shouldBe` True
it "reconstitute on the canonical UserRegistration log returns thunk-free state" $ do
-- Bang-bind the (state, regs) tuple to mirror the realistic
-- embedder pattern: after each 'step' the application forces the
-- result before observing it. Without this, 'noThunks' would
-- correctly report the *outer* tuple-projection thunk and never
-- reach the RegFile spine — that is a binding artefact, not a
-- leak in the state itself.
case reconstitute userReg canonicalLog of
Nothing -> expectationFailure "reconstitute returned Nothing"
Just (!s, !regs) -> do
sResult <- noThunks ["vertex"] s
regsResult <- noThunks ["regfile"] regs
case regsResult of
Nothing -> pure ()
Just ti -> expectationFailure $ "regfile thunk: " <> show ti
case sResult of
Nothing -> pure ()
Just ti -> expectationFailure $ "vertex thunk: " <> show ti
it "a deliberately-lazy RegFile reports a thunk (sanity check)" $ do
let leaky :: RegFile '[ '("x", Int)]
leaky = RCons (Proxy @"x") leakySlotValue RNil
result <- noThunks ["leaky"] leaky
case result of
Just _ -> pure ()
Nothing ->
expectationFailure
"expected NoThunks to detect the unevaluated leakySlotValue thunk"