packages feed

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

{-# LANGUAGE DerivingStrategies #-}

module CanonSpec (tests) where

import Data.Int (Int64)
import Data.Word (Word32)
import Moonlight.Core
import Prelude
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (Assertion, testCase, (@?=))
import Test.Tasty.QuickCheck (NonNegative (..), Property, Testable, testProperty, (==>))

data CanonLawName
  = CanonCanonicalizeIdempotent
  | CanonCanonicalizeNormalizesNegativeZero
  | CanonCanonicalizeRejectsInfinite
  | CanonCanonicalizeRejectsNaN
  | CanonCanonicalizeReturnsCanonical
  | CanonIsCanonicalIdentity
  | CanonMkFiniteDoubleAgreesWithCanonicalize
  | CanonMkNonNegativeFiniteCanonicalizes
  | CanonMkPositiveFiniteRejectsNonPositive
  | CanonQuantizeFinitePrecisionTotal
  | CanonQuantizeRejectsInvalidPrecision
  | CanonQuantizeRejectsNonFinite
  | CanonQuantizeSaturatesMaximum
  | CanonQuantizeSaturatesMinimum
  deriving stock (Eq, Ord, Show)

instance IsLawName CanonLawName where
  lawNameText = constructorLawName . show
lawProperty :: Testable property => CanonLawName -> property -> TestTree
lawProperty lawName =
  testProperty (lawNameText lawName)

lawCase :: CanonLawName -> Assertion -> TestTree
lawCase lawName =
  testCase (lawNameText lawName)
finiteDouble :: Double -> Bool
finiteDouble value =
  not (isNaN value) && not (isInfinite value)

validHashPrecision :: Word32 -> Word32
validHashPrecision precision =
  precision `mod` 10

canonicalizeReturnsCanonical :: Double -> Bool
canonicalizeReturnsCanonical value =
  case canonicalize value of
    Right canonicalValue ->
      isCanonical canonicalValue
    Left _ ->
      True

mkFiniteDoubleAgreesWithCanonicalize :: Double -> Bool
mkFiniteDoubleAgreesWithCanonicalize value =
  case (mkFiniteDouble "test" value, canonicalize value) of
    (Right finiteValue, Right canonicalValue) ->
      finiteValue == canonicalValue
    (Left _, Left _) ->
      True
    _ ->
      False

quantizeFinitePrecisionTotal :: Word32 -> Double -> Property
quantizeFinitePrecisionTotal precision value =
  finiteDouble value ==>
    case quantizeForHash (validHashPrecision precision) value of
      Right _ -> True
      Left _ -> False

tests :: TestTree
tests =
  testGroup
    "Canon"
    [ testGroup
        "canonicalize"
        [ lawCase CanonCanonicalizeRejectsNaN $
            canonicalize (0 / 0) @?= Left (NonFiniteValue CanonicalizeContext NaNInput),
          lawCase CanonCanonicalizeRejectsInfinite $ do
            canonicalize (1 / 0) @?= Left (NonFiniteValue CanonicalizeContext InfiniteInput)
            canonicalize ((-1) / 0) @?= Left (NonFiniteValue CanonicalizeContext InfiniteInput),
          lawCase CanonCanonicalizeNormalizesNegativeZero $
            canonicalize (-0.0) @?= Right 0.0,
          lawProperty CanonCanonicalizeIdempotent $ \(x :: Double) ->
            case canonicalize x of
              Right y -> canonicalize y == Right y
              Left _ -> True,
          lawProperty CanonCanonicalizeReturnsCanonical canonicalizeReturnsCanonical
        ],
      testGroup
        "isCanonical"
        [ lawProperty CanonIsCanonicalIdentity $ \(x :: Double) ->
            isCanonical x ==> canonicalize x == Right x
        ],
      testGroup
        "finite constructors"
        [ lawProperty CanonMkFiniteDoubleAgreesWithCanonicalize mkFiniteDoubleAgreesWithCanonicalize,
          lawProperty CanonMkNonNegativeFiniteCanonicalizes $ \(NonNegative x :: NonNegative Double) ->
            finiteDouble x ==>
              case mkNonNegativeFiniteDouble "test" x of
                Right y -> isCanonical y && y >= 0.0
                Left _ -> False,
          lawProperty CanonMkPositiveFiniteRejectsNonPositive $ \(NonNegative x :: NonNegative Double) ->
            finiteDouble x ==>
              case mkPositiveFiniteDouble "test" (negate x) of
                Left _ -> True
                Right _ -> False
        ],
      testGroup
        "quantizeForHash"
        [ lawProperty CanonQuantizeFinitePrecisionTotal quantizeFinitePrecisionTotal,
          lawProperty CanonQuantizeRejectsInvalidPrecision $ \(precision :: Word32) (x :: Double) ->
            precision > 9 ==> case quantizeForHash precision x of
              Left _ -> True
              Right _ -> False,
          testCase "quantize rejects invalid precision with typed error" $
            quantizeForHash 10 1.0 @?= Left (QuantizePrecisionTooLarge 10),
          lawCase CanonQuantizeRejectsNonFinite $ do
            quantizeForHash 0 (0 / 0) @?= Left (NonFiniteValue QuantizeContext NaNInput)
            quantizeForHash 0 (1 / 0) @?= Left (NonFiniteValue QuantizeContext InfiniteInput),
          lawCase CanonQuantizeSaturatesMaximum $
            quantizeForHash 0 (fromIntegral (maxBound :: Int64) * 2.0) @?= Right maxBound,
          lawCase CanonQuantizeSaturatesMinimum $
            quantizeForHash 0 (fromIntegral (minBound :: Int64) * 2.0) @?= Right minBound
        ]
    ]