git.iko.soy/typeable/libphonenumber
on main help sign in
Add hspec tests
commitd4dec190fc3772af49cd65df7e416c0968807ebd authormniip <mniip@mniip.com> dateTue 21 Mar 2023, 10:27 (4 years ago) parent831cd18
3 files changed+259−6
libphonenumber.cabal+15 test/Leak.hs+6−6 test/Spec.hs+238
libphonenumber.cabal+15
@@ -73,6 +73,20 @@ library
7373 default-extensions: NoFieldSelectors
7474 ghc-options: -Wall -Wextra -Werror
7575
76+test-suite spec_test
77+ type: exitcode-stdio-1.0
78+ build-depends:
79+ base
80+ , bytestring
81+ , containers
82+ , hspec
83+ , libphonenumber
84+ , QuickCheck
85+ main-is: Spec.hs
86+ hs-source-dirs: test
87+ default-language: Haskell2010
88+ ghc-options: -Wall -Wextra
89+
7690test-suite leak_test
7791 type: exitcode-stdio-1.0
7892 build-depends:
@@ -82,3 +96,4 @@ test-suite leak_test
8296 main-is: Leak.hs
8397 hs-source-dirs: test
8498 default-language: Haskell2010
99+ ghc-options: -Wall -Wextra
test/Leak.hs+6−6
@@ -74,7 +74,7 @@ main = do
7474 carrierCode = "10"
7575 nonGeoCode = 800
7676 phoneString = "+41 44 668 1800"
77− Right phoneNumber = parseNumber KeepRawInput noRegion phoneString
77+ Right phoneNumber = parseNumber KeepRawInput Nothing phoneString
7878 hammer iter (\(PhoneNumber a b c d e f g h) -> PhoneNumber a b c d e f g h) phoneNumber
7979 hammer iter supportedTypesForRegion region
8080 hammer iter supportedTypesForNonGeoEntity nonGeoCode
@@ -96,7 +96,7 @@ main = do
9696 hammer iter (formatNumber (OutOfCountry region False)) phoneNumber
9797 hammer iter truncateTooLongNumber phoneNumber
9898 hammer iter numberType phoneNumber
99− hammer iter (isValidNumber (Just region)) phoneNumber
99+ hammer iter (isValidNumber (Just (Right region))) phoneNumber
100100 hammer iter regionForNumber phoneNumber
101101 hammer iter countryCodeForRegion region
102102 hammer iter regionForCountryCode countryCallingCode
@@ -107,10 +107,10 @@ main = do
107107 hammer iter canBeInternationallyDialed phoneNumber
108108 hammer iter isGeographicalNumber phoneNumber
109109 hammer iter (isGeographicalNumberType FixedLine) countryCallingCode
110− hammer iter (parseNumber Canonicalize region) phoneString
111− hammer iter (parseNumber Canonicalize region) ""
112− hammer iter (parseNumber KeepRawInput region) phoneString
113− hammer iter (parseNumber KeepRawInput region) ""
110+ hammer iter (parseNumber Canonicalize (Just region)) phoneString
111+ hammer iter (parseNumber Canonicalize (Just region)) ""
112+ hammer iter (parseNumber KeepRawInput (Just region)) phoneString
113+ hammer iter (parseNumber KeepRawInput (Just region)) ""
114114 hammer iter (matchNumbers (Left phoneString)) (Left phoneString)
115115 hammer iter (matchNumbers (Left phoneString)) (Right phoneNumber)
116116 hammer iter (matchNumbers (Right phoneNumber)) (Left phoneString)
test/Spec.hsnew file+238
@@ -0,0 +1,238 @@
1+{-# LANGUAGE DerivingStrategies #-}
2+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+import Control.Applicative
5+import Control.Monad
6+import qualified Data.ByteString as BS
7+import qualified Data.ByteString.Char8 as BSC
8+import Data.Either
9+import Data.PhoneNumber.Number
10+import Data.PhoneNumber.Util
11+import qualified Data.Set as S
12+import Test.Hspec
13+import Test.Hspec.QuickCheck
14+import Test.QuickCheck
15+
16+newtype CC = CC CountryCode
17+ deriving newtype (Show)
18+
19+instance Arbitrary CC where
20+ arbitrary = CC . CountryCode <$> choose (-10, 1000)
21+ shrink (CC (CountryCode cc)) = CC . CountryCode <$> shrink cc
22+
23+newtype Reg = Reg Region
24+ deriving newtype (Show)
25+
26+instance Arbitrary Reg where
27+ arbitrary = do
28+ c1 <- elements ['A'..'Z']
29+ c2 <- elements ['A'..'Z']
30+ pure $ Reg $ Region $ BSC.pack [c1, c2]
31+
32+newtype MReg = MReg (Maybe Region)
33+ deriving newtype (Show)
34+
35+instance Arbitrary MReg where
36+ arbitrary = MReg . fmap (\(Reg r) -> r) <$> arbitrary
37+ shrink (MReg (Just _)) = [MReg Nothing]
38+ shrink (MReg Nothing) = []
39+
40+newtype ParserInput = ParserInput BS.ByteString
41+ deriving newtype (Show)
42+
43+instance Arbitrary ParserInput where
44+ arbitrary = do
45+ len <- choose (0, 20)
46+ xs <- replicateM len $ frequency
47+ [ (10, elements "#()+-0123456789")
48+ , (1, elements "'*,./:;") ]
49+ pure $ ParserInput $ BSC.pack xs
50+ shrink (ParserInput bs) = ParserInput . BS.pack <$> shrink (BS.unpack bs)
51+
52+data ParsedNumber = ParsedNumber (ParseMode, Maybe Region, ParserInput) PhoneNumber
53+ deriving stock (Show)
54+
55+instance Arbitrary ParsedNumber where
56+ arbitrary = liftA3 (,,) (elements [Canonicalize, KeepRawInput]) arbitrary arbitrary
57+ `suchThatMap` \(mode, MReg mRegion, ParserInput input) ->
58+ case parseNumber mode mRegion input of
59+ Right pn -> Just $ ParsedNumber (mode, mRegion, ParserInput input) pn
60+ _ -> Nothing
61+ shrink (ParsedNumber (mode, region, input) _) = do
62+ ParserInput input' <- shrink input
63+ case parseNumber mode region input' of
64+ Right pn -> pure $ ParsedNumber (mode, region, ParserInput input') pn
65+ _ -> empty
66+
67+regZZ, reg001 :: Region
68+regZZ = "ZZ"
69+reg001 = "001"
70+
71+cc0 :: CountryCode
72+cc0 = 0
73+
74+main :: IO ()
75+main = hspec $ modifyMaxSuccess (* 10) $ do
76+ describe "supportedRegions" $ do
77+ prop "doesn't contain ZZ" $ regZZ `S.notMember` supportedRegions
78+ prop "doesn't contain 001" $ reg001 `S.notMember` supportedRegions
79+
80+ describe "supportedGlobalNetworkCallingCodes" $ do
81+ prop "doesn't contain 0" $ cc0 `S.notMember` supportedGlobalNetworkCallingCodes
82+ prop "is a subset of supportedCallingCodes" $
83+ supportedGlobalNetworkCallingCodes `S.isSubsetOf` supportedCallingCodes
84+
85+ describe "supportedCallingCodes" $ do
86+ prop "doesn't contain 0" $ cc0 `S.notMember` supportedCallingCodes
87+
88+ describe "supportedTypesForRegion" $ do
89+ prop "doesn't contain FixedLineOrMobile" $ \(Reg region) ->
90+ FixedLineOrMobile `S.notMember` supportedTypesForRegion region
91+
92+ describe "supportedTypesForNonGeoEntity" $ do
93+ prop "doesn't contain FixedLineOrMobile" $ \(CC cc) ->
94+ FixedLineOrMobile `S.notMember` supportedTypesForNonGeoEntity cc
95+
96+ describe "convertAlphaNumber" $ do
97+ prop "doesn't change the parsing of a number" $ \(MReg mRegion) (ParserInput str) ->
98+ parseNumber Canonicalize mRegion (convertAlphaNumber str)
99+ === parseNumber Canonicalize mRegion str
100+ prop "returns a number that is not alpha" $ \(ParserInput str) ->
101+ not $ isAlphaNumber $ convertAlphaNumber str
102+
103+ describe "normalizeNumber" $ do
104+ prop "returns ASCII digits unmodified" $
105+ forAll (elements [Digits, Dialable]) $ \mode ->
106+ forAll (listOf $ elements ['0'..'9']) $ \str ->
107+ BSC.pack str == normalizeNumber mode (BSC.pack str)
108+ describe "Digits" $ do
109+ prop "returns only ASCII digits" $ \str ->
110+ BSC.all (`elem` ['0'..'9']) $ normalizeNumber Digits $ BSC.pack str
111+ prop "doesn't randomly fail" $ \str ->
112+ not (BS.null $ normalizeNumber Digits $ "0" <> BS.pack str)
113+ .&&. not (BS.null $ normalizeNumber Digits $ BS.pack str <> "0")
114+ describe "Dialable" $ do
115+ -- PhoneNumberUtil::NormalizeDiallableCharsOnly will error on any invalid UTF8
116+ xprop "doesn't randomly fail" $ \str ->
117+ not (BS.null $ normalizeNumber Digits $ "0" <> BS.pack str)
118+ .&&. not (BS.null $ normalizeNumber Digits $ BS.pack str <> "0")
119+
120+ describe "nationalSignificantNumber" $ do
121+ prop "is non-empty" $ \(ParsedNumber _ pn) ->
122+ not $ BS.null $ nationalSignificantNumber pn
123+ prop "returns only ASCII digits" $ \(ParsedNumber _ pn) ->
124+ BSC.all (`elem` ['0'..'9']) $ nationalSignificantNumber pn
125+ modifyMaxDiscardRatio (* 10) $ do
126+ prop "concatenated with country code parses to the original (if valid and not MX)" $
127+ \(ParsedNumber (_, _, ParserInput input) pn) ->
128+ -- Apparently "+52112210000000" makes the parser/formatter do weird
129+ -- stuff as it tries to handle the deprecated (?) mobile token ("1"
130+ -- after "+52")
131+ if "5211" `BS.isPrefixOf` normalizeNumber Digits input
132+ then discard
133+ else isValidNumber Nothing pn ==>
134+ let
135+ cc = countryCode pn
136+ nsn = nationalSignificantNumber pn
137+ str = "+" <> BSC.pack (show cc) <> nsn
138+ in case parseNumber Canonicalize Nothing str of
139+ Left err -> error $ show err
140+ Right pn' -> cc === countryCode pn'
141+ .&&. nsn === nationalSignificantNumber pn'
142+
143+ describe "countryMobileToken" $ do
144+ prop "returns only ASCII digits" $ \(CC cc) ->
145+ BSC.all (`elem` ['0'..'9']) $ countryMobileToken cc
146+
147+ describe "formatNumber" $ do
148+ prop "with International produces a parseable result" $
149+ \(ParsedNumber _ pn) -> possibleNumber Unknown pn == IsPossible ==>
150+ isRight $ parseNumber Canonicalize Nothing
151+ $ formatNumber International pn
152+ prop "with E164 produces a parseable result" $
153+ \(ParsedNumber _ pn) -> possibleNumber Unknown pn == IsPossible ==>
154+ isRight $ parseNumber Canonicalize Nothing
155+ $ formatNumber E164 pn
156+ prop "with RFC3966 produces a parseable result" $
157+ \(ParsedNumber _ pn) -> possibleNumber Unknown pn == IsPossible ==>
158+ isRight $ parseNumber Canonicalize Nothing
159+ $ formatNumber RFC3966 pn
160+
161+ describe "truncateTooLongNumber" $ do
162+ pure ()
163+
164+ describe "numberType" $ do
165+ pure ()
166+
167+ describe "isValidNumber" $ do
168+ modifyMaxDiscardRatio (* 10) $ do
169+ prop "is preserved under parse . format" $ \(ParsedNumber _ pn) ->
170+ isValidNumber Nothing pn ==>
171+ case parseNumber Canonicalize Nothing $ formatNumber E164 pn of
172+ Left err -> error $ show err
173+ Right pn' -> isValidNumber Nothing pn'
174+
175+ describe "regionForNumber" $ do
176+ prop "doesn't return ZZ" $ \(ParsedNumber _ pn) ->
177+ regionForNumber pn =/= Just (Right regZZ)
178+ prop "doesn't return 001" $ \(ParsedNumber _ pn) ->
179+ regionForNumber pn =/= Just (Right reg001)
180+ prop "returns a supported region" $ \(ParsedNumber _ pn) ->
181+ case regionForNumber pn of
182+ Just (Right region) -> region `S.member` supportedRegions
183+ _ -> discard
184+ prop "returns Region001 exactly for supported non-geo entities" $
185+ \(ParsedNumber _ pn) ->
186+ (countryCode pn `S.member` supportedGlobalNetworkCallingCodes)
187+ === (regionForNumber pn == Just (Left Region001))
188+
189+ describe "countryCodeForRegion" $ do
190+ prop "doesn't return 0" $ \(Reg region) ->
191+ countryCodeForRegion region =/= Just cc0
192+ prop "returns codes in supportedCallingCodes" $ \(Reg region) ->
193+ case countryCodeForRegion region of
194+ Nothing -> discard
195+ Just cc -> cc `S.member` supportedCallingCodes
196+
197+ describe "regionForCountryCode" $ do
198+ prop "doesn't return ZZ" $ \(CC cc) ->
199+ regionForCountryCode cc =/= Just (Right regZZ)
200+ prop "doesn't return 001" $ \(CC cc) ->
201+ regionForCountryCode cc =/= Just (Right reg001)
202+ prop "returns Region001 exactly for supported non-geo entities" $ \(CC cc) ->
203+ (cc `S.member` supportedGlobalNetworkCallingCodes)
204+ === (regionForCountryCode cc == Just (Left Region001))
205+
206+ describe "regionsForCountryCode" $ do
207+ prop "doesn't return ZZ" $ \(CC cc) ->
208+ Right regZZ `notElem` regionsForCountryCode cc
209+ prop "doesn't return 001" $ \(CC cc) ->
210+ Right reg001 `notElem` regionsForCountryCode cc
211+
212+ describe "isNANPACountry" $ do
213+ pure ()
214+
215+ describe "nddPrefixForRegion" $ do
216+ prop "with True returns only ASCII digits" $ \(Reg region) ->
217+ BSC.all (`elem` ['0'..'9']) $ nddPrefixForRegion True region
218+
219+ describe "possibleNumber" $ do
220+ modifyMaxDiscardRatio (* 10) $ do
221+ prop "with Unknown is more lenient than isValidNumber" $
222+ \(ParsedNumber _ pn) ->
223+ isValidNumber Nothing pn ==> possibleNumber Unknown pn === IsPossible
224+
225+ describe "canBeInternationallyDialed" $ do
226+ pure ()
227+
228+ describe "isGeographicalNumber" $ do
229+ prop "matches isGeographicalNumberType" $ \(ParsedNumber _ pn) ->
230+ isGeographicalNumber pn
231+ === isGeographicalNumberType (numberType pn) (countryCode pn)
232+
233+ describe "isGeographicalNumberType" $ do
234+ pure ()
235+
236+ describe "parseNumber" $ do
237+ prop "doesn't return a number with CountryCode 0" $ \(ParsedNumber _ pn) ->
238+ countryCode pn /= cc0