diff --git a/.scalafmt.conf b/.scalafmt.conf index 1ed3bc5ff..f54f45cce 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.8.2" +version = "3.8.3" maxColumn = 120 align.preset = most continuationIndent.defnSite = 2 diff --git a/modules/redis-it/src/test/resources/docker-compose.yml b/modules/redis-it/src/test/resources/docker-compose.yml index eec55f5cf..023a2bbc7 100644 --- a/modules/redis-it/src/test/resources/docker-compose.yml +++ b/modules/redis-it/src/test/resources/docker-compose.yml @@ -1,56 +1,56 @@ version: "3.3" services: - single-node-0: + single-node0: image: bitnami/redis:7.2 environment: - 'ALLOW_EMPTY_PASSWORD=yes' - single-node-1: + single-node1: image: bitnami/redis:7.2 environment: - 'ALLOW_EMPTY_PASSWORD=yes' - cluster-node-0: + cluster-node0: image: bitnami/redis-cluster:7.2 environment: - 'ALLOW_EMPTY_PASSWORD=yes' - - 'REDIS_NODES=cluster-node-0 cluster-node-1 cluster-node-2 cluster-node-3 cluster-node-4 cluster-node-5' + - 'REDIS_NODES=cluster-node0 cluster-node1 cluster-node2 cluster-node3 cluster-node4 cluster-node5' - cluster-node-1: + cluster-node1: image: bitnami/redis-cluster:7.2 environment: - 'ALLOW_EMPTY_PASSWORD=yes' - - 'REDIS_NODES=cluster-node-0 cluster-node-1 cluster-node-2 cluster-node-3 cluster-node-4 cluster-node-5' + - 'REDIS_NODES=cluster-node0 cluster-node1 cluster-node2 cluster-node3 cluster-node4 cluster-node5' - cluster-node-2: + cluster-node2: image: bitnami/redis-cluster:7.2 environment: - 'ALLOW_EMPTY_PASSWORD=yes' - - 'REDIS_NODES=cluster-node-0 cluster-node-1 cluster-node-2 cluster-node-3 cluster-node-4 cluster-node-5' + - 'REDIS_NODES=cluster-node0 cluster-node1 cluster-node2 cluster-node3 cluster-node4 cluster-node5' - cluster-node-3: + cluster-node3: image: bitnami/redis-cluster:7.2 environment: - 'ALLOW_EMPTY_PASSWORD=yes' - - 'REDIS_NODES=cluster-node-0 cluster-node-1 cluster-node-2 cluster-node-3 cluster-node-4 cluster-node-5' + - 'REDIS_NODES=cluster-node0 cluster-node1 cluster-node2 cluster-node3 cluster-node4 cluster-node5' - cluster-node-4: + cluster-node4: image: bitnami/redis-cluster:7.2 environment: - 'ALLOW_EMPTY_PASSWORD=yes' - - 'REDIS_NODES=cluster-node-0 cluster-node-1 cluster-node-2 cluster-node-3 cluster-node-4 cluster-node-5' + - 'REDIS_NODES=cluster-node0 cluster-node1 cluster-node2 cluster-node3 cluster-node4 cluster-node5' - cluster-node-5: + cluster-node5: image: bitnami/redis-cluster:7.2 depends_on: - - cluster-node-0 - - cluster-node-1 - - cluster-node-2 - - cluster-node-3 - - cluster-node-4 + - cluster-node0 + - cluster-node1 + - cluster-node2 + - cluster-node3 + - cluster-node4 environment: - 'ALLOW_EMPTY_PASSWORD=yes' - 'REDIS_CLUSTER_REPLICAS=1' - - 'REDIS_NODES=cluster-node-0 cluster-node-1 cluster-node-2 cluster-node-3 cluster-node-4 cluster-node-5' + - 'REDIS_NODES=cluster-node0 cluster-node1 cluster-node2 cluster-node3 cluster-node4 cluster-node5' - 'REDIS_CLUSTER_CREATOR=yes' diff --git a/modules/redis-it/src/test/scala/zio/redis/ApiSpec.scala b/modules/redis-it/src/test/scala/zio/redis/ApiSpec.scala index 882fc6f46..7c022f432 100644 --- a/modules/redis-it/src/test/scala/zio/redis/ApiSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/ApiSpec.scala @@ -24,9 +24,9 @@ object ApiSpec suite("Redis commands")(ClusterSuite, SingleNodeSuite) .provideShared( compose( - service(BaseSpec.SingleNode0, ".*Ready to accept connections.*"), - service(BaseSpec.SingleNode1, ".*Ready to accept connections.*"), - service(BaseSpec.MasterNode, ".*Cluster correctly created.*") + service(IntegrationSpec.SingleNode0, ".*Ready to accept connections.*"), + service(IntegrationSpec.SingleNode1, ".*Ready to accept connections.*"), + service(IntegrationSpec.MasterNode, ".*Cluster correctly created.*") ) ) @@ sequential @@ withLiveEnvironment @@ -48,7 +48,7 @@ object ApiSpec Redis.cluster, masterNodeConfig, ZLayer.succeed(ProtobufCodecSupplier) - ).filterNotTags(_.contains(BaseSpec.ClusterExecutorUnsupported)) + ).filterNotTags(_.contains(IntegrationSpec.ClusterExecutorUnsupported)) .getOrElse(Spec.empty) @@ flaky @@ ifEnvNotSet("CI") private final val SingleNodeSuite = @@ -68,7 +68,7 @@ object ApiSpec ).provideSomeShared[DockerComposeContainer]( Redis.singleNode, RedisSubscription.singleNode, - singleNodeConfig(BaseSpec.SingleNode0), + singleNodeConfig(IntegrationSpec.SingleNode0), ZLayer.succeed(ProtobufCodecSupplier) ) } diff --git a/modules/redis-it/src/test/scala/zio/redis/ClusterSpec.scala b/modules/redis-it/src/test/scala/zio/redis/ClusterSpec.scala index 1d355b357..b846bee7e 100644 --- a/modules/redis-it/src/test/scala/zio/redis/ClusterSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/ClusterSpec.scala @@ -4,7 +4,7 @@ import com.dimafeng.testcontainers.DockerComposeContainer import zio._ import zio.test._ -trait ClusterSpec extends BaseSpec { +trait ClusterSpec extends IntegrationSpec { def clusterSpec: Spec[DockerComposeContainer & Redis, RedisError] = suite("cluster")( suite("slots")( @@ -17,8 +17,8 @@ trait ClusterSpec extends BaseSpec { ZIO .foreach(0 to 5) { n => ZIO - .attempt(docker.getServiceHost(s"cluster-node-$n", port)) - .map(host => RedisUri(s"$host:$port")) + .attempt(docker.getServiceHost(s"cluster-node$n", port)) + .map(host => RedisUri(host, port)) } .orDie actual = res.map(_.master.address) ++ res.flatMap(_.slaves.map(_.address)) diff --git a/modules/redis-it/src/test/scala/zio/redis/ConnectionSpec.scala b/modules/redis-it/src/test/scala/zio/redis/ConnectionSpec.scala index b6e38026f..a18ee0a0d 100644 --- a/modules/redis-it/src/test/scala/zio/redis/ConnectionSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/ConnectionSpec.scala @@ -5,7 +5,7 @@ import zio.test.Assertion._ import zio.test.TestAspect._ import zio.test._ -trait ConnectionSpec extends BaseSpec { +trait ConnectionSpec extends IntegrationSpec { def connectionSuite: Spec[Redis, RedisError] = suite("connection")( suite("authenticating")( diff --git a/modules/redis-it/src/test/scala/zio/redis/GeoSpec.scala b/modules/redis-it/src/test/scala/zio/redis/GeoSpec.scala index ce0559b49..57a50b3f1 100644 --- a/modules/redis-it/src/test/scala/zio/redis/GeoSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/GeoSpec.scala @@ -4,7 +4,7 @@ import zio.test.Assertion._ import zio.test._ import zio.{Chunk, ZIO} -trait GeoSpec extends BaseSpec { +trait GeoSpec extends IntegrationSpec { def geoSuite: Spec[Redis, RedisError] = suite("geo")( test("geoAdd followed by geoPos") { diff --git a/modules/redis-it/src/test/scala/zio/redis/HashSpec.scala b/modules/redis-it/src/test/scala/zio/redis/HashSpec.scala index bab9bb212..997e8dd0e 100644 --- a/modules/redis-it/src/test/scala/zio/redis/HashSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/HashSpec.scala @@ -4,7 +4,7 @@ import zio.test.Assertion._ import zio.test._ import zio.{Chunk, ZIO} -trait HashSpec extends BaseSpec { +trait HashSpec extends IntegrationSpec { def hashSuite: Spec[Redis, RedisError] = suite("hash")( suite("hSet, hGet, hGetAll and hDel")( diff --git a/modules/redis-it/src/test/scala/zio/redis/HyperLogLogSpec.scala b/modules/redis-it/src/test/scala/zio/redis/HyperLogLogSpec.scala index 6cc82bd22..05738907d 100644 --- a/modules/redis-it/src/test/scala/zio/redis/HyperLogLogSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/HyperLogLogSpec.scala @@ -4,7 +4,7 @@ import zio.ZIO import zio.test.Assertion._ import zio.test._ -trait HyperLogLogSpec extends BaseSpec { +trait HyperLogLogSpec extends IntegrationSpec { def hyperLogLogSuite: Spec[Redis, RedisError] = suite("hyperloglog")( suite("add elements")( diff --git a/modules/redis-it/src/test/scala/zio/redis/BaseSpec.scala b/modules/redis-it/src/test/scala/zio/redis/IntegrationSpec.scala similarity index 84% rename from modules/redis-it/src/test/scala/zio/redis/BaseSpec.scala rename to modules/redis-it/src/test/scala/zio/redis/IntegrationSpec.scala index 81b6119cc..611bb6128 100644 --- a/modules/redis-it/src/test/scala/zio/redis/BaseSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/IntegrationSpec.scala @@ -12,7 +12,7 @@ import zio.{ULayer, _} import java.io.File import java.util.UUID -trait BaseSpec extends ZIOSpecDefault { +trait IntegrationSpec extends ZIOSpecDefault { implicit def summonCodec[A: Schema]: BinaryCodec[A] = ProtobufCodec.protobufCodec override def aspects: Chunk[TestAspectAtLeastR[Live]] = @@ -30,8 +30,8 @@ trait BaseSpec extends ZIOSpecDefault { ZLayer { for { docker <- ZIO.service[DockerComposeContainer] - hostAndPort <- docker.getHostAndPort(BaseSpec.MasterNode)(6379) - uri = RedisUri(s"${hostAndPort._1}:${hostAndPort._2}") + hostAndPort <- docker.getHostAndPort(IntegrationSpec.MasterNode)(6379) + uri = RedisUri(hostAndPort._1, hostAndPort._2) } yield RedisClusterConfig(Chunk(uri)) } @@ -53,7 +53,7 @@ trait BaseSpec extends ZIOSpecDefault { * - fork/join approach for commands that operate on keys with different slots */ final val clusterExecutorUnsupported: TestAspectPoly = - tag(BaseSpec.ClusterExecutorUnsupported) + tag(IntegrationSpec.ClusterExecutorUnsupported) final val genStringRedisTypeOption: Gen[Any, Option[RedisType]] = Gen.option(Gen.constSample(Sample.noShrink(RedisType.String))) @@ -68,9 +68,9 @@ trait BaseSpec extends ZIOSpecDefault { ZIO.succeed(UUID.randomUUID().toString) } -object BaseSpec { +object IntegrationSpec { final val ClusterExecutorUnsupported = "cluster executor not supported" - final val MasterNode = "cluster-node-5" - final val SingleNode0 = "single-node-0" - final val SingleNode1 = "single-node-1" + final val MasterNode = "cluster-node5" + final val SingleNode0 = "single-node0" + final val SingleNode1 = "single-node1" } diff --git a/modules/redis-it/src/test/scala/zio/redis/KeysSpec.scala b/modules/redis-it/src/test/scala/zio/redis/KeysSpec.scala index 14928cf2f..eddc8d940 100644 --- a/modules/redis-it/src/test/scala/zio/redis/KeysSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/KeysSpec.scala @@ -7,7 +7,7 @@ import zio.test.Assertion.{exists => _, _} import zio.test.TestAspect.{restore => _, _} import zio.test._ -trait KeysSpec extends BaseSpec { +trait KeysSpec extends IntegrationSpec { def keysSuite: Spec[DockerComposeContainer & Redis, RedisError] = { suite("keys")( test("set followed by get") { @@ -177,7 +177,7 @@ trait KeysSpec extends BaseSpec { _ <- redis.set(key, value) response <- redis .migrate( - BaseSpec.SingleNode1, + IntegrationSpec.SingleNode1, 6379, key, 0L, @@ -199,7 +199,7 @@ trait KeysSpec extends BaseSpec { redis <- ZIO.service[Redis] _ <- redis.set(key, value) response <- redis.migrate( - BaseSpec.SingleNode1, + IntegrationSpec.SingleNode1, 6379L, key, 0L, @@ -225,7 +225,7 @@ trait KeysSpec extends BaseSpec { .provideLayer(secondExecutor) // also add to second Redis response <- redis .migrate( - BaseSpec.SingleNode1, + IntegrationSpec.SingleNode1, 6379, key, 0L, @@ -515,7 +515,7 @@ trait KeysSpec extends BaseSpec { private val secondExecutor = ZLayer .makeSome[DockerComposeContainer, Redis]( - singleNodeConfig(BaseSpec.SingleNode1), + singleNodeConfig(IntegrationSpec.SingleNode1), ZLayer.succeed[CodecSupplier](ProtobufCodecSupplier), Redis.singleNode ) diff --git a/modules/redis-it/src/test/scala/zio/redis/ListSpec.scala b/modules/redis-it/src/test/scala/zio/redis/ListSpec.scala index b6161449e..fa6ecfd38 100644 --- a/modules/redis-it/src/test/scala/zio/redis/ListSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/ListSpec.scala @@ -8,7 +8,7 @@ import zio.test._ import java.util.concurrent.TimeUnit -trait ListSpec extends BaseSpec { +trait ListSpec extends IntegrationSpec { def listSuite: Spec[Redis, Any] = suite("lists")( suite("pop")( @@ -341,6 +341,14 @@ trait ListSpec extends BaseSpec { range <- redis.lRange(key, 0 to 1).returning[String] } yield assert(range)(equalTo(Chunk("hello", "world"))) }, + test("lRange elements by exclusive range") { + for { + redis <- ZIO.service[Redis] + key <- uuid + _ <- redis.lPush(key, "c", "b", "a") + range <- redis.lRange(key, 0 until 2).returning[String] + } yield assert(range)(equalTo(Chunk("a", "b"))) + }, test("lRange two elements negative indices") { for { redis <- ZIO.service[Redis] @@ -349,6 +357,14 @@ trait ListSpec extends BaseSpec { range <- redis.lRange(key, -2 to -1).returning[String] } yield assert(range)(equalTo(Chunk("hello", "world"))) }, + test("lRange elements by exclusive range with negative indices") { + for { + redis <- ZIO.service[Redis] + key <- uuid + _ <- redis.lPush(key, "d", "c", "b", "a") + range <- redis.lRange(key, -3 until -1).returning[String] + } yield assert(range)(equalTo(Chunk("b", "c"))) + }, test("lRange start out of bounds") { for { redis <- ZIO.service[Redis] diff --git a/modules/redis-it/src/test/scala/zio/redis/PubSubSpec.scala b/modules/redis-it/src/test/scala/zio/redis/PubSubSpec.scala index 92ffa4d91..5c10e2166 100644 --- a/modules/redis-it/src/test/scala/zio/redis/PubSubSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/PubSubSpec.scala @@ -7,7 +7,7 @@ import zio.{Promise, ZIO} import scala.util.Random -trait PubSubSpec extends BaseSpec { +trait PubSubSpec extends IntegrationSpec { def pubSubSuite: Spec[Redis with RedisSubscription, RedisError] = suite("pubSubs")( suite("subscribe")( diff --git a/modules/redis-it/src/test/scala/zio/redis/ScriptingSpec.scala b/modules/redis-it/src/test/scala/zio/redis/ScriptingSpec.scala index faa7fb8a7..33d4eff1c 100644 --- a/modules/redis-it/src/test/scala/zio/redis/ScriptingSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/ScriptingSpec.scala @@ -11,7 +11,7 @@ import zio.test._ import scala.util.Random -trait ScriptingSpec extends BaseSpec { +trait ScriptingSpec extends IntegrationSpec { def scriptingSpec: Spec[Redis, RedisError] = suite("scripting")( suite("eval")( diff --git a/modules/redis-it/src/test/scala/zio/redis/SetsSpec.scala b/modules/redis-it/src/test/scala/zio/redis/SetsSpec.scala index a0adb0710..75e801afe 100644 --- a/modules/redis-it/src/test/scala/zio/redis/SetsSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/SetsSpec.scala @@ -6,7 +6,7 @@ import zio.stream.ZStream import zio.test.Assertion._ import zio.test._ -trait SetsSpec extends BaseSpec { +trait SetsSpec extends IntegrationSpec { def setsSuite: Spec[Redis, RedisError] = suite("sets")( suite("sAdd")( diff --git a/modules/redis-it/src/test/scala/zio/redis/SortedSetsSpec.scala b/modules/redis-it/src/test/scala/zio/redis/SortedSetsSpec.scala index fdb7cc657..5e1b72685 100644 --- a/modules/redis-it/src/test/scala/zio/redis/SortedSetsSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/SortedSetsSpec.scala @@ -6,7 +6,7 @@ import zio.stream.ZStream import zio.test.Assertion._ import zio.test._ -trait SortedSetsSpec extends BaseSpec { +trait SortedSetsSpec extends IntegrationSpec { def sortedSetsSuite: Spec[Redis, RedisError] = suite("sorted sets")( suite("bzPopMax")( @@ -28,6 +28,21 @@ trait SortedSetsSpec extends BaseSpec { result <- redis.bzPopMax(duration, key1, key2, key3).returning[String] } yield assert(result)(isSome(equalTo((key1, tokyo)))) ), + test("infinity score in set")( + for { + redis <- ZIO.service[Redis] + key1 <- uuid + key2 <- uuid + duration = Duration.fromMillis(1000) + delhi = MemberScore("Delhi", 1d) + london = MemberScore("London", 3d) + tokyo = MemberScore("Tokyo", 5d) + edge = MemberScore("The edge of universe", Double.PositiveInfinity) + _ <- redis.zAdd(key1)(delhi, edge) + _ <- redis.zAdd(key2)(london, tokyo) + result <- redis.bzPopMax(duration, key1, key2).returning[String] + } yield assert(result)(isSome(equalTo((key1, edge)))) + ), test("empty set")( for { redis <- ZIO.service[Redis] @@ -53,6 +68,21 @@ trait SortedSetsSpec extends BaseSpec { result <- redis.bzPopMin(duration, key1, key2, key3).returning[String] } yield assert(result)(isSome(equalTo((key2, delhi)))) ), + test("negative infinity score in set")( + for { + redis <- ZIO.service[Redis] + key1 <- uuid + key2 <- uuid + duration = Duration.fromMillis(1000) + delhi = MemberScore("Delhi", 1d) + london = MemberScore("London", 3d) + paris = MemberScore("Paris", 4d) + quark = MemberScore("Quark", Double.NegativeInfinity) + _ <- redis.zAdd(key1)(delhi, quark) + _ <- redis.zAdd(key2)(london, paris) + result <- redis.bzPopMin(duration, key1, key2).returning[String] + } yield assert(result)(isSome(equalTo((key1, quark)))) + ), test("empty set")( for { redis <- ZIO.service[Redis] @@ -94,7 +124,18 @@ trait SortedSetsSpec extends BaseSpec { for { redis <- ZIO.service[Redis] key <- uuid - added <- redis.zAdd(key)(MemberScore("a", 1d), MemberScore("b", 2d), MemberScore("c", 3d)) + added <- redis.zAdd(key)(MemberScore("a", 1d), MemberScore("b", 3.1415e50), MemberScore("c", 3d)) + } yield assert(added)(equalTo(3L)) + }, + test("multiple elements with negative & positive infinity") { + for { + redis <- ZIO.service[Redis] + key <- uuid + added <- redis.zAdd(key)( + MemberScore("neg infinity", Double.NegativeInfinity), + MemberScore("a", 1d), + MemberScore("pos infinity", Double.PositiveInfinity) + ) } yield assert(added)(equalTo(3L)) }, test("error when not set") { @@ -831,7 +872,7 @@ trait SortedSetsSpec extends BaseSpec { } yield assert(result.toList)(isEmpty)) ), suite("zRange")( - test("non-empty set") { + test("all members for non-empty set") { for { redis <- ZIO.service[Redis] key <- uuid @@ -840,9 +881,54 @@ trait SortedSetsSpec extends BaseSpec { london = MemberScore("London", 3d) paris = MemberScore("Paris", 4d) tokyo = MemberScore("Tokyo", 5d) - _ <- redis.zAdd(key)(delhi, mumbai, london, tokyo, paris) + edge = MemberScore("The edge of universe", Double.PositiveInfinity) + quark = MemberScore("Quark", Double.NegativeInfinity) + _ <- redis.zAdd(key)(edge, delhi, mumbai, london, tokyo, paris, quark) result <- redis.zRange(key, 0 to -1).returning[String] - } yield assert(result.toList)(equalTo(List("Delhi", "Mumbai", "London", "Paris", "Tokyo"))) + } yield assert(result.toList)( + equalTo( + List("Quark", "Delhi", "Mumbai", "London", "Paris", "Tokyo", "The edge of universe") + ) + ) + }, + test("some members for non-empty set by inclusive range with negative bounds") { + for { + redis <- ZIO.service[Redis] + key <- uuid + delhi = MemberScore("Delhi", 1d) + mumbai = MemberScore("Mumbai", 2d) + london = MemberScore("London", 3d) + paris = MemberScore("Paris", 4d) + tokyo = MemberScore("Tokyo", 5d) + _ <- redis.zAdd(key)(delhi, mumbai, london, tokyo, paris) + result <- redis.zRange(key, -3 to -2).returning[String] + } yield assert(result.toList)(equalTo(List("London", "Paris"))) + }, + test("some members for non-empty set by inclusive range with positive bounds") { + for { + redis <- ZIO.service[Redis] + key <- uuid + delhi = MemberScore("Delhi", 1d) + mumbai = MemberScore("Mumbai", 2d) + london = MemberScore("London", 3d) + paris = MemberScore("Paris", 4d) + tokyo = MemberScore("Tokyo", 5d) + _ <- redis.zAdd(key)(delhi, mumbai, london, tokyo, paris) + result <- redis.zRange(key, 0 to 2).returning[String] + } yield assert(result.toList)(equalTo(List("Delhi", "Mumbai", "London"))) + }, + test("some members for non-empty set by exclusive range") { + for { + redis <- ZIO.service[Redis] + key <- uuid + delhi = MemberScore("Delhi", 1d) + mumbai = MemberScore("Mumbai", 2d) + london = MemberScore("London", 3d) + paris = MemberScore("Paris", 4d) + tokyo = MemberScore("Tokyo", 5d) + _ <- redis.zAdd(key)(delhi, mumbai, london, tokyo, paris) + result <- redis.zRange(key, 1 until -2).returning[String] + } yield assert(result.toList)(equalTo(List("Mumbai", "London"))) }, test("empty set") { for { @@ -862,10 +948,12 @@ trait SortedSetsSpec extends BaseSpec { london = MemberScore("London", 3d) paris = MemberScore("Paris", 4d) tokyo = MemberScore("Tokyo", 5d) - _ <- redis.zAdd(key)(delhi, mumbai, london, tokyo, paris) + edge = MemberScore("The edge of universe", Double.PositiveInfinity) + quark = MemberScore("Quark", Double.NegativeInfinity) + _ <- redis.zAdd(key)(edge, delhi, mumbai, quark, london, tokyo, paris) result <- redis.zRangeWithScores(key, 0 to -1).returning[String] } yield assert(result.toList)( - equalTo(List(delhi, mumbai, london, paris, tokyo)) + equalTo(List(quark, delhi, mumbai, london, paris, tokyo, edge)) ) }, test("empty set") { @@ -1398,6 +1486,18 @@ trait SortedSetsSpec extends BaseSpec { members <- scanAll(key) } yield assert(members)(equalTo(Chunk(a, b, c))) }, + test("with infinity in set") { + for { + redis <- ZIO.service[Redis] + key <- uuid + a = MemberScore("a", 1d) + b = MemberScore("b", 2d) + inf = MemberScore("inf", Double.PositiveInfinity) + negInf = MemberScore("neg inf", Double.NegativeInfinity) + _ <- redis.zAdd(key)(a, b, inf, negInf) + members <- scanAll(key) + } yield assert(members)(equalTo(Chunk(negInf, a, b, inf))) + }, test("empty set") { for { redis <- ZIO.service[Redis] @@ -1470,6 +1570,14 @@ trait SortedSetsSpec extends BaseSpec { result <- redis.zScore(key, "Delhi") } yield assert(result)(isSome(equalTo(10.0))) }, + test("infinity score in set") { + for { + redis <- ZIO.service[Redis] + key <- uuid + _ <- redis.zAdd(key)(MemberScore("Delhi", 10d), MemberScore("Infinity", Double.PositiveInfinity)) + result <- redis.zScore(key, "Infinity") + } yield assert(result)(isSome(equalTo(Double.PositiveInfinity))) + }, test("empty set") { for { redis <- ZIO.service[Redis] @@ -1499,6 +1607,27 @@ trait SortedSetsSpec extends BaseSpec { key <- uuid result <- redis.zMScore(key, "Hyderabad") } yield assert(result)(equalTo(Chunk(None))) + }, + test("infinity score") { + for { + redis <- ZIO.service[Redis] + key <- uuid + _ <- redis.zAdd(key)( + MemberScore("Delhi", 10d), + MemberScore("Infinity", Double.PositiveInfinity), + MemberScore("-Infinity", Double.NegativeInfinity) + ) + result <- redis.zMScore(key, "Infinity", "-Infinity", "Delhi", "Ankh-Morpork") + } yield assert(result)( + equalTo( + Chunk( + Some(Double.PositiveInfinity), + Some(Double.NegativeInfinity), + Some(10d), + None + ) + ) + ) } ), suite("zUnion")( diff --git a/modules/redis-it/src/test/scala/zio/redis/StreamsSpec.scala b/modules/redis-it/src/test/scala/zio/redis/StreamsSpec.scala index db8e8a594..01a57e1b9 100644 --- a/modules/redis-it/src/test/scala/zio/redis/StreamsSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/StreamsSpec.scala @@ -6,7 +6,7 @@ import zio.test.Assertion._ import zio.test.TestAspect.{flaky, ignore} import zio.test._ -trait StreamsSpec extends BaseSpec { +trait StreamsSpec extends IntegrationSpec { def streamsSuite: Spec[Redis, RedisError] = suite("streams")( suite("xAck")( diff --git a/modules/redis-it/src/test/scala/zio/redis/StringsSpec.scala b/modules/redis-it/src/test/scala/zio/redis/StringsSpec.scala index e70cb2970..48ddd36b1 100644 --- a/modules/redis-it/src/test/scala/zio/redis/StringsSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/StringsSpec.scala @@ -6,7 +6,7 @@ import zio.test.Assertion.{exists => _, _} import zio.test.TestAspect.{flaky, ignore} import zio.test._ -trait StringsSpec extends BaseSpec { +trait StringsSpec extends IntegrationSpec { def stringsSuite: Spec[Redis, RedisError] = suite("strings")( suite("append")( @@ -77,10 +77,18 @@ trait StringsSpec extends BaseSpec { for { redis <- ZIO.service[Redis] key <- uuid - _ <- redis.set(key, "value") + _ <- redis.set(key, "value") // "alu" => 01100001 01101100 01110101 count <- redis.bitCount(key, Some(1 to 3)) } yield assert(count)(equalTo(12L)) }, + test("over non-empty string with exclusive range") { + for { + redis <- ZIO.service[Redis] + key <- uuid + _ <- redis.set(key, "value") + count <- redis.bitCount(key, Some(1 until 4)) + } yield assert(count)(equalTo(12L)) + }, test("over non-empty string with range that is too large") { for { redis <- ZIO.service[Redis] @@ -1047,6 +1055,14 @@ trait StringsSpec extends BaseSpec { substr <- redis.getRange(key, 1 to 3).returning[String] } yield assert(substr)(isSome(equalTo("alu"))) }, + test("from non-empty string by exclusive range") { + for { + redis <- ZIO.service[Redis] + key <- uuid + _ <- redis.set(key, "value") + substr <- redis.getRange(key, 1 until 3).returning[String] + } yield assert(substr)(isSome(equalTo("al"))) + }, test("with range that exceeds non-empty string length") { for { redis <- ZIO.service[Redis] diff --git a/modules/redis-it/src/test/scala/zio/redis/internal/ClusterExecutorSpec.scala b/modules/redis-it/src/test/scala/zio/redis/internal/ClusterExecutorSpec.scala index 319900239..b470553eb 100644 --- a/modules/redis-it/src/test/scala/zio/redis/internal/ClusterExecutorSpec.scala +++ b/modules/redis-it/src/test/scala/zio/redis/internal/ClusterExecutorSpec.scala @@ -6,7 +6,7 @@ import zio.redis.options.Cluster.{Slot, SlotsAmount} import zio.test.TestAspect.{flaky, ifEnvNotSet} import zio.test._ -object ClusterExecutorSpec extends BaseSpec { +object ClusterExecutorSpec extends IntegrationSpec { def spec: Spec[TestEnvironment, Any] = suite("Cluster executor")( test("check cluster responsiveness when ASK redirect happens") { @@ -62,7 +62,7 @@ object ClusterExecutorSpec extends BaseSpec { } ).provideShared( Redis.cluster, - compose(service(BaseSpec.MasterNode, ".*Cluster correctly created.*")), + compose(service(IntegrationSpec.MasterNode, ".*Cluster correctly created.*")), masterNodeConfig, ZLayer.succeed(ProtobufCodecSupplier) ) @@ flaky @@ ifEnvNotSet("CI") diff --git a/modules/redis/src/main/scala/zio/redis/Input.scala b/modules/redis/src/main/scala/zio/redis/Input.scala index 2d405152b..5cde6684b 100644 --- a/modules/redis/src/main/scala/zio/redis/Input.scala +++ b/modules/redis/src/main/scala/zio/redis/Input.scala @@ -337,8 +337,14 @@ object Input { } final case class MemberScoreInput[M: BinaryCodec]() extends Input[MemberScore[M]] { - def encode(data: MemberScore[M]): RespCommand = - RespCommand(RespCommandArgument.Value(data.score.toString), RespCommandArgument.Value(data.member)) + def encode(data: MemberScore[M]): RespCommand = { + val score = data.score match { + case Double.NegativeInfinity => "-inf" + case Double.PositiveInfinity => "+inf" + case d: Double => d.toString.toLowerCase + } + RespCommand(RespCommandArgument.Value(score), RespCommandArgument.Value(data.member)) + } } case object NoAckInput extends Input[NoAck] { @@ -381,8 +387,10 @@ object Input { } case object RangeInput extends Input[Range] { - def encode(data: Range): RespCommand = - RespCommand(RespCommandArgument.Value(data.start.toString), RespCommandArgument.Value(data.end.toString)) + def encode(data: Range): RespCommand = { + val end = if (data.isInclusive) data.end else data.end - 1 + RespCommand(RespCommandArgument.Value(data.start.toString), RespCommandArgument.Value(end.toString)) + } } case object RankInput extends Input[Rank] { diff --git a/modules/redis/src/main/scala/zio/redis/Output.scala b/modules/redis/src/main/scala/zio/redis/Output.scala index 254b32a20..e72f9811b 100644 --- a/modules/redis/src/main/scala/zio/redis/Output.scala +++ b/modules/redis/src/main/scala/zio/redis/Output.scala @@ -101,7 +101,7 @@ object Output { val host = MultiStringOutput.unsafeDecode(values(0)) val port = LongOutput.unsafeDecode(values(1)) val nodeId = MultiStringOutput.unsafeDecode(values(2)) - Node(nodeId, RedisUri(s"$host:$port")) + Node(nodeId, RedisUri(host, port.toInt)) case other => throw ProtocolError(s"$other isn't an array") } } @@ -128,6 +128,14 @@ object Output { } } + case object DoubleOrInfinity extends Output[Double] { + protected def tryDecode(respValue: RespValue): Double = + respValue match { + case RespValue.BulkString(bytes) => decodeDouble(bytes, withInfinity = true) + case other => throw ProtocolError(s"$other isn't a double or an infinity.") + } + } + private object DurationOutput extends Output[Long] { protected def tryDecode(respValue: RespValue): Long = respValue match { @@ -729,11 +737,19 @@ object Output { } } - private def decodeDouble(bytes: Chunk[Byte]): Double = { + private def decodeDouble(bytes: Chunk[Byte], withInfinity: Boolean = false): Double = { val text = new String(bytes.toArray, StandardCharsets.UTF_8) - try text.toDouble - catch { - case _: NumberFormatException => throw ProtocolError(s"'$text' isn't a double.") + text match { + case "inf" if withInfinity => Double.PositiveInfinity + case "-inf" if withInfinity => Double.NegativeInfinity + case _ => + try text.toDouble + catch { + case _: NumberFormatException => + throw ProtocolError( + if (withInfinity) s"'$text' isn't a double or an infinity." else s"'$text' isn't a double." + ) + } } } diff --git a/modules/redis/src/main/scala/zio/redis/RedisUri.scala b/modules/redis/src/main/scala/zio/redis/RedisUri.scala index bb253a645..107da063f 100644 --- a/modules/redis/src/main/scala/zio/redis/RedisUri.scala +++ b/modules/redis/src/main/scala/zio/redis/RedisUri.scala @@ -26,6 +26,9 @@ object RedisUri { val splitting = hostAndPort.split(':') val host = splitting(0) val port = splitting(1).toInt - RedisUri(s"$host:$port") + RedisUri(host, port, ssl = false, sni = None) } + + def apply(host: String, port: Int): RedisUri = + RedisUri(host, port, ssl = false, sni = None) } diff --git a/modules/redis/src/main/scala/zio/redis/api/SortedSets.scala b/modules/redis/src/main/scala/zio/redis/api/SortedSets.scala index 222bce133..98516baf6 100644 --- a/modules/redis/src/main/scala/zio/redis/api/SortedSets.scala +++ b/modules/redis/src/main/scala/zio/redis/api/SortedSets.scala @@ -39,7 +39,8 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { * @return * A three-element Chunk with the first element being the name of the key where a member was popped, the second * element is the popped member itself, and the third element is the score of the popped element. An empty chunk is - * returned when no element could be popped and the timeout expired. + * returned when no element could be popped and the timeout expired. Double.PositiveInfinity and + * Double.NegativeInfinity are valid scores as well. */ final def bzPopMax[K: Schema]( timeout: Duration, @@ -49,7 +50,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { new ResultBuilder1[({ type lambda[x] = Option[(K, MemberScore[x])] })#lambda, G] { def returning[M: Schema]: G[Option[(K, MemberScore[M])]] = { val memberScoreOutput = - Tuple3Output(ArbitraryOutput[K](), ArbitraryOutput[M](), DoubleOutput).map { case (k, m, s) => + Tuple3Output(ArbitraryOutput[K](), ArbitraryOutput[M](), DoubleOrInfinity).map { case (k, m, s) => (k, MemberScore(m, s)) } @@ -76,7 +77,8 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { * @return * A three-element Chunk with the first element being the name of the key where a member was popped, the second * element is the popped member itself, and the third element is the score of the popped element. An empty chunk is - * returned when no element could be popped and the timeout expired. + * returned when no element could be popped and the timeout expired. Double.PositiveInfinity and + * Double.NegativeInfinity are valid scores as well. */ final def bzPopMin[K: Schema]( timeout: Duration, @@ -86,7 +88,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { new ResultBuilder1[({ type lambda[x] = Option[(K, MemberScore[x])] })#lambda, G] { def returning[M: Schema]: G[Option[(K, MemberScore[M])]] = { val memberScoreOutput = - Tuple3Output(ArbitraryOutput[K](), ArbitraryOutput[M](), DoubleOutput).map { case (k, m, s) => + Tuple3Output(ArbitraryOutput[K](), ArbitraryOutput[M](), DoubleOrInfinity).map { case (k, m, s) => (k, MemberScore(m, s)) } @@ -248,7 +250,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { NonEmptyList(ArbitraryKeyInput[K]()), WithScoresInput ), - ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput) + ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity) .map(_.map { case (m, s) => MemberScore(m, s) }) ) command.run((keys.size + 1, (key, keys.toList), WithScores)) @@ -366,7 +368,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { OptionalInput(WeightsInput), WithScoresInput ), - ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput) + ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity) .map(_.map { case (m, s) => MemberScore(m, s) }) ) command.run((keys.size + 1, (key, keys.toList), aggregate, weights, WithScores)) @@ -437,10 +439,11 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { * Keys of the rest sets * @return * List of scores or None associated with the specified member values (a double precision floating point number). + * Double.PositiveInfinity and Double.NegativeInfinity are valid scores as well. */ final def zMScore[K: Schema](key: K, keys: K*): G[Chunk[Option[Double]]] = { val command = - RedisCommand(ZMScore, NonEmptyList(ArbitraryKeyInput[K]()), ChunkOutput(OptionalOutput(DoubleOutput))) + RedisCommand(ZMScore, NonEmptyList(ArbitraryKeyInput[K]()), ChunkOutput(OptionalOutput(DoubleOrInfinity))) command.run((key, keys.toList)) } @@ -462,7 +465,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { val command = RedisCommand( ZPopMax, Tuple2(ArbitraryKeyInput[K](), OptionalInput(LongInput)), - ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput) + ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity) .map(_.map { case (m, s) => MemberScore(m, s) }) ) command.run((key, count)) @@ -487,7 +490,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { val command = RedisCommand( ZPopMin, Tuple2(ArbitraryKeyInput[K](), OptionalInput(LongInput)), - ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput) + ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity) .map(_.map { case (m, s) => MemberScore(m, s) }) ) command.run((key, count)) @@ -550,7 +553,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { val command = RedisCommand( ZRandMember, Tuple3(ArbitraryKeyInput[K](), LongInput, WithScoresInput), - ZRandMemberTuple2Output(ArbitraryOutput[M](), DoubleOutput) + ZRandMemberTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity) .map(_.map { case (m, s) => MemberScore(m, s) }) ) @@ -593,7 +596,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { val command = RedisCommand( ZRange, Tuple3(ArbitraryKeyInput[K](), RangeInput, WithScoresInput), - ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput) + ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity) .map(_.map { case (m, s) => MemberScore(m, s) }) ) command.run((key, range, WithScores)) @@ -693,7 +696,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { WithScoresInput, OptionalInput(LimitInput) ), - ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput) + ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity) .map(_.map { case (m, s) => MemberScore(m, s) }) ) command.run((key, scoreRange.min.asString, scoreRange.max.asString, WithScores, limit)) @@ -735,7 +738,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { RedisCommand( ZRank, Tuple3(ArbitraryKeyInput[K](), ArbitraryValueInput[M](), WithScoreInput), - OptionalOutput(Tuple2Output(LongOutput, DoubleOutput).map { case (r, s) => RankScore(r, s) }) + OptionalOutput(Tuple2Output(LongOutput, DoubleOrInfinity).map { case (r, s) => RankScore(r, s) }) ) command.run((key, member, WithScore)) } @@ -849,7 +852,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { val command = RedisCommand( ZRevRange, Tuple3(ArbitraryKeyInput[K](), RangeInput, WithScoresInput), - ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput) + ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity) .map(_.map { case (m, s) => MemberScore(m, s) }) ) command.run((key, range, WithScores)) @@ -953,7 +956,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { WithScoresInput, OptionalInput(LimitInput) ), - ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput) + ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity) .map(_.map { case (m, s) => MemberScore(m, s) }) ) command.run((key, scoreRange.max.asString, scoreRange.min.asString, WithScores, limit)) @@ -993,7 +996,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { val command = RedisCommand( ZRevRank, Tuple3(ArbitraryKeyInput[K](), ArbitraryValueInput[M](), WithScoreInput), - OptionalOutput(Tuple2Output(LongOutput, DoubleOutput).map { case (r, s) => RankScore(r, s) }) + OptionalOutput(Tuple2Output(LongOutput, DoubleOrInfinity).map { case (r, s) => RankScore(r, s) }) ) command.run((key, member, WithScore)) } @@ -1021,7 +1024,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { new ResultBuilder1[({ type lambda[x] = (Long, MemberScores[x]) })#lambda, G] { def returning[M: Schema]: G[(Long, Chunk[MemberScore[M]])] = { val memberScoresOutput = - ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput).map(_.map { case (m, s) => MemberScore(m, s) }) + ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity).map(_.map { case (m, s) => MemberScore(m, s) }) val command = RedisCommand( @@ -1042,13 +1045,14 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { * @param member * Member of sorted set * @return - * The score of member (a double precision floating point number. + * The score of member (a double precision floating point number). + * Double.PositiveInfinity and Double.NegativeInfinity are valid scores as well. */ final def zScore[K: Schema, M: Schema](key: K, member: M): G[Option[Double]] = { val command = RedisCommand( ZScore, Tuple2(ArbitraryKeyInput[K](), ArbitraryValueInput[M]()), - OptionalOutput(DoubleOutput) + OptionalOutput(DoubleOrInfinity) ) command.run((key, member)) } @@ -1122,7 +1126,7 @@ trait SortedSets[G[+_]] extends RedisEnvironment[G] { OptionalInput(AggregateInput), WithScoresInput ), - ChunkTuple2Output(ArbitraryOutput[M](), DoubleOutput) + ChunkTuple2Output(ArbitraryOutput[M](), DoubleOrInfinity) .map(_.map { case (m, s) => MemberScore(m, s) }) ) command.run((keys.size + 1, (key, keys.toList), weights, aggregate, WithScores)) diff --git a/modules/redis/src/test/scala/zio/redis/InputSpec.scala b/modules/redis/src/test/scala/zio/redis/InputSpec.scala index eed3c3cbf..d753f895f 100644 --- a/modules/redis/src/test/scala/zio/redis/InputSpec.scala +++ b/modules/redis/src/test/scala/zio/redis/InputSpec.scala @@ -552,6 +552,11 @@ object InputSpec extends BaseSpec { result <- ZIO.attempt(MemberScoreInput[String]().encode(MemberScore("", 4.2d))) } yield assert(result)(equalTo(RespCommand(Value("4.2"), Value("")))) }, + test("with positive score in scientific notation and non-empty member") { + for { + result <- ZIO.attempt(MemberScoreInput[String]().encode(MemberScore("member", 3.141592e100))) + } yield assert(result)(equalTo(RespCommand(Value("3.141592e100"), Value("member")))) + }, test("with negative score and empty member") { for { result <- ZIO.attempt(MemberScoreInput[String]().encode(MemberScore("", -4.2d))) @@ -576,6 +581,16 @@ object InputSpec extends BaseSpec { for { result <- ZIO.attempt(MemberScoreInput[String]().encode(MemberScore("member", 0d))) } yield assert(result)(equalTo(RespCommand(Value("0.0"), Value("member")))) + }, + test("with positive infinity score and non-empty member") { + for { + result <- ZIO.attempt(MemberScoreInput[String]().encode(MemberScore("member", Double.PositiveInfinity))) + } yield assert(result)(equalTo(RespCommand(Value("+inf"), Value("member")))) + }, + test("with negative infinity score and non-empty member") { + for { + result <- ZIO.attempt(MemberScoreInput[String]().encode(MemberScore("member", Double.NegativeInfinity))) + } yield assert(result)(equalTo(RespCommand(Value("-inf"), Value("member")))) } ), suite("NoInput")( @@ -634,23 +649,43 @@ object InputSpec extends BaseSpec { suite("Range")( test("with positive start and positive end") { for { - result <- ZIO.attempt(RangeInput.encode(Range(1, 5))) + result <- ZIO.attempt(RangeInput.encode(1 to 5)) } yield assert(result)(equalTo(RespCommand(Value("1"), Value("5")))) }, test("with negative start and positive end") { for { - result <- ZIO.attempt(RangeInput.encode(Range(-1, 5))) + result <- ZIO.attempt(RangeInput.encode(-1 to 5)) } yield assert(result)(equalTo(RespCommand(Value("-1"), Value("5")))) }, test("with positive start and negative end") { for { - result <- ZIO.attempt(RangeInput.encode(Range(1, -5))) + result <- ZIO.attempt(RangeInput.encode(1 to -5)) } yield assert(result)(equalTo(RespCommand(Value("1"), Value("-5")))) }, test("with negative start and negative end") { for { - result <- ZIO.attempt(RangeInput.encode(Range(-1, -5))) + result <- ZIO.attempt(RangeInput.encode(-1 to -5)) } yield assert(result)(equalTo(RespCommand(Value("-1"), Value("-5")))) + }, + test("with positive start and exclusive positive end") { + for { + result <- ZIO.attempt(RangeInput.encode(1 until 3)) + } yield assert(result)(equalTo(RespCommand(Value("1"), Value("2")))) + }, + test("with positive start and exclusive negative end") { + for { + result <- ZIO.attempt(RangeInput.encode(1 until -1)) + } yield assert(result)(equalTo(RespCommand(Value("1"), Value("-2")))) + }, + test("with negative start and exclusive positive end") { + for { + result <- ZIO.attempt(RangeInput.encode(-5 until 8)) + } yield assert(result)(equalTo(RespCommand(Value("-5"), Value("7")))) + }, + test("with negative start and exclusive negative end") { + for { + result <- ZIO.attempt(RangeInput.encode(-5 until -3)) + } yield assert(result)(equalTo(RespCommand(Value("-5"), Value("-4")))) } ), suite("Pattern")( diff --git a/modules/redis/src/test/scala/zio/redis/OutputSpec.scala b/modules/redis/src/test/scala/zio/redis/OutputSpec.scala index 39b141cc8..81a0588a3 100644 --- a/modules/redis/src/test/scala/zio/redis/OutputSpec.scala +++ b/modules/redis/src/test/scala/zio/redis/OutputSpec.scala @@ -5,6 +5,7 @@ import zio.redis.Output._ import zio.redis.RedisError._ import zio.redis.internal.PubSub.{PushMessage, SubscriptionKey} import zio.redis.internal.RespValue +import zio.redis.options.Cluster import zio.test.Assertion._ import zio.test._ @@ -55,6 +56,12 @@ object OutputSpec extends BaseSpec { res <- ZIO.attempt(DoubleOutput.unsafeDecode(RespValue.bulkString(num.toString))) } yield assert(res)(equalTo(num)) }, + test("extract numbers in scientific notation") { + val num = 42.321e100d + for { + res <- ZIO.attempt(DoubleOutput.unsafeDecode(RespValue.bulkString(num.toString.toLowerCase))) + } yield assert(res)(equalTo(num)) + }, test("report number format exceptions as protocol errors") { val bad = "ok" for { @@ -62,6 +69,36 @@ object OutputSpec extends BaseSpec { } yield assert(res)(isLeft(equalTo(ProtocolError(s"'$bad' isn't a double.")))) } ), + suite("double or infinity")( + test("extract numbers") { + val num = 42.3 + for { + res <- ZIO.attempt(DoubleOrInfinity.unsafeDecode(RespValue.bulkString(num.toString))) + } yield assert(res)(equalTo(num)) + }, + test("extract numbers in scientific notation") { + val num = 42.321e100d + for { + res <- ZIO.attempt(DoubleOrInfinity.unsafeDecode(RespValue.bulkString(num.toString.toLowerCase))) + } yield assert(res)(equalTo(num)) + }, + test("extract infinity") { + for { + res <- ZIO.attempt(DoubleOrInfinity.unsafeDecode(RespValue.bulkString("inf"))) + } yield assert(res)(equalTo(Double.PositiveInfinity)) + }, + test("extract negative infinity") { + for { + res <- ZIO.attempt(DoubleOrInfinity.unsafeDecode(RespValue.bulkString("-inf"))) + } yield assert(res)(equalTo(Double.NegativeInfinity)) + }, + test("report number format exceptions as protocol errors") { + val bad = "ok" + for { + res <- ZIO.attempt(DoubleOrInfinity.unsafeDecode(RespValue.bulkString(bad))).either + } yield assert(res)(isLeft(equalTo(ProtocolError(s"'$bad' isn't a double or an infinity.")))) + } + ), suite("durations")( suite("milliseconds")( test("extract milliseconds") { @@ -992,6 +1029,136 @@ object OutputSpec extends BaseSpec { assertZIO(ZIO.attempt(PushMessageOutput.unsafeDecode(input)))(equalTo(expected)) } + ), + suite("ClusterPartition")( + test("3 masters cluster") { + val response = + RespValue.array( + RespValue.array( + RespValue.Integer(0L), + RespValue.Integer(5460L), + RespValue.array( + RespValue.bulkString("127.0.0.1"), + RespValue.Integer(6379L), + RespValue.bulkString("node1"), + RespValue.array() + ) + ), + RespValue.array( + RespValue.Integer(5461L), + RespValue.Integer(10922L), + RespValue.array( + RespValue.bulkString("127.0.0.2"), + RespValue.Integer(6379L), + RespValue.bulkString("node2"), + RespValue.array() + ) + ), + RespValue.array( + RespValue.Integer(10923L), + RespValue.Integer(16383L), + RespValue.array( + RespValue.bulkString("127.0.0.3"), + RespValue.Integer(6379L), + RespValue.bulkString("node3"), + RespValue.array() + ) + ) + ) + + val expected = Chunk( + Cluster.Partition( + Cluster.SlotRange(0L, 5460L), + Cluster.Node("node1", RedisUri("127.0.0.1", 6379)), + slaves = Chunk.empty + ), + Cluster.Partition( + Cluster.SlotRange(5461L, 10922L), + Cluster.Node("node2", RedisUri("127.0.0.2", 6379)), + slaves = Chunk.empty + ), + Cluster.Partition( + Cluster.SlotRange(10923L, 16383L), + Cluster.Node("node3", RedisUri("127.0.0.3", 6379)), + slaves = Chunk.empty + ) + ) + + assertZIO(ZIO.attempt(ChunkOutput(ClusterPartitionOutput).unsafeDecode(response)))(hasSameElements(expected)) + }, + test("3 masters with 2 replicas cluster") { + val response = + RespValue.array( + RespValue.array( + RespValue.Integer(0L), + RespValue.Integer(5460L), + RespValue.array( + RespValue.bulkString("127.0.0.1"), + RespValue.Integer(6379L), + RespValue.bulkString("node1"), + RespValue.array() + ), + RespValue.array( + RespValue.bulkString("127.0.1.1"), + RespValue.Integer(6379L), + RespValue.bulkString("replica1"), + RespValue.array() + ) + ), + RespValue.array( + RespValue.Integer(5461L), + RespValue.Integer(10922L), + RespValue.array( + RespValue.bulkString("127.0.0.2"), + RespValue.Integer(6379L), + RespValue.bulkString("node2"), + RespValue.array() + ), + RespValue.array( + RespValue.bulkString("127.0.1.2"), + RespValue.Integer(6379L), + RespValue.bulkString("replica2"), + RespValue.array() + ) + ), + RespValue.array( + RespValue.Integer(10923L), + RespValue.Integer(16383L), + RespValue.array( + RespValue.bulkString("127.0.0.3"), + RespValue.Integer(6379L), + RespValue.bulkString("node3"), + RespValue.array() + ), + RespValue.array( + RespValue.bulkString("127.0.1.3"), + RespValue.Integer(6379L), + RespValue.bulkString("replica3"), + RespValue.array() + ) + ) + ) + + val expected = Chunk( + Cluster.Partition( + Cluster.SlotRange(0L, 5460L), + Cluster.Node("node1", RedisUri("127.0.0.1", 6379)), + slaves = Chunk.single(Cluster.Node("replica1", RedisUri("127.0.1.1", 6379))) + ), + Cluster.Partition( + Cluster.SlotRange(5461L, 10922L), + Cluster.Node("node2", RedisUri("127.0.0.2", 6379)), + slaves = Chunk.single(Cluster.Node("replica2", RedisUri("127.0.1.2", 6379))) + ), + Cluster.Partition( + Cluster.SlotRange(10923L, 16383L), + Cluster.Node("node3", RedisUri("127.0.0.3", 6379)), + slaves = Chunk.single(Cluster.Node("replica3", RedisUri("127.0.1.3", 6379))) + ) + ) + + assertZIO(ZIO.attempt(ChunkOutput(ClusterPartitionOutput).unsafeDecode(response)))(hasSameElements(expected)) + } ) ) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index cb27e26cc..82e9c2c27 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -5,13 +5,13 @@ object Dependencies { val CatsEffect = "3.5.4" val EmbeddedRedis = "0.6" val Redis4Cats = "1.7.1" - val Sttp = "3.9.7" - val TlsChannel = "0.9.0" + val Sttp = "3.9.8" + val TlsChannel = "0.9.1" val ZHttp = "2.0.0-RC11" val ZioConfig = "4.0.2" val ZioJson = "0.6.2" - val ZioSchema = "1.2.1" - val ZioTestContainers = "0.4.1" + val ZioSchema = "1.2.2" + val ZioTestContainers = "0.5.0" } lazy val Benchmarks = diff --git a/project/build.properties b/project/build.properties index be54e7763..136f452e0 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 1.10.0 +sbt.version = 1.10.1 diff --git a/sbt b/sbt index 43a52e83d..3083eb302 100755 --- a/sbt +++ b/sbt @@ -34,8 +34,8 @@ set -o pipefail -declare -r sbt_release_version="1.10.0" -declare -r sbt_unreleased_version="1.10.0" +declare -r sbt_release_version="1.10.1" +declare -r sbt_unreleased_version="1.10.1" declare -r latest_213="2.13.14" declare -r latest_212="2.12.19"