module Main (main) where import qualified Conduit import qualified Control.Concurrent.MVar as MVar import Data.List.NonEmpty (NonEmpty ((:|))) import qualified Dict import qualified Environment import qualified Expect import qualified NonEmptyDict import qualified Platform import qualified Redis import qualified Redis.Counter import qualified Redis.Hash import qualified Redis.Internal as Internal import qualified Redis.List import qualified Redis.Mock as Mock import qualified Redis.Real as Real import qualified Redis.Settings as Settings import qualified Task import qualified Test import qualified Prelude main :: Prelude.IO () main = Conduit.withAcquire getHandlers (Test.run << tests) -- put this at the top of the file so that adding tests doesn't push -- the line number of the source location of this file down, which would -- change golden test results spanForTask :: Show e => Task e () -> Expect.Expectation' Platform.TracingSpan spanForTask task = Expect.fromIO <| do spanVar <- MVar.newEmptyMVar res <- Platform.rootTracingSpanIO "test-request" (MVar.putMVar spanVar) "test-root" (\log -> Task.attempt log task) case res of Err err -> Prelude.fail <| Text.toList (Debug.toString err) Ok _ -> MVar.takeMVar spanVar |> map constantValuesForVariableFields tests :: TestHandlers -> Test.Test tests TestHandlers {realHandler, mockHandler} = Test.describe "Redis Library" [ Test.describe "query tests using mock handler" (queryTests mockHandler), Test.describe "query tests using real handler" (queryTests realHandler), Test.describe "observability tests" (observabilityTests realHandler) ] -- We want to test all of our potential makeApi alternatives because it's easy -- to break. Right now they all share code but if we change that, we would -- break the observability usability without noticing. -- -- All the `srcLocFile` fields in the golden result files should contain the -- value "test/Main.hs". If it points to one of the src files of the redis -- library it means stack frames for redis query in bugsnag, newrelic, etc will -- not point to the application code making the query! observabilityTests :: Redis.Handler -> List Test.Test observabilityTests handler = [ Test.test "Redis.query reports the span data we expect" <| \() -> do span <- Redis.query handler (Redis.ping api) |> spanForTask span |> Debug.toString |> Expect.equalToContentsOf "test/golden-results/observability-spec-reporting-redis-query", Test.test "Redis.transaction reports the span data we expect" <| \() -> do span <- Redis.transaction handler (Redis.ping api) |> spanForTask span |> Debug.toString |> Expect.equalToContentsOf "test/golden-results/observability-spec-reporting-redis-transaction", Test.test "Redis.Hash.query reports the span data we expect" <| \() -> do span <- Redis.Hash.query handler (Redis.Hash.ping hashApi) |> spanForTask span |> Debug.toString |> Expect.equalToContentsOf "test/golden-results/observability-spec-reporting-redis-hash-query", Test.test "Redis.Hash.transaction reports the span data we expect" <| \() -> do span <- Redis.Hash.transaction handler (Redis.Hash.ping hashApi) |> spanForTask span |> Debug.toString |> Expect.equalToContentsOf "test/golden-results/observability-spec-reporting-redis-hash-transaction", Test.test "Redis.List.query reports the span data we expect" <| \() -> do span <- Redis.List.query handler (Redis.List.ping listApi) |> spanForTask span |> Debug.toString |> Expect.equalToContentsOf "test/golden-results/observability-spec-reporting-redis-list-query", Test.test "Redis.List.transaction reports the span data we expect" <| \() -> do span <- Redis.List.transaction handler (Redis.List.ping listApi) |> spanForTask span |> Debug.toString |> Expect.equalToContentsOf "test/golden-results/observability-spec-reporting-redis-list-transaction", Test.test "Redis.Counter.query reports the span data we expect" <| \() -> do span <- Redis.Counter.query handler (Redis.Counter.ping counterApi) |> spanForTask span |> Debug.toString |> Expect.equalToContentsOf "test/golden-results/observability-spec-reporting-redis-counter-query", Test.test "Redis.Counter.transaction reports the span data we expect" <| \() -> do span <- Redis.Counter.transaction handler (Redis.Counter.ping counterApi) |> spanForTask span |> Debug.toString |> Expect.equalToContentsOf "test/golden-results/observability-spec-reporting-redis-counter-transaction" ] queryTests :: Redis.Handler -> List Test.Test queryTests redisHandler = [ Test.test "get and set" <| \() -> do Redis.set api "bob" "hello!" |> Redis.query testNS |> Expect.succeeds result <- Redis.get api "bob" |> Redis.query testNS |> Expect.succeeds Expect.equal result (Just "hello!"), Test.test "namespaces namespace" <| \() -> do let nsHandler1 = addNamespace "NS1" redisHandler let nsHandler2 = addNamespace "NS2" redisHandler Redis.set api "bob" "hello!" |> Redis.query nsHandler1 |> Expect.succeeds Redis.set api "bob" "goodbye" |> Redis.query nsHandler2 |> Expect.succeeds result1 <- Redis.get api "bob" |> Redis.query nsHandler1 |> Expect.succeeds result2 <- Redis.get api "bob" |> Redis.query nsHandler2 |> Expect.succeeds Expect.all [ \() -> Expect.notEqual result1 result2, \() -> Expect.equal (Just "hello!") result1, \() -> Expect.equal (Just "goodbye") result2 ] (), Test.test "getset" <| \() -> do Redis.set api "getset" "1" |> Redis.query testNS |> Expect.succeeds result1 <- Redis.getset api "getset" "2" |> Redis.query testNS |> Expect.succeeds result2 <- Redis.get api "getset" |> Redis.query testNS |> Expect.succeeds Expect.all [ \() -> Expect.equal (Just "1") result1, \() -> Expect.equal (Just "2") result2 ] (), Test.test "del dels" <| \() -> do Redis.set api "del" "mistake..." |> Redis.query testNS |> Expect.succeeds _ <- Redis.del api ("del" :| []) |> Redis.query testNS |> Expect.succeeds result <- Redis.get api "del" |> Redis.query testNS |> Expect.succeeds Expect.equal Nothing result, Test.test "del counts" <| \() -> do Redis.set api "delCount" "A thing" |> Redis.query testNS |> Expect.succeeds result <- Redis.del api ("delCount" :| ["key that doesn't exist"]) |> Redis.query testNS |> Expect.succeeds Expect.equal 1 result, Test.test "json roundtrip" <| \() -> do let testData :: [Int] = [1, 2, 3] Redis.set jsonApi' "JSON list" testData |> Redis.query testNS |> Expect.succeeds result <- Redis.get jsonApi' "JSON list" |> Redis.query testNS |> Expect.succeeds Expect.equal (Just testData) result, Test.test "mget retrieves a mapping of the requested keys and their corresponding values" <| \() -> do Redis.set api "mgetTest::key1" "value 1" |> Redis.query testNS |> Expect.succeeds Redis.set api "mgetTest::key3" "value 3" |> Redis.query testNS |> Expect.succeeds result <- Redis.mget api ("mgetTest::key1" :| ["mgetTest::key2", "mgetTest::key3"]) |> Redis.query testNS |> Expect.succeeds Expect.equal (Dict.toList result) [("mgetTest::key1", "value 1"), ("mgetTest::key3", "value 3")], Test.test "mget json roundtrip" <| \() -> do Redis.set jsonApi' "Json.mgetTest::key1" ([1, 2] :: [Int]) |> Redis.query testNS |> Expect.succeeds Redis.set jsonApi' "Json.mgetTest::key2" ([3, 4] :: [Int]) |> Redis.query testNS |> Expect.succeeds result <- Redis.mget jsonApi' ("Json.mgetTest::key1" :| ["Json.mgetTest::key2"]) |> Redis.query testNS |> Expect.succeeds Expect.equal (Dict.toList result) [ ("Json.mgetTest::key1", [1, 2]), ("Json.mgetTest::key2", [3, 4]) ], Test.test "mset allows setting multiple values at once" <| \() -> do let firstKey = "msetTest::key1" let firstValue = "value 1" let nonEmptyDict = NonEmptyDict.init firstKey firstValue (Dict.fromList [("msetTest::key2", "value 2")]) let dict = NonEmptyDict.toDict nonEmptyDict Redis.mset api nonEmptyDict |> Redis.query testNS |> Expect.succeeds result <- Redis.mget api (firstKey :| Dict.keys dict) |> Redis.query testNS |> Expect.succeeds Expect.equal result dict, Test.test "Json.mset allows setting multiple JSON values at once" <| \() -> do let firstKey = "Json.msetTest::key1" let firstValue = [1, 2] let nonEmptyDict = NonEmptyDict.init firstKey firstValue (Dict.fromList [("Json.msetTest::key2", [3, 4] :: [Int])]) let dict = NonEmptyDict.toDict nonEmptyDict Redis.mset jsonApi' nonEmptyDict |> Redis.query testNS |> Expect.succeeds result <- Redis.mget jsonApi' (firstKey :| Dict.keys dict) |> Redis.query testNS |> Expect.succeeds Expect.equal result dict, Test.test "transaction preserves order" <| \() -> do [ Redis.List.del listApi ("order" :| []), Redis.List.rpush listApi "order" ("1" :| []), Redis.List.rpush listApi "order" ("2" :| []), Redis.List.rpush listApi "order" ("3" :| []) ] |> Redis.sequence |> map (\_ -> ()) |> Redis.transaction testNS |> Expect.succeeds result <- Redis.List.lrange listApi "order" 0 (-1) |> Redis.query testNS |> Expect.succeeds Expect.equal result ["1", "2", "3"], Test.test "sequence is happy doing nothing" <| \() -> do _ <- Redis.sequence [] |> Redis.transaction testNS |> Expect.succeeds Redis.sequence [] |> Redis.query testNS |> Expect.succeeds |> map (\_ -> ()), Test.test "hmset inserts at least one field" <| \() -> do Redis.Hash.hmset hashApi "hmset-insert-test" (NonEmptyDict.init "field" "val" Dict.empty) |> Redis.query redisHandler |> Expect.succeeds response <- Redis.Hash.hget hashApi "hmset-insert-test" "field" |> Redis.query redisHandler |> Expect.succeeds response |> Expect.equal (Just "val"), Test.test "hmset overwrites at least existing field" <| \() -> do Redis.Hash.hset hashApi "hmset-overwrite-test" "field" "old-val" |> Redis.query redisHandler |> Expect.succeeds Redis.Hash.hmset hashApi "hmset-overwrite-test" (NonEmptyDict.init "field" "val" Dict.empty) |> Redis.query redisHandler |> Expect.succeeds response <- Redis.Hash.hget hashApi "hmset-overwrite-test" "field" |> Redis.query redisHandler |> Expect.succeeds response |> Expect.equal (Just "val"), Test.test "hmset inserts multiple fields" <| \() -> do NonEmptyDict.init "field" "val" (Dict.fromList [("field2", "val2")]) |> Redis.Hash.hmset hashApi "hmset-insert-multiple-test" |> Redis.query redisHandler |> Expect.succeeds field <- Redis.Hash.hget hashApi "hmset-insert-multiple-test" "field" |> Redis.query redisHandler |> Expect.succeeds field |> Expect.equal (Just "val") field2 <- Redis.Hash.hget hashApi "hmset-insert-multiple-test" "field2" |> Redis.query redisHandler |> Expect.succeeds field2 |> Expect.equal (Just "val2") ] where testNS = addNamespace "testNamespace" redisHandler data TestHandlers = TestHandlers { mockHandler :: Redis.Handler, realHandler :: Redis.Handler } getHandlers :: Conduit.Acquire TestHandlers getHandlers = do settings <- Conduit.liftIO (Environment.decode Settings.decoder) let realHandler = Real.handler "tests" settings {Settings.defaultExpiry = Settings.ExpireKeysAfterSeconds 1} mockHandler <- Conduit.liftIO <| Mock.handlerIO NriPrelude.map (TestHandlers mockHandler) realHandler addNamespace :: Text -> Redis.Handler -> Redis.Handler addNamespace namespace handler' = handler' {Internal.namespace = Internal.namespace handler' ++ ":" ++ namespace} api :: Redis.Api Text Text api = Redis.textApi identity hashApi :: Redis.Hash.Api Text Text Text hashApi = Redis.Hash.textApi identity identity Just listApi :: Redis.List.Api Text Text listApi = Redis.List.textApi identity counterApi :: Redis.Counter.Api Text counterApi = Redis.Counter.makeApi identity jsonApi' :: Redis.Api Text [Int] jsonApi' = Redis.jsonApi identity -- | Timestamps recorded in spans would make each test result different from the -- last. This helper sets all timestamps to zero to prevent this. -- -- Similarly the db URI changes in each test, because we create temporary test -- database. To prevent this from failing tests we set the URI to a standard -- value. constantValuesForVariableFields :: Platform.TracingSpan -> Platform.TracingSpan constantValuesForVariableFields span = span { Platform.started = 0, Platform.finished = 0, Platform.allocated = 0, Platform.children = map constantValuesForVariableFields (Platform.children span) }