git.iko.soy/typeable/libphonenumber
on main help sign in
Add hspec tests
d4dec19mniip
libphonenumber.cabal+15
@@ -73,6 +73,20 @@ library
73 default-extensions: NoFieldSelectors
73 default-extensions: NoFieldSelectors
74 ghc-options: -Wall -Wextra -Werror
74 ghc-options: -Wall -Wextra -Werror
75
75
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+
76test-suite leak_test
90test-suite leak_test
77 type: exitcode-stdio-1.0
91 type: exitcode-stdio-1.0
78 build-depends:
92 build-depends:
@@ -82,3 +96,4 @@ test-suite leak_test
82 main-is: Leak.hs
96 main-is: Leak.hs
83 hs-source-dirs: test
97 hs-source-dirs: test
84 default-language: Haskell2010
98 default-language: Haskell2010
99+ ghc-options: -Wall -Wextra
test/Leak.hs+6−6
@@ -74,7 +74,7 @@ main = do
74 carrierCode = "10"
74 carrierCode = "10"
75 nonGeoCode = 800
75 nonGeoCode = 800
76 phoneString = "+41 44 668 1800"
76 phoneString = "+41 44 668 1800"
77− Right phoneNumber = parseNumber KeepRawInput noRegion phoneString
77+ Right phoneNumber = parseNumber KeepRawInput Nothing phoneString
78 hammer iter (\(PhoneNumber a b c d e f g h) -> PhoneNumber a b c d e f g h) phoneNumber
78 hammer iter (\(PhoneNumber a b c d e f g h) -> PhoneNumber a b c d e f g h) phoneNumber
79 hammer iter supportedTypesForRegion region
79 hammer iter supportedTypesForRegion region
80 hammer iter supportedTypesForNonGeoEntity nonGeoCode
80 hammer iter supportedTypesForNonGeoEntity nonGeoCode
@@ -96,7 +96,7 @@ main = do
96 hammer iter (formatNumber (OutOfCountry region False)) phoneNumber
96 hammer iter (formatNumber (OutOfCountry region False)) phoneNumber
97 hammer iter truncateTooLongNumber phoneNumber
97 hammer iter truncateTooLongNumber phoneNumber
98 hammer iter numberType phoneNumber
98 hammer iter numberType phoneNumber
99− hammer iter (isValidNumber (Just region)) phoneNumber
99+ hammer iter (isValidNumber (Just (Right region))) phoneNumber
100 hammer iter regionForNumber phoneNumber
100 hammer iter regionForNumber phoneNumber
101 hammer iter countryCodeForRegion region
101 hammer iter countryCodeForRegion region
102 hammer iter regionForCountryCode countryCallingCode
102 hammer iter regionForCountryCode countryCallingCode
@@ -107,10 +107,10 @@ main = do
107 hammer iter canBeInternationallyDialed phoneNumber
107 hammer iter canBeInternationallyDialed phoneNumber
108 hammer iter isGeographicalNumber phoneNumber
108 hammer iter isGeographicalNumber phoneNumber
109 hammer iter (isGeographicalNumberType FixedLine) countryCallingCode
109 hammer iter (isGeographicalNumberType FixedLine) countryCallingCode
110− hammer iter (parseNumber Canonicalize region) phoneString
110+ hammer iter (parseNumber Canonicalize (Just region)) phoneString
111− hammer iter (parseNumber Canonicalize region) ""
111+ hammer iter (parseNumber Canonicalize (Just region)) ""
112− hammer iter (parseNumber KeepRawInput region) phoneString
112+ hammer iter (parseNumber KeepRawInput (Just region)) phoneString
113− hammer iter (parseNumber KeepRawInput region) ""
113+ hammer iter (parseNumber KeepRawInput (Just region)) ""
114 hammer iter (matchNumbers (Left phoneString)) (Left phoneString)
114 hammer iter (matchNumbers (Left phoneString)) (Left phoneString)
115 hammer iter (matchNumbers (Left phoneString)) (Right phoneNumber)
115 hammer iter (matchNumbers (Left phoneString)) (Right phoneNumber)
116 hammer iter (matchNumbers (Right phoneNumber)) (Left phoneString)
116 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