From c68a2f1948ec7f23b65388a2cc9f057582d014d3 Mon Sep 17 00:00:00 2001 From: Jonathan Knowles Date: Wed, 4 Dec 2019 08:31:17 +0000 Subject: [PATCH 1/6] Generalize `genUniformTime` to allow a specific range. --- lib/test-utils/src/Test/Utils/Time.hs | 59 +++++++++++++++++++-------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/lib/test-utils/src/Test/Utils/Time.hs b/lib/test-utils/src/Test/Utils/Time.hs index 6291dc3f776..416e647e549 100644 --- a/lib/test-utils/src/Test/Utils/Time.hs +++ b/lib/test-utils/src/Test/Utils/Time.hs @@ -9,13 +9,19 @@ module Test.Utils.Time ( UniformTime , genUniformTime + , genUniformTimeWithinRange , getUniformTime ) where import Prelude import Data.Time - ( Day (ModifiedJulianDay), NominalDiffTime, UTCTime (..), addUTCTime ) + ( Day (ModifiedJulianDay) + , NominalDiffTime + , UTCTime (..) + , addUTCTime + , toModifiedJulianDay + ) import Test.QuickCheck ( Arbitrary, Gen, arbitrary, choose, oneof ) @@ -31,30 +37,51 @@ instance Arbitrary UniformTime where -- | Generate 'UTCTime' values over a uniform range of dates and a mixture of -- time precisions. -- +-- Dates will be generated in a range that's bounded by 'defaultLowerBound' and +-- 'defaultUpperBound'. + genUniformTime :: Gen UTCTime -genUniformTime = oneof - [ genWith - hoursToNominalDiffTime - hoursInOneDay - , genWith - secondsToNominalDiffTime - secondsInOneDay - , genWith - picosecondsToNominalDiffTime - picosecondsInOneDay - ] +genUniformTime = genUniformTimeWithinRange defaultLowerBound defaultUpperBound + +-- | Generate 'UTCTime' values over a uniform range of dates and a mixture of +-- time precisions. +-- +-- Dates will be generated in a range that's bounded by the given minimum and +-- maximum Julian day arguments. +-- +genUniformTimeWithinRange :: Day -> Day -> Gen UTCTime +genUniformTimeWithinRange lowerBound upperBound + | lowerBound > upperBound = error $ + "genUniformTimeWithinRange: invalid bounds: " + <> show (lowerBound, upperBound) + | otherwise = oneof + [ genWith + hoursToNominalDiffTime + hoursInOneDay + , genWith + secondsToNominalDiffTime + secondsInOneDay + , genWith + picosecondsToNominalDiffTime + picosecondsInOneDay + ] where genWith :: (Integer -> NominalDiffTime) -> Integer -> Gen UTCTime genWith unitsToNominalDiffTime unitsInOneDay = do numberOfDays <- ModifiedJulianDay - <$> choose (0, daysInFiftyYears) + <$> choose + ( toModifiedJulianDay lowerBound + , toModifiedJulianDay upperBound + ) timeSinceMidnight <- unitsToNominalDiffTime <$> choose (0, unitsInOneDay) pure $ addUTCTime timeSinceMidnight (UTCTime numberOfDays 0) --- | The approximate number of days in fifty years. -daysInFiftyYears :: Integral a => a -daysInFiftyYears = 365 * 50 +defaultLowerBound :: Day +defaultLowerBound = ModifiedJulianDay 0 + +defaultUpperBound :: Day +defaultUpperBound = ModifiedJulianDay $ 365 * 50 -- | The number of hours in a day. hoursInOneDay :: Integral a => a From 9f2f97a3b8fbcd5dd76b233cc5d7d43bf8d13c51 Mon Sep 17 00:00:00 2001 From: Jonathan Knowles Date: Wed, 4 Dec 2019 09:23:21 +0000 Subject: [PATCH 2/6] Add function `epochStartTime`. This function computes the time at which an epoch starts. --- lib/core/src/Cardano/Wallet/Primitive/Types.hs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/core/src/Cardano/Wallet/Primitive/Types.hs b/lib/core/src/Cardano/Wallet/Primitive/Types.hs index 9b92a0d3a12..884c5c034e7 100644 --- a/lib/core/src/Cardano/Wallet/Primitive/Types.hs +++ b/lib/core/src/Cardano/Wallet/Primitive/Types.hs @@ -89,6 +89,7 @@ module Cardano.Wallet.Primitive.Types , SlotNo (..) , EpochNo (..) , unsafeEpochNo + , epochStartTime , SlotParameters (..) , SlotLength (..) , EpochLength (..) @@ -1095,6 +1096,10 @@ unsafeEpochNo epochNo maxEpochNo :: Word32 maxEpochNo = fromIntegral @Word31 $ unEpochNo maxBound +-- | Calculate the time at which an epoch begins. +epochStartTime :: SlotParameters -> EpochNo -> UTCTime +epochStartTime sps e = slotStartTime sps $ SlotId e 0 + instance NFData SlotId instance Buildable SlotId where From 5af53ff06fc05d5a1520dda78bbcab78bcd4e1c6 Mon Sep 17 00:00:00 2001 From: Jonathan Knowles Date: Wed, 4 Dec 2019 09:29:19 +0000 Subject: [PATCH 3/6] Add `SlotParametersAndTimePoint` type and `Arbitrary` instance. This enables generation of arbitrary slot parameters together with an arbitrary point in time, where the point in time falls into one of three categories: 1. occurs during the lifetime of the blockchain; 2. occurs before the earliest representable slot; 3. occurs after the latest representable slot. --- .../Cardano/Wallet/Primitive/TypesSpec.hs | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs b/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs index 66e7de18c9f..c4f250b2c25 100644 --- a/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs +++ b/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs @@ -54,6 +54,7 @@ import Cardano.Wallet.Primitive.Types , WalletName (..) , balance , computeUtxoStatistics + , epochStartTime , excluding , flatSlot , fromFlatSlot @@ -121,7 +122,7 @@ import Data.Text import Data.Text.Class ( TextDecodingError (..), fromText, toText ) import Data.Time - ( UTCTime ) + ( Day (ModifiedJulianDay), UTCTime, toModifiedJulianDay, utctDay ) import Data.Time.Utils ( utcTimePred, utcTimeSucc ) import Data.Word @@ -171,7 +172,7 @@ import Test.QuickCheck.Monadic import Test.Text.Roundtrip ( textRoundtrip ) import Test.Utils.Time - ( genUniformTime, getUniformTime ) + ( genUniformTime, genUniformTimeWithinRange, getUniformTime ) import qualified Data.ByteArray as BA import qualified Data.ByteString as BS @@ -1165,6 +1166,36 @@ instance {-# OVERLAPS #-} Arbitrary (SlotParameters, SlotId) where (el', slot') <- shrink (el, slot) pure (SlotParameters el' sl st, slot') +-- | Combines a 'SlotParameters' object and a single point in time. +-- +-- The point in time falls into one of the following categories: +-- +-- 1. occurs during the lifetime of the blockchain; +-- 2. occurs before the earliest representable slot; +-- 3. occurs after the latest representable slot. +-- +data SlotParametersAndTimePoint = SlotParametersAndTimePoint + { getSlotParameters :: SlotParameters + , getTimePoint :: UTCTime + } deriving (Eq, Show) + +instance Arbitrary SlotParametersAndTimePoint where + arbitrary = do + sps <- arbitrary + let timeA = 0 + let timeB = toModifiedJulianDay $ utctDay $ epochStartTime sps minBound + let timeC = toModifiedJulianDay $ utctDay $ epochStartTime sps maxBound + let timeD = timeC * 2 + (lowerBound, upperBound) <- oneof $ fmap pure + [ (timeA, timeB) + , (timeB, timeC) + , (timeC, timeD) + ] + time <- genUniformTimeWithinRange + (ModifiedJulianDay lowerBound) + (ModifiedJulianDay upperBound) + pure $ SlotParametersAndTimePoint sps time + -- | Note, for functions which works with both an epoch length and a slot id, -- we need to make sure that the 'slotNumber' doesn't exceed the epoch length, -- otherwise, all computations get mixed up. From 89ee70fdf711eb31da7eab2d70c1910961784d45 Mon Sep 17 00:00:00 2001 From: Jonathan Knowles Date: Wed, 4 Dec 2019 09:34:03 +0000 Subject: [PATCH 4/6] Add functions `epochPred` and `epochSucc`, with property tests. These functions find the predecessor and successor of an epoch, respectively. --- .../src/Cardano/Wallet/Primitive/Types.hs | 16 ++++++++ .../Cardano/Wallet/Primitive/TypesSpec.hs | 40 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/lib/core/src/Cardano/Wallet/Primitive/Types.hs b/lib/core/src/Cardano/Wallet/Primitive/Types.hs index 884c5c034e7..535046e57e4 100644 --- a/lib/core/src/Cardano/Wallet/Primitive/Types.hs +++ b/lib/core/src/Cardano/Wallet/Primitive/Types.hs @@ -90,6 +90,8 @@ module Cardano.Wallet.Primitive.Types , EpochNo (..) , unsafeEpochNo , epochStartTime + , epochPred + , epochSucc , SlotParameters (..) , SlotLength (..) , EpochLength (..) @@ -1100,6 +1102,20 @@ unsafeEpochNo epochNo epochStartTime :: SlotParameters -> EpochNo -> UTCTime epochStartTime sps e = slotStartTime sps $ SlotId e 0 +-- | Return the epoch immediately before the given epoch, or 'Nothing' if there +-- is no representable epoch before the given epoch. +epochPred :: EpochNo -> Maybe EpochNo +epochPred (EpochNo e) + | e == minBound = Nothing + | otherwise = Just $ EpochNo $ pred e + +-- | Return the epoch immediately after the given epoch, or 'Nothing' if there +-- is no representable epoch after the given epoch. +epochSucc :: EpochNo -> Maybe EpochNo +epochSucc (EpochNo e) + | e == maxBound = Nothing + | otherwise = Just $ EpochNo $ succ e + instance NFData SlotId instance Buildable SlotId where diff --git a/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs b/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs index c4f250b2c25..0286755909d 100644 --- a/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs +++ b/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs @@ -54,7 +54,9 @@ import Cardano.Wallet.Primitive.Types , WalletName (..) , balance , computeUtxoStatistics + , epochPred , epochStartTime + , epochSucc , excluding , flatSlot , fromFlatSlot @@ -146,6 +148,7 @@ import Test.QuickCheck , NonNegative (..) , NonZero (..) , Property + , Small (..) , arbitraryBoundedEnum , arbitraryPrintableChar , arbitrarySizedBoundedIntegral @@ -414,6 +417,43 @@ spec = do property $ \(a :: Int) (b :: Int) -> compare (InclusiveBound a) (InclusiveBound b) === compare a b + describe "Epoch arithmetic: predecessors and successors" $ do + + let succN n = applyN n (epochSucc =<<) + let predN n = applyN n (epochPred =<<) + + it "epochPred minBound == Nothing" $ + epochPred minBound === Nothing + + it "epochSucc maxBound == Nothing" $ + epochSucc maxBound === Nothing + + it "(applyN n epochSucc) . (applyN n epochPred) == id" $ + withMaxSuccess 1000 $ property $ + \(Small epochWord) (Small n) -> + let epoch = EpochNo epochWord + withinBounds = minBound + n <= unEpochNo epoch + expectedResult = + if withinBounds then Just epoch else Nothing + in + checkCoverage $ + cover 10 withinBounds "within bounds" $ + cover 10 (not withinBounds) "out of bounds" $ + expectedResult === succN n (predN n $ Just epoch) + + it "(applyN n epochPred) . (applyN n epochSucc) == id" $ + withMaxSuccess 1000 $ property $ + \(Small epochWord) (Small n) -> + let epoch = EpochNo $ maxBound - epochWord + withinBounds = maxBound - n >= unEpochNo epoch + expectedResult = + if withinBounds then Just epoch else Nothing + in + checkCoverage $ + cover 10 withinBounds "within bounds" $ + cover 10 (not withinBounds) "out of bounds" $ + expectedResult === predN n (succN n $ Just epoch) + describe "Slot arithmetic" $ do it "slotFloor (slotStartTime slotMinBound) == Just slotMinBound" $ From 7c2f1e321c96302a8ede843ae04c5b0ac9b1c723 Mon Sep 17 00:00:00 2001 From: Jonathan Knowles Date: Wed, 4 Dec 2019 09:41:10 +0000 Subject: [PATCH 5/6] Add `Arbitrary` instance for `EpochNo` in `Primitive.TypesSpec`. --- lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs b/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs index 0286755909d..f152849323e 100644 --- a/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs +++ b/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs @@ -1144,6 +1144,10 @@ instance Arbitrary BlockHeader where , pure $ Hash "BLOCK03" ] +instance Arbitrary EpochNo where + arbitrary = EpochNo <$> arbitrary + shrink = genericShrink + instance Arbitrary SlotId where shrink _ = [] arbitrary = do From 93d713fb37132234e11a2da940cdbb83e99b497f Mon Sep 17 00:00:00 2001 From: Jonathan Knowles Date: Wed, 4 Dec 2019 09:41:31 +0000 Subject: [PATCH 6/6] Add `epochCeiling` and `epochFloor` with related property tests. --- .../src/Cardano/Wallet/Primitive/Types.hs | 32 ++++ .../Cardano/Wallet/Primitive/TypesSpec.hs | 146 ++++++++++++++++++ 2 files changed, 178 insertions(+) diff --git a/lib/core/src/Cardano/Wallet/Primitive/Types.hs b/lib/core/src/Cardano/Wallet/Primitive/Types.hs index 535046e57e4..e70397c1bf1 100644 --- a/lib/core/src/Cardano/Wallet/Primitive/Types.hs +++ b/lib/core/src/Cardano/Wallet/Primitive/Types.hs @@ -92,6 +92,8 @@ module Cardano.Wallet.Primitive.Types , epochStartTime , epochPred , epochSucc + , epochCeiling + , epochFloor , SlotParameters (..) , SlotLength (..) , EpochLength (..) @@ -1116,6 +1118,36 @@ epochSucc (EpochNo e) | e == maxBound = Nothing | otherwise = Just $ EpochNo $ succ e +-- | For the given time 't', calculate the number of the earliest epoch with +-- start time 's' such that 't ≤ s'. +-- +-- Returns 'Nothing' if the calculation would result in an epoch number that is +-- not representable. +epochCeiling :: SlotParameters -> UTCTime -> Maybe EpochNo +epochCeiling sps t + | t < timeMin = Just minBound + | t > timeMax = Nothing + | otherwise = case slotCeiling sps t of + SlotId epoch 0 -> Just epoch + SlotId epoch _ -> epochSucc epoch + where + timeMin = epochStartTime sps minBound + timeMax = epochStartTime sps maxBound + +-- | For the given time 't', calculate the number of the latest epoch with +-- start time 's' such that 's ≤ t'. +-- +-- Returns 'Nothing' if the calculation would result in an epoch number that is +-- not representable. +epochFloor :: SlotParameters -> UTCTime -> Maybe EpochNo +epochFloor sps t + | t < timeMin = Nothing + | t > timeMax = Just maxBound + | otherwise = epochNumber <$> slotFloor sps t + where + timeMin = epochStartTime sps minBound + timeMax = epochStartTime sps maxBound + instance NFData SlotId instance Buildable SlotId where diff --git a/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs b/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs index f152849323e..810ab49b20b 100644 --- a/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs +++ b/lib/core/test/unit/Cardano/Wallet/Primitive/TypesSpec.hs @@ -54,6 +54,8 @@ import Cardano.Wallet.Primitive.Types , WalletName (..) , balance , computeUtxoStatistics + , epochCeiling + , epochFloor , epochPred , epochStartTime , epochSucc @@ -454,6 +456,150 @@ spec = do cover 10 (not withinBounds) "out of bounds" $ expectedResult === predN n (succN n $ Just epoch) + describe "Epoch arithmetic: epochCeiling: core properties" $ do + + it "epochStartTime (epochCeiling t) >= t" $ + withMaxSuccess 1000 $ property $ + \(SlotParametersAndTimePoint sps time) -> do + let timeMaximum = epochStartTime sps maxBound + let withinBounds = time <= timeMaximum + checkCoverage $ + cover 10 withinBounds "within bounds" $ + cover 10 (not withinBounds) "out of bounds" $ + case epochCeiling sps time of + Nothing -> not withinBounds + Just en -> time <= epochStartTime sps en + + it "epochStartTime (epochPred (epochCeiling t)) < t" $ + withMaxSuccess 1000 $ property $ + \(SlotParametersAndTimePoint sps time) -> do + let timeMaximum = epochStartTime sps maxBound + let withinBounds = time <= timeMaximum + checkCoverage $ + cover 10 withinBounds "within bounds" $ + cover 10 (not withinBounds) "out of bounds" $ + case epochCeiling sps time of + Nothing -> not withinBounds + Just e1 -> case epochPred e1 of + Nothing -> e1 == minBound + Just e2 -> time > epochStartTime sps e2 + + describe "Epoch arithmetic: epochFloor: core properties" $ do + + it "epochStartTime (epochFloor t) <= t" $ + withMaxSuccess 1000 $ property $ + \(SlotParametersAndTimePoint sps time) -> do + let timeMinimum = epochStartTime sps minBound + let withinBounds = time >= timeMinimum + checkCoverage $ + cover 10 withinBounds "within bounds" $ + cover 10 (not withinBounds) "out of bounds" $ + case epochFloor sps time of + Nothing -> not withinBounds + Just en -> time >= epochStartTime sps en + + it "epochStartTime (epochSucc (epochFloor t)) > t" $ + withMaxSuccess 1000 $ property $ + \(SlotParametersAndTimePoint sps time) -> do + let timeMinimum = epochStartTime sps minBound + let withinBounds = time >= timeMinimum + checkCoverage $ + cover 10 withinBounds "within bounds" $ + cover 10 (not withinBounds) "out of bounds" $ + case epochFloor sps time of + Nothing -> not withinBounds + Just e1 -> case epochSucc e1 of + Nothing -> e1 == maxBound + Just e2 -> time < epochStartTime sps e2 + + describe "Epoch arithmetic: epochCeiling: boundary conditions" $ do + + it "epochCeiling . epochStartTime == id" $ + withMaxSuccess 1000 $ property $ \(sps, epoch) -> + epochCeiling sps (epochStartTime sps epoch) + === Just epoch + + it "epochCeiling . utcTimePred . epochStartTime == id" $ + withMaxSuccess 1000 $ property $ \(sps, epoch) -> + epoch > minBound ==> do + let fun = epochCeiling sps + . utcTimePred + . epochStartTime sps + Just epoch === fun epoch + + it "epochPred . epochCeiling . utcTimeSucc . epochStartTime == id" $ + withMaxSuccess 1000 $ property $ \(sps, epoch) -> + epoch < maxBound ==> do + let fun = (epochPred =<<) + . epochCeiling sps + . utcTimeSucc + . epochStartTime sps + Just epoch === fun epoch + + it "epochCeiling (epochStartTime minBound) == minBound" $ + withMaxSuccess 1000 $ property $ \sps -> + epochCeiling sps (epochStartTime sps minBound) + === Just minBound + + it "epochCeiling (utcTimePred (epochStartTime minBound)) == minBound" $ + withMaxSuccess 1000 $ property $ \sps -> + epochCeiling sps (utcTimePred (epochStartTime sps minBound)) + === Just minBound + + it "epochCeiling (epochStartTime maxBound) == maxBound" $ + withMaxSuccess 1000 $ property $ \sps -> + epochCeiling sps (epochStartTime sps maxBound) + === Just maxBound + + it "epochCeiling (utcTimeSucc (epochStartTime maxBound)) == Nothing" $ + withMaxSuccess 1000 $ property $ \sps -> + epochCeiling sps (utcTimeSucc (epochStartTime sps maxBound)) + === Nothing + + describe "Epoch arithmetic: epochFloor: boundary conditions" $ do + + it "epochFloor . epochStartTime == id" $ + withMaxSuccess 1000 $ property $ \(sps, epoch) -> + epochFloor sps (epochStartTime sps epoch) + === Just epoch + + it "epochFloor . utcTimeSucc . epochStartTime == id" $ + withMaxSuccess 1000 $ property $ \(sps, epoch) -> + epoch < maxBound ==> do + let fun = epochFloor sps + . utcTimeSucc + . epochStartTime sps + Just epoch === fun epoch + + it "epochSucc . epochFloor . utcTimePred . epochStartTime == id" $ + withMaxSuccess 1000 $ property $ \(sps, epoch) -> + epoch > minBound ==> do + let fun = (epochSucc =<<) + . epochFloor sps + . utcTimePred + . epochStartTime sps + Just epoch === fun epoch + + it "epochFloor (epochStartTime minBound) == minBound" $ + withMaxSuccess 1000 $ property $ \sps -> + epochFloor sps (epochStartTime sps minBound) + === Just minBound + + it "epochFloor (utcTimePred (epochStartTime minBound)) == Nothing" $ + withMaxSuccess 1000 $ property $ \sps -> + epochFloor sps (utcTimePred (epochStartTime sps minBound)) + === Nothing + + it "epochFloor (epochStartTime maxBound) == maxBound" $ + withMaxSuccess 1000 $ property $ \sps -> + epochFloor sps (epochStartTime sps maxBound) + === Just maxBound + + it "epochFloor (utcTimeSucc (epochStartTime maxBound)) == maxBound" $ + withMaxSuccess 1000 $ property $ \sps -> + epochFloor sps (utcTimeSucc (epochStartTime sps maxBound)) + === Just maxBound + describe "Slot arithmetic" $ do it "slotFloor (slotStartTime slotMinBound) == Just slotMinBound" $