text-iso8601-0.1: tests/text-iso8601-tests.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Main (main) where
import Data.Functor.Classes (liftEq)
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import Data.Text.Lazy.Builder (Builder, toLazyText)
import Data.Time.LocalTime.Compat (TimeZone (..), ZonedTime (..))
import Data.Typeable (Typeable, typeRep)
import Test.QuickCheck (Arbitrary, counterexample,
property)
import Test.QuickCheck.Instances ()
import Test.Tasty (TestTree, defaultMain, testGroup)
import Test.Tasty.HUnit (assertFailure, testCase)
import Test.Tasty.QuickCheck (testProperty)
import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import qualified Data.Time.FromText as T
import qualified Data.Time.ToText as T
main :: IO ()
main = defaultMain $ testGroup "text-iso8601"
[ testGroup "roundtrip"
[ roundtrip (==) T.buildDay T.parseDay
, roundtrip (==) T.buildLocalTime T.parseLocalTime
, roundtrip eqTZ T.buildTimeZone T.parseTimeZone
, roundtrip (==) T.buildUTCTime T.parseUTCTime
, roundtrip eqZT T.buildZonedTime T.parseZonedTime
, roundtrip (==) T.buildTimeOfDay T.parseTimeOfDay
, roundtrip (==) T.buildYear T.parseYear
, roundtrip (==) T.buildMonth T.parseMonth
, roundtrip (==) T.buildQuarter T.parseQuarter
, roundtrip (==) T.buildQuarterOfYear T.parseQuarterOfYear
]
, testGroup "accepts"
-- we accept space instead of T
-- RFC3339 has a note suggesting allowing this.
[ accepts T.parseUTCTime "2023-06-09 02:35:33Z"
-- 60 second is always accepted
, accepts T.parseUTCTime "2023-06-09T02:35:60Z"
-- examples from RFC3339
, accepts T.parseUTCTime "1985-04-12T23:20:50.52Z"
, accepts T.parseUTCTime "1996-12-19T16:39:57-08:00"
, accepts T.parseUTCTime "1990-12-31T23:59:60Z"
, accepts T.parseUTCTime "1990-12-31T15:59:60-08:00"
, accepts T.parseUTCTime "1937-01-01T12:00:27.87+00:20"
-- we accept time without seconds
, accepts T.parseUTCTime "1937-01-01 12:00Z"
, accepts T.parseLocalTime "1937-01-01 12:00"
-- ISO8601 allows various offsets, while RFC3339 only +-HH:MM
, accepts T.parseUTCTime "1990-12-31T15:59:60-0800" -- no colon
, accepts T.parseUTCTime "1990-12-31T15:59:60-08" -- just hour
-- accepts +23:59
, accepts T.parseUTCTime "1937-01-01T12:00:00+23:59"
, accepts T.parseUTCTime "1937-01-01T12:00:00-23:59"
]
, testGroup "rejected"
-- https://github.com/haskell/aeson/issues/1033
[ rejects T.parseUTCTime "2023-06-09T02:35:33 Z"
-- Y2K years
, rejects T.parseDay "99-12-12"
-- we don't accept lowercase T or Z
-- RFC3339 says we MAY limit, i.e. requiring they should be uppercase.
, rejects T.parseUTCTime "2023-06-09T02:35:33z"
, rejects T.parseUTCTime "2023-06-09t02:35:33Z"
-- accepts +23:59, but not 24 or 60
, rejects T.parseUTCTime "1937-01-01T12:00:00+24:59"
, rejects T.parseUTCTime "1937-01-01T12:00:00-23:60"
]
]
eqTZ :: TimeZone -> TimeZone -> Bool
eqTZ a b = timeZoneMinutes a == timeZoneMinutes b
eqZT :: ZonedTime -> ZonedTime -> Bool
eqZT (ZonedTime lt tz) (ZonedTime lt' tz') =
lt == lt' && eqTZ tz tz'
roundtrip
:: forall a. (Typeable a, Arbitrary a, Show a)
=> (a -> a -> Bool) -> (a -> Builder) -> (Text -> Either String a) -> TestTree
roundtrip eq build parse = testProperty (show (typeRep (Proxy :: Proxy a))) $ \x ->
let lt = toLazyText (build x)
y = parse (LT.toStrict lt)
in counterexample (LT.unpack lt) $
counterexample (show y) $
property (liftEq eq y (Right x))
rejects :: forall a. (Typeable a, Show a) => (Text -> Either String a) -> String -> TestTree
rejects parse inp = testCase (show (typeRep (Proxy :: Proxy a)) ++ " rejects " ++ show inp) $ do
case parse (T.pack inp) of
Left _ -> return ()
Right a -> assertFailure $ "Unexpectedly accepted: " ++ show a
accepts :: forall a. (Typeable a, Show a) => (Text -> Either String a) -> String -> TestTree
accepts parse inp = testCase (show (typeRep (Proxy :: Proxy a)) ++ " accepts " ++ show inp) $ do
case parse (T.pack inp) of
Left err -> assertFailure $ "Unexpectedly rejected: " ++ err
Right _ -> return ()