packages feed

aws-eventbridge-cron-0.1.0.0: test/AWS/EventBridge/RateSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module AWS.EventBridge.RateSpec (tests) where

import AWS.EventBridge.Rate
import Data.Time.Clock (secondsToNominalDiffTime)
import qualified Data.Text as T
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))
import Test.Tasty.QuickCheck as QC
import TestSupport (assertLeft)

tests :: TestTree
tests =
  testGroup "rate"
    [ manualTests
    , propertyTests
    ]

manualTests :: TestTree
manualTests = testGroup "manual"
  [ testCase "parse minutes" $ parseRateText "rate(5 minutes)" @?= Right (RateMinutes 5)
  , testCase "parse minute singular" $ parseRateText "rate(1 minute)" @?= Right (RateMinutes 1)
  , testCase "parse hours plural" $ parseRateText "rate(10 hours)" @?= Right (RateHours 10)
  , testCase "parse hour singular" $ parseRateText "rate(1 hour)" @?= Right (RateHours 1)
  , testCase "parse days" $ parseRateText "rate(3 days)" @?= Right (RateDays 3)
  , testCase "parse day singular" $ parseRateText "rate(1 day)" @?= Right (RateDays 1)
  , testCase "parse trims surrounding whitespace" $ parseRateText "  rate(2 hours)\n" @?= Right (RateHours 2)
  , testCase "parse without separator fails" $ assertLeft (parseRateText "rate(5minutes)")
  , testCase "parse negative rejected" $ assertLeft (parseRateText "rate(-5 minutes)")
  , testCase "evaluate minutes" $
      evaluateRateT (RateMinutes 15) @?= Right (secondsToNominalDiffTime 900)
  , testCase "evaluate hours" $
      evaluateRateT (RateHours 2) @?= Right (secondsToNominalDiffTime 7200)
  , testCase "evaluate days" $
      evaluateRateT (RateDays 1) @?= Right (secondsToNominalDiffTime 86400)
  , testCase "evaluate minutes upper bound" $
    evaluateRateT (RateMinutes 31536000)
        @?= Right (secondsToNominalDiffTime (fromInteger (31536000 * 60)))
  , testCase "evaluate hours upper bound" $
    evaluateRateT (RateHours 8760)
        @?= Right (secondsToNominalDiffTime (fromInteger (8760 * 3600)))
  , testCase "evaluate days upper bound" $
    evaluateRateT (RateDays 365)
        @?= Right (secondsToNominalDiffTime (fromInteger (365 * 86400)))
  , testCase "minutes below range rejected" $
      assertLeft (evaluateRateT (RateMinutes 0))
  , testCase "minutes above range rejected" $
      assertLeft (evaluateRateT (RateMinutes 40000000))
  , testCase "hours below range rejected" $
      assertLeft (evaluateRateT (RateHours 0))
  , testCase "hours above range rejected" $
      assertLeft (evaluateRateT (RateHours 9000))
  , testCase "days below range rejected" $
      assertLeft (evaluateRateT (RateDays 0))
  , testCase "days above range rejected" $
      assertLeft (evaluateRateT (RateDays 400))
  , testCase "invalid unit fails" $
      case parseRateText "rate(5 weeks)" of
        Left _ -> pure ()
        Right val -> fail ("unexpected success: " <> show val)
  ]

propertyTests :: TestTree
propertyTests = testGroup "properties"
  [ QC.testProperty "minutes evaluate to consistent seconds" propMinutesEvaluate
  , QC.testProperty "hours evaluate to consistent seconds" propHoursEvaluate
  , QC.testProperty "days evaluate to consistent seconds" propDaysEvaluate
  , QC.testProperty "parser round-trips generated expressions" propParserRoundTrip
  ]

propMinutesEvaluate :: QC.Property
propMinutesEvaluate =
  QC.forAll (QC.chooseInt (1, 31536000)) $ \m ->
    evaluateRateT (RateMinutes m)
      QC.=== Right (secondsToNominalDiffTime (fromIntegral (m * 60)))

propHoursEvaluate :: QC.Property
propHoursEvaluate =
  QC.forAll (QC.chooseInt (1, 8760)) $ \h ->
    evaluateRateT (RateHours h)
      QC.=== Right (secondsToNominalDiffTime (fromIntegral (h * 3600)))

propDaysEvaluate :: QC.Property
propDaysEvaluate =
  QC.forAll (QC.chooseInt (1, 365)) $ \d ->
    evaluateRateT (RateDays d)
      QC.=== Right (secondsToNominalDiffTime (fromIntegral (d * 86400)))

propParserRoundTrip :: QC.Property
propParserRoundTrip =
  QC.forAll genRateExprText $ \(expr, input) ->
    parseRateText input QC.=== Right expr

genRateExprText :: QC.Gen (RateExprT, T.Text)
genRateExprText =
  QC.oneof
    [ genCase RateMinutes 31536000 "minute"
    , genCase RateHours 8760 "hour"
    , genCase RateDays 365 "day"
    ]

genCase :: (Int -> RateExprT) -> Int -> String -> QC.Gen (RateExprT, T.Text)
genCase constructor upperBound unitLabel = do
  n <- QC.chooseInt (1, upperBound)
  plural <- QC.elements [False, True]
  prefix <- QC.elements ["", " ", "\t", "\n"]
  suffix <- QC.elements ["", " ", "\t", "\n"]
  gap <- QC.elements [" ", "  ", "\t"]
  let token = unitLabel <> if plural then "s" else ""
      rendered = prefix <> "rate(" <> show n <> gap <> token <> ")" <> suffix
  pure (constructor n, T.pack rendered)