From ec2a083f9c9e01fd89416542e70189eef261d1e1 Mon Sep 17 00:00:00 2001 From: Jonathan Knowles Date: Wed, 17 Jul 2019 08:57:11 +0000 Subject: [PATCH] Move tests for conversion of time values to and from text to a dedicated module. --- lib/cli/test/unit/Cardano/CLISpec.hs | 170 +------------- lib/core/cardano-wallet-core.cabal | 1 + lib/core/test/unit/Data/Time/TextSpec.hs | 271 +++++++++++++++++++++++ 3 files changed, 273 insertions(+), 169 deletions(-) create mode 100644 lib/core/test/unit/Data/Time/TextSpec.hs diff --git a/lib/cli/test/unit/Cardano/CLISpec.hs b/lib/cli/test/unit/Cardano/CLISpec.hs index 358d426c7cc..ddc1020b448 100644 --- a/lib/cli/test/unit/Cardano/CLISpec.hs +++ b/lib/cli/test/unit/Cardano/CLISpec.hs @@ -38,8 +38,6 @@ import Data.Bifunctor ( bimap ) import Data.ByteArray.Encoding ( Base (Base16), convertFromBase, convertToBase ) -import Data.Either - ( isLeft, isRight ) import Data.Proxy ( Proxy (..) ) import Data.Text @@ -51,7 +49,7 @@ import Options.Applicative import System.IO ( Handle, IOMode (..), hClose, openFile ) import Test.Hspec - ( Spec, describe, it, shouldBe, shouldSatisfy ) + ( Spec, describe, it, shouldBe ) import Test.QuickCheck ( Arbitrary (..) , Large (..) @@ -61,7 +59,6 @@ import Test.QuickCheck , cover , genericShrink , property - , (.&&.) , (===) ) import Test.QuickCheck.Instances.Time @@ -274,127 +271,6 @@ spec = do textRoundtrip $ Proxy @(Port "test") textRoundtrip $ Proxy @MnemonicSize - describe "ISO 8601 encoding of dates and times" $ do - - describe "Can decode valid ISO 8601 strings" $ do - - describe "Basic format" $ do - describe "UTC+0 timezone (Z)" $ do - canDecodeValidIso8601Time "20080915T155300Z" - canDecodeValidIso8601Time "20080915T155300.1Z" - canDecodeValidIso8601Time "20080915T155300.12Z" - describe "UTC+0 timezone (manually specified)" $ do - canDecodeValidIso8601Time "20080915T155300+0000" - canDecodeValidIso8601Time "20080915T155300.1+0000" - canDecodeValidIso8601Time "20080915T155300.12+0000" - describe "UTC+8 timezone (manually specified)" $ do - canDecodeValidIso8601Time "20080915T155300+0800" - canDecodeValidIso8601Time "20080915T155300.1+0800" - canDecodeValidIso8601Time "20080915T155300.12+0800" - describe "UTC-8 timezone (manually specified)" $ do - canDecodeValidIso8601Time "20080915T155300-0800" - canDecodeValidIso8601Time "20080915T155300.1-0800" - canDecodeValidIso8601Time "20080915T155300.12-0800" - - describe "Extended format" $ do - describe "UTC+0 timezone (Z)" $ do - canDecodeValidIso8601Time "2008-09-15T15:53:00Z" - canDecodeValidIso8601Time "2008-09-15T15:53:00.1Z" - canDecodeValidIso8601Time "2008-09-15T15:53:00.12Z" - describe "UTC+0 timezone (manually specified)" $ do - canDecodeValidIso8601Time "2008-09-15T15:53:00+00:00" - canDecodeValidIso8601Time "2008-09-15T15:53:00.1+00:00" - canDecodeValidIso8601Time "2008-09-15T15:53:00.12+00:00" - describe "UTC+8 timezone (manually specified)" $ do - canDecodeValidIso8601Time "2008-09-15T15:53:00+08:00" - canDecodeValidIso8601Time "2008-09-15T15:53:00.1+08:00" - canDecodeValidIso8601Time "2008-09-15T15:53:00.12+08:00" - describe "UTC-8 timezone (manually specified)" $ do - canDecodeValidIso8601Time "2008-09-15T15:53:00-08:00" - canDecodeValidIso8601Time "2008-09-15T15:53:00.1-08:00" - canDecodeValidIso8601Time "2008-09-15T15:53:00.12-08:00" - - describe "Cannot decode invalid ISO 8601 strings" $ do - - describe "Strings that are not time values" $ do - cannotDecodeInvalidIso8601Time "" - cannotDecodeInvalidIso8601Time "w" - cannotDecodeInvalidIso8601Time "wibble" - - describe "Basic format" $ do - describe "Dates without times" $ do - cannotDecodeInvalidIso8601Time "2008" - cannotDecodeInvalidIso8601Time "200809" - cannotDecodeInvalidIso8601Time "20080915" - describe "Missing timezones" $ do - cannotDecodeInvalidIso8601Time "20080915T155300" - cannotDecodeInvalidIso8601Time "20080915T155300.1" - cannotDecodeInvalidIso8601Time "20080915T155300.12" - describe "Invalid timezone characters" $ do - cannotDecodeInvalidIso8601Time "20080915T155300A" - cannotDecodeInvalidIso8601Time "20080915T155300.1A" - cannotDecodeInvalidIso8601Time "20080915T155300.12A" - describe "Invalid date-time separators" $ do - cannotDecodeInvalidIso8601Time "20080915S155300Z" - cannotDecodeInvalidIso8601Time "20080915S155300.1Z" - cannotDecodeInvalidIso8601Time "20080915S155300.12Z" - describe "Missing date-time separators" $ do - cannotDecodeInvalidIso8601Time "20080915155300Z" - cannotDecodeInvalidIso8601Time "20080915155300.1Z" - cannotDecodeInvalidIso8601Time "20080915155300.12Z" - - describe "Extended format" $ do - describe "Dates without times" $ do - cannotDecodeInvalidIso8601Time "2008" - cannotDecodeInvalidIso8601Time "2008-09" - cannotDecodeInvalidIso8601Time "2008-09-15" - describe "Missing timezones" $ do - cannotDecodeInvalidIso8601Time "2008-09-15T15:53:00" - cannotDecodeInvalidIso8601Time "2008-09-15T15:53:00.1" - cannotDecodeInvalidIso8601Time "2008-09-15T15:53:00.12" - describe "Invalid timezone characters" $ do - cannotDecodeInvalidIso8601Time "2008-09-15T15:53:00A" - cannotDecodeInvalidIso8601Time "2008-09-15T15:53:00.1A" - cannotDecodeInvalidIso8601Time "2008-09-15T15:53:00.12A" - describe "Invalid date-time separators" $ do - cannotDecodeInvalidIso8601Time "2008-09-15S15:53:00Z" - cannotDecodeInvalidIso8601Time "2008-09-15S15:53:00.1Z" - cannotDecodeInvalidIso8601Time "2008-09-15S15:53:00.12Z" - describe "Missing date-time separators" $ do - cannotDecodeInvalidIso8601Time "2008-09-1515:53:00Z" - cannotDecodeInvalidIso8601Time "2008-09-1515:53:00.1Z" - cannotDecodeInvalidIso8601Time "2008-09-1515:53:00.12Z" - - describe "Equivalent times are decoded equivalently" $ do - - describe "Times with the same date" $ do - ensureIso8601TimesEquivalent - "2008-08-08T12:00:00+01:00" - "2008-08-08T11:00:00Z" - ensureIso8601TimesEquivalent - "2008-08-08T12:00:00+08:00" - "2008-08-08T04:00:00Z" - ensureIso8601TimesEquivalent - "2008-08-08T12:00:00-01:00" - "2008-08-08T13:00:00Z" - ensureIso8601TimesEquivalent - "2008-08-08T12:00:00-08:00" - "2008-08-08T20:00:00Z" - - describe "Times with different dates" $ do - ensureIso8601TimesEquivalent - "2008-08-08T00:00:00+01:00" - "2008-08-07T23:00:00Z" - ensureIso8601TimesEquivalent - "2008-08-08T00:00:00+08:00" - "2008-08-07T16:00:00Z" - ensureIso8601TimesEquivalent - "2008-08-08T23:00:00-01:00" - "2008-08-09T00:00:00Z" - ensureIso8601TimesEquivalent - "2008-08-08T23:00:00-08:00" - "2008-08-09T07:00:00Z" - describe "Port decoding from text" $ do let err = TextDecodingError $ "expected a TCP port number between " @@ -537,50 +413,6 @@ instance Arbitrary (Port "test") where | p == minBound = [] | otherwise = [pred p] -{------------------------------------------------------------------------------- - Helper Functions --------------------------------------------------------------------------------} - --- | Checks that the specified 'Text' CAN be decoded as an 'Iso8601Time' --- value using the 'FromText` instance. --- -canDecodeValidIso8601Time :: Text -> Spec -canDecodeValidIso8601Time text = - it ("Can decode as ISO 8601 time: " <> T.unpack text) $ property $ do - let result = fromText @Iso8601Time text - -- Internally, 'Iso8601Time' values are always stored canonically as - -- times in the UTC+0 timezone. Any original timezone information is - -- lost. So we check that a roundtrip text conversion can be applied - -- to the result of parsing the original input, rather than to the - -- original input itself. - (result `shouldSatisfy` isRight) - .&&. - ((fromText . toText =<< result) `shouldBe` result) - --- | Checks that the specified 'Text' CANNOT be decoded as an 'Iso8601Time' --- value using the 'FromText' instance. --- -cannotDecodeInvalidIso8601Time :: Text -> Spec -cannotDecodeInvalidIso8601Time text = - it ("Cannot decode as ISO 8601 time: " <> T.unpack text) $ property $ do - fromText @Iso8601Time text `shouldSatisfy` isLeft - --- | Checks that the specified "Text' values can both be decoded as valid --- 'Iso8601Time' values, and that the resultant values are equal. --- -ensureIso8601TimesEquivalent :: Text -> Text -> Spec -ensureIso8601TimesEquivalent t1 t2 = it title $ property $ - (r1 `shouldBe` r2) - .&&. (r1 `shouldSatisfy` isRight) - .&&. (r2 `shouldSatisfy` isRight) - - where - r1 = fromText @Iso8601Time t1 - r2 = fromText @Iso8601Time t2 - title = mempty - <> "Equivalent ISO 8601 times are decoded equivalently: " - <> show (t1, t2) - {------------------------------------------------------------------------------- Dummy Target -------------------------------------------------------------------------------} diff --git a/lib/core/cardano-wallet-core.cabal b/lib/core/cardano-wallet-core.cabal index a3dc0cfd0d1..017b558d05a 100644 --- a/lib/core/cardano-wallet-core.cabal +++ b/lib/core/cardano-wallet-core.cabal @@ -191,6 +191,7 @@ test-suite unit Cardano.Wallet.TransactionSpec Cardano.WalletSpec Data.QuantitySpec + Data.Time.TextSpec Network.Wai.Middleware.LoggingSpec benchmark db diff --git a/lib/core/test/unit/Data/Time/TextSpec.hs b/lib/core/test/unit/Data/Time/TextSpec.hs new file mode 100644 index 00000000000..b2a3e70ecfe --- /dev/null +++ b/lib/core/test/unit/Data/Time/TextSpec.hs @@ -0,0 +1,271 @@ +module Data.Time.TextSpec + ( spec + ) where + +import Prelude + +import Control.Monad + ( forM_ ) +import Data.List + ( (\\) ) +import Data.Maybe + ( isJust, maybeToList ) +import Data.Text + ( Text ) +import Data.Time.Text + ( TimeFormat (..) + , iso8601 + , iso8601BasicLocal + , iso8601BasicUtc + , iso8601ExtendedLocal + , iso8601ExtendedUtc + , utcTimeFromText + , utcTimeToText + ) +import Test.Hspec + ( Spec, describe, it, shouldBe, shouldSatisfy ) +import Test.QuickCheck + ( property, (.&&.), (===) ) +import Test.QuickCheck.Instances.Time + () + +import qualified Data.Text as T + +spec :: Spec +spec = describe "Conversion of UTC time values to and from text" $ do + + describe "Roundtrip conversion to and from text succeeds for all formats" $ + forM_ allSupportedFormats $ \tf -> + it (timeFormatName tf) $ property $ \t -> + utcTimeFromText [tf] (utcTimeToText tf t) + `shouldBe` Just t + + describe "Parsing valid strings succeeds for all formats" $ + forM_ allSupportedFormats $ \tf -> describe (timeFormatName tf) $ + forM_ (validForFormat tf) $ \es -> describe (title es) $ + forM_ (examples es) $ \e -> + it (T.unpack e) $ property $ + utcTimeFromText [tf] e `shouldSatisfy` isJust + + describe "Parsing invalid strings fails for all formats" $ + forM_ allSupportedFormats $ \tf -> describe (timeFormatName tf) $ + forM_ (invalidForFormat tf) $ \es -> describe (title es) $ + forM_ (examples es) $ \e -> + it (T.unpack e) $ property $ + utcTimeFromText [tf] e === Nothing + + describe "Equivalent times are decoded equivalently." $ do + + describe "Times with identical dates" $ do + ensureTimesDecodeEquivalently iso8601 + "2008-08-08T12:00:00+01:00" + "2008-08-08T11:00:00Z" + ensureTimesDecodeEquivalently iso8601 + "2008-08-08T12:00:00+08:00" + "2008-08-08T04:00:00Z" + ensureTimesDecodeEquivalently iso8601 + "2008-08-08T12:00:00-01:00" + "2008-08-08T13:00:00Z" + ensureTimesDecodeEquivalently iso8601 + "2008-08-08T12:00:00-08:00" + "2008-08-08T20:00:00Z" + + describe "Times with different dates" $ do + ensureTimesDecodeEquivalently iso8601 + "2008-08-08T00:00:00+01:00" + "2008-08-07T23:00:00Z" + ensureTimesDecodeEquivalently iso8601 + "2008-08-08T00:00:00+08:00" + "2008-08-07T16:00:00Z" + ensureTimesDecodeEquivalently iso8601 + "2008-08-08T23:00:00-01:00" + "2008-08-09T00:00:00Z" + ensureTimesDecodeEquivalently iso8601 + "2008-08-08T23:00:00-08:00" + "2008-08-09T07:00:00Z" + +-- | Checks that the specified "Text' values can both be decoded according to +-- the specified formats, and that the resultant values are equal. +-- +ensureTimesDecodeEquivalently :: [TimeFormat] -> Text -> Text -> Spec +ensureTimesDecodeEquivalently tf t1 t2 = it testTitle $ property $ + (r1 `shouldBe` r2) + .&&. (r1 `shouldSatisfy` isJust) + .&&. (r2 `shouldSatisfy` isJust) + + where + r1 = utcTimeFromText tf t1 + r2 = utcTimeFromText tf t2 + testTitle = mempty + <> "Equivalent times are decoded equivalently: " + <> show (t1, t2) + +-- | Represents a set of example input strings. +data ExampleSet = ExampleSet + { title :: String + , examples :: [Text] + } deriving Eq + +-- | A list of all formats supported by `utcTimeFromText` and `utcTimeToText`. +allSupportedFormats :: [TimeFormat] +allSupportedFormats = + [ iso8601BasicUtc + , iso8601BasicLocal + , iso8601ExtendedUtc + , iso8601ExtendedLocal + ] + +-- | Return all known valid examples for the given time format. The examples +-- returned should all parse successfully when `utcTimeFromText` is called +-- with the given format. +validForFormat :: TimeFormat -> [ExampleSet] +validForFormat format = maybeToList $ lookup format + [ (iso8601BasicUtc, validIso8601BasicUtc) + , (iso8601BasicLocal, validIso8601BasicLocal) + , (iso8601ExtendedUtc, validIso8601ExtendedUtc) + , (iso8601ExtendedLocal, validIso8601ExtendedLocal) + ] + +-- | Return all known invalid examples for the given time format. The examples +-- returned should all fail to parse when `utcTimeFromText` is called with +-- the given format. +invalidForFormat :: TimeFormat -> [ExampleSet] +invalidForFormat format = allExamples \\ validForFormat format + +-- | The list of all example time strings. Whether or not a given string is +-- valid depends on the particular list of formats that is passed to the +-- `utcTimeFromText` function. +allExamples :: [ExampleSet] +allExamples = + [ validIso8601BasicUtc + , validIso8601BasicLocal + , validIso8601ExtendedUtc + , validIso8601ExtendedLocal + , nonTimeExamples + , iso8601BasicWithoutTimes + , iso8601BasicWithoutTimezones + , iso8601BasicWithInvalidTimezones + , iso8601BasicWithInvalidDateTimeSeparators + , iso8601BasicWithMissingDateTimeSeparators + , iso8601ExtendedWithoutTimes + , iso8601ExtendedWithoutTimezones + , iso8601ExtendedWithInvalidTimezones + , iso8601ExtendedWithInvalidDateTimeSeparators + , iso8601ExtendedWithMissingDateTimeSeparators ] + +validIso8601BasicUtc :: ExampleSet +validIso8601BasicUtc = ExampleSet + "Valid ISO 8601 basic UTC times" + [ "20080915T155300Z" + , "20080915T155300.1Z" + , "20080915T155300.12Z" ] + +validIso8601BasicLocal :: ExampleSet +validIso8601BasicLocal = ExampleSet + "Valid ISO 8601 basic local times" + [ "20080915T155300+0000" + , "20080915T155300+0800" + , "20080915T155300-0800" + , "20080915T155300.1+0000" + , "20080915T155300.1+0800" + , "20080915T155300.1-0800" + , "20080915T155300.12+0000" + , "20080915T155300.12+0800" + , "20080915T155300.12-0800" ] + +validIso8601ExtendedUtc :: ExampleSet +validIso8601ExtendedUtc = ExampleSet + "Valid ISO 8601 extended UTC times" + [ "2008-09-15T15:53:00Z" + , "2008-09-15T15:53:00.1Z" + , "2008-09-15T15:53:00.12Z" ] + +validIso8601ExtendedLocal :: ExampleSet +validIso8601ExtendedLocal = ExampleSet + "Valid ISO 8601 extended local times" + [ "2008-09-15T15:53:00+00:00" + , "2008-09-15T15:53:00+08:00" + , "2008-09-15T15:53:00-08:00" + , "2008-09-15T15:53:00.1+00:00" + , "2008-09-15T15:53:00.1+08:00" + , "2008-09-15T15:53:00.1-08:00" + , "2008-09-15T15:53:00.12+00:00" + , "2008-09-15T15:53:00.12+08:00" + , "2008-09-15T15:53:00.12-08:00" ] + +nonTimeExamples :: ExampleSet +nonTimeExamples = ExampleSet + "Non-time examples" + [ "" + , "w" + , "wibble" ] + +iso8601BasicWithoutTimes :: ExampleSet +iso8601BasicWithoutTimes = ExampleSet + "ISO 8601 basic format without times" + [ "2008" + , "200809" + , "20080915" ] + +iso8601BasicWithoutTimezones :: ExampleSet +iso8601BasicWithoutTimezones = ExampleSet + "ISO 8601 basic format without timezones" + [ "20080915T155300" + , "20080915T155300.1" + , "20080915T155300.12" ] + +iso8601BasicWithInvalidTimezones :: ExampleSet +iso8601BasicWithInvalidTimezones = ExampleSet + "ISO 8601 basic format with invalid timezones" + [ "20080915T155300A" + , "20080915T155300.1A" + , "20080915T155300.12A" ] + +iso8601BasicWithInvalidDateTimeSeparators :: ExampleSet +iso8601BasicWithInvalidDateTimeSeparators = ExampleSet + "ISO 8601 basic format with invalid datetime separators" + [ "20080915S155300Z" + , "20080915S155300.1Z" + , "20080915S155300.12Z" ] + +iso8601BasicWithMissingDateTimeSeparators :: ExampleSet +iso8601BasicWithMissingDateTimeSeparators = ExampleSet + "ISO 8601 basic format with missing datetime separators" + [ "20080915155300Z" + , "20080915155300.1Z" + , "20080915155300.12Z" ] + +iso8601ExtendedWithoutTimes :: ExampleSet +iso8601ExtendedWithoutTimes = ExampleSet + "ISO 8601 extended format without times" + [ "2008" + , "2008-09" + , "2008-09-15" ] + +iso8601ExtendedWithoutTimezones :: ExampleSet +iso8601ExtendedWithoutTimezones = ExampleSet + "ISO 8601 extended format without timezones" + [ "2008-09-15T15:53:00" + , "2008-09-15T15:53:00.1" + , "2008-09-15T15:53:00.12" ] + +iso8601ExtendedWithInvalidTimezones :: ExampleSet +iso8601ExtendedWithInvalidTimezones = ExampleSet + "ISO 8601 extended format with invalid timezones" + [ "2008-09-15T15:53:00A" + , "2008-09-15T15:53:00.1A" + , "2008-09-15T15:53:00.12A" ] + +iso8601ExtendedWithInvalidDateTimeSeparators :: ExampleSet +iso8601ExtendedWithInvalidDateTimeSeparators = ExampleSet + "ISO 8601 extended format with invalid date-time separators" + [ "2008-09-15S15:53:00Z" + , "2008-09-15S15:53:00.1Z" + , "2008-09-15S15:53:00.12Z" ] + +iso8601ExtendedWithMissingDateTimeSeparators :: ExampleSet +iso8601ExtendedWithMissingDateTimeSeparators = ExampleSet + "ISO 8601 extended format with missing date-time separators" + [ "2008-09-1515:53:00Z" + , "2008-09-1515:53:00.1Z" + , "2008-09-1515:53:00.12Z" ]