1{-# LANGUAGE DeriveGeneric #-}
2{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE ScopedTypeVariables #-}
4{-# OPTIONS_GHC -fno-warn-orphans #-}
7import Control.Applicative
9import Control.Monad.Reader
11import qualified Data.HashMap.Strict as HM
13import Data.List.NonEmpty (NonEmpty (..))
14import qualified Data.List.NonEmpty as NE
15import qualified Data.Map.Strict as M
16import Data.Text (Text)
17import qualified Data.Text as T
18import Data.Time.Calendar (Day (..))
19import Data.Time.Clock (UTCTime (..),
21import qualified Data.Vector as V
22import Database.Bloodhound
23import GHC.Generics (Generic)
24import Network.HTTP.Client
25import qualified Network.HTTP.Types.Status as NHTS
26import Prelude hiding (filter, putStrLn)
28import Test.QuickCheck.Property.Monoid
30import Test.Hspec.QuickCheck (prop)
34testServer = Server "http://localhost:9200"
36testIndex = IndexName "bloodhound-tests-twitter-1"
37testMapping :: MappingName
38testMapping = MappingName "tweet"
40withTestEnv :: BH IO a -> IO a
41withTestEnv = withBH defaultManagerSettings testServer
43validateStatus :: Response body -> Int -> Expectation
44validateStatus resp expected =
45 (NHTS.statusCode $ responseStatus resp)
46 `shouldBe` (expected :: Int)
48createExampleIndex :: BH IO Reply
49createExampleIndex = createIndex defaultIndexSettings testIndex
50deleteExampleIndex :: BH IO Reply
51deleteExampleIndex = deleteIndex testIndex
53data ServerVersion = ServerVersion Int Int Int deriving (Show, Eq, Ord)
56es14 = ServerVersion 1 4 0
59es13 = ServerVersion 1 3 0
62es12 = ServerVersion 1 2 0
65es11 = ServerVersion 1 1 0
68es10 = ServerVersion 1 0 0
70serverBranch :: ServerVersion -> ServerVersion
71serverBranch (ServerVersion majorVer minorVer patchVer) =
72 ServerVersion majorVer minorVer patchVer
74mkServerVersion :: [Int] -> Maybe ServerVersion
75mkServerVersion [majorVer, minorVer, patchVer] =
76 Just (ServerVersion majorVer minorVer patchVer)
77mkServerVersion _ = Nothing
79getServerVersion :: IO (Maybe ServerVersion)
80getServerVersion = liftM extractVersion (withTestEnv getStatus)
82 version' = T.splitOn "." . number . version
83 toInt = read . T.unpack
84 parseVersion v = map toInt (version' v)
85 extractVersion = join . liftM (mkServerVersion . parseVersion)
87testServerBranch :: IO (Maybe ServerVersion)
88testServerBranch = getServerVersion >>= \v -> return $ liftM serverBranch v
90atleast :: ServerVersion -> IO Bool
91atleast v = testServerBranch >>= \x -> return $ x >= Just (serverBranch v)
93atmost :: ServerVersion -> IO Bool
94atmost v = testServerBranch >>= \x -> return $ x <= Just (serverBranch v)
96is :: ServerVersion -> IO Bool
97is v = testServerBranch >>= \x -> return $ x == Just (serverBranch v)
99when' :: Monad m => m Bool -> m () -> m ()
100when' b f = b >>= \x -> when x f
102data Location = Location { lat :: Double
103 , lon :: Double } deriving (Eq, Generic, Show)
105data Tweet = Tweet { user :: Text
106 , postDate :: UTCTime
109 , location :: Location }
110 deriving (Eq, Generic, Show)
113instance FromJSON Tweet
114instance ToJSON Location
115instance FromJSON Location
117data TweetMapping = TweetMapping deriving (Eq, Show)
119instance ToJSON TweetMapping where
120 toJSON TweetMapping =
122 object ["properties" .=
123 object ["location" .= object ["type" .= ("geo_point" :: Text)]]]]
126exampleTweet = Tweet { user = "bitemyapp"
128 (ModifiedJulianDay 55000)
129 (secondsToDiffTime 10)
130 , message = "Use haskell!"
132 , location = Location 40.12 (-71.34) }
135otherTweet = Tweet { user = "notmyapp"
137 (ModifiedJulianDay 55000)
138 (secondsToDiffTime 11)
139 , message = "Use haskell!"
141 , location = Location 40.12 (-71.34) }
143insertData :: BH IO ()
145 _ <- deleteExampleIndex
146 _ <- createExampleIndex
147 _ <- putMapping testIndex testMapping TweetMapping
148 _ <- indexDocument testIndex testMapping exampleTweet (DocId "1")
149 _ <- refreshIndex testIndex
152insertOther :: BH IO ()
154 _ <- indexDocument testIndex testMapping otherTweet (DocId "2")
155 _ <- refreshIndex testIndex
158searchTweet :: Search -> BH IO (Either String Tweet)
159searchTweet search = do
160 reply <- searchByIndex testIndex search
161 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
162 let myTweet = fmap (hitSource . head . hits . searchHits) result
165searchExpectNoResults :: Search -> BH IO ()
166searchExpectNoResults search = do
167 reply <- searchByIndex testIndex search
168 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
169 let emptyHits = fmap (hits . searchHits) result
171 emptyHits `shouldBe` Right []
173searchExpectAggs :: Search -> BH IO ()
174searchExpectAggs search = do
175 reply <- searchAll search
176 let isEmpty x = return (M.null x)
177 let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
179 (result >>= aggregations >>= isEmpty) `shouldBe` Just False
181searchValidBucketAgg :: (BucketAggregation a, FromJSON a, Show a) => Search -> Text -> (Text -> AggregationResults -> Maybe (Bucket a)) -> BH IO ()
182searchValidBucketAgg search aggKey extractor = do
183 reply <- searchAll search
184 let bucketDocs = docCount . head . buckets
185 let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
186 let count = result >>= aggregations >>= extractor aggKey >>= \x -> return (bucketDocs x)
188 count `shouldBe` Just 1
190searchTermsAggHint :: [ExecutionHint] -> BH IO ()
191searchTermsAggHint hints = do
192 let terms hint = TermsAgg $ (mkTermsAggregation "user") { termExecutionHint = Just hint }
193 let search hint = mkAggregateSearch Nothing $ mkAggregations "users" $ terms hint
194 forM_ hints $ searchExpectAggs . search
195 forM_ hints (\x -> searchValidBucketAgg (search x) "users" toTerms)
197searchTweetHighlight :: Search -> BH IO (Either String (Maybe HitHighlight))
198searchTweetHighlight search = do
199 reply <- searchByIndex testIndex search
200 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
201 let myHighlight = fmap (hitHighlight . head . hits . searchHits) result
204data BulkTest = BulkTest { name :: Text } deriving (Eq, Generic, Show)
205instance FromJSON BulkTest
206instance ToJSON BulkTest
208noDuplicates :: Eq a => [a] -> Bool
209noDuplicates xs = nub xs == xs
211instance Arbitrary RegexpFlags where
212 arbitrary = oneof [ pure AllRegexpFlags
214 , SomeRegexpFlags <$> arbitrary
217instance Arbitrary a => Arbitrary (NonEmpty a) where
218 arbitrary = liftA2 (:|) arbitrary arbitrary
220instance Arbitrary RegexpFlag where
221 arbitrary = oneof [ pure AnyString
229arbitraryScore :: Gen Score
230arbitraryScore = fmap getPositive <$> arbitrary
232instance Arbitrary Text where
233 arbitrary = T.pack <$> arbitrary
235instance (Arbitrary k, Arbitrary v, Ord k) => Arbitrary (M.Map k v) where
236 arbitrary = M.fromList <$> arbitrary
238instance Arbitrary IndexName where
239 arbitrary = IndexName <$> arbitrary
241instance Arbitrary MappingName where
242 arbitrary = MappingName <$> arbitrary
244instance Arbitrary DocId where
245 arbitrary = DocId <$> arbitrary
247instance Arbitrary a => Arbitrary (Hit a) where
248 arbitrary = Hit <$> arbitrary
256instance Arbitrary a => Arbitrary (SearchHits a) where
257 arbitrary = sized $ \n -> resize (n `div` 2) $ do
258 tot <- getPositive <$> arbitrary
259 score <- arbitraryScore
261 return $ SearchHits tot score hs
266 describe "index create/delete API" $ do
267 it "creates and then deletes the requested index" $ withTestEnv $ do
269 _ <- deleteExampleIndex
270 resp <- createExampleIndex
271 deleteResp <- deleteExampleIndex
273 validateStatus resp 200
274 validateStatus deleteResp 200
277 describe "document API" $ do
278 it "indexes, gets, and then deletes the generated document" $ withTestEnv $ do
280 docInserted <- getDocument testIndex testMapping (DocId "1")
281 let newTweet = eitherDecode
282 (responseBody docInserted) :: Either String (EsResult Tweet)
283 liftIO $ (fmap _source newTweet `shouldBe` Right exampleTweet)
286 describe "bulk API" $ do
287 it "inserts all documents we request" $ withTestEnv $ do
289 let firstTest = BulkTest "blah"
290 let secondTest = BulkTest "bloo"
291 let firstDoc = BulkIndex testIndex
292 testMapping (DocId "2") (toJSON firstTest)
293 let secondDoc = BulkCreate testIndex
294 testMapping (DocId "3") (toJSON secondTest)
295 let stream = V.fromList [firstDoc, secondDoc]
297 _ <- refreshIndex testIndex
298 fDoc <- getDocument testIndex testMapping (DocId "2")
299 sDoc <- getDocument testIndex testMapping (DocId "3")
300 let maybeFirst = eitherDecode $ responseBody fDoc :: Either String (EsResult BulkTest)
301 let maybeSecond = eitherDecode $ responseBody sDoc :: Either String (EsResult BulkTest)
303 fmap _source maybeFirst `shouldBe` Right firstTest
304 fmap _source maybeSecond `shouldBe` Right secondTest
307 describe "query API" $ do
308 it "returns document for term query and identity filter" $ withTestEnv $ do
310 let query = TermQuery (Term "user" "bitemyapp") Nothing
311 let filter = IdentityFilter <&&> IdentityFilter
312 let search = mkSearch (Just query) (Just filter)
313 myTweet <- searchTweet search
315 myTweet `shouldBe` Right exampleTweet
317 it "returns document for terms query and identity filter" $ withTestEnv $ do
319 let query = TermsQuery (NE.fromList [(Term "user" "bitemyapp")])
320 let filter = IdentityFilter <&&> IdentityFilter
321 let search = mkSearch (Just query) (Just filter)
322 myTweet <- searchTweet search
324 myTweet `shouldBe` Right exampleTweet
326 it "returns document for match query" $ withTestEnv $ do
328 let query = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
329 let search = mkSearch (Just query) Nothing
330 myTweet <- searchTweet search
332 myTweet `shouldBe` Right exampleTweet
334 it "returns document for multi-match query" $ withTestEnv $ do
336 let fields = [FieldName "user", FieldName "message"]
337 let query = QueryMultiMatchQuery $ mkMultiMatchQuery fields (QueryString "bitemyapp")
338 let search = mkSearch (Just query) Nothing
339 myTweet <- searchTweet search
341 myTweet `shouldBe` Right exampleTweet
343 it "returns document for bool query" $ withTestEnv $ do
345 let innerQuery = QueryMatchQuery $
346 mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
347 let query = QueryBoolQuery $
348 mkBoolQuery [innerQuery] [] []
349 let search = mkSearch (Just query) Nothing
350 myTweet <- searchTweet search
352 myTweet `shouldBe` Right exampleTweet
354 it "returns document for boosting query" $ withTestEnv $ do
356 let posQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
357 let negQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "notmyapp")
358 let query = QueryBoostingQuery $ BoostingQuery posQuery negQuery (Boost 0.2)
359 let search = mkSearch (Just query) Nothing
360 myTweet <- searchTweet search
362 myTweet `shouldBe` Right exampleTweet
364 it "returns document for common terms query" $ withTestEnv $ do
366 let query = QueryCommonTermsQuery $
367 CommonTermsQuery (FieldName "user")
368 (QueryString "bitemyapp")
369 (CutoffFrequency 0.0001)
370 Or Or Nothing Nothing Nothing Nothing
371 let search = mkSearch (Just query) Nothing
372 myTweet <- searchTweet search
374 myTweet `shouldBe` Right exampleTweet
377 describe "sorting" $ do
378 it "returns documents in the right order" $ withTestEnv $ do
381 let sortSpec = DefaultSortSpec $ mkSort (FieldName "age") Ascending
382 let search = Search Nothing
383 (Just IdentityFilter) (Just [sortSpec]) Nothing Nothing
384 False (From 0) (Size 10)
385 reply <- searchByIndex testIndex search
386 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
387 let myTweet = fmap (hitSource . head . hits . searchHits) result
389 myTweet `shouldBe` Right otherTweet
392 describe "filtering API" $ do
393 it "returns document for composed boolmatch and identity" $ withTestEnv $ do
395 let queryFilter = BoolFilter (MustMatch (Term "user" "bitemyapp") False)
397 let search = mkSearch Nothing (Just queryFilter)
398 myTweet <- searchTweet search
400 myTweet `shouldBe` Right exampleTweet
402 it "returns document for term filter" $ withTestEnv $ do
404 let termFilter = TermFilter (Term "user" "bitemyapp") False
405 let search = mkSearch Nothing (Just termFilter)
406 myTweet <- searchTweet search
408 myTweet `shouldBe` Right exampleTweet
410 it "returns document for existential filter" $ withTestEnv $ do
412 let search = mkSearch Nothing (Just (ExistsFilter (FieldName "user")))
413 myTweet <- searchTweet search
415 myTweet `shouldBe` Right exampleTweet
417 it "returns document for geo boundingbox filter" $ withTestEnv $ do
419 let box = GeoBoundingBox (LatLon 40.73 (-74.1)) (LatLon 40.10 (-71.12))
420 let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
421 let geoFilter = GeoBoundingBoxFilter bbConstraint
422 let search = mkSearch Nothing (Just geoFilter)
423 myTweet <- searchTweet search
425 myTweet `shouldBe` Right exampleTweet
427 it "doesn't return document for nonsensical boundingbox filter" $ withTestEnv $ do
429 let box = GeoBoundingBox (LatLon 0.73 (-4.1)) (LatLon 0.10 (-1.12))
430 let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
431 let geoFilter = GeoBoundingBoxFilter bbConstraint
432 let search = mkSearch Nothing (Just geoFilter)
433 searchExpectNoResults search
435 it "returns document for geo distance filter" $ withTestEnv $ do
437 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
438 let distance = Distance 10.0 Miles
439 let optimizeBbox = OptimizeGeoFilterType GeoFilterMemory
440 let geoFilter = GeoDistanceFilter geoPoint distance SloppyArc optimizeBbox False
441 let search = mkSearch Nothing (Just geoFilter)
442 myTweet <- searchTweet search
444 myTweet `shouldBe` Right exampleTweet
446 it "returns document for geo distance range filter" $ withTestEnv $ do
448 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
449 let distanceRange = DistanceRange (Distance 0.0 Miles) (Distance 10.0 Miles)
450 let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
451 let search = mkSearch Nothing (Just geoFilter)
452 myTweet <- searchTweet search
454 myTweet `shouldBe` Right exampleTweet
456 it "doesn't return document for wild geo distance range filter" $ withTestEnv $ do
458 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
459 let distanceRange = DistanceRange (Distance 100.0 Miles) (Distance 1000.0 Miles)
460 let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
461 let search = mkSearch Nothing (Just geoFilter)
462 searchExpectNoResults search
464 it "returns document for geo polygon filter" $ withTestEnv $ do
466 let points = [LatLon 40.0 (-70.00),
467 LatLon 40.0 (-72.00),
468 LatLon 41.0 (-70.00),
469 LatLon 41.0 (-72.00)]
470 let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
471 let search = mkSearch Nothing (Just geoFilter)
472 myTweet <- searchTweet search
474 myTweet `shouldBe` Right exampleTweet
476 it "doesn't return document for bad geo polygon filter" $ withTestEnv $ do
478 let points = [LatLon 40.0 (-70.00),
479 LatLon 40.0 (-71.00),
480 LatLon 41.0 (-70.00),
481 LatLon 41.0 (-71.00)]
482 let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
483 let search = mkSearch Nothing (Just geoFilter)
484 searchExpectNoResults search
486 it "returns document for ids filter" $ withTestEnv $ do
488 let filter = IdsFilter (MappingName "tweet") [DocId "1"]
489 let search = mkSearch Nothing (Just filter)
490 myTweet <- searchTweet search
492 myTweet `shouldBe` Right exampleTweet
494 it "returns document for Double range filter" $ withTestEnv $ do
496 let filter = RangeFilter (FieldName "age")
497 (RangeDoubleGtLt (GreaterThan 1000.0) (LessThan 100000.0))
498 RangeExecutionIndex False
499 let search = mkSearch Nothing (Just filter)
500 myTweet <- searchTweet search
502 myTweet `shouldBe` Right exampleTweet
504 it "returns document for UTCTime date filter" $ withTestEnv $ do
506 let filter = RangeFilter (FieldName "postDate")
508 (GreaterThanD (UTCTime
509 (ModifiedJulianDay 55000)
510 (secondsToDiffTime 9)))
512 (ModifiedJulianDay 55000)
513 (secondsToDiffTime 11))))
514 RangeExecutionIndex False
515 let search = mkSearch Nothing (Just filter)
516 myTweet <- searchTweet search
518 myTweet `shouldBe` Right exampleTweet
521 it "returns document for regexp filter" $ withTestEnv $ do
523 let filter = RegexpFilter (FieldName "user") (Regexp "bite.*app")
524 AllRegexpFlags (CacheName "test") False (CacheKey "key")
525 let search = mkSearch Nothing (Just filter)
526 myTweet <- searchTweet search
528 myTweet `shouldBe` Right exampleTweet
530 it "doesn't return document for non-matching regexp filter" $ withTestEnv $ do
532 let filter = RegexpFilter (FieldName "user")
533 (Regexp "boy") AllRegexpFlags
534 (CacheName "test") False (CacheKey "key")
535 let search = mkSearch Nothing (Just filter)
536 searchExpectNoResults search
538 describe "Aggregation API" $ do
539 it "returns term aggregation results" $ withTestEnv $ do
541 let terms = TermsAgg $ mkTermsAggregation "user"
542 let search = mkAggregateSearch Nothing $ mkAggregations "users" terms
543 searchExpectAggs search
544 searchValidBucketAgg search "users" toTerms
546 it "can give collection hint parameters to term aggregations" $ when' (atleast es13) $ withTestEnv $ do
548 let terms = TermsAgg $ (mkTermsAggregation "user") { termCollectMode = Just BreadthFirst }
549 let search = mkAggregateSearch Nothing $ mkAggregations "users" terms
550 searchExpectAggs search
551 searchValidBucketAgg search "users" toTerms
553 it "can give execution hint paramters to term aggregations" $ when' (atmost es11) $ withTestEnv $ do
555 searchTermsAggHint [Map, Ordinals]
557 it "can give execution hint paramters to term aggregations" $ when' (is es12) $ withTestEnv $ do
559 searchTermsAggHint [GlobalOrdinals, GlobalOrdinalsHash, GlobalOrdinalsLowCardinality, Map, Ordinals]
561 it "can give execution hint paramters to term aggregations" $ when' (atleast es12) $ withTestEnv $ do
563 searchTermsAggHint [GlobalOrdinals, GlobalOrdinalsHash, GlobalOrdinalsLowCardinality, Map]
565 it "returns date histogram aggregation results" $ withTestEnv $ do
567 let histogram = DateHistogramAgg $ mkDateHistogram (FieldName "postDate") Minute
568 let search = mkAggregateSearch Nothing (mkAggregations "byDate" histogram)
569 searchExpectAggs search
570 searchValidBucketAgg search "byDate" toDateHistogram
572 it "returns date histogram using fractional date" $ withTestEnv $ do
574 let periods = [Year, Quarter, Month, Week, Day, Hour, Minute, Second]
575 let fractionals = map (FractionalInterval 1.5) [Weeks, Days, Hours, Minutes, Seconds]
576 let intervals = periods ++ fractionals
577 let histogram = mkDateHistogram (FieldName "postDate")
578 let search interval = mkAggregateSearch Nothing $ mkAggregations "byDate" $ DateHistogramAgg (histogram interval)
579 let expect interval = searchExpectAggs (search interval)
580 let valid interval = searchValidBucketAgg (search interval) "byDate" toDateHistogram
581 forM_ intervals expect
582 forM_ intervals valid
584 describe "Highlights API" $ do
586 it "returns highlight from query when there should be one" $ withTestEnv $ do
589 let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
590 let testHighlight = Highlights Nothing [FieldHighlight (FieldName "message") Nothing]
592 let search = mkHighlightSearch (Just query) testHighlight
593 myHighlight <- searchTweetHighlight search
595 myHighlight `shouldBe` Right (Just (M.fromList [("message",["Use <em>haskell</em>!"])]))
597 it "doesn't return highlight from a query when it shouldn't" $ withTestEnv $ do
600 let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
601 let testHighlight = Highlights Nothing [FieldHighlight (FieldName "user") Nothing]
603 let search = mkHighlightSearch (Just query) testHighlight
604 myHighlight <- searchTweetHighlight search
606 myHighlight `shouldBe` Right Nothing
610 describe "ToJSON RegexpFlags" $ do
611 it "generates the correct JSON for AllRegexpFlags" $
612 toJSON AllRegexpFlags `shouldBe` String "ALL"
614 it "generates the correct JSON for NoRegexpFlags" $
615 toJSON NoRegexpFlags `shouldBe` String "NONE"
617 it "generates the correct JSON for SomeRegexpFlags" $
618 let flags = AnyString :| [ Automaton
623 in toJSON (SomeRegexpFlags flags) `shouldBe` String "ANYSTRING|AUTOMATON|COMPLEMENT|EMPTY|INTERSECTION|INTERVAL"
625 prop "removes duplicates from flags" $ \(flags :: RegexpFlags) ->
626 let String str = toJSON flags
627 flagStrs = T.splitOn "|" str
628 in noDuplicates flagStrs
630 describe "omitNulls" $ do
631 it "checks that omitNulls drops list elements when it should" $
632 let dropped = omitNulls $ [ "test1" .= (toJSON ([] :: [Int]))
633 , "test2" .= (toJSON ("some value" :: Text))]
634 in dropped `shouldBe` Object (HM.fromList [("test2", String "some value")])
636 it "checks that omitNulls doesn't drop list elements when it shouldn't" $
637 let notDropped = omitNulls $ [ "test1" .= (toJSON ([1] :: [Int]))
638 , "test2" .= (toJSON ("some value" :: Text))]
639 in notDropped `shouldBe` Object (HM.fromList [ ("test1", Array (V.fromList [Number 1.0]))
640 , ("test2", String "some value")])
641 it "checks that omitNulls drops non list elements when it should" $
642 let dropped = omitNulls $ [ "test1" .= (toJSON Null)
643 , "test2" .= (toJSON ("some value" :: Text))]
644 in dropped `shouldBe` Object (HM.fromList [("test2", String "some value")])
645 it "checks that omitNulls doesn't drop non list elements when it shouldn't" $
646 let notDropped = omitNulls $ [ "test1" .= (toJSON (1 :: Int))
647 , "test2" .= (toJSON ("some value" :: Text))]
648 in notDropped `shouldBe` Object (HM.fromList [ ("test1", Number 1.0)
649 , ("test2", String "some value")])
650 describe "Monoid (SearchHits a)" $ do
651 prop "abides the monoid laws" $ eq $
652 prop_Monoid (T :: T (SearchHits ()))
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