phino-0.0.114: 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)
import Text.Read (readMaybe)
spec :: Spec
spec = do
describe "Show instance" $
forM_
[ ("displays a disabled must as zero", MtDisabled, "0")
, ("displays a positive exact value", MtExact 42, "42")
, ("displays a large exact value", MtExact 999999, "999999")
, ("displays an exact value of one", MtExact 1, "1")
, ("displays a range with both bounds", MtRange (Just 1) (Just 5), "1..5")
, ("displays a range with equal bounds", MtRange (Just 3) (Just 3), "3..3")
, ("displays a large range", MtRange (Just 0) (Just 1000), "0..1000")
, ("displays a range with only a minimum", MtRange (Just 5) Nothing, "5..")
, ("displays a range with only a zero minimum", MtRange (Just 0) Nothing, "0..")
, ("displays a range with only a maximum", MtRange Nothing (Just 10), "..10")
, ("displays a range with only a zero maximum", MtRange Nothing (Just 0), "..0")
, ("displays a range with no bounds", MtRange Nothing Nothing, "..")
]
(\(desc, must, expected) -> it desc (show must `shouldBe` expected))
describe "Read instance" $
forM_
[ ("parses zero as MtDisabled", "0", Just MtDisabled)
, ("parses a single-digit exact value", "5", Just (MtExact 5))
, ("parses a multi-digit exact value", "123", Just (MtExact 123))
, ("parses a large exact value", "999999", Just (MtExact 999999))
, ("rejects a negative exact value", "-1", Nothing)
, ("rejects a large negative exact value", "-999", Nothing)
, ("rejects alphabetic input", "abc", Nothing)
, ("rejects mixed alphanumeric input", "12abc", Nothing)
, ("rejects empty input", "", Nothing)
, ("rejects unicode input", "日本語", Nothing)
, ("parses a simple range", "1..5", Just (MtRange (Just 1) (Just 5)))
, ("parses a range with equal bounds", "3..3", Just (MtRange (Just 3) (Just 3)))
, ("parses a range with a zero start", "0..10", Just (MtRange (Just 0) (Just 10)))
, ("parses a minimum-only range", "5..", Just (MtRange (Just 5) Nothing))
, ("parses a minimum-only range with a zero minimum", "0..", Just (MtRange (Just 0) Nothing))
, ("parses a maximum-only range", "..10", Just (MtRange Nothing (Just 10)))
, ("parses a maximum-only range with a zero maximum", "..0", Just (MtRange Nothing (Just 0)))
, ("round-trips an empty range with dots only", "..", Just (MtRange Nothing Nothing))
, ("rejects a range with a negative minimum", "-1..5", Nothing)
, ("rejects a range with a negative maximum", "1..-5", Nothing)
, ("rejects an inverted range where min exceeds max", "10..5", Nothing)
, ("rejects a range with an alphabetic minimum", "abc..5", Nothing)
, ("rejects a range with an alphabetic maximum", "5..abc", Nothing)
, ("rejects a range with both parts alphabetic", "abc..xyz", Nothing)
, ("rejects a range with more than one '..' separator", "3.4..5", Nothing)
, ("rejects a maximum-only range with a negative bound", "..-5", Nothing)
, ("rejects a minimum-only range with a negative bound", "-5..", Nothing)
]
(\(desc, input, expected) -> it desc ((readMaybe input :: Maybe Must) `shouldBe` expected))
describe "Eq instance" $
forM_
[ ("MtDisabled equals itself", MtDisabled, MtDisabled, True)
, ("equal MtExact values are equal", MtExact 5, MtExact 5, True)
, ("different MtExact values are not equal", MtExact 5, MtExact 10, False)
, ("equal MtRange values are equal", MtRange (Just 1) (Just 5), MtRange (Just 1) (Just 5), True)
, ("MtRange values with different minimums are not equal", MtRange (Just 1) (Just 5), MtRange (Just 2) (Just 5), False)
, ("MtRange values with different maximums are not equal", MtRange (Just 1) (Just 5), MtRange (Just 1) (Just 6), False)
, ("MtDisabled is not equal to MtExact", MtDisabled, MtExact 0, False)
, ("MtExact is not equal to MtRange", MtExact 5, MtRange (Just 5) (Just 5), False)
]
(\(desc, lhs, rhs, expected) -> it desc ((lhs == rhs) `shouldBe` expected))
describe "inRange" $
forM_
[ ("MtDisabled accepts zero", MtDisabled, 0, True)
, ("MtDisabled accepts a large positive value", MtDisabled, 999999, True)
, ("MtDisabled accepts a negative value", MtDisabled, -42, True)
, ("MtExact accepts the exact value", MtExact 5, 5, True)
, ("MtExact rejects a value below it", MtExact 5, 4, False)
, ("MtExact rejects a value above it", MtExact 5, 6, False)
, ("MtRange accepts a value within bounds", MtRange (Just 1) (Just 10), 5, True)
, ("MtRange accepts the minimum bound", MtRange (Just 1) (Just 10), 1, True)
, ("MtRange accepts the maximum bound", MtRange (Just 1) (Just 10), 10, True)
, ("MtRange rejects a value below the minimum", MtRange (Just 5) (Just 10), 4, False)
, ("MtRange rejects a value above the maximum", MtRange (Just 1) (Just 5), 6, False)
, ("a minimum-only range accepts the minimum", MtRange (Just 5) Nothing, 5, True)
, ("a minimum-only range accepts a value above the minimum", MtRange (Just 5) Nothing, 100, True)
, ("a minimum-only range rejects a value below the minimum", MtRange (Just 5) Nothing, 4, False)
, ("a maximum-only range accepts the maximum", MtRange Nothing (Just 10), 10, True)
, ("a maximum-only range accepts a value below the maximum", MtRange Nothing (Just 10), 0, True)
, ("a maximum-only range rejects a value above the maximum", MtRange Nothing (Just 10), 11, False)
, ("an unbounded range accepts zero", MtRange Nothing Nothing, 0, True)
, ("an unbounded range accepts a large positive value", MtRange Nothing Nothing, 999999, True)
, ("an unbounded range accepts a negative value", MtRange Nothing Nothing, -42, True)
]
(\(desc, must, value, expected) -> it desc (inRange must value `shouldBe` expected))
describe "exceedsUpperBound" $
forM_
[ ("MtDisabled never exceeds for zero", MtDisabled, 0, False)
, ("MtDisabled never exceeds for a large positive value", MtDisabled, 999999, False)
, ("MtExact at the bound does not exceed", MtExact 5, 5, False)
, ("MtExact below the bound does not exceed", MtExact 5, 4, False)
, ("MtExact above the bound exceeds", MtExact 5, 6, True)
, ("MtRange at the maximum does not exceed", MtRange (Just 0) (Just 10), 10, False)
, ("MtRange below the maximum does not exceed", MtRange (Just 0) (Just 10), 5, False)
, ("MtRange above the maximum exceeds", MtRange (Just 0) (Just 10), 11, True)
, ("a maximum-less MtRange never exceeds for zero", MtRange (Just 0) Nothing, 0, False)
, ("a maximum-less MtRange never exceeds for a large positive value", MtRange (Just 0) Nothing, 999999, False)
]
(\(desc, must, value, expected) -> it desc (exceedsUpperBound must value `shouldBe` expected))
describe "validateMust" $
forM_
[ ("MtDisabled is always valid", MtDisabled, Nothing)
, ("a positive MtExact is valid", MtExact 5, Nothing)
, ("a range with both bounds is valid", MtRange (Just 1) (Just 10), Nothing)
, ("a minimum-only range is valid", MtRange (Just 5) Nothing, Nothing)
, ("a maximum-only range is valid", MtRange Nothing (Just 10), Nothing)
, ("an unbounded range is valid", MtRange Nothing Nothing, Nothing)
, ("an inverted range is rejected", MtRange (Just 10) (Just 5), Just "--must range invalid: minimum (10) is greater than maximum (5)")
, ("a zero MtExact is rejected", MtExact 0, Just "--must exact value must be positive")
, ("a negative MtExact is rejected", MtExact (-3), Just "--must exact value must be positive")
, ("a negative minimum is rejected", MtRange (Just (-1)) (Just 5), Just "--must minimum must be non-negative")
, ("a negative maximum is rejected", MtRange (Just 0) (Just (-1)), Just "--must maximum must be non-negative")
]
(\(desc, must, expected) -> it desc (validateMust must `shouldBe` expected))