packages feed

range-1.0.0.0: Test/RangeParser.hs

module Test.RangeParser
   ( rangeParserTestCases
   ) where

import Test.Framework (Test, testGroup)
import Test.QuickCheck
import Test.Framework.Providers.QuickCheck2

import Data.Ranges
import Data.Range.Parser

-- ---------------------------------------------------------------------------
-- Helpers
-- ---------------------------------------------------------------------------

-- | Check that parsing @input@ produces a 'Ranges' equal to @mergeRanges expected@.
shouldParse :: String -> [Range Integer] -> Bool
shouldParse input expected = case parseRanges input of
   Right result -> result == mergeRanges expected
   Left _       -> False

shouldFail :: String -> Bool
shouldFail input = case (parseRanges input :: Either ParseError (Ranges Integer)) of
   Left _  -> True
   Right _ -> False

-- ---------------------------------------------------------------------------
-- Haddock example tests
-- ---------------------------------------------------------------------------

prop_haddock_example :: Bool
prop_haddock_example = shouldParse "-5,8-10,13-15,20-"
   [ UpperBoundRange (Bound 5 Inclusive)
   , SpanRange (Bound 8 Inclusive) (Bound 10 Inclusive)
   , SpanRange (Bound 13 Inclusive) (Bound 15 Inclusive)
   , LowerBoundRange (Bound 20 Inclusive)
   ]

test_haddock :: Test
test_haddock = testGroup "haddock examples"
   [ testProperty "documented example parses correctly" prop_haddock_example
   ]

-- ---------------------------------------------------------------------------
-- Singleton ranges
-- ---------------------------------------------------------------------------

prop_parse_singleton :: Positive Integer -> Bool
prop_parse_singleton (Positive n) = shouldParse (show n) [SingletonRange n]

prop_parse_singleton_zero :: Bool
prop_parse_singleton_zero = shouldParse "0" [SingletonRange 0]

test_singletons :: Test
test_singletons = testGroup "singleton ranges"
   [ testProperty "positive integer parses as singleton" prop_parse_singleton
   , testProperty "zero parses as singleton" prop_parse_singleton_zero
   ]

-- ---------------------------------------------------------------------------
-- Span ranges
-- ---------------------------------------------------------------------------

prop_parse_span :: (Positive Integer, Positive Integer) -> Bool
prop_parse_span (Positive a, Positive b) =
   shouldParse (show a ++ "-" ++ show b)
      [SpanRange (Bound a Inclusive) (Bound b Inclusive)]

test_spans :: Test
test_spans = testGroup "span ranges"
   [ testProperty "a-b parses as span" prop_parse_span
   ]

-- ---------------------------------------------------------------------------
-- Bound ranges
-- ---------------------------------------------------------------------------

prop_parse_lower_bound :: Positive Integer -> Bool
prop_parse_lower_bound (Positive n) =
   shouldParse (show n ++ "-") [LowerBoundRange (Bound n Inclusive)]

prop_parse_upper_bound :: Positive Integer -> Bool
prop_parse_upper_bound (Positive n) =
   shouldParse ("-" ++ show n) [UpperBoundRange (Bound n Inclusive)]

test_bounds :: Test
test_bounds = testGroup "bound ranges"
   [ testProperty "n- parses as lower bound" prop_parse_lower_bound
   , testProperty "-n parses as upper bound" prop_parse_upper_bound
   ]

-- ---------------------------------------------------------------------------
-- Wildcard / infinite range
-- ---------------------------------------------------------------------------

prop_parse_wildcard :: Bool
prop_parse_wildcard = shouldParse "*" [InfiniteRange]

-- InfiniteRange absorbs everything; the canonical result is just inf.
prop_parse_wildcard_in_union :: Bool
prop_parse_wildcard_in_union = shouldParse "*,5" [InfiniteRange, SingletonRange 5]

test_wildcard :: Test
test_wildcard = testGroup "wildcard / infinite range"
   [ testProperty "* parses as InfiniteRange" prop_parse_wildcard
   , testProperty "* in union parses correctly" prop_parse_wildcard_in_union
   ]

-- ---------------------------------------------------------------------------
-- Union (comma-separated)
-- ---------------------------------------------------------------------------

prop_parse_union :: Bool
prop_parse_union = shouldParse "1,2,3"
   [SingletonRange 1, SingletonRange 2, SingletonRange 3]

prop_parse_mixed_union :: Bool
prop_parse_mixed_union = shouldParse "5,10-20,30-"
   [ SingletonRange 5
   , SpanRange (Bound 10 Inclusive) (Bound 20 Inclusive)
   , LowerBoundRange (Bound 30 Inclusive)
   ]

test_union :: Test
test_union = testGroup "union (comma-separated)"
   [ testProperty "singletons separated by commas" prop_parse_union
   , testProperty "mixed types separated by commas" prop_parse_mixed_union
   ]

-- ---------------------------------------------------------------------------
-- Edge cases and invalid inputs
-- ---------------------------------------------------------------------------

prop_empty_string_parses :: Bool
prop_empty_string_parses = case (parseRanges "" :: Either ParseError (Ranges Integer)) of
   Right result -> result == mempty
   _            -> False

-- The parser uses sepBy which returns [] on no matches,
-- so non-range input like "abc" parses as Right mempty.
-- This is a known limitation of the current parser design.
prop_non_range_input_parses_empty :: Bool
prop_non_range_input_parses_empty =
   case (parseRanges "abc" :: Either ParseError (Ranges Integer)) of
      Right result -> result == mempty
      _            -> False

test_edge_cases :: Test
test_edge_cases = testGroup "edge cases"
   [ testProperty "empty string produces empty list" prop_empty_string_parses
   , testProperty "non-range input produces empty list" prop_non_range_input_parses_empty
   ]

-- ---------------------------------------------------------------------------
-- Invalid inputs (must fail)
--
-- The parser commits after consuming a union separator. If no valid range
-- follows the separator, it produces a Left rather than silently succeeding.
-- ---------------------------------------------------------------------------

-- "1," — trailing comma: separator consumed, then end-of-input reached
-- before the next range element.
prop_trailing_comma_fails :: Bool
prop_trailing_comma_fails = shouldFail "1,"

-- "1,2,3," — trailing comma after multiple valid ranges.
prop_trailing_comma_after_many_fails :: Bool
prop_trailing_comma_after_many_fails = shouldFail "1,2,3,"

-- "1,,2" — double comma: separator consumed, then another comma is found
-- where a range element is expected.
prop_double_comma_fails :: Bool
prop_double_comma_fails = shouldFail "1,,2"

-- "-" alone is the range separator with nothing on either side.
-- spanRange wraps in try so it backtracks; singletonRange needs digits.
-- The overall parser returns empty rather than failing (no input consumed).
-- This test documents that behaviour — it is NOT a failure case.
prop_bare_separator_parses_empty :: Bool
prop_bare_separator_parses_empty =
   case (parseRanges "-" :: Either ParseError (Ranges Integer)) of
      Right result -> result == mempty
      _            -> False

test_invalid :: Test
test_invalid = testGroup "invalid inputs"
   [ testProperty "trailing comma produces parse error"            prop_trailing_comma_fails
   , testProperty "trailing comma after many ranges fails"         prop_trailing_comma_after_many_fails
   , testProperty "double comma produces parse error"              prop_double_comma_fails
   , testProperty "bare separator parses as empty (not an error)"  prop_bare_separator_parses_empty
   ]

-- ---------------------------------------------------------------------------
-- Custom parser args
-- ---------------------------------------------------------------------------

prop_custom_separators :: Bool
prop_custom_separators =
   let args = defaultArgs { unionSeparator = ";", rangeSeparator = ".." }
   in case customParseRanges args "1..5;10" :: Either ParseError (Ranges Integer) of
      Right result -> result == mergeRanges
         [ SpanRange (Bound 1 Inclusive) (Bound 5 Inclusive)
         , SingletonRange 10
         ]
      Left _ -> False

test_custom :: Test
test_custom = testGroup "custom parser args"
   [ testProperty "custom separators work" prop_custom_separators
   ]

-- ---------------------------------------------------------------------------
-- Export
-- ---------------------------------------------------------------------------

rangeParserTestCases :: [Test]
rangeParserTestCases =
   [ test_haddock
   , test_singletons
   , test_spans
   , test_bounds
   , test_wildcard
   , test_union
   , test_edge_cases
   , test_invalid
   , test_custom
   ]