packages feed

moonlight-core-0.1.0.0: test/numeric/NicheSpec.hs

{-# LANGUAGE DerivingStrategies #-}

module NicheSpec (tests) where

import Data.Bifunctor (first)
import Data.Text (Text, pack)
import Numeric.Natural (Natural)
import Prelude (Double, Either (..), Eq, Int, Maybe (..), Ord, Show, String, fromIntegral, map, maxBound, show, traverse, uncurry, (/), (+), (.))
import Moonlight.Core (IsLawName (..), constructorLawName)
import Moonlight.Core
  ( ActiveStressor,
    MoonlightError (NonFiniteValue),
    MoonlightErrorContext (CanonicalizeContext),
    NicheValidationError (..),
    NonFiniteInput (NaNInput),
    activeStressorId,
    activeStressorIntensity,
    activeStressorSetEntries,
    activeStressorSetFromList,
    activeStressorSetTopEntries,
    contextSignatureBins,
    mkActiveStressor,
    mkContextSignature,
    mkStressorId,
    mkTopoSample,
    renderStressorId,
  )
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (Assertion, (@?=), assertFailure, testCase)

data NicheLawName
  = NicheActiveStressorSetCanonicalOrder
  | NicheActiveStressorSetDeduplicatesByMaximumIntensity
  | NicheActiveStressorTopEntriesPrefix
  | NicheActiveStressorTopEntriesSaturatesHugeLimit
  | NicheContextSignaturePreservesBins
  | NicheStressorIdRejectsEmpty
  | NicheTopoSampleRejectsNegativeSlope
  | NicheTopoSampleRejectsNonFiniteCanonicalScalar
  deriving stock (Eq, Ord, Show)

instance IsLawName NicheLawName where
  lawNameText = constructorLawName . show

lawCase :: NicheLawName -> Assertion -> TestTree
lawCase lawName =
  testCase (lawNameText lawName)

tests :: TestTree
tests =
  testGroup
    "Niche"
    [ lawCase NicheStressorIdRejectsEmpty testInvalidStressorId,
      lawCase NicheTopoSampleRejectsNegativeSlope testInvalidTopoSample,
      lawCase NicheTopoSampleRejectsNonFiniteCanonicalScalar testInvalidTopoSampleCanonicalScalar,
      lawCase NicheActiveStressorSetDeduplicatesByMaximumIntensity testActiveStressorSetCanonical,
      lawCase NicheActiveStressorSetCanonicalOrder testActiveStressorSetCanonical,
      lawCase NicheActiveStressorTopEntriesPrefix testActiveStressorTopEntries,
      lawCase NicheActiveStressorTopEntriesSaturatesHugeLimit testActiveStressorTopEntriesHugeLimit,
      lawCase NicheContextSignaturePreservesBins testContextSignature
    ]

testInvalidStressorId :: Assertion
testInvalidStressorId =
  mkStressorId (pack "") @?= Nothing

testInvalidTopoSample :: Assertion
testInvalidTopoSample =
  mkTopoSample (-1.0) 0.0 0.0 @?= Left (NegativeTopoSlope (-1.0))

testInvalidTopoSampleCanonicalScalar :: Assertion
testInvalidTopoSampleCanonicalScalar =
  mkTopoSample (0 / 0) 0.0 0.0 @?= Left (NicheCanonicalScalarRejected (NonFiniteValue CanonicalizeContext NaNInput))

testActiveStressorSetCanonical :: Assertion
testActiveStressorSetCanonical =
  case buildStressors of
    Left err -> assertFailure err
    Right stressors ->
      map describeStressor (activeStressorSetEntries (activeStressorSetFromList stressors))
        @?= [(pack "ash", 0.9), (pack "bog", 0.8), (pack "cinder", 0.8)]

testActiveStressorTopEntries :: Assertion
testActiveStressorTopEntries =
  case buildStressors of
    Left err -> assertFailure err
    Right stressors ->
      map describeStressor (activeStressorSetTopEntries 2 (activeStressorSetFromList stressors))
        @?= [(pack "ash", 0.9), (pack "bog", 0.8)]

testActiveStressorTopEntriesHugeLimit :: Assertion
testActiveStressorTopEntriesHugeLimit =
  case buildStressors of
    Left err -> assertFailure err
    Right stressors ->
      map describeStressor (activeStressorSetTopEntries justPastIntLimit (activeStressorSetFromList stressors))
        @?= [(pack "ash", 0.9), (pack "bog", 0.8), (pack "cinder", 0.8)]

justPastIntLimit :: Natural
justPastIntLimit =
  fromIntegral (maxBound :: Int) + 1


testContextSignature :: Assertion
testContextSignature =
  contextSignatureBins (mkContextSignature [3, 1, 4, 1]) @?= [3, 1, 4, 1]

buildStressors :: Either String [ActiveStressor]
buildStressors =
  case traverse mkStressorId stressorNames of
    Nothing -> Left "expected valid stressor ids"
    Just [bogId, ashId, cinderId] ->
      first show
        ( traverse
            (uncurry mkActiveStressor)
            [ (bogId, 0.2),
              (ashId, 0.9),
              (bogId, 0.8),
              (cinderId, 0.8)
            ]
        )
    Just _ -> Left "unexpected stressor id cardinality"
  where
    stressorNames = map pack ["bog", "ash", "cinder"]

describeStressor :: ActiveStressor -> (Text, Double)
describeStressor stressor =
  (renderStressorId (activeStressorId stressor), activeStressorIntensity stressor)