Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ buildscript {
ext.testContainers_version = '2.0.5'
ext.wiremock_version = '3.13.0'
ext.klogging_version = '7.0.0'
ext.libVersion = "6.1.0"
ext.libVersion = "6.1.1"
ext.libGroup = 'io.github.toronik'
repositories {
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,36 +43,10 @@ open class RedisContainerSystem @JvmOverloads constructor(

override fun running() = isRunning

fun <T> exec(f: (KredsClient) -> T) = client.use(f)

fun keys(pattern: String = "*", vararg types: String): Collection<String> = exec { cl ->
runBlocking {
cl.keys(pattern)
.let { keys -> if (types.isEmpty()) keys else keys.filter { cl.type(it) in types } }
.sorted()
}
}
override fun <T> exec(f: (KredsClient) -> T) = client.use(f)

override fun clean(): String = exec { runBlocking { it.flushAll() } }

fun type(key: String): String = exec { runBlocking { it.type(key) } }
fun del(vararg keys: String) = exec { runBlocking { it.del(*keys) } }
fun exists(vararg keys: String) = exec { runBlocking { it.exists(*keys) } }
fun exists(key: String) = exec { runBlocking { it.exists(key) == 1L } }
fun getMap(key: String): Map<String, String> =
exec { runBlocking { it.hgetAll(key).chunked(2).associate { it[0] to it[1] } } }

fun setMap(key: String, field: String, value: String): Long = exec { runBlocking { it.hset(key, field to value) } }
fun setMap(key: String, map: Map<String, String>): Long =
exec { cl -> runBlocking { map.toList().let { cl.hset(key, it.first(), *it.drop(1).toTypedArray()) } } }

fun getList(key: String, start: Int = 0, stop: Int = -1) = exec { runBlocking { it.lrange(key, start, stop) } }
fun setList(key: String, vararg values: String) =
exec { runBlocking { it.rpush(key, values.first(), *values.drop(1).toTypedArray()) } }

fun get(key: String): String? = exec { runBlocking { it.get(key) } }
fun set(key: String, value: String): String? = exec { runBlocking { it.set(key, value) } }

data class Config @JvmOverloads constructor(val host: String = "localhost", val port: Int = PORT) :
ExternalSystemConfig("env.redis.host" to host, "env.redis.port" to port.toString())

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.github.adven27.env.redis

import io.github.adven27.env.core.ExternalSystem
import io.github.crackthecodeabhi.kreds.connection.KredsClient
import kotlinx.coroutines.runBlocking

/**
* Type-safe interface for all Redis systems in an [io.github.adven27.env.core.Environment].
Expand All @@ -16,11 +18,46 @@ import io.github.adven27.env.core.ExternalSystem
* env.env<RedisSystem>("REDIS").clean()
* ```
*
* Data helpers ([keys], [getMap], [setMap], ...) are built on [exec] and behave the same
* for every implementation, so specs work with `env<RedisSystem>()` both against a container and a remote Redis.
*
* Contract:
* - [RedisContainerSystem.clean] flushes the entire Redis instance (FLUSHALL).
* - [ResettableRedisRemote.clean] flushes only its namespaced logical database (FLUSHDB),
* leaving other databases intact.
* leaving other databases intact; its [exec] always runs in that database.
*/
@Suppress("TooManyFunctions", "SpreadOperator")
interface RedisSystem : ExternalSystem {
fun clean(): Any

/** Runs [f] with a client connected to the database this system owns. */
fun <T> exec(f: (KredsClient) -> T): T

fun keys(pattern: String = "*", vararg types: String): Collection<String> = exec { cl ->
runBlocking {
cl.keys(pattern)
.let { keys -> if (types.isEmpty()) keys else keys.filter { cl.type(it) in types } }
.sorted()
}
}

fun type(key: String): String = exec { runBlocking { it.type(key) } }
fun del(vararg keys: String): Long = exec { runBlocking { it.del(*keys) } }
fun exists(vararg keys: String): Long = exec { runBlocking { it.exists(*keys) } }
fun exists(key: String): Boolean = exec { runBlocking { it.exists(key) == 1L } }
fun getMap(key: String): Map<String, String> =
exec { runBlocking { it.hgetAll(key).chunked(2).associate { it[0] to it[1] } } }

fun setMap(key: String, field: String, value: String): Long = exec { runBlocking { it.hset(key, field to value) } }
fun setMap(key: String, map: Map<String, String>): Long =
exec { cl -> runBlocking { map.toList().let { cl.hset(key, it.first(), *it.drop(1).toTypedArray()) } } }

fun getList(key: String, start: Int = 0, stop: Int = -1): List<String> =
exec { runBlocking { it.lrange(key, start, stop) } }

fun setList(key: String, vararg values: String): Long =
exec { runBlocking { it.rpush(key, values.first(), *values.drop(1).toTypedArray()) } }

fun get(key: String): String? = exec { runBlocking { it.get(key) } }
fun set(key: String, value: String): String? = exec { runBlocking { it.set(key, value) } }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,39 @@ import io.github.adven27.env.core.ExternalSystem
import io.github.adven27.env.core.ExternalSystemConfig
import io.github.adven27.env.core.shouldReset
import io.github.crackthecodeabhi.kreds.connection.Endpoint
import io.github.crackthecodeabhi.kreds.connection.KredsClient
import io.github.crackthecodeabhi.kreds.connection.newClient
import kotlinx.coroutines.runBlocking

@Suppress("unused")
fun resettableRedis(props: Map<String, String>): ExternalSystem = ResettableRedisRemote(props)
@JvmName("resettableRedisSystem")
fun resettableRedis(props: Map<String, String>): RedisSystem = ResettableRedisRemote(props)

private class ResettableRedisRemote(private val props: Map<String, String>) : RedisSystem {
/** Keeps the 5.6.2 JVM signature for binaries compiled against it. */
@Suppress("unused")
@Deprecated("Binary compatibility only", level = DeprecationLevel.HIDDEN)
@JvmName("resettableRedis")
fun resettableRedisCompat(props: Map<String, String>): ExternalSystem = ResettableRedisRemote(props)

/**
* Redis that is already running (e.g. a shared test stack): owns only the logical database `env.redis.database`.
* Every call, including the [RedisSystem] data helpers, runs in that database.
*/
class ResettableRedisRemote(private val props: Map<String, String>) : RedisSystem {
override val config: ExternalSystemConfig = ExternalSystemConfig(props)

private val endpoint get() = Endpoint.from("${props.getValue("env.redis.host")}:${props.getValue("env.redis.port")}")
private val database get() = props.getValue("env.redis.database").toULong()

override fun start(fixedEnv: Boolean) {
props.forEach(System::setProperty)
if (!shouldReset()) return

val host = props.getValue("env.redis.host")
val port = props.getValue("env.redis.port")
val database = props.getValue("env.redis.database").toULong()
val nsOwner = System.getenv("TEST_NS_OWNER") ?: System.getProperty("TEST_NS_OWNER") ?: ""

newClient(Endpoint.from("$host:$port")).use { cl ->
exec { cl ->
runBlocking {
cl.select(database)
cl.flushDb()
cl.set("__ns_owner__", "$nsOwner port=${database.toLong()}")
cl.set(NS_OWNER_KEY, "$nsOwner port=${database.toLong()}")
}
}
}
Expand All @@ -39,10 +49,18 @@ private class ResettableRedisRemote(private val props: Map<String, String>) : Re
* Flushes only the namespaced logical database configured for this remote system.
* Safe to call in [@BeforeExample] — does NOT flush other logical databases.
*/
override fun clean(): Any = newClient(Endpoint.from("${props.getValue("env.redis.host")}:${props.getValue("env.redis.port")}")).use { cl ->
runBlocking {
cl.select(props.getValue("env.redis.database").toULong())
cl.flushDb()
}
override fun clean(): Any = exec { runBlocking { it.flushDb() } }

override fun <T> exec(f: (KredsClient) -> T): T = newClient(endpoint).use { cl ->
runBlocking { cl.select(database) }
f(cl)
}

/** Same as [RedisSystem.keys] but without the namespace owner marker, so specs see the same keys as in a container. */
override fun keys(pattern: String, vararg types: String): Collection<String> =
super.keys(pattern, *types).filter { it != NS_OWNER_KEY }

companion object {
private const val NS_OWNER_KEY = "__ns_owner__"
}
}
Loading
Loading