packages feed

keiki-0.2.0.0: test/Keiki/CompositionHomomorphismSpec.hs

module Keiki.CompositionHomomorphismSpec (spec) where

import Control.Exception (evaluate)
import Control.Monad (foldM, forM_)
import Data.Text (Text)
import Data.Time (UTCTime)
import Keiki.Composition (Composite (..), compose)
import Keiki.CompositionSpec (AlertCmd, AlertRegs, alertSource, sampleTrigger)
import Keiki.Core
import Keiki.Fixtures.ComposeStateful
import Keiki.Fixtures.EmailDelivery (EmailRegs, emailDelivery)
import Keiki.Generics (Append)
import Test.Hspec

sequentialStep ::
  SymTransducer (HsPred rs1 ci) rs1 s1 ci mid ->
  SymTransducer (HsPred rs2 mid) rs2 s2 mid co ->
  (s1, RegFile rs1) ->
  (s2, RegFile rs2) ->
  ci ->
  Maybe ((s1, RegFile rs1), (s2, RegFile rs2), [co])
sequentialStep t1 t2 state1 state2 command = do
  (s1', regs1', mids) <- step t1 state1 command
  let feed (s2, regs2, outputs) mid = do
        (s2', regs2', emitted) <- step t2 (s2, regs2) mid
        pure (s2', regs2', outputs <> emitted)
  (s2', regs2', outputs) <- foldM feed (fst state2, snd state2, []) mids
  pure ((s1', regs1'), (s2', regs2'), outputs)

spec :: Spec
spec = do
  describe "bounded-exhaustive compose homomorphism" $ do
    it "agrees for counterSource then lastValueSink through three steps" $
      forM_ [replicate n Tick | n <- [0 .. 3]] checkCounterSequence

    it "agrees for pairSource then twoPhaseSink, including later rejection" $
      forM_ [[], [Go], [Go, Go]] checkPhaseSequence

    it "records the wrong-order guard as a deliberate refinement of sequential bottom" $ do
      evaluate
        ( step
            wrongOrderSink
            (initial wrongOrderSink, initialRegs wrongOrderSink)
            (M2A 5)
        )
        `shouldThrow` errorCall "evalTerm: TInpCtorField guard violation: M2B"
      let pipeline = compose m2aSource wrongOrderSink
      case step pipeline (initial pipeline, initialRegs pipeline) ProduceA of
        Just (_, _, outputs) -> outputs `shouldBe` [SawA 5]
        Nothing -> expectationFailure "expected composition to refine the guard error to a defined step"

    it "agrees for alertSource then emailDelivery, including terminal rejection" $
      forM_ [[], [sampleTrigger], [sampleTrigger, sampleTrigger]] checkAlertSequence

checkCounterSequence :: [SourceCmd] -> Expectation
checkCounterSequence =
  go
    (initial pipeline, initialRegs pipeline)
    (initial counterSource, initialRegs counterSource)
    (initial lastValueSink, initialRegs lastValueSink)
  where
    pipeline = compose counterSource lastValueSink

    go _ _ _ [] = pure ()
    go compositeState sourceState sinkState (command : rest) =
      case ( step pipeline compositeState command,
             sequentialStep counterSource lastValueSink sourceState sinkState command
           ) of
        (Nothing, Nothing) -> pure ()
        (Just (compositeVertex, compositeRegs, compositeOutputs), Just ((sourceVertex, sourceRegs), (sinkVertex, sinkRegs), sequentialOutputs)) -> do
          compositeVertex `shouldBe` Composite sourceVertex sinkVertex
          compositeOutputs `shouldBe` sequentialOutputs
          readSourceCount compositeRegs
            `shouldBe` sourceRegs ! (#srcCount :: Index CounterRegs Int)
          readSinkLast compositeRegs
            `shouldBe` sinkRegs ! (#sinkLast :: Index SinkRegs Int)
          go
            (compositeVertex, compositeRegs)
            (sourceVertex, sourceRegs)
            (sinkVertex, sinkRegs)
            rest
        _ -> expectationFailure "counter pipeline disagreed on accept/reject"

checkPhaseSequence :: [PairCmd] -> Expectation
checkPhaseSequence =
  go
    (initial pipeline, initialRegs pipeline)
    (initial pairSource, initialRegs pairSource)
    (initial twoPhaseSink, initialRegs twoPhaseSink)
  where
    pipeline = compose pairSource twoPhaseSink

    go _ _ _ [] = pure ()
    go compositeState sourceState sinkState (command : rest) =
      case ( step pipeline compositeState command,
             sequentialStep pairSource twoPhaseSink sourceState sinkState command
           ) of
        (Nothing, Nothing) -> pure ()
        (Just (compositeVertex, compositeRegs, compositeOutputs), Just ((sourceVertex, sourceRegs), (sinkVertex, sinkRegs), sequentialOutputs)) -> do
          compositeVertex `shouldBe` Composite sourceVertex sinkVertex
          compositeOutputs `shouldBe` sequentialOutputs
          readPhase compositeRegs
            `shouldBe` sinkRegs ! (#phase :: Index PhaseRegs Int)
          go
            (compositeVertex, compositeRegs)
            (sourceVertex, sourceRegs)
            (sinkVertex, sinkRegs)
            rest
        _ -> expectationFailure "two-phase pipeline disagreed on accept/reject"

checkAlertSequence :: [AlertCmd] -> Expectation
checkAlertSequence =
  go
    (initial pipeline, initialRegs pipeline)
    (initial alertSource, initialRegs alertSource)
    (initial emailDelivery, initialRegs emailDelivery)
  where
    pipeline = compose alertSource emailDelivery

    go _ _ _ [] = pure ()
    go compositeState sourceState sinkState (command : rest) =
      case ( step pipeline compositeState command,
             sequentialStep alertSource emailDelivery sourceState sinkState command
           ) of
        (Nothing, Nothing) -> pure ()
        (Just (compositeVertex, compositeRegs, compositeOutputs), Just ((sourceVertex, sourceRegs), (sinkVertex, sinkRegs), sequentialOutputs)) -> do
          compositeVertex `shouldBe` Composite sourceVertex sinkVertex
          compositeOutputs `shouldBe` sequentialOutputs
          compareAlertRegisters compositeRegs sourceRegs sinkRegs
          go
            (compositeVertex, compositeRegs)
            (sourceVertex, sourceRegs)
            (sinkVertex, sinkRegs)
            rest
        _ -> expectationFailure "alert/email pipeline disagreed on accept/reject"

compareAlertRegisters ::
  RegFile (Append AlertRegs EmailRegs) ->
  RegFile AlertRegs ->
  RegFile EmailRegs ->
  Expectation
compareAlertRegisters compositeRegs sourceRegs sinkRegs = do
  compositeRegs ! (#alertRecipient :: Index (Append AlertRegs EmailRegs) Text)
    `shouldBe` sourceRegs ! (#alertRecipient :: Index AlertRegs Text)
  compositeRegs ! (#alertSubject :: Index (Append AlertRegs EmailRegs) Text)
    `shouldBe` sourceRegs ! (#alertSubject :: Index AlertRegs Text)
  compositeRegs ! (#alertAt :: Index (Append AlertRegs EmailRegs) UTCTime)
    `shouldBe` sourceRegs ! (#alertAt :: Index AlertRegs UTCTime)
  compositeRegs ! (#emailRecipient :: Index (Append AlertRegs EmailRegs) Text)
    `shouldBe` sinkRegs ! (#emailRecipient :: Index EmailRegs Text)
  compositeRegs ! (#emailSubject :: Index (Append AlertRegs EmailRegs) Text)
    `shouldBe` sinkRegs ! (#emailSubject :: Index EmailRegs Text)
  compositeRegs ! (#emailSentAt :: Index (Append AlertRegs EmailRegs) UTCTime)
    `shouldBe` sinkRegs ! (#emailSentAt :: Index EmailRegs UTCTime)