1{-# LANGUAGE DeriveGeneric #-}
2{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE ScopedTypeVariables #-}
4{-# OPTIONS_GHC -fno-warn-orphans #-}
7import Control.Applicative
8import Control.Exception
10import Control.Monad.Reader
12import qualified Data.HashMap.Strict as HM
14import Data.List.NonEmpty (NonEmpty (..))
15import qualified Data.List.NonEmpty as NE
16import qualified Data.Map.Strict as M
17import Data.Text (Text)
18import qualified Data.Text as T
19import Data.Time.Calendar (Day (..))
20import Data.Time.Clock (UTCTime (..),
22import qualified Data.Vector as V
23import Database.Bloodhound
24import GHC.Generics (Generic)
25import Network.HTTP.Client
26import qualified Network.HTTP.Types.Status as NHTS
27import Prelude hiding (filter)
29import Test.QuickCheck.Property.Monoid (prop_Monoid, eq, T(..))
31import Test.Hspec.QuickCheck (prop)
35testServer = Server "http://localhost:9200"
37testIndex = IndexName "bloodhound-tests-twitter-1"
38testMapping :: MappingName
39testMapping = MappingName "tweet"
41withTestEnv :: BH IO a -> IO a
42withTestEnv = withBH defaultManagerSettings testServer
44validateStatus :: Response body -> Int -> Expectation
45validateStatus resp expected =
46 (NHTS.statusCode $ responseStatus resp)
47 `shouldBe` (expected :: Int)
49createExampleIndex :: BH IO Reply
50createExampleIndex = createIndex defaultIndexSettings testIndex
51deleteExampleIndex :: BH IO Reply
52deleteExampleIndex = deleteIndex testIndex
54data ServerVersion = ServerVersion Int Int Int deriving (Show, Eq, Ord)
57es14 = ServerVersion 1 4 0
60es13 = ServerVersion 1 3 0
63es12 = ServerVersion 1 2 0
66es11 = ServerVersion 1 1 0
69es10 = ServerVersion 1 0 0
71serverBranch :: ServerVersion -> ServerVersion
72serverBranch (ServerVersion majorVer minorVer patchVer) =
73 ServerVersion majorVer minorVer patchVer
75mkServerVersion :: [Int] -> Maybe ServerVersion
76mkServerVersion [majorVer, minorVer, patchVer] =
77 Just (ServerVersion majorVer minorVer patchVer)
78mkServerVersion _ = Nothing
80getServerVersion :: IO (Maybe ServerVersion)
81getServerVersion = liftM extractVersion (withTestEnv getStatus)
83 version' = T.splitOn "." . number . version
84 toInt = read . T.unpack
85 parseVersion v = map toInt (version' v)
86 extractVersion = join . liftM (mkServerVersion . parseVersion)
88testServerBranch :: IO (Maybe ServerVersion)
89testServerBranch = getServerVersion >>= \v -> return $ liftM serverBranch v
91atleast :: ServerVersion -> IO Bool
92atleast v = testServerBranch >>= \x -> return $ x >= Just (serverBranch v)
94atmost :: ServerVersion -> IO Bool
95atmost v = testServerBranch >>= \x -> return $ x <= Just (serverBranch v)
97is :: ServerVersion -> IO Bool
98is v = testServerBranch >>= \x -> return $ x == Just (serverBranch v)
100when' :: Monad m => m Bool -> m () -> m ()
101when' b f = b >>= \x -> when x f
103data Location = Location { lat :: Double
104 , lon :: Double } deriving (Eq, Generic, Show)
106data Tweet = Tweet { user :: Text
107 , postDate :: UTCTime
110 , location :: Location }
111 deriving (Eq, Generic, Show)
114instance FromJSON Tweet
115instance ToJSON Location
116instance FromJSON Location
118data TweetMapping = TweetMapping deriving (Eq, Show)
120instance ToJSON TweetMapping where
121 toJSON TweetMapping =
123 object ["properties" .=
124 object [ "user" .= object ["type" .= ("string" :: Text)]
125 -- Serializing the date as a date is breaking other tests, mysteriously.
126 -- , "postDate" .= object [ "type" .= ("date" :: Text)
127 -- , "format" .= ("YYYY-MM-dd`T`HH:mm:ss.SSSZZ" :: Text)]
128 , "message" .= object ["type" .= ("string" :: Text)]
129 , "age" .= object ["type" .= ("integer" :: Text)]
130 , "location" .= object ["type" .= ("geo_point" :: Text)]
134exampleTweet = Tweet { user = "bitemyapp"
136 (ModifiedJulianDay 55000)
137 (secondsToDiffTime 10)
138 , message = "Use haskell!"
140 , location = Location 40.12 (-71.34) }
143otherTweet = Tweet { user = "notmyapp"
145 (ModifiedJulianDay 55000)
146 (secondsToDiffTime 11)
147 , message = "Use haskell!"
149 , location = Location 40.12 (-71.34) }
151resetIndex :: BH IO ()
153 _ <- deleteExampleIndex
154 _ <- createExampleIndex
155 _ <- putMapping testIndex testMapping TweetMapping
158insertData :: BH IO Reply
161 insertData' defaultIndexDocumentSettings
163insertData' :: IndexDocumentSettings -> BH IO Reply
165 r <- indexDocument testIndex testMapping ids exampleTweet (DocId "1")
166 _ <- refreshIndex testIndex
169insertOther :: BH IO ()
171 _ <- indexDocument testIndex testMapping defaultIndexDocumentSettings otherTweet (DocId "2")
172 _ <- refreshIndex testIndex
175searchTweet :: Search -> BH IO (Either String Tweet)
176searchTweet search = do
177 reply <- searchByIndex testIndex search
178 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
179 let myTweet = fmap (hitSource . head . hits . searchHits) result
182searchExpectNoResults :: Search -> BH IO ()
183searchExpectNoResults search = do
184 reply <- searchByIndex testIndex search
185 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
186 let emptyHits = fmap (hits . searchHits) result
188 emptyHits `shouldBe` Right []
190searchExpectAggs :: Search -> BH IO ()
191searchExpectAggs search = do
192 reply <- searchAll search
193 let isEmpty x = return (M.null x)
194 let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
196 (result >>= aggregations >>= isEmpty) `shouldBe` Just False
198searchValidBucketAgg :: (BucketAggregation a, FromJSON a, Show a) =>
199 Search -> Text -> (Text -> AggregationResults -> Maybe (Bucket a)) -> BH IO ()
200searchValidBucketAgg search aggKey extractor = do
201 reply <- searchAll search
202 let bucketDocs = docCount . head . buckets
203 let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
204 let count = result >>= aggregations >>= extractor aggKey >>= \x -> return (bucketDocs x)
206 count `shouldBe` Just 1
208searchTermsAggHint :: [ExecutionHint] -> BH IO ()
209searchTermsAggHint hints = do
210 let terms hint = TermsAgg $ (mkTermsAggregation "user") { termExecutionHint = Just hint }
211 let search hint = mkAggregateSearch Nothing $ mkAggregations "users" $ terms hint
212 forM_ hints $ searchExpectAggs . search
213 forM_ hints (\x -> searchValidBucketAgg (search x) "users" toTerms)
215searchTweetHighlight :: Search -> BH IO (Either String (Maybe HitHighlight))
216searchTweetHighlight search = do
217 reply <- searchByIndex testIndex search
218 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
219 let myHighlight = fmap (hitHighlight . head . hits . searchHits) result
222data BulkTest = BulkTest { name :: Text } deriving (Eq, Generic, Show)
223instance FromJSON BulkTest
224instance ToJSON BulkTest
226noDuplicates :: Eq a => [a] -> Bool
227noDuplicates xs = nub xs == xs
229instance Arbitrary RegexpFlags where
230 arbitrary = oneof [ pure AllRegexpFlags
232 , SomeRegexpFlags <$> arbitrary
235instance Arbitrary a => Arbitrary (NonEmpty a) where
236 arbitrary = liftA2 (:|) arbitrary arbitrary
238instance Arbitrary RegexpFlag where
239 arbitrary = oneof [ pure AnyString
247arbitraryScore :: Gen Score
248arbitraryScore = fmap getPositive <$> arbitrary
250instance Arbitrary Text where
251 arbitrary = T.pack <$> arbitrary
253instance (Arbitrary k, Arbitrary v, Ord k) => Arbitrary (M.Map k v) where
254 arbitrary = M.fromList <$> arbitrary
256instance Arbitrary IndexName where
257 arbitrary = IndexName <$> arbitrary
259instance Arbitrary MappingName where
260 arbitrary = MappingName <$> arbitrary
262instance Arbitrary DocId where
263 arbitrary = DocId <$> arbitrary
265instance Arbitrary a => Arbitrary (Hit a) where
266 arbitrary = Hit <$> arbitrary
274instance Arbitrary a => Arbitrary (SearchHits a) where
275 arbitrary = sized $ \n -> resize (n `div` 2) $ do
276 tot <- getPositive <$> arbitrary
277 score <- arbitraryScore
279 return $ SearchHits tot score hs
284 describe "index create/delete API" $ do
285 it "creates and then deletes the requested index" $ withTestEnv $ do
287 _ <- deleteExampleIndex
288 resp <- createExampleIndex
289 deleteResp <- deleteExampleIndex
291 validateStatus resp 200
292 validateStatus deleteResp 200
295 describe "document API" $ do
296 it "indexes, gets, and then deletes the generated document" $ withTestEnv $ do
298 docInserted <- getDocument testIndex testMapping (DocId "1")
299 let newTweet = eitherDecode
300 (responseBody docInserted) :: Either String (EsResult Tweet)
301 liftIO $ (fmap _source newTweet `shouldBe` Right exampleTweet)
303 it "can use optimistic concurrency control" $ withTestEnv $ do
304 let ev = ExternalDocVersion minBound
305 let cfg = defaultIndexDocumentSettings { idsVersionControl = ExternalGT ev }
307 res <- insertData' cfg
308 liftIO $ isCreated res `shouldBe` True
309 res' <- insertData' cfg
310 liftIO $ isVersionConflict res' `shouldBe` True
312 describe "bulk API" $ do
313 it "inserts all documents we request" $ withTestEnv $ do
315 let firstTest = BulkTest "blah"
316 let secondTest = BulkTest "bloo"
317 let firstDoc = BulkIndex testIndex
318 testMapping (DocId "2") (toJSON firstTest)
319 let secondDoc = BulkCreate testIndex
320 testMapping (DocId "3") (toJSON secondTest)
321 let stream = V.fromList [firstDoc, secondDoc]
323 _ <- refreshIndex testIndex
324 fDoc <- getDocument testIndex testMapping (DocId "2")
325 sDoc <- getDocument testIndex testMapping (DocId "3")
326 let maybeFirst = eitherDecode $ responseBody fDoc :: Either String (EsResult BulkTest)
327 let maybeSecond = eitherDecode $ responseBody sDoc :: Either String (EsResult BulkTest)
329 fmap _source maybeFirst `shouldBe` Right firstTest
330 fmap _source maybeSecond `shouldBe` Right secondTest
333 describe "query API" $ do
334 it "returns document for term query and identity filter" $ withTestEnv $ do
336 let query = TermQuery (Term "user" "bitemyapp") Nothing
337 let filter = IdentityFilter <&&> IdentityFilter
338 let search = mkSearch (Just query) (Just filter)
339 myTweet <- searchTweet search
341 myTweet `shouldBe` Right exampleTweet
343 it "returns document for terms query and identity filter" $ withTestEnv $ do
345 let query = TermsQuery (NE.fromList [(Term "user" "bitemyapp")])
346 let filter = IdentityFilter <&&> IdentityFilter
347 let search = mkSearch (Just query) (Just filter)
348 myTweet <- searchTweet search
350 myTweet `shouldBe` Right exampleTweet
352 it "returns document for match query" $ withTestEnv $ do
354 let query = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
355 let search = mkSearch (Just query) Nothing
356 myTweet <- searchTweet search
358 myTweet `shouldBe` Right exampleTweet
360 it "returns document for multi-match query" $ withTestEnv $ do
362 let fields = [FieldName "user", FieldName "message"]
363 let query = QueryMultiMatchQuery $ mkMultiMatchQuery fields (QueryString "bitemyapp")
364 let search = mkSearch (Just query) Nothing
365 myTweet <- searchTweet search
367 myTweet `shouldBe` Right exampleTweet
369 it "returns document for bool query" $ withTestEnv $ do
371 let innerQuery = QueryMatchQuery $
372 mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
373 let query = QueryBoolQuery $
374 mkBoolQuery [innerQuery] [] []
375 let search = mkSearch (Just query) Nothing
376 myTweet <- searchTweet search
378 myTweet `shouldBe` Right exampleTweet
380 it "returns document for boosting query" $ withTestEnv $ do
382 let posQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
383 let negQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "notmyapp")
384 let query = QueryBoostingQuery $ BoostingQuery posQuery negQuery (Boost 0.2)
385 let search = mkSearch (Just query) Nothing
386 myTweet <- searchTweet search
388 myTweet `shouldBe` Right exampleTweet
390 it "returns document for common terms query" $ withTestEnv $ do
392 let query = QueryCommonTermsQuery $
393 CommonTermsQuery (FieldName "user")
394 (QueryString "bitemyapp")
395 (CutoffFrequency 0.0001)
396 Or Or Nothing Nothing Nothing Nothing
397 let search = mkSearch (Just query) Nothing
398 myTweet <- searchTweet search
400 myTweet `shouldBe` Right exampleTweet
403 describe "sorting" $ do
404 it "returns documents in the right order" $ withTestEnv $ do
407 let sortSpec = DefaultSortSpec $ mkSort (FieldName "age") Ascending
408 let search = Search Nothing
409 (Just IdentityFilter) (Just [sortSpec]) Nothing Nothing
410 False (From 0) (Size 10)
411 reply <- searchByIndex testIndex search
412 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
413 let myTweet = fmap (hitSource . head . hits . searchHits) result
415 myTweet `shouldBe` Right otherTweet
418 describe "filtering API" $ do
419 it "returns document for composed boolmatch and identity" $ withTestEnv $ do
421 let queryFilter = BoolFilter (MustMatch (Term "user" "bitemyapp") False)
423 let search = mkSearch Nothing (Just queryFilter)
424 myTweet <- searchTweet search
426 myTweet `shouldBe` Right exampleTweet
428 it "returns document for term filter" $ withTestEnv $ do
430 let termFilter = TermFilter (Term "user" "bitemyapp") False
431 let search = mkSearch Nothing (Just termFilter)
432 myTweet <- searchTweet search
434 myTweet `shouldBe` Right exampleTweet
436 it "returns document for existential filter" $ withTestEnv $ do
438 let search = mkSearch Nothing (Just (ExistsFilter (FieldName "user")))
439 myTweet <- searchTweet search
441 myTweet `shouldBe` Right exampleTweet
443 it "returns document for geo boundingbox filter" $ withTestEnv $ do
445 let box = GeoBoundingBox (LatLon 40.73 (-74.1)) (LatLon 40.10 (-71.12))
446 let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
447 let geoFilter = GeoBoundingBoxFilter bbConstraint
448 let search = mkSearch Nothing (Just geoFilter)
449 myTweet <- searchTweet search
451 myTweet `shouldBe` Right exampleTweet
453 it "doesn't return document for nonsensical boundingbox filter" $ withTestEnv $ do
455 let box = GeoBoundingBox (LatLon 0.73 (-4.1)) (LatLon 0.10 (-1.12))
456 let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
457 let geoFilter = GeoBoundingBoxFilter bbConstraint
458 let search = mkSearch Nothing (Just geoFilter)
459 searchExpectNoResults search
461 it "returns document for geo distance filter" $ withTestEnv $ do
463 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
464 let distance = Distance 10.0 Miles
465 let optimizeBbox = OptimizeGeoFilterType GeoFilterMemory
466 let geoFilter = GeoDistanceFilter geoPoint distance SloppyArc optimizeBbox False
467 let search = mkSearch Nothing (Just geoFilter)
468 myTweet <- searchTweet search
470 myTweet `shouldBe` Right exampleTweet
472 it "returns document for geo distance range filter" $ withTestEnv $ do
474 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
475 let distanceRange = DistanceRange (Distance 0.0 Miles) (Distance 10.0 Miles)
476 let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
477 let search = mkSearch Nothing (Just geoFilter)
478 myTweet <- searchTweet search
480 myTweet `shouldBe` Right exampleTweet
482 it "doesn't return document for wild geo distance range filter" $ withTestEnv $ do
484 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
485 let distanceRange = DistanceRange (Distance 100.0 Miles) (Distance 1000.0 Miles)
486 let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
487 let search = mkSearch Nothing (Just geoFilter)
488 searchExpectNoResults search
490 it "returns document for geo polygon filter" $ withTestEnv $ do
492 let points = [LatLon 40.0 (-70.00),
493 LatLon 40.0 (-72.00),
494 LatLon 41.0 (-70.00),
495 LatLon 41.0 (-72.00)]
496 let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
497 let search = mkSearch Nothing (Just geoFilter)
498 myTweet <- searchTweet search
500 myTweet `shouldBe` Right exampleTweet
502 it "doesn't return document for bad geo polygon filter" $ withTestEnv $ do
504 let points = [LatLon 40.0 (-70.00),
505 LatLon 40.0 (-71.00),
506 LatLon 41.0 (-70.00),
507 LatLon 41.0 (-71.00)]
508 let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
509 let search = mkSearch Nothing (Just geoFilter)
510 searchExpectNoResults search
512 it "returns document for ids filter" $ withTestEnv $ do
514 let filter = IdsFilter (MappingName "tweet") [DocId "1"]
515 let search = mkSearch Nothing (Just filter)
516 myTweet <- searchTweet search
518 myTweet `shouldBe` Right exampleTweet
520 it "returns document for Double range filter" $ withTestEnv $ do
522 let filter = RangeFilter (FieldName "age")
523 (RangeDoubleGtLt (GreaterThan 1000.0) (LessThan 100000.0))
524 RangeExecutionIndex False
525 let search = mkSearch Nothing (Just filter)
526 myTweet <- searchTweet search
528 myTweet `shouldBe` Right exampleTweet
530 it "returns document for UTCTime date filter" $ withTestEnv $ do
532 let filter = RangeFilter (FieldName "postDate")
534 (GreaterThanD (UTCTime
535 (ModifiedJulianDay 54000)
536 (secondsToDiffTime 0)))
538 (ModifiedJulianDay 55000)
539 (secondsToDiffTime 11))))
540 RangeExecutionIndex False
541 let search = mkSearch Nothing (Just filter)
542 myTweet <- searchTweet search
544 myTweet `shouldBe` Right exampleTweet
547 it "returns document for regexp filter" $ withTestEnv $ do
549 let filter = RegexpFilter (FieldName "user") (Regexp "bite.*app")
550 AllRegexpFlags (CacheName "test") False (CacheKey "key")
551 let search = mkSearch Nothing (Just filter)
552 myTweet <- searchTweet search
554 myTweet `shouldBe` Right exampleTweet
556 it "doesn't return document for non-matching regexp filter" $ withTestEnv $ do
558 let filter = RegexpFilter (FieldName "user")
559 (Regexp "boy") AllRegexpFlags
560 (CacheName "test") False (CacheKey "key")
561 let search = mkSearch Nothing (Just filter)
562 searchExpectNoResults search
564 describe "Aggregation API" $ do
565 it "returns term aggregation results" $ withTestEnv $ do
567 let terms = TermsAgg $ mkTermsAggregation "user"
568 let search = mkAggregateSearch Nothing $ mkAggregations "users" terms
569 searchExpectAggs search
570 searchValidBucketAgg search "users" toTerms
572 it "can give collection hint parameters to term aggregations" $ when' (atleast es13) $ withTestEnv $ do
574 let terms = TermsAgg $ (mkTermsAggregation "user") { termCollectMode = Just BreadthFirst }
575 let search = mkAggregateSearch Nothing $ mkAggregations "users" terms
576 searchExpectAggs search
577 searchValidBucketAgg search "users" toTerms
579 it "can give execution hint parameters to term aggregations" $ when' (atmost es11) $ withTestEnv $ do
581 searchTermsAggHint [Map, Ordinals]
583 it "can give execution hint parameters to term aggregations" $ when' (is es12) $ withTestEnv $ do
585 searchTermsAggHint [GlobalOrdinals, GlobalOrdinalsHash, GlobalOrdinalsLowCardinality, Map, Ordinals]
587 it "can give execution hint parameters to term aggregations" $ when' (atleast es12) $ withTestEnv $ do
589 searchTermsAggHint [GlobalOrdinals, GlobalOrdinalsHash, GlobalOrdinalsLowCardinality, Map]
591 -- Interaction of date serialization and date histogram aggregation is broken.
592 -- it "returns date histogram aggregation results" $ withTestEnv $ do
594 -- let histogram = DateHistogramAgg $ mkDateHistogram (FieldName "postDate") Minute
595 -- let search = mkAggregateSearch Nothing (mkAggregations "byDate" histogram)
596 -- searchExpectAggs search
597 -- searchValidBucketAgg search "byDate" toDateHistogram
599 -- it "returns date histogram using fractional date" $ withTestEnv $ do
601 -- let periods = [Year, Quarter, Month, Week, Day, Hour, Minute, Second]
602 -- let fractionals = map (FractionalInterval 1.5) [Weeks, Days, Hours, Minutes, Seconds]
603 -- let intervals = periods ++ fractionals
604 -- let histogram = mkDateHistogram (FieldName "postDate")
605 -- let search interval = mkAggregateSearch Nothing $ mkAggregations "byDate" $ DateHistogramAgg (histogram interval)
606 -- let expect interval = searchExpectAggs (search interval)
607 -- let valid interval = searchValidBucketAgg (search interval) "byDate" toDateHistogram
608 -- forM_ intervals expect
609 -- forM_ intervals valid
611 describe "Highlights API" $ do
613 it "returns highlight from query when there should be one" $ withTestEnv $ do
616 let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
617 let testHighlight = Highlights Nothing [FieldHighlight (FieldName "message") Nothing]
619 let search = mkHighlightSearch (Just query) testHighlight
620 myHighlight <- searchTweetHighlight search
622 myHighlight `shouldBe` Right (Just (M.fromList [("message",["Use <em>haskell</em>!"])]))
624 it "doesn't return highlight from a query when it shouldn't" $ withTestEnv $ do
627 let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
628 let testHighlight = Highlights Nothing [FieldHighlight (FieldName "user") Nothing]
630 let search = mkHighlightSearch (Just query) testHighlight
631 myHighlight <- searchTweetHighlight search
633 myHighlight `shouldBe` Right Nothing
637 describe "ToJSON RegexpFlags" $ do
638 it "generates the correct JSON for AllRegexpFlags" $
639 toJSON AllRegexpFlags `shouldBe` String "ALL"
641 it "generates the correct JSON for NoRegexpFlags" $
642 toJSON NoRegexpFlags `shouldBe` String "NONE"
644 it "generates the correct JSON for SomeRegexpFlags" $
645 let flags = AnyString :| [ Automaton
650 in toJSON (SomeRegexpFlags flags) `shouldBe` String "ANYSTRING|AUTOMATON|COMPLEMENT|EMPTY|INTERSECTION|INTERVAL"
652 prop "removes duplicates from flags" $ \(flags :: RegexpFlags) ->
653 let String str = toJSON flags
654 flagStrs = T.splitOn "|" str
655 in noDuplicates flagStrs
657 describe "omitNulls" $ do
658 it "checks that omitNulls drops list elements when it should" $
659 let dropped = omitNulls $ [ "test1" .= (toJSON ([] :: [Int]))
660 , "test2" .= (toJSON ("some value" :: Text))]
661 in dropped `shouldBe` Object (HM.fromList [("test2", String "some value")])
663 it "checks that omitNulls doesn't drop list elements when it shouldn't" $
664 let notDropped = omitNulls $ [ "test1" .= (toJSON ([1] :: [Int]))
665 , "test2" .= (toJSON ("some value" :: Text))]
666 in notDropped `shouldBe` Object (HM.fromList [ ("test1", Array (V.fromList [Number 1.0]))
667 , ("test2", String "some value")])
668 it "checks that omitNulls drops non list elements when it should" $
669 let dropped = omitNulls $ [ "test1" .= (toJSON Null)
670 , "test2" .= (toJSON ("some value" :: Text))]
671 in dropped `shouldBe` Object (HM.fromList [("test2", String "some value")])
672 it "checks that omitNulls doesn't drop non list elements when it shouldn't" $
673 let notDropped = omitNulls $ [ "test1" .= (toJSON (1 :: Int))
674 , "test2" .= (toJSON ("some value" :: Text))]
675 in notDropped `shouldBe` Object (HM.fromList [ ("test1", Number 1.0)
676 , ("test2", String "some value")])
677 describe "Monoid (SearchHits a)" $ do
678 prop "abides the monoid laws" $ eq $
679 prop_Monoid (T :: T (SearchHits ()))
681 describe "mkDocVersion" $ do
682 prop "can never construct an out of range docVersion" $ \i ->
683 let res = mkDocVersion i
685 Nothing -> property True
686 Just dv -> (dv >= minBound) .&&.
687 (dv <= maxBound) .&&.
688 docVersionNumber dv === i
690 describe "Enum DocVersion" $ do
691 it "follows the laws of Enum, Bounded" $ do
692 evaluate (succ maxBound :: DocVersion) `shouldThrow` anyErrorCall
693 evaluate (pred minBound :: DocVersion) `shouldThrow` anyErrorCall
694 evaluate (toEnum 0 :: DocVersion) `shouldThrow` anyErrorCall
695 evaluate (toEnum 9200000000000000001 :: DocVersion) `shouldThrow` anyErrorCall
696 enumFrom (pred maxBound :: DocVersion) `shouldBe` [pred maxBound, maxBound]
697 enumFrom (pred maxBound :: DocVersion) `shouldBe` [pred maxBound, maxBound]
698 enumFromThen minBound (pred maxBound :: DocVersion) `shouldBe` [minBound, pred maxBound]
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list