phino-0.0.0.53: test/MustSpec.hs
-- SPDX-FileCopyrightText: Copyright (c) 2025 Objectionary.com
-- SPDX-License-Identifier: MIT
{- | Tests for the Must module that provides constraint specification
for rewriting rules with exact counts and ranges.
-}
module MustSpec where
import Control.Monad (forM_)
import Must (Must (..), exceedsUpperBound, inRange, validateMust)
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)
import Text.Read (readMaybe)
spec :: Spec
spec = do
describe "Show instance renders MtDisabled" $
it "displays zero" $
show MtDisabled `shouldBe` "0"
describe "Show instance renders MtExact" $
forM_
[ ("positive integer", MtExact 42, "42")
, ("large integer", MtExact 999999, "999999")
, ("one", MtExact 1, "1")
]
( \(desc, must, expected) ->
it desc $ show must `shouldBe` expected
)
describe "Show instance renders MtRange with both bounds" $
forM_
[ ("small range", MtRange (Just 1) (Just 5), "1..5")
, ("same bounds", MtRange (Just 3) (Just 3), "3..3")
, ("large range", MtRange (Just 0) (Just 1000), "0..1000")
]
( \(desc, must, expected) ->
it desc $ show must `shouldBe` expected
)
describe "Show instance renders MtRange with only minimum" $
forM_
[ ("minimum only", MtRange (Just 5) Nothing, "5..")
, ("zero minimum", MtRange (Just 0) Nothing, "0..")
]
( \(desc, must, expected) ->
it desc $ show must `shouldBe` expected
)
describe "Show instance renders MtRange with only maximum" $
forM_
[ ("maximum only", MtRange Nothing (Just 10), "..10")
, ("zero maximum", MtRange Nothing (Just 0), "..0")
]
( \(desc, must, expected) ->
it desc $ show must `shouldBe` expected
)
describe "Show instance renders MtRange with no bounds" $
it "displays empty range" $
show (MtRange Nothing Nothing) `shouldBe` ".."
describe "Read instance parses zero as MtDisabled" $
it "parses disabled" $
(readMaybe "0" :: Maybe Must) `shouldBe` Just MtDisabled
describe "Read instance parses positive integers as MtExact" $
forM_
[ ("single digit", "5", Just (MtExact 5))
, ("multi digit", "123", Just (MtExact 123))
, ("large number", "999999", Just (MtExact 999999))
]
( \(desc, input, expected) ->
it desc $ (readMaybe input :: Maybe Must) `shouldBe` expected
)
describe "Read instance rejects negative integers" $
forM_
[ ("negative one", "-1")
, ("negative large", "-999")
]
( \(desc, input) ->
it desc $ (readMaybe input :: Maybe Must) `shouldBe` Nothing
)
describe "Read instance rejects non-numeric input" $
forM_
[ ("alphabetic", "abc")
, ("mixed", "12abc")
, ("empty", "")
, ("unicode", "日本語")
]
( \(desc, input) ->
it desc $ (readMaybe input :: Maybe Must) `shouldBe` Nothing
)
describe "Read instance parses full range" $
forM_
[ ("simple range", "1..5", Just (MtRange (Just 1) (Just 5)))
, ("same bounds", "3..3", Just (MtRange (Just 3) (Just 3)))
, ("zero start", "0..10", Just (MtRange (Just 0) (Just 10)))
]
( \(desc, input, expected) ->
it desc $ (readMaybe input :: Maybe Must) `shouldBe` expected
)
describe "Read instance parses minimum-only range" $
forM_
[ ("with minimum", "5..", Just (MtRange (Just 5) Nothing))
, ("zero minimum", "0..", Just (MtRange (Just 0) Nothing))
]
( \(desc, input, expected) ->
it desc $ (readMaybe input :: Maybe Must) `shouldBe` expected
)
describe "Read instance parses maximum-only range" $
forM_
[ ("with maximum", "..10", Just (MtRange Nothing (Just 10)))
, ("zero maximum", "..0", Just (MtRange Nothing (Just 0)))
]
( \(desc, input, expected) ->
it desc $ (readMaybe input :: Maybe Must) `shouldBe` expected
)
describe "Read instance rejects empty range" $
it "fails on dots only" $
(readMaybe ".." :: Maybe Must) `shouldBe` Nothing
describe "Read instance rejects invalid range with negative minimum" $
it "fails on negative min" $
(readMaybe "-1..5" :: Maybe Must) `shouldBe` Nothing
describe "Read instance rejects invalid range with negative maximum" $
it "fails on negative max" $
(readMaybe "1..-5" :: Maybe Must) `shouldBe` Nothing
describe "Read instance rejects range where min exceeds max" $
it "fails on inverted range" $
(readMaybe "10..5" :: Maybe Must) `shouldBe` Nothing
describe "Read instance rejects non-numeric range parts" $
forM_
[ ("alphabetic min", "abc..5")
, ("alphabetic max", "5..abc")
, ("both alphabetic", "abc..xyz")
]
( \(desc, input) ->
it desc $ (readMaybe input :: Maybe Must) `shouldBe` Nothing
)
describe "Eq instance compares MtDisabled" $
it "equals itself" $
MtDisabled == MtDisabled `shouldBe` True
describe "Eq instance compares MtExact" $
forM_
[ ("same values equal", MtExact 5, MtExact 5, True)
, ("different values not equal", MtExact 5, MtExact 10, False)
]
( \(desc, lhs, rhs, expected) ->
it desc $ (lhs == rhs) `shouldBe` expected
)
describe "Eq instance compares MtRange" $
forM_
[ ("same ranges equal", MtRange (Just 1) (Just 5), MtRange (Just 1) (Just 5), True)
, ("different min not equal", MtRange (Just 1) (Just 5), MtRange (Just 2) (Just 5), False)
, ("different max not equal", MtRange (Just 1) (Just 5), MtRange (Just 1) (Just 6), False)
]
( \(desc, lhs, rhs, expected) ->
it desc $ (lhs == rhs) `shouldBe` expected
)
describe "Eq instance compares different types" $
forM_
[ ("disabled vs exact", MtDisabled, MtExact 0, False)
, ("exact vs range", MtExact 5, MtRange (Just 5) (Just 5), False)
]
( \(desc, lhs, rhs, expected) ->
it desc $ (lhs == rhs) `shouldBe` expected
)
describe "inRange with MtDisabled accepts any value" $
forM_
[ ("zero", 0)
, ("large positive", 999999)
, ("negative", -42)
]
( \(desc, val) ->
it desc $ inRange MtDisabled val `shouldBe` True
)
describe "inRange with MtExact checks equality" $
forM_
[ ("exact match", MtExact 5, 5, True)
, ("below exact", MtExact 5, 4, False)
, ("above exact", MtExact 5, 6, False)
]
( \(desc, must, val, expected) ->
it desc $ inRange must val `shouldBe` expected
)
describe "inRange with MtRange checks bounds" $
forM_
[ ("within range", MtRange (Just 1) (Just 10), 5, True)
, ("at minimum", MtRange (Just 1) (Just 10), 1, True)
, ("at maximum", MtRange (Just 1) (Just 10), 10, True)
, ("below minimum", MtRange (Just 5) (Just 10), 4, False)
, ("above maximum", MtRange (Just 1) (Just 5), 6, False)
]
( \(desc, must, val, expected) ->
it desc $ inRange must val `shouldBe` expected
)
describe "inRange with minimum-only range" $
forM_
[ ("at minimum", MtRange (Just 5) Nothing, 5, True)
, ("above minimum", MtRange (Just 5) Nothing, 100, True)
, ("below minimum", MtRange (Just 5) Nothing, 4, False)
]
( \(desc, must, val, expected) ->
it desc $ inRange must val `shouldBe` expected
)
describe "inRange with maximum-only range" $
forM_
[ ("at maximum", MtRange Nothing (Just 10), 10, True)
, ("below maximum", MtRange Nothing (Just 10), 0, True)
, ("above maximum", MtRange Nothing (Just 10), 11, False)
]
( \(desc, must, val, expected) ->
it desc $ inRange must val `shouldBe` expected
)
describe "inRange with unbounded range" $
forM_
[ ("zero", 0)
, ("large positive", 999999)
, ("negative", -42)
]
( \(desc, val) ->
it desc $ inRange (MtRange Nothing Nothing) val `shouldBe` True
)
describe "exceedsUpperBound with MtDisabled" $
forM_
[ ("zero", 0)
, ("large positive", 999999)
]
( \(desc, val) ->
it desc $ exceedsUpperBound MtDisabled val `shouldBe` False
)
describe "exceedsUpperBound with MtExact" $
forM_
[ ("at bound", MtExact 5, 5, False)
, ("below bound", MtExact 5, 4, False)
, ("above bound", MtExact 5, 6, True)
]
( \(desc, must, val, expected) ->
it desc $ exceedsUpperBound must val `shouldBe` expected
)
describe "exceedsUpperBound with MtRange with maximum" $
forM_
[ ("at maximum", MtRange (Just 0) (Just 10), 10, False)
, ("below maximum", MtRange (Just 0) (Just 10), 5, False)
, ("above maximum", MtRange (Just 0) (Just 10), 11, True)
]
( \(desc, must, val, expected) ->
it desc $ exceedsUpperBound must val `shouldBe` expected
)
describe "exceedsUpperBound with MtRange without maximum" $
forM_
[ ("zero", 0)
, ("large positive", 999999)
]
( \(desc, val) ->
it desc $ exceedsUpperBound (MtRange (Just 0) Nothing) val `shouldBe` False
)
describe "validateMust with MtDisabled" $
it "returns nothing" $
validateMust MtDisabled `shouldBe` Nothing
describe "validateMust with valid MtExact" $
it "returns nothing for positive" $
validateMust (MtExact 5) `shouldBe` Nothing
describe "validateMust with valid MtRange" $
forM_
[ ("both bounds", MtRange (Just 1) (Just 10))
, ("minimum only", MtRange (Just 5) Nothing)
, ("maximum only", MtRange Nothing (Just 10))
, ("no bounds", MtRange Nothing Nothing)
]
( \(desc, must) ->
it desc $ validateMust must `shouldBe` Nothing
)
describe "validateMust with inverted MtRange" $
it "returns error message" $
validateMust (MtRange (Just 10) (Just 5)) `shouldSatisfy` present
describe "validateMust with zero MtExact" $
it "returns error for zero" $
validateMust (MtExact 0) `shouldSatisfy` present
describe "validateMust with negative minimum in range" $
it "returns error for negative min" $
validateMust (MtRange (Just (-1)) (Just 5)) `shouldSatisfy` present
describe "validateMust with negative maximum in range" $
it "returns error for negative max" $
validateMust (MtRange (Just 0) (Just (-1))) `shouldSatisfy` present
where
present (Just _) = True
present Nothing = False