packages feed

moonlight-pale-0.1.0.0: test/laws/RestrictionSpec.hs

{-# LANGUAGE GHC2024 #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wmissing-local-signatures #-}

module RestrictionSpec
  ( tests,
  )
where

import Data.List.NonEmpty (NonEmpty (..))
import Moonlight.Pale.Test.Laws.Restriction
  ( FiniteRestrictionError (..),
    FiniteRestrictionLaw,
    compileFiniteRestrictionLaw,
    finiteRestrictionLaws,
  )
import Moonlight.Pale.Test.Laws.Suite (lawGroup, renderLawSuite)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertEqual, assertFailure, testCase)

data ChainCell
  = ChainBottom
  | ChainMiddle
  | ChainTop
  deriving stock (Eq, Ord, Show)

data OwnedSection
  = OwnedSection !ChainCell !Int
  | RestrictionSourceMismatch !ChainCell !ChainCell !Int
  deriving stock (Eq, Show)

tests :: TestTree
tests =
  testGroup
    "Moonlight.Pale.Test.Laws.Restriction"
    [ renderFiniteRestriction "chain restriction suite" chainRestriction,
      testCase "source and target identities apply identity at the typed object" $ do
        let sourceSection = OwnedSection ChainBottom 7
            direct = restrictOwnedSection ChainBottom ChainMiddle sourceSection
            reversedSourceIdentity =
              restrictOwnedSection
                ChainBottom
                ChainBottom
                (restrictOwnedSection ChainBottom ChainMiddle sourceSection)
            reversedTargetIdentity =
              restrictOwnedSection
                ChainBottom
                ChainMiddle
                (restrictOwnedSection ChainMiddle ChainMiddle sourceSection)
        assertEqual
          "source identity"
          direct
          ( restrictOwnedSection
              ChainBottom
              ChainMiddle
              (restrictOwnedSection ChainBottom ChainBottom sourceSection)
          )
        assertEqual
          "target identity"
          direct
          ( restrictOwnedSection
              ChainMiddle
              ChainMiddle
              (restrictOwnedSection ChainBottom ChainMiddle sourceSection)
          )
        assertBool
          "the former source equation applies the identity to the wrong fiber"
          (reversedSourceIdentity /= direct)
        assertBool
          "the former target equation applies the identity before entering its fiber"
          (reversedTargetIdentity /= direct),
      testCase "duplicate cells retain both dense positions" $
        assertRestrictionErrors
          "duplicate ChainBottom"
          (DuplicateRestrictionCell ChainBottom 0 2 :| [])
          duplicateCellRestriction,
      testCase "sections outside the finite cell universe are rejected" $
        assertRestrictionErrors
          "unknown source cell"
          (SectionCellOutsideUniverse ChainTop :| [])
          unknownSectionRestriction,
      testCase "non-reflexive relations cannot compile" $
        assertRestrictionErrors
          "both missing identities are reported"
          ( RestrictionRelationNotReflexive ChainBottom
              :| [RestrictionRelationNotReflexive ChainMiddle]
          )
          nonReflexiveRestriction,
      testCase "contradictory two-way order cannot compile" $
        assertRestrictionErrors
          "antisymmetry rejects distinct mutually related cells"
          (RestrictionRelationNotAntisymmetric ChainBottom ChainMiddle :| [])
          contradictoryRestriction,
      testCase "non-transitive relation cannot compile" $
        assertRestrictionErrors
          "the missing bottom-to-top edge is typed"
          (RestrictionRelationNotTransitive ChainBottom ChainMiddle :| [])
          nonTransitiveRestriction
    ]

chainRestriction ::
  Either
    (NonEmpty (FiniteRestrictionError ChainCell))
    (FiniteRestrictionLaw ChainCell OwnedSection)
chainRestriction =
  compileFiniteRestrictionLaw
    "chain"
    chainCells
    chainLeq
    chainSections
    restrictOwnedSection

duplicateCellRestriction ::
  Either
    (NonEmpty (FiniteRestrictionError ChainCell))
    (FiniteRestrictionLaw ChainCell OwnedSection)
duplicateCellRestriction =
  compileFiniteRestrictionLaw
    "duplicate"
    (ChainBottom :| [ChainMiddle, ChainBottom])
    chainLeq
    []
    restrictOwnedSection

unknownSectionRestriction ::
  Either
    (NonEmpty (FiniteRestrictionError ChainCell))
    (FiniteRestrictionLaw ChainCell OwnedSection)
unknownSectionRestriction =
  compileFiniteRestrictionLaw
    "unknown section"
    (ChainBottom :| [ChainMiddle])
    chainLeq
    [(ChainTop, OwnedSection ChainTop 0)]
    restrictOwnedSection

nonReflexiveRestriction ::
  Either
    (NonEmpty (FiniteRestrictionError ChainCell))
    (FiniteRestrictionLaw ChainCell OwnedSection)
nonReflexiveRestriction =
  compileFiniteRestrictionLaw
    "non-reflexive"
    (ChainBottom :| [ChainMiddle])
    (\_ _ -> False)
    []
    restrictOwnedSection

contradictoryRestriction ::
  Either
    (NonEmpty (FiniteRestrictionError ChainCell))
    (FiniteRestrictionLaw ChainCell OwnedSection)
contradictoryRestriction =
  compileFiniteRestrictionLaw
    "contradictory"
    (ChainBottom :| [ChainMiddle])
    (\_ _ -> True)
    []
    restrictOwnedSection

nonTransitiveRestriction ::
  Either
    (NonEmpty (FiniteRestrictionError ChainCell))
    (FiniteRestrictionLaw ChainCell OwnedSection)
nonTransitiveRestriction =
  compileFiniteRestrictionLaw
    "non-transitive"
    chainCells
    adjacentChainLeq
    []
    restrictOwnedSection

chainCells :: NonEmpty ChainCell
chainCells = ChainBottom :| [ChainMiddle, ChainTop]

chainSections :: [(ChainCell, OwnedSection)]
chainSections =
  fmap
    (\cell -> (cell, OwnedSection cell (chainRank cell)))
    (toList chainCells)

restrictOwnedSection :: ChainCell -> ChainCell -> OwnedSection -> OwnedSection
restrictOwnedSection sourceCell targetCell section =
  case section of
    OwnedSection owner payload
      | owner == sourceCell -> OwnedSection targetCell payload
      | otherwise -> RestrictionSourceMismatch sourceCell owner payload
    RestrictionSourceMismatch expectedSource actualSource payload ->
      RestrictionSourceMismatch expectedSource actualSource payload

chainLeq :: ChainCell -> ChainCell -> Bool
chainLeq leftCell rightCell =
  chainRank leftCell <= chainRank rightCell

adjacentChainLeq :: ChainCell -> ChainCell -> Bool
adjacentChainLeq leftCell rightCell =
  leftCell == rightCell
    || (leftCell == ChainBottom && rightCell == ChainMiddle)
    || (leftCell == ChainMiddle && rightCell == ChainTop)

chainRank :: ChainCell -> Int
chainRank cell =
  case cell of
    ChainBottom -> 0
    ChainMiddle -> 1
    ChainTop -> 2

renderFiniteRestriction ::
  (Show cell, Show val, Eq val) =>
  String ->
  Either (NonEmpty (FiniteRestrictionError cell)) (FiniteRestrictionLaw cell val) ->
  TestTree
renderFiniteRestriction label restrictionResult =
  case restrictionResult of
    Left errors ->
      testCase (label <> " compiles") $
        assertFailure ("expected valid finite restriction law: " <> show errors)
    Right restrictionLaw ->
      renderLawSuite (lawGroup label (finiteRestrictionLaws restrictionLaw))

assertRestrictionErrors ::
  (Eq cell, Show cell) =>
  String ->
  NonEmpty (FiniteRestrictionError cell) ->
  Either
    (NonEmpty (FiniteRestrictionError cell))
    (FiniteRestrictionLaw cell val) ->
  IO ()
assertRestrictionErrors label expectedErrors restrictionResult =
  case restrictionResult of
    Left actualErrors -> assertEqual label expectedErrors actualErrors
    Right _ -> assertFailure (label <> ": expected finite restriction compilation to fail")

toList :: NonEmpty a -> [a]
toList (firstValue :| remainingValues) =
  firstValue : remainingValues