e11y-0.1.0.0: test/Test.hs
-- Copyright 2024 Shea Levy
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE UnicodeSyntax #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Main where
import Control.Exception
import Control.Monad.Catch.Pure (CatchT (..))
import Control.Monad.Catch.Pure qualified as Catch
import Control.Monad.ST
import Control.Monad.StrictIdentity
import Control.Monad.Trans.Class
import Control.Monad.With
import Data.Coerce
import Data.Either
import Data.Foldable
import Data.Functor.Identity
import Data.GeneralAllocate
import Data.Kind
import Data.Maybe
import Data.Proxy
import Data.Sequence
import Observe.Event
import Observe.Event.Backend.Data
import Test.Syd
deriving via WithNoContinuation StrictIdentity instance MonadWith StrictIdentity
data TestSelector ∷ Type → Type where
Test ∷ TestSelector TestField
data TestField = TestField deriving (Eq, Show)
type instance SubSelector TestField = SubTestSelector
data SubTestSelector ∷ Type → Type where
SubTest ∷ SubTestSelector SubTestField
data SubTestField = SubTestField deriving (Eq, Show)
type instance SubSelector SubTestField = NoEventsSelector
newtype EqSomeException = EqSomeException SomeException deriving newtype (Show)
instance Eq EqSomeException where
EqSomeException e1 == EqSomeException e2 = show e1 == show e2
data DataEventTestSelectorFields
= DataEventTestSelectorTestFields [TestField]
| DataEventTestSelectorSubTestFields [SubTestField]
deriving (Eq, Show)
data DataEventTestSelectorSelector
= DataEventTestSelectorTest
| DataEventTestSelectorTestSubTest
deriving (Eq, Show)
data DataEventTestSelector = DataEventTestSelector
{ idx ∷ !Int
, selector ∷ !DataEventTestSelectorSelector
, parent ∷ !(Maybe (Either Int DataEventTestSelector))
, causes ∷ ![Either Int DataEventTestSelector]
, err ∷ !(Maybe EqSomeException)
, fields ∷ !DataEventTestSelectorFields
, instant ∷ !Bool
}
deriving (Eq, Show)
convDataEventTestSelector ∷ DataEvent TestSelector → DataEventTestSelector
convDataEventTestSelector ev@(DataEvent _ selectors _ _ _ fields _) =
DataEventTestSelector
{ idx = ev.idx
, parent = fmap convDataEventTestSelector <$> ev.parent
, causes = fmap convDataEventTestSelector <$> ev.causes
, err = coerce ev.err
, fields = fields'
, selector
, instant = ev.instant
}
where
(fields', selector) = case selectors of
Leaf Test → (DataEventTestSelectorTestFields (toList fields), DataEventTestSelectorTest)
Test :/ Leaf SubTest → (DataEventTestSelectorSubTestFields (toList fields), DataEventTestSelectorTestSubTest)
Test :/ SubTest :/ Leaf impossible → case impossible of {}
Test :/ SubTest :/ impossible :/ _ → case impossible of {}
newtype TestException = TestException Int deriving (Show)
instance Exception TestException
data ParentSelector field where
ParentA ∷ ParentSelector AField
ParentB ∷ ParentSelector BField
newtype AField = AField Int deriving (Eq)
type instance SubSelector AField = NoEventsSelector
data BField = BYes | BNo deriving (Eq)
type instance SubSelector BField = ChildSelector
data ChildSelector field where
Child ∷ ChildSelector CField
data CField = CField deriving (Eq)
type instance SubSelector CField = NoEventsSelector
data SomeSelectors = ∀ field. (Eq field) ⇒ SomeSelectors (Selectors ParentSelector field)
main ∷ IO ()
main = sydTest $ do
describe "withEvent" $ do
it "creates the event" $
let
m_ev = runST $ do
be ← newDataEventBackend
let
?e11yBackend = be
idx ← withEvent Test $ do
pure eventReference
evs ← (fmap convDataEventTestSelector <$>) <$> getEvents be
pure $ index evs idx
in
shouldSatisfyNamed m_ev "was finalized and selected with the Test selector" (maybe False (\ev → ev.selector == DataEventTestSelectorTest))
it "sets the backend to accept sub-selectors in the inner scope" $
let
m_ev = runST $ do
be ← newDataEventBackend
let
?e11yBackend = be
idx ← withEvent Test $ do
withEvent SubTest $ do
addEventField SubTestField
pure eventReference
evs ← (fmap convDataEventTestSelector <$>) <$> getEvents be
pure $ index evs idx
in
shouldSatisfyNamed m_ev "was finalized and selected with the SubTest selector" (maybe False (\ev → ev.selector == DataEventTestSelectorTestSubTest))
it "sets the backend to create child events" $
let
m_evs = runST $ do
be ← newDataEventBackend
let
?e11yBackend = be
(parentIdx, childIdx) ← withEvent Test $ do
let parentIdx = eventReference
withEvent SubTest $ do
pure (parentIdx, eventReference)
evs ← (fmap convDataEventTestSelector <$>) <$> getEvents be
pure (index evs parentIdx, index evs childIdx)
in
case m_evs of
(Just parent, Just child) →
shouldSatisfyNamed
child
"is a child of the parent"
(\child' → child'.parent == Just (Right parent))
(Nothing, _) → expectationFailure "Parent event was not finalized"
(_, Nothing) → expectationFailure "Child event was not finalized"
it "records and propagates exceptions" $
let
m_m_ev = runST $ do
be ← newDataEventBackend
let
?e11yBackend = be
e_res ← runCatchT $ withEvent Test $ do
Catch.throwM $ TestException eventReference
case e_res of
Left e → case fromException e of
Just (TestException idx) → do
evs ← (fmap convDataEventTestSelector <$>) <$> getEvents be
pure . Just $ index evs idx
Nothing → pure Nothing
_ → pure Nothing
in
case m_m_ev of
Nothing → expectationFailure "TestException not thrown"
Just m_ev →
shouldSatisfyNamed
m_ev
"was finalized with a TestException"
(maybe False (\ev → ev.err == (Just . EqSomeException . SomeException $ TestException ev.idx)))
describe "the MonadTrans EventBackendIn instance" $
it "lifts the EventBackend instance through a transformer" $
runST $ do
be ← newDataEventBackend @_ @TestSelector
let
?e11yBackend = be
e_res ← runCatchT $ withEvent Test $ do
addEventField TestField
subRef ← instantEvent SubTest []
evs ← (fmap convDataEventTestSelector <$>) <$> lift (getEvents be)
pure $ case index evs subRef of
Nothing → False
Just ev → ev.fields == DataEventTestSelectorSubTestFields []
pure $ fromRight False e_res
describe "finalizeEvent" $
it "finalizes the event" $
let
m_ev = runST $ do
be ← newDataEventBackend
let
?e11yBackend = be
idx ← withEvent Test $ do
finalizeEvent $ Just (SomeException $ TestException 0)
pure eventReference
evs ← (fmap convDataEventTestSelector <$>) <$> getEvents be
pure $ index evs idx
in
shouldSatisfyNamed
m_ev
"was finalized with a manual exception"
(maybe False (\ev → ev.err == (Just . EqSomeException $ SomeException $ TestException 0)))
describe "addEventField" $
it "adds a field to the event" $
let
m_ev = runST $ do
be ← newDataEventBackend
let
?e11yBackend = be
idx ← withEvent Test $ do
addEventField TestField
pure eventReference
evs ← (fmap convDataEventTestSelector <$>) <$> getEvents be
pure $ index evs idx
in
shouldSatisfyNamed
m_ev
"was finalized and has a TestField field"
(maybe False (\ev → ev.fields == DataEventTestSelectorTestFields [TestField]))
describe "DataEventBackend" $ do
it "captures the first event finalization" $
let
m_ev = runST $ do
be ← newDataEventBackend
let ?e11yBackend = be
idx ← withEvent Test $ do
finalizeEvent Nothing
finalizeEvent $ Just (SomeException $ TestException 0)
pure eventReference
evs ← (fmap convDataEventTestSelector <$>) <$> getEvents be
pure $ index evs idx
in
shouldSatisfyNamed
m_ev
"was finalized without error"
(maybe False (\ev → isNothing ev.err))
it "emits incomplete events as Nothing" $
let
-- The parentIdx type annotation is needed because its
-- type inside the block is EventReference (DataEventBackend (ST s) TestSelector.
-- s cannot escape from runST, and even though that type is always Int regardless
-- of s GHC doesn't know that.
-- The m_ev type annotation is needed because of https://gitlab.haskell.org/ghc/ghc/-/issues/24333
(parentIdx ∷ Int, m_ev ∷ Maybe DataEventTestSelector) = runST $ do
be ← newDataEventBackend
let ?e11yBackend = be
withEvent Test $ do
let parentIdx' = eventReference
withEvent SubTest $ do
finalizeEvent Nothing
evs ← (fmap convDataEventTestSelector <$>) <$> getEvents be
pure (parentIdx', index evs eventReference)
in
case m_ev of
Nothing → expectationFailure "child event not finalized"
Just ev →
shouldSatisfyNamed
ev
"parent is unknown"
(\ev' → ev'.parent == Just (Left parentIdx))
describe "withRelatedEvent" $
it "sets specified causes" $
let
m_cause_effect = runST $ do
be ← newDataEventBackend
let ?e11yBackend = be
(causeIdx, effectIdx) ← do
withEvent Test $ do
let causeIdx = eventReference
withRelatedEvent SubTest Nothing [causeIdx] $ do
pure (causeIdx, eventReference)
evs ← (fmap convDataEventTestSelector <$>) <$> getEvents be
pure (index evs causeIdx, index evs effectIdx)
in
case m_cause_effect of
(Just cause, Just effect) →
shouldSatisfyNamed
(cause, effect)
"fst causes snd"
(\(cause', effect') → effect'.causes == [Right cause'])
(Nothing, _) → expectationFailure "cause not finalized"
(_, Nothing) → expectationFailure "effect not finalized"
describe "instantEvent" $
it "emits an event" $
let
m_ev = runST $ do
be ← newDataEventBackend
let ?e11yBackend = be
idx ← instantEvent Test [TestField]
evs ← (fmap convDataEventTestSelector <$>) <$> getEvents be
pure $ index evs idx
in
case m_ev of
Nothing → expectationFailure "event not finalized"
Just ev →
shouldSatisfyNamed
ev
"is instant has a TestField field"
(\ev' → ev'.instant && ev'.fields == DataEventTestSelectorTestFields [TestField])
describe "the no-op EventBackend instance for Proxy" $
it "does nothing successfully" $
runStrictIdentity $ do
let ?e11yBackend = Proxy @TestSelector
() ← withEvent Test $ do
addEventField TestField
let !_ = eventReference
finalizeEvent Nothing
instantEvent SubTest [SubTestField]
pure True
describe "the backend-combining EventBackend instance for (,)" $
it "sequentially calls both backends" $
let
(evs1, evs2, idx1s ∷ (Int, Int), idx2s ∷ (Int, Int)) = runST $ do
be1 ← newDataEventBackend
be2 ← newDataEventBackend
let ?e11yBackend = (be1, be2)
(idx1, idx2) ← withEvent Test $ do
addEventField TestField
let idx1 = eventReference
idx2 ← instantEvent SubTest [SubTestField]
finalizeEvent Nothing
pure (idx1, idx2)
(,,idx1,idx2)
<$> ((fmap convDataEventTestSelector <$>) <$> getEvents be1)
<*> ((fmap convDataEventTestSelector <$>) <$> getEvents be2)
(idx11, idx12) = idx1s
(idx21, idx22) = idx2s
expectedEv1 =
DataEventTestSelector
{ idx = 0
, selector = DataEventTestSelectorTest
, parent = Nothing
, causes = []
, err = Nothing
, fields = DataEventTestSelectorTestFields [TestField]
, instant = False
}
expectedEv2 =
DataEventTestSelector
{ idx = 1
, selector = DataEventTestSelectorTestSubTest
, parent = Just (Right expectedEv1)
, causes = []
, err = Nothing
, fields = DataEventTestSelectorSubTestFields [SubTestField]
, instant = True
}
in
do
shouldBe idx11 idx12
shouldBe idx21 idx22
evs ←
if evs1 == evs2
then pure evs1
else expectationFailure "event lists don't match"
shouldBe evs $ fromList [Just expectedEv1, Just expectedEv2]
describe "selectorRendering" $
it "matches a manual definition" $
let
possibleSelectors =
[ SomeSelectors (Leaf ParentA)
, SomeSelectors (Leaf ParentB)
, SomeSelectors (ParentB :/ Leaf Child)
]
manual ∷ Selectors ParentSelector field → field
manual (Leaf ParentA) = AField 0
manual (ParentA :/ Leaf impossible) = case impossible of {}
manual (ParentA :/ impossible :/ _) = case impossible of {}
manual (Leaf ParentB) = BYes
manual (ParentB :/ Leaf Child) = CField
manual (ParentB :/ Child :/ Leaf impossible) = case impossible of {}
manual (ParentB :/ Child :/ impossible :/ _) = case impossible of {}
usingSelectorRendering ∷ Selectors ParentSelector field → field
usingSelectorRendering =
runIdentity
. selectorRendering
( \case
ParentA → noSubEventsSelectorRendering (Identity $ AField 0)
ParentB →
SelectorRendering
{ renderTopSelector = Identity BYes
, renderSubSelector = renderChildSelector
}
)
renderChildSelector ∷ Selectors ChildSelector field → Identity field
renderChildSelector = selectorRendering $ \Child →
noSubEventsSelectorRendering $ Identity CField
predicate ∷ SomeSelectors → Bool
predicate (SomeSelectors sel) = manual sel == usingSelectorRendering sel
in
all predicate possibleSelectors