aws-eventbridge-cron-0.1.0.0: test/AWS/EventBridge/DayOfMonthSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module AWS.EventBridge.DayOfMonthSpec (tests) where
import AWS.EventBridge.DayOfMonth
import Data.Time.Calendar (fromGregorian, gregorianMonthLength)
import Data.Time.Calendar.WeekDate (toWeekDate)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))
import Test.Tasty.QuickCheck as QC
import TestSupport
( assertLeft
, expectEvalWith
, expectParseWith
, strictlyAscending
, withinBoundsInt
)
tests :: TestTree
tests =
testGroup "day-of-month"
[ unitTests
, propertyTests
]
unitTests :: TestTree
unitTests = testGroup "manual"
[ testCase "parse L" $ parseDayOfMonthText "L" @?= Right DomLast
, testCase "parse L-2" $ parseDayOfMonthText "L-2" @?= Right (DomLastOffset 2)
, testCase "parse 15W" $ parseDayOfMonthText "15W" @?= Right (DomClosestWeekday 15)
, testCase "parse numeric union" $ do
expr <- expectParseWith "day-of-month" parseDayOfMonthText "1,5,10-12"
result <- expectEvalWith "day-of-month" (evaluateDayOfMonthT 2024 2) expr
result @?= [1,5,10,11,12]
, testCase "parse ? cannot union" $ assertLeft (parseDayOfMonthText "?,5")
, testCase "evaluate DomLast" $ do
let dim = gregorianMonthLength 2025 3
result <- expectEvalWith "day-of-month" (evaluateDayOfMonthT 2025 3) DomLast
result @?= [dim]
, testCase "closestWeekday Saturday at start" $
closestWeekday 2025 3 1 @?= Just 3
, testCase "closestWeekday Sunday at end" $
closestWeekday 2021 1 31 @?= Just 29
, testCase "lastWeekday handles weekend" $
lastWeekday 2021 1 @?= 29
, testCase "DomLastOffset clamps" $ do
result <- expectEvalWith "day-of-month" (evaluateDayOfMonthT 2024 5) (DomLastOffset 2)
result @?= [29]
, testCase "DomLastOffset rejects overshoot" $
assertLeft (evaluateDayOfMonthT 2024 2 (DomLastOffset 29))
]
propertyTests :: TestTree
propertyTests = testGroup "properties"
[ QC.testProperty "closestWeekday stays within bounds and weekdays" propClosestWeekdayWithinBounds
, QC.testProperty "closestWeekday rejects out-of-range days" propClosestWeekdayRejectsOutOfRange
, QC.testProperty "lastWeekday produces weekday inside month" propLastWeekdayIsWeekday
, QC.testProperty "DomLast evaluates to final day" propDomLastMatchesMonthLength
, QC.testProperty "DomLastOffset yields expected day when within month" propDomLastOffsetWithinMonth
, QC.testProperty "DomLastOffset rejects offsets >= month length" propDomLastOffsetTooLarge
, QC.testProperty "Numeric expressions produce in-range ascending days" propNumericExpressionsWithinBounds
, QC.testProperty "DomAny evaluates to empty set" propDomAnyEmpty
]
propClosestWeekdayWithinBounds :: QC.Property
propClosestWeekdayWithinBounds =
QC.forAll genYearMonth $ \(y, m) ->
QC.forAll (QC.chooseInt (1, gregorianMonthLength y m)) $ \d ->
let result = closestWeekday y m d
dim = gregorianMonthLength y m
in case result of
Nothing -> QC.counterexample "expected Just" False
Just w ->
let (_, _, dow) = toWeekDate (fromGregorian y m w)
in QC.counterexample (failureMsg y m d w dow)
(w >= 1 && w <= dim && dow >= 1 && dow <= 5 && abs (w - d) <= 2)
propClosestWeekdayRejectsOutOfRange :: QC.Property
propClosestWeekdayRejectsOutOfRange =
QC.forAll genYearMonth $ \(y, m) ->
QC.forAll outOfRangeDay $ \d ->
closestWeekday y m d QC.=== Nothing
propLastWeekdayIsWeekday :: QC.Property
propLastWeekdayIsWeekday =
QC.forAll genYearMonth $ \(y, m) ->
let lw = lastWeekday y m
dim = gregorianMonthLength y m
(_, _, dow) = toWeekDate (fromGregorian y m lw)
in QC.counterexample (failureMsg y m dim lw dow)
(lw >= 1 && lw <= dim && dow >= 1 && dow <= 5)
propDomLastMatchesMonthLength :: QC.Property
propDomLastMatchesMonthLength =
QC.forAll genYearMonth $ \(y, m) ->
case evaluateDayOfMonthT y m DomLast of
Left err -> QC.counterexample err False
Right xs ->
let dim = gregorianMonthLength y m
in QC.counterexample ("expected " <> show [dim] <> ", got " <> show xs)
(xs == [dim])
propDomLastOffsetWithinMonth :: QC.Property
propDomLastOffsetWithinMonth =
QC.forAll genDomLastOffsetWithin $ \(y, m, off) ->
case evaluateDayOfMonthT y m (DomLastOffset off) of
Left err -> QC.counterexample err False
Right xs ->
let dim = gregorianMonthLength y m
expected = dim - off
in QC.counterexample ("expected " <> show [expected] <> ", got " <> show xs)
(xs == [expected])
propDomLastOffsetTooLarge :: QC.Property
propDomLastOffsetTooLarge =
QC.forAll genDomLastOffsetTooLarge $ \(y, m, off) ->
case evaluateDayOfMonthT y m (DomLastOffset off) of
Left _ -> QC.property True
Right xs ->
let dim = gregorianMonthLength y m
in QC.counterexample
("unexpected success " <> show xs <> " for month length " <> show dim <> " and offset " <> show off)
False
propNumericExpressionsWithinBounds :: QC.Property
propNumericExpressionsWithinBounds =
QC.forAll genYearMonth $ \(y, m) ->
QC.forAll genNumericExpr $ \expr ->
case evaluateDayOfMonthT y m expr of
Left err -> QC.counterexample ("expr=" <> show expr <> ": " <> err) False
Right xs ->
let dim = gregorianMonthLength y m
message = "expr=" <> show expr <> " xs=" <> show xs <> " dim=" <> show dim
in QC.counterexample message (withinBoundsInt 1 dim xs && strictlyAscending xs)
propDomAnyEmpty :: QC.Property
propDomAnyEmpty =
QC.forAll genYearMonth $ \(y, m) ->
case evaluateDayOfMonthT y m DomAny of
Left err -> QC.counterexample err False
Right xs -> QC.counterexample ("xs=" <> show xs) (null xs)
genYearMonth :: QC.Gen (Integer, Int)
genYearMonth = do
year <- QC.chooseInt (1900, 2100)
month <- QC.chooseInt (1, 12)
pure (fromIntegral year, month)
outOfRangeDay :: QC.Gen Int
outOfRangeDay = QC.oneof [QC.chooseInt (-5, 0), QC.chooseInt (32, 40)]
genDomLastOffsetWithin :: QC.Gen (Integer, Int, Int)
genDomLastOffsetWithin = do
(y, m) <- genYearMonth
let dim = gregorianMonthLength y m
off <- QC.chooseInt (1, dim - 1)
pure (y, m, off)
genDomLastOffsetTooLarge :: QC.Gen (Integer, Int, Int)
genDomLastOffsetTooLarge = do
(y, m) <- genYearMonth
let dim = gregorianMonthLength y m
off <- QC.chooseInt (dim, dim + 10)
pure (y, m, off)
genNumericExpr :: QC.Gen DayOfMonthExprT
genNumericExpr = QC.sized go
where
go n
| n <= 1 = QC.oneof base
| otherwise = QC.frequency
[ (3, QC.oneof base)
, (2, genRange)
, (2, genStep)
, (2, genUnion (n - 1))
]
base =
[ pure DomAll
, DomAt <$> QC.chooseInt (1, 31)
]
genRange = do
start <- QC.chooseInt (1, 31)
end <- QC.chooseInt (start, 31)
pure (DomRange start end)
genStep = do
start <- QC.chooseInt (1, 31)
step <- QC.chooseInt (1, 10)
pure (DomStep start step)
genUnion depth = do
let sub = max 0 depth
left <- go sub
right <- go sub
pure (DomUnion left right)
failureMsg :: Integer -> Int -> Int -> Int -> Int -> String
failureMsg y m d w dow =
"y=" <> show y <> " m=" <> show m <> " requested=" <> show d <>
" resolved=" <> show w <> " weekday=" <> show dow