{-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Internal.CoinSpec ( spec ) where import Prelude import Data.Maybe ( catMaybes, fromMaybe ) import Internal.Coin ( Coin ) import Test.Hspec ( Spec, describe, it, shouldBe, shouldSatisfy ) import Test.QuickCheck ( Arbitrary (..) , NonNegative (..) , Property , checkCoverage , cover , oneof , property ) import qualified Internal.Coin as C spec :: Spec spec = do describe "Coin properties" $ do it "Only construction of non-negative values is possible." $ checkCoverage prop_construction it "Coverage of generated values is acceptable." $ checkCoverage prop_generation it "Addition" $ checkCoverage prop_add it "Multiplication" $ checkCoverage prop_mul it "Subtraction" $ checkCoverage prop_sub it "Division" $ checkCoverage prop_div it "Modulus" $ checkCoverage prop_mod it "Distance" $ checkCoverage prop_distance prop_construction :: Integer -> Property prop_construction i = property $ cover 10 (i < 0) "input is negative" $ cover 10 (i > 0) "input is positive" $ cover 2 (i == 0) "input is zero" $ if i < 0 then C.coinFromIntegral i `shouldBe` Nothing else (C.coinToIntegral <$> C.coinFromIntegral i) `shouldBe` Just i prop_generation :: Coin -> Property prop_generation n = property $ cover 2 (n == C.zero) "value is zero" $ cover 2 (n == C.one) "value is one" $ cover 10 (n > C.one) "value is more than one" True prop_add :: Coin -> Coin -> Property prop_add x y = property $ C.coinToIntegral @Integer (x `C.add` y) `shouldBe` (C.coinToIntegral x + C.coinToIntegral y) prop_mul :: Coin -> Integer -> Property prop_mul x y = property $ cover 2 (y == 0) "scaling factor is zero" $ cover 8 (y < 0) "scaling factor is negative" $ cover 8 (y > 0) "scaling factor is positive" $ case (x `C.mul` y) of Nothing -> y `shouldSatisfy` (< 0) Just r -> (C.coinToIntegral x * y) `shouldBe` C.coinToIntegral @Integer r prop_sub :: Coin -> Coin -> Property prop_sub x y = property $ cover 4 (x == y) "values are equal" $ cover 8 (x < y) "x < y" $ cover 8 (x > y) "x > y" $ case x `C.sub` y of Nothing -> x `shouldSatisfy` (< y) Just r -> C.coinToIntegral x - C.coinToIntegral y `shouldBe` C.coinToIntegral @Integer r prop_div :: Coin -> Integer -> Property prop_div x y = property $ cover 2 (y == 0) "denominator is zero" $ cover 8 (y < 0) "denominator is negative" $ cover 8 (y > 0) "denominator is positive" $ case (x `C.div` y) of Nothing -> y `shouldSatisfy` (<= 0) Just r -> (C.coinToIntegral x `div` y) `shouldBe` C.coinToIntegral @Integer r prop_mod :: Coin -> Integer -> Property prop_mod x y = property $ cover 2 (y == 0) "denominator is zero" $ cover 8 (y < 0) "denominator is negative" $ cover 8 (y > 0) "denominator is positive" $ case (x `C.mod` y) of Nothing -> y `shouldSatisfy` (<= 0) Just r -> (C.coinToIntegral x `mod` y) `shouldBe` C.coinToIntegral @Integer r prop_distance :: Coin -> Coin -> Property prop_distance x y = property $ cover 4 (x == y) "values are equal" $ cover 8 (x < y) "x < y" $ cover 8 (x > y) "x > y" $ C.coinToIntegral @Integer (x `C.distance` y) `shouldBe` abs (C.coinToIntegral x - C.coinToIntegral y) instance Arbitrary Coin where arbitrary = oneof [ pure C.zero , pure C.one , somethingElse ] where somethingElse = fromMaybe C.zero . C.coinFromIntegral @Integer . getNonNegative <$> arbitrary shrink n = catMaybes $ C.coinFromIntegral @Integer <$> shrink (C.coinToIntegral @Integer n)