aws-eventbridge-cron-0.1.0.0: test/AWS/EventBridge/MonthsSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module AWS.EventBridge.MonthsSpec (tests) where
import AWS.EventBridge.Months
import Data.List (nub, sort)
import qualified Data.Set as Set
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
, expectEvalEquals
, expectEvalWith
, expectParseWith
, strictlyAscending
, withinBoundsInt
)
tests :: TestTree
tests =
testGroup "months"
[ manualTests
, propertyTests
]
manualTests :: TestTree
manualTests = testGroup "manual"
[ testCase "parse *" $ parseMonthsText "*" @?= Right AllMonths
, testCase "parse numeric literal" $ parseMonthsText "3" @?= Right (AtMonth 3)
, testCase "parse month name" $ parseMonthsText "Feb" @?= Right (AtMonth 2)
, testCase "parse range" $ parseMonthsText "3-5" @?= Right (RangeMonth 3 5)
, testCase "parse step" $ parseMonthsText "6/2" @?= Right (StepMonth 6 2)
, testCase "parse mixed union" $ do
expr <- expectParseWith "months" parseMonthsText "Jan,4,Aug"
result <- expectEvalWith "months" evaluateMonthT expr
result @?= [1,4,8]
, testCase "parse union with duplicates collapses" $ do
expr <- expectParseWith "months" parseMonthsText "Jan,jan,1"
result <- expectEvalWith "months" evaluateMonthT expr
result @?= [1]
, testCase "evaluate AllMonths" $ expectEvalEquals [1..12] (evaluateMonthT AllMonths)
, testCase "invalid month literal fails" $ assertLeft (evaluateMonthT (AtMonth 20))
, testCase "invalid month name fails" $
case parseMonthsText "Foo" of
Left _ -> pure ()
Right val -> fail ("unexpected success: " <> show val)
, testCase "descending range rejected" $
assertLeft (evaluateMonthT (RangeMonth 5 3))
, testCase "step start out of range fails" $
assertLeft (evaluateMonthT (StepMonth 0 2))
, testCase "step increment zero fails" $
assertLeft (evaluateMonthT (StepMonth 5 0))
]
propertyTests :: TestTree
propertyTests = testGroup "properties"
[ QC.testProperty "AllMonths covers 1..12" propAllMonthsRange
, QC.testProperty "AtMonth returns singleton" propAtMonthSingleton
, QC.testProperty "Generated expressions evaluate within bounds" propExpressionsWithinBounds
, QC.testProperty "Parse of comma list matches sorted unique" propParseCommaSeparated
, QC.testProperty "Named months parse to expected indices" propNamedMonthsParse
, QC.testProperty "Invalid literal month rejected" propInvalidMonthRejected
, QC.testProperty "Range month evaluates inclusively" propRangeMonthInclusive
, QC.testProperty "Descending range rejected" propRangeMonthRejectsDescending
, QC.testProperty "Step month forms arithmetic progression" propStepMonthProgression
, QC.testProperty "Union month combines operands" propUnionMonthCombines
]
propAllMonthsRange :: QC.Property
propAllMonthsRange =
case evaluateMonthT AllMonths of
Left err -> QC.counterexample err False
Right xs -> xs QC.=== [1..12]
propAtMonthSingleton :: QC.Property
propAtMonthSingleton =
QC.forAll (QC.chooseInt (1, 12)) $ \m ->
case evaluateMonthT (AtMonth m) of
Left err -> QC.counterexample err False
Right xs -> xs QC.=== [m]
propExpressionsWithinBounds :: QC.Property
propExpressionsWithinBounds =
QC.forAll genMonthExpr $ \expr ->
case evaluateMonthT expr of
Left err -> QC.counterexample ("expr=" <> show expr <> ": " <> err) False
Right xs ->
let message = "expr=" <> show expr <> " xs=" <> show xs
in QC.counterexample message (withinBoundsInt 1 12 xs && strictlyAscending xs)
propParseCommaSeparated :: QC.Property
propParseCommaSeparated =
QC.forAll (QC.listOf1 genMonthTokenText) $ \monthsTxt ->
let txt = T.intercalate "," monthsTxt
expected = sort (nub (map parseTokenUnsafe monthsTxt))
in case parseMonthsText txt of
Left err -> QC.counterexample err False
Right expr ->
case evaluateMonthT expr of
Left evalErr -> QC.counterexample evalErr False
Right xs -> xs QC.=== expected
propNamedMonthsParse :: QC.Property
propNamedMonthsParse =
QC.forAll genNamedMonthText $ \nameTxt ->
case parseMonthsText nameTxt of
Left err -> QC.counterexample err False
Right expr ->
case expr of
AtMonth m ->
let expected = parseTokenUnsafe nameTxt
in QC.counterexample ("expected " <> show expected <> ", got " <> show m) (m == expected)
other -> QC.counterexample ("expected AtMonth, got " <> show other) False
propInvalidMonthRejected :: QC.Property
propInvalidMonthRejected =
QC.forAll invalidMonth $ \m ->
case evaluateMonthT (AtMonth m) of
Left _ -> QC.property True
Right xs -> QC.counterexample ("unexpected success: " <> show xs) False
propRangeMonthInclusive :: QC.Property
propRangeMonthInclusive =
QC.forAll (QC.chooseInt (1, 12)) $ \start ->
QC.forAll (QC.chooseInt (start, 12)) $ \end ->
case evaluateMonthT (RangeMonth start end) of
Left err -> QC.counterexample err False
Right xs -> xs QC.=== [start..end]
propRangeMonthRejectsDescending :: QC.Property
propRangeMonthRejectsDescending =
QC.forAll (QC.chooseInt (2, 12)) $ \start ->
QC.forAll (QC.chooseInt (1, start - 1)) $ \end ->
case evaluateMonthT (RangeMonth start end) of
Left _ -> QC.property True
Right xs -> QC.counterexample ("unexpected success: " <> show xs) False
propStepMonthProgression :: QC.Property
propStepMonthProgression =
QC.forAll (QC.chooseInt (1, 12)) $ \start ->
QC.forAll (QC.chooseInt (1, 6)) $ \step ->
case evaluateMonthT (StepMonth start step) of
Left err -> QC.counterexample err False
Right xs ->
QC.counterexample ("step output=" <> show xs)
(withinBoundsInt 1 12 xs && isStepProgression start step xs)
propUnionMonthCombines :: QC.Property
propUnionMonthCombines =
QC.forAll genMonthExpr $ \lhs ->
QC.forAll genMonthExpr $ \rhs ->
case ( evaluateMonthT lhs
, evaluateMonthT rhs
, evaluateMonthT (UnionMonth lhs rhs)
) of
(Right xs, Right ys, Right zs) ->
let expected = Set.toAscList (Set.fromList (xs ++ ys))
msg = "lhs=" <> show lhs <> " rhs=" <> show rhs <> " expected=" <> show expected <> " actual=" <> show zs
in QC.counterexample msg (zs == expected)
(Left err, _, _) -> QC.counterexample ("lhs failed: " <> err) False
(_, Left err, _) -> QC.counterexample ("rhs failed: " <> err) False
(_, _, Left err) -> QC.counterexample ("union failed: " <> err) False
-- Generators
genMonthExpr :: QC.Gen MonthsExprT
genMonthExpr = 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 AllMonths
, AtMonth <$> QC.chooseInt (1, 12)
]
genRange = do
start <- QC.chooseInt (1, 12)
end <- QC.chooseInt (start, 12)
pure (RangeMonth start end)
genStep = do
start <- QC.chooseInt (1, 12)
step <- QC.chooseInt (1, 6)
pure (StepMonth start step)
genUnion depth = do
let sub = max 0 depth
left <- go sub
right <- go sub
pure (UnionMonth left right)
genMonthTokenText :: QC.Gen T.Text
genMonthTokenText = QC.oneof
[ T.pack . show <$> QC.chooseInt (1, 12)
, QC.elements (map T.pack ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"])
]
genNamedMonthText :: QC.Gen T.Text
genNamedMonthText =
QC.elements (map T.pack ["JAN","feb","Mar","apr","may","JUN","jul","AuG","sep","OCT","nov","dec"])
invalidMonth :: QC.Gen Int
invalidMonth = QC.oneof [QC.chooseInt (-12, 0), QC.chooseInt (13, 36)]
-- Helpers
parseTokenUnsafe :: T.Text -> Int
parseTokenUnsafe t =
case parseAt (T.strip t) of
Right m -> m
Left err -> error ("unexpected parse failure: " <> err)
where
parseAt txt = do
expr <- parseMonthsText txt
case expr of
AtMonth m -> Right m
_ -> Left ("expected AtMonth for token " <> show txt)
isStepProgression :: Int -> Int -> [Int] -> Bool
isStepProgression _ _ [] = True
isStepProgression start step (x0 : rest) =
x0 == start && all (== step) (consecutiveDiffs x0 rest)
where
consecutiveDiffs _ [] = []
consecutiveDiffs prev (y : ys) = (y - prev) : consecutiveDiffs y ys