Scala client added thanks to Alejanro Crosa

This commit is contained in:
antirez
2009-09-02 10:34:27 +02:00
parent 4563648714
commit 7c44bbb110
26 changed files with 1601 additions and 0 deletions

View File

@ -0,0 +1,8 @@
package com.redis
/**
* Redis client Connection
*
*/
case class Connection(val host: String, val port: Int) extends SocketOperations

View File

@ -0,0 +1,66 @@
package com.redis
/**
* Hash Ring
*
*/
import java.util.zip.CRC32
import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable.Map
trait HashRing {
val replicas: Int
var sortedKeys: List[Long] = List()
var cluster = new ArrayBuffer[Redis]
val ring = Map[Long, Redis]()
// Adds the node to the hashRing
// including a number of replicas.
def addNode(node: Redis) = {
cluster += node
(1 to replicas).foreach{ replica =>
val key = calculateChecksum(node+":"+replica)
ring += (key -> node)
sortedKeys = sortedKeys ::: List(key)
}
sortedKeys = sortedKeys.sort(_ < _)
}
// get the node in the hash ring for this key
def getNode(key: String) = getNodePos(key)._1
def getNodePos(key: String): (Redis, Int) = {
val crc = calculateChecksum(key)
val idx = binarySearch(crc)
(ring(sortedKeys(idx)), idx)
}
// TODO this should perform a Bynary search
def binarySearch(value: Long): Int = {
var upper = (sortedKeys.length -1)
var lower = 0
var idx = 0
var comp: Long = 0
while(lower <= upper){
idx = (lower + upper) / 2
comp = sortedKeys(idx)
if(comp == value) { return idx }
if(comp < value) { upper = idx -1 }
if(comp > value) { lower = idx +1 }
}
return upper
}
// Computes the CRC-32 of the given String
def calculateChecksum(value: String): Long = {
val checksum = new CRC32
checksum.update(value.getBytes)
checksum.getValue
}
}

View File

@ -0,0 +1,47 @@
package com.redis.operations
/**
* Redis key space operations
*
*/
trait KeySpaceOperations{
val connection: Connection
var db: Int
// KEYS
// returns all the keys matching the glob-style pattern.
def keys(pattern: String): Array[String] = {
connection.write("KEYS "+pattern+"\r\n")
connection.readResponse.toString.split(" ")
}
// RANDKEY
// return a randomly selected key from the currently selected DB.
def randomKey: String = {
connection.write("RANDOMKEY\r\n")
connection.readResponse.toString.split('+')(1)
}
// RENAME (oldkey, newkey)
// atomically renames the key oldkey to newkey.
def rename(oldkey: String, newkey: String): Boolean = {
connection.write("RENAME "+oldkey+" "+newkey+"\r\n")
connection.readBoolean
}
// RENAMENX (oldkey, newkey)
// rename oldkey into newkey but fails if the destination key newkey already exists.
def renamenx(oldkey: String, newkey: String): Boolean = {
connection.write("RENAMENX "+oldkey+" "+newkey+"\r\n")
connection.readBoolean
}
// DBSIZE
// return the size of the db.
def dbSize: Int = {
connection.write("DBSIZE\r\n")
connection.readInt
}
}

View File

@ -0,0 +1,94 @@
package com.redis.operations
/**
* Redis list operations
*
*/
trait ListOperations{
def getConnection(key: String): Connection
// add the string value to the head (LPUSH) or tail (RPUSH) of the list stored at key.
// If the key does not exist an empty list is created just before the append operation. If the key exists but is not a List an error is returned.
// LPUSH
def pushHead(key: String, value: String): Boolean = {
val connection = getConnection(key)
connection.write("LPUSH "+key+" "+value.length+"\r\n"+value+"\r\n")
connection.readBoolean
}
// RPUSH
def pushTail(key: String, value: String): Boolean = {
val connection = getConnection(key)
connection.write("RPUSH "+key+" "+value.length+"\r\n"+value+"\r\n")
connection.readBoolean
}
// LPOP
// atomically return and remove the first (LPOP) or last (RPOP) element of the list
def popHead(key: String): String = {
val connection = getConnection(key)
connection.write("LPOP "+key+"\r\n")
connection.readString
}
// RPOP
// atomically return and remove the first (LPOP) or last (RPOP) element of the list
def popTail(key: String): String = {
val connection = getConnection(key)
connection.write("RPOP "+key+"\r\n")
connection.readString
}
// LINDEX
// return the especified element of the list stored at the specified key. 0 is the first element, 1 the second and so on.
// Negative indexes are supported, for example -1 is the last element, -2 the penultimate and so on.
def listIndex(key: String, index: Int): String = {
val connection = getConnection(key)
connection.write("LINDEX "+key+" "+index+"\r\n")
connection.readString
}
// LSET
// set the list element at index with the new value. Out of range indexes will generate an error
def listSet(key: String, index: Int, value: String): Boolean = {
val connection = getConnection(key)
connection.write("LSET "+key+" "+index+" "+value.length+"\r\n"+value+"\r\n")
connection.readBoolean
}
// LLEN
// return the length of the list stored at the specified key.
// If the key does not exist zero is returned (the same behaviour as for empty lists). If the value stored at key is not a list an error is returned.
def listLength(key: String): Int = {
val connection = getConnection(key)
connection.write("LLEN "+key+"\r\n")
connection.readInt
}
// LRANGE
// return the specified elements of the list stored at the specified key.
// Start and end are zero-based indexes. 0 is the first element of the list (the list head), 1 the next element and so on.
def listRange(key: String, start: Int, end: Int): List[String] = {
val connection = getConnection(key)
connection.write("LRANGE "+key+" "+start+" "+end+"\r\n")
connection.readList
}
// LTRIM
// Trim an existing list so that it will contain only the specified range of elements specified.
def listTrim(key: String, start: Int, end: Int): Boolean = {
val connection = getConnection(key)
connection.write("LTRIM "+key+" "+start+" "+end+"\r\n")
connection.readBoolean
}
// LREM
// Remove the first count occurrences of the value element from the list.
def listRem(key: String, count: Int, value: String): Boolean = {
val connection = getConnection(key)
connection.write("LREM "+key+" "+count+" "+value.length+"\r\n"+value+"\r\n")
connection.readBoolean
}
}

View File

@ -0,0 +1,121 @@
package com.redis.operations
/**
* Redis node operations
*
*/
trait NodeOperations {
val connection: Connection
var db: Int
// SAVE
// save the DB on disk now.
def save: Boolean = {
connection.write("SAVE\r\n")
connection.readBoolean
}
// BGSAVE
// save the DB in the background.
def bgSave: Boolean = {
connection.write("BGSAVE\r\n")
connection.readBoolean
}
// LASTSAVE
// return the UNIX TIME of the last DB SAVE executed with success.
def lastSave: Int = {
connection.write("LASTSAVE\r\n")
connection.readInt
}
// SHUTDOWN
// Stop all the clients, save the DB, then quit the server.
def shutdown: Boolean = {
connection.write("SHUTDOWN\r\n")
connection.readBoolean
}
// MGET (key, key, key, ...)
// get the values of all the specified keys.
def mget(keys: String*) = {
connection.write("MGET "+keys.mkString(" ")+"\r\n")
connection.readList
}
// INFO
// the info command returns different information and statistics about the server.
def info = {
connection.write("INFO\r\n")
connection.readResponse
}
// MONITOR
// is a debugging command that outputs the whole sequence of commands received by the Redis server.
def monitor: Boolean = {
connection.write("MONITOR\r\n")
connection.readBoolean
}
// SLAVEOF
// The SLAVEOF command can change the replication settings of a slave on the fly.
def slaveOf(options: Any): Boolean = options match {
case (host: String, port: Int) => {
connection.write("SLAVEOF "+host+" "+port.toString+"\r\n")
connection.readBoolean
}
case _ => setAsMaster
}
def setAsMaster: Boolean = {
connection.write("SLAVEOF NO ONE\r\n")
connection.readBoolean
}
// SELECT (index)
// selects the DB to connect, defaults to 0 (zero).
def selectDb(index: Int): Boolean = {
connection.write("SELECT "+index+"\r\n")
connection.readBoolean match {
case true => { db = index; true }
case _ => false
}
}
// FLUSHDB the DB
// removes all the DB data.
def flushDb: Boolean = {
connection.write("FLUSHDB\r\n")
connection.readBoolean
}
// FLUSHALL the DB's
// removes data from all the DB's.
def flushAll: Boolean = {
connection.write("FLUSHALL\r\n")
connection.readBoolean
}
// MOVE
// Move the specified key from the currently selected DB to the specified destination DB.
def move(key: String, db: Int) = {
connection.write("MOVE "+key+" "+db.toString+"\r\n")
connection.readBoolean
}
// QUIT
// exits the server.
def quit: Boolean = {
connection.write("QUIT\r\n")
connection.disconnect
}
// AUTH
// auths with the server.
def auth(secret: String): Boolean = {
connection.write("AUTH "+secret+"\r\n")
connection.readBoolean
}
}

View File

@ -0,0 +1,124 @@
package com.redis.operations
/**
* Redis operations
*
*/
trait Operations{
def getConnection(key: String): Connection
// SET (key, value)
// SET (key, value, expiry)
// sets the key with the specified value, and with an optional expiry.
def set(key: String, value: String) = setKey(key, value)
def set(key: String, value: String, expiry: Int) = { setKey(key, value) && expire(key, expiry) }
// SET KEY (key, value)
// sets the key with the specified value.
def setKey(key: String, value: String): Boolean = {
val connection = getConnection(key)
connection.write("SET "+key+" "+value.length+"\r\n"+value+"\r\n")
connection.readBoolean
}
// EXPIRE (key, expiry)
// sets the expire time (in sec.) for the specified key.
def expire(key: String, expiry: Int): Boolean = {
val connection = getConnection(key)
connection.write("EXPIRE "+key+" "+expiry+"\r\n")
connection.readBoolean
}
// GET (key)
// gets the value for the specified key.
def get(key: String): String = {
val connection = getConnection(key)
val a = connection.write("GET "+key+"\r\n")
connection.readResponse match {
case r: String => r.toString
case _ => null
}
}
// GETSET (key, value)
// is an atomic set this value and return the old value command.
def getSet(key: String, value: String): String = {
val connection = getConnection(key)
val a = connection.write("GETSET "+key+" "+value.length+"\r\n"+value+"\r\n")
connection.readResponse match {
case r: String => r.toString
case _ => null
}
}
// SETNX (key, value)
// sets the value for the specified key, only if the key is not there.
def setUnlessExists(key: String, value: String): Boolean = {
val connection = getConnection(key)
connection.write("SETNX "+key+" "+value.length+"\r\n"+value+"\r\n")
connection.readBoolean
}
// DELETE (key)
// deletes the specified key.
def delete(key: String): Boolean = {
val connection = getConnection(key)
connection.write("DEL "+key+"\r\n")
connection.readBoolean
}
// INCR (key)
// INCR (key, increment)
// increments the specified key, optional the increment value.
def incr(x: Any): Int = x match {
case (key: String, increment: Int) => incrBy(key, increment)
case (key: String) => incrOne(key)
case _ => 0
}
def incrBy(key: String, increment: Int): Int = {
val connection = getConnection(key)
connection.write("INCRBY "+key+" "+increment+"\r\n")
connection.readInt
}
def incrOne(key: String): Int = {
val connection = getConnection(key)
connection.write("INCR "+key+"\r\n")
connection.readInt
}
// DECR (key)
// DECRBY (key, decrement)
// decrements the specified key, optional the decrement value.
def decr(key: String, decrement: Int) = decrBy(key, decrement)
def decr(key: String) = decrOne(key)
def decrBy(key: String, decrement: Int): Int = {
val connection = getConnection(key)
connection.write("DECRBY "+key+" "+decrement+"\r\n")
connection.readInt
}
def decrOne(key: String): Int = {
val connection = getConnection(key)
connection.write("DECR "+key+"\r\n")
connection.readInt
}
// EXISTS (key)
// test if the specified key exists.
def exists(key: String): Boolean = {
val connection = getConnection(key)
connection.write("EXISTS "+key+"\r\n")
connection.readBoolean
}
// TYPE (key)
// return the type of the value stored at key in form of a string.
def getType(key: String): Any = {
val connection = getConnection(key)
connection.write("TYPE "+key+"\r\n")
connection.readResponse
}
}

View File

@ -0,0 +1,115 @@
package com.redis.operations
/**
* Redis set operations
*
*/
trait SetOperations{
def getConnection(key: String): Connection
// SADD
// Add the specified member to the set value stored at key.
def setAdd(key: String, value: String): Boolean = {
val connection = getConnection(key)
connection.write("SADD "+key+" "+value.length+"\r\n"+value+"\r\n")
connection.readBoolean
}
// SREM
// Remove the specified member from the set value stored at key.
def setDelete(key: String, value: String): Boolean = {
val connection = getConnection(key)
connection.write("SREM "+key+" "+value.length+"\r\n"+value+"\r\n")
connection.readBoolean
}
// SCARD
// Return the number of elements (the cardinality) of the Set at key.
def setCount(key: String): Int = {
val connection = getConnection(key)
connection.write("SCARD "+key+"\r\n")
connection.readInt
}
// SMEMBERS
// Return all the members of the Set value at key.
def setMembers(key: String): Set[String] = {
val connection = getConnection(key)
connection.write("SMEMBERS "+key+"\r\n")
connection.readSet
}
// SPOP
// Remove and return (pop) a random element from the Set value at key.
def setPop(key: String): String = {
val connection = getConnection(key)
connection.write("SPOP "+key+"\r\n")
connection.readString
}
// SMOVE
// Move the specified member from one Set to another atomically.
def setMove(sourceKey: String, destKey: String, value: String): Boolean = {
val connection = getConnection(sourceKey)
connection.write("SMOVE "+sourceKey+" "+destKey+" "+value+"\r\n")
connection.readBoolean
}
// SISMEMBER
// Test if the specified value is a member of the Set at key.
def setMemberExists(key: String, value: String): Boolean = {
val connection = getConnection(key)
connection.write("SISMEMBER "+key+" "+value.length+"\r\n"+value+"\r\n")
connection.readBoolean
}
// SINTER
// Return the intersection between the Sets stored at key1, key2, ..., keyN.
def setIntersect(keys: String*): Set[String] = {
val connection = getConnection(keys(0))
connection.write("SINTER "+keys.mkString(" ")+"\r\n")
connection.readSet
}
// SINTERSTORE
// Compute the intersection between the Sets stored at key1, key2, ..., keyN, and store the resulting Set at dstkey.
def setInterStore(key: String, keys: String*): Boolean = {
val connection = getConnection(key)
connection.write("SINTERSTORE "+key+" "+keys.mkString(" ")+"\r\n")
connection.readBoolean
}
// SDIFF
// Return the difference between the Set stored at key1 and all the Sets key2, ..., keyN.
def setDiff(keys: String*): Set[String] = {
val connection = getConnection(keys(0))
connection.write("SDIFF "+keys.mkString(" ")+"\r\n")
connection.readSet
}
// SDIFFSTORE
// Compute the difference between the Set key1 and all the Sets key2, ..., keyN, and store the resulting Set at dstkey.
def setDiffStore(key: String, keys: String*): Boolean = {
val connection = getConnection(key)
connection.write("SDIFFSTORE "+key+" "+keys.mkString(" ")+"\r\n")
connection.readBoolean
}
// SUNION
// Return the union between the Sets stored at key1, key2, ..., keyN.
def setUnion(keys: String*): Set[String] = {
val connection = getConnection(keys(0))
connection.write("SUNION "+keys.mkString(" ")+"\r\n")
connection.readSet
}
// SUNIONSTORE
// Compute the union between the Sets stored at key1, key2, ..., keyN, and store the resulting Set at dstkey.
def setUnionStore(key: String, keys: String*): Boolean = {
val connection = getConnection(key)
connection.write("SUNIONSTORE "+key+" "+keys.mkString(" ")+"\r\n")
connection.readBoolean
}
}

View File

@ -0,0 +1,28 @@
package com.redis.operations
/**
* Redis sort operations
*
*/
trait SortOperations{
def getConnection(key: String): Connection
// SORT
// Sort a Set or a List accordingly to the specified parameters.
def sort(args: Any): List[String] = args match {
case (key: String, command: String) => doSort(key, command)
case (key: String) => doSort(key, "")
}
def doSort(key: String, command: String): List[String] = {
val connection = getConnection(key)
if(command != "") {
connection.write("SORT "+key+" "+command+"\r\n")
} else {
connection.write("SORT "+key+"\r\n")
}
connection.readList
}
}

View File

@ -0,0 +1,33 @@
package com.redis
import com.redis.operations._
/**
* Redis client
*
*/
class Redis(val host: String, val port: Int) extends Operations with ListOperations with SetOperations with NodeOperations with KeySpaceOperations with SortOperations {
// auxiliary constructor
def this() = this("localhost", 6379)
// Points to the connection to a server instance
val connection = Connection(host, port)
var db: Int = 0
// Connect and Disconnect to the Redis server
def connect = connection.connect
def disconnect = connection.disconnect
def connected: Boolean = connection.connected
// Establish the connection to the server instance on initialize
connect
// Get Redis Client connection.
def getConnection(key: String) = getConnection
def getConnection = connection
// Outputs a formatted representation of the Redis server.
override def toString = connection.host+":"+connection.port+" <connected:"+connection.connected+">"
}

View File

@ -0,0 +1,42 @@
package com.redis
import com.redis.operations._
/**
* Redis cluster
*
*/
import scala.collection.mutable.ArrayBuffer
class RedisCluster(val hosts: String*) extends Operations with ListOperations with SetOperations with HashRing with SortOperations {
// Get Redis Client connection inside the HashRing.
def getConnection(key: String) = {
getNode(key).connection
}
// Default value used on MemCache client.
private val NUMBER_OF_REPLICAS = 160
val replicas = NUMBER_OF_REPLICAS
// Outputs a formatted representation of the Redis server.
override def toString = cluster.mkString(", ")
// Connect the client and add it to the cluster.
def connectClient(host: String): Boolean = {
val h = host.split(":")(0)
val p = host.split(":")(1)
val client = new Redis(h.toString, p.toString.toInt)
addNode(client)
client.connected
}
// Connect all clients in the cluster.
def connect = cluster.map(c => c.connect)
// Initialize cluster.
def initialize_cluster = hosts.map(connectClient(_))
initialize_cluster
}

View File

@ -0,0 +1,167 @@
package com.redis
/**
* Socket operations
*
*/
import java.io._
import java.net.Socket
trait SocketOperations {
// Response codes from the Redis server
// they tell you what's coming next from the server.
val ERR: String = "-"
val OK: String = "+OK"
val SINGLE: String = "+"
val BULK: String = "$"
val MULTI: String = "*"
val INT:String = ":"
val host: String
val port: Int
// File descriptors.
var socket: Socket = null
var out: OutputStream = null
var in: BufferedReader = null
def getOutputStream: OutputStream = out
def getInputStream: BufferedReader = in
def getSocket: Socket = socket
def connected = { getSocket != null }
def reconnect = { disconnect && connect; }
// Connects the socket, and sets the input and output streams.
def connect: Boolean = {
try {
socket = new Socket(host, port)
out = getSocket.getOutputStream
in = new BufferedReader(new InputStreamReader(getSocket.getInputStream));
true
} catch {
case _ => clear_fd; false;
}
}
// Disconnects the socket.
def disconnect: Boolean = {
try {
socket.close
out.close
in.close
clear_fd
true
} catch {
case _ => false
}
}
def clear_fd = {
socket = null
out = null
in = null
}
// Reads the server responses as Scala types.
def readString: String = readResponse.toString // Reads the server response as an Int
def readInt: Int = Integer.parseInt(readResponse.toString) // Reads the server response as an Int
def readList: List[String] = listReply(readResponse.toString) // Reads the server response as a List
def readSet: Set[String] = setReply(readResponse.toString) // Reads the server response as a String
def readBoolean: Boolean = readResponse match {
case 1 => true
case OK => true
case _ => false
}
// Read from Input Stream.
def readline: String = {
try {
getInputStream.readLine()
} catch {
case _ => ERR;
}
}
// Gets the type of response the server is going to send.
def readtype = {
val res = readline
if(res !=null){
(res(0).toString(), res)
}else{
("-", "")
}
}
// Reads the response from the server based on the response code.
def readResponse = {
val responseType = readtype
try{
responseType._1 match {
case ERR => reconnect; // RECONNECT
case SINGLE => lineReply(responseType._2)
case BULK => bulkReply(responseType._2)
case MULTI => responseType._2
case INT => integerReply(responseType._2)
case _ => reconnect; // RECONNECT
}
}catch{
case e: Exception => false
}
}
def integerReply(response: String): Int = Integer.parseInt(response.split(":")(1).toString)
def lineReply(response: String): String = response
def listReply(response: String): List[String] = {
val total = Integer.parseInt(response.split('*')(1))
var list: List[String] = List()
for(i <- 1 to total){
list = (list ::: List(bulkReply(readtype._2)))
}
list
}
def bulkReply(response: String) = {
if(response(1).toString() != ERR){
var length: Int = Integer.parseInt(response.split('$')(1).split("\r\n")(0))
var line, res: String = ""
while(length >= 0){
line = readline
length -= (line.length+2)
res += line
if(length > 0) res += "\r\n"
}
res
}else{ null }
}
def setReply(response: String): Set[String] = {
val total = Integer.parseInt(response.split('*')(1))
var set: Set[String] = Set()
for(i <- 1 to total){
set += bulkReply(readtype._2)
}
set
}
// Wraper for the socket write operation.
def write_to_socket(data: String)(op: OutputStream => Unit) = op(getOutputStream)
// Writes data to a socket using the specified block.
def write(data: String) = {
if(!connected) connect;
write_to_socket(data){
getSocket =>
try {
getSocket.write(data.getBytes)
} catch {
case _ => reconnect;
}
}
}
}

View File

@ -0,0 +1,27 @@
import org.specs._
import com.redis._
import org.specs.mock.Mockito
import org.mockito.Mock._
import org.mockito.Mockito._
import org.mockito.Mockito.doNothing
object RedisClientSpec extends Specification with Mockito {
"Redis Client" should {
var client: Redis = null
"print formatted client status" in {
client = new Redis("localhost", 121212)
client.toString must be matching("localhost:121212 <connected:false>")
}
"get the same connection when passing key para or not since it's a single node" in {
client.getConnection("key") mustEqual client.getConnection
}
"use db zero as default" in {
client.db mustEqual 0
}
}
}

View File

@ -0,0 +1,51 @@
import org.specs._
import com.redis._
import org.specs.mock.Mockito
import org.mockito.Mock._
import org.mockito.Mockito._
import org.mockito.Mockito.doNothing
object RedisClusterSpec extends Specification with Mockito {
"Redis Cluster" should {
var cluster: RedisCluster = null
var mockedRedis: Redis = null
doBefore {
cluster = new RedisCluster("localhost:11221", "localhost:99991")
mockedRedis = mock[Redis]
}
"print formatted client status" in {
cluster.toString must be matching("localhost:11221 <connected:false>, localhost:99991 <connected:false>")
}
"get the connection for the specified key" in {
cluster.getConnection("key") mustEqual Connection("localhost", 99991)
cluster.getConnection("anotherkey") mustEqual Connection("localhost", 11221)
}
"use the default number of replicas" in {
cluster.replicas mustEqual 160
}
"initialize cluster" in {
val initializedCluster = cluster.initialize_cluster
initializedCluster.size mustEqual 2
initializedCluster(0) mustEqual false
initializedCluster(1) mustEqual false
}
"connect all the redis instances" in {
cluster.cluster(1) = mockedRedis
cluster.cluster(1).connect returns true
val connectResult = cluster.connect
connectResult.size mustEqual 2
connectResult(0) mustEqual false
connectResult(1) mustEqual true
cluster.cluster(1).connect was called
}
}
}

View File

@ -0,0 +1,97 @@
import org.specs._
import com.redis._
import java.io._
import java.net.Socket
import org.specs.mock.Mockito
import org.mockito.Mock._
import org.mockito.Mockito._
class SocketOperationTest(val host:String, val port: Int) extends SocketOperations
object SocketOperationsSpec extends Specification with Mockito {
"Socket Operations" should {
var socketOperation: SocketOperationTest = null
var socket: Socket = null
var in: BufferedReader = null
doBefore {
socketOperation = new SocketOperationTest("localhost", 6379666)
socket = mock[Socket]
in = mock[BufferedReader]
socketOperation.socket = socket
socketOperation.in = in
}
def readOkFromInput = { when(in.readLine()).thenReturn(socketOperation.OK) }
def readSingleFromInput = { when(in.readLine()).thenReturn(socketOperation.SINGLE) }
def readBulkFromInput = { in.readLine() returns("$6\r\nfoobar\r\n") thenReturns("$6\r\nfoobar\r\n") }
def readIntFromInput = { when(in.readLine()).thenReturn(socketOperation.INT+"666") }
"tell if it's connected" in {
socketOperation.connected mustEqual true
socketOperation.socket = null
socketOperation.connected mustEqual false
}
"return false when can't connect" in {
socketOperation.connect mustEqual false
}
"return current data input stream" in {
socketOperation.getInputStream mustEqual in
}
"read a line from socket" in {
readOkFromInput
socketOperation.in mustEqual in
socketOperation.readline mustEqual socketOperation.OK
}
"read type response" in {
readOkFromInput
socketOperation.readtype mustEqual ("+", socketOperation.OK)
}
"when reading responses" in {
"read OK" in {
readOkFromInput
socketOperation.readResponse mustEqual socketOperation.OK
}
"read single line" in {
readSingleFromInput
socketOperation.readResponse mustEqual socketOperation.SINGLE
}
"reconnect on error" in {
socketOperation.readResponse mustEqual false
socket.close was called
socketOperation.connected mustEqual true
}
"read in bulk" in {
// readBulkFromInput
// this shouldn't be the response, it doesn't seem to work return and then returns.
// Here's what should happen: '$6\r\n' on first readLine and then 'foobar\r\n'
readBulkFromInput
socketOperation.readtype mustEqual ("$", "$6\r\nfoobar\r\n")
socketOperation.readResponse mustEqual "$6\r\nfoobar\r\n"
socketOperation.bulkReply("$6\r\nfoobar\r\n") was called
}
"read integer" in {
readIntFromInput
socketOperation.readInt mustEqual 666
}
"read a boolean return value" in {
readOkFromInput
socketOperation.readBoolean mustEqual true
}
}
}
}

View File

@ -0,0 +1,13 @@
import org.specs._
import com.redis._
import com.redis.operations._
import org.specs.mock.Mockito
import org.mockito.Mock._
import org.mockito.Mockito._
import org.mockito.Mockito.doNothing
class RedisTestClient(val connection: Connection) extends Operations with ListOperations with SetOperations with NodeOperations with KeySpaceOperations with SortOperations {
var db: Int = 0
def getConnection(key: String): Connection = connection
}

View File

@ -0,0 +1,50 @@
import org.specs._
import com.redis._
import org.specs.mock.Mockito
import org.mockito.Mock._
import org.mockito.Mockito._
import org.mockito.Mockito.doNothing
object KeySpaceOperationsSpec extends Specification with Mockito {
"Redis Client Key Operations" should {
var client: RedisTestClient = null
var connection: Connection = null
doBefore{
connection = mock[Connection]
client = new RedisTestClient(connection)
}
"return all keys matching" in {
connection.readResponse returns "akey anotherkey adiffkey"
client.keys("a*")
connection.write("KEYS a*\r\n") was called
}
"return a random key" in {
connection.readResponse returns "+somerandonkey"
client.randomKey mustEqual "somerandonkey"
connection.write("RANDOMKEY\r\n") was called
}
"remame a key" in {
connection.readBoolean returns true
client.rename("a", "b") must beTrue
connection.write("RENAME a b\r\n") was called
}
"rename a key only if destintation doesn't exist" in {
connection.readBoolean returns false
client.renamenx("a", "b") must beFalse
connection.write("RENAMENX a b\r\n") was called
}
"tell the size of the db, # of keys" in {
connection.readInt returns 4
client.dbSize mustEqual 4
connection.write("DBSIZE\r\n") was called
}
}
}

View File

@ -0,0 +1,81 @@
import org.specs._
import com.redis._
import org.specs.mock.Mockito
import org.mockito.Mock._
import org.mockito.Mockito._
import org.mockito.Mockito.doNothing
object ListOperationsSpec extends Specification with Mockito {
"Redis Client List Operations" should {
var client: RedisTestClient = null
var connection: Connection = null
doBefore{
connection = mock[Connection]
client = new RedisTestClient(connection)
}
"push to head" in {
connection.readBoolean returns true
client.pushHead("k", "v") must beTrue
connection.write("LPUSH k 1\r\nv\r\n") was called
}
"push to tail" in {
connection.readBoolean returns true
client.pushTail("k", "v") must beTrue
connection.write("RPUSH k 1\r\nv\r\n") was called
}
"pop from head" in {
connection.readString returns "value"
client.popHead("key") mustEqual "value"
connection.write("LPOP key\r\n") was called
}
"pop from tail" in {
connection.readString returns "value"
client.popTail("key") mustEqual "value"
connection.write("RPOP key\r\n") was called
}
"return list index" in {
connection.readString returns "value"
client.listIndex("k", 2) mustEqual "value"
connection.write("LINDEX k 2\r\n") was called
}
"return set element at index" in {
connection.readBoolean returns true
client.listSet("k", 1, "value") mustEqual true
connection.write("LSET k 1 5\r\nvalue\r\n") was called
}
"return list size" in {
connection.readInt returns 3
client.listLength("k") mustEqual 3
connection.write("LLEN k\r\n") was called
}
"return list range" in {
val listResult: List[String] = List("one", "two", "three", "four", "five")
connection.readList returns listResult
client.listRange("k", 2, 4) mustEqual listResult
connection.write("LRANGE k 2 4\r\n") was called
}
"trim a list" in {
connection.readBoolean returns true
client.listTrim("k", 2, 4) mustEqual true
connection.write("LTRIM k 2 4\r\n") was called
}
"remove occurrences of a value in the list" in {
connection.readBoolean returns true
client.listRem("k", 2, "value") mustEqual true
connection.write("LREM k 2 5\r\nvalue\r\n") was called
}
}
}

View File

@ -0,0 +1,113 @@
import org.specs._
import com.redis._
import org.specs.mock.Mockito
import org.mockito.Mock._
import org.mockito.Mockito._
import org.mockito.Mockito.doNothing
object NodeOperationsSpec extends Specification with Mockito {
"Redis Client Node Operations" should {
var client: RedisTestClient = null
var connection: Connection = null
doBefore{
connection = mock[Connection]
client = new RedisTestClient(connection)
}
"save the db to disk" in {
connection.readBoolean returns true
client.save must beTrue
connection.write("SAVE\r\n") was called
}
"return the last time saved data to the db" in {
connection.readInt returns 1250421891
client.lastSave mustEqual 1250421891
connection.write("LASTSAVE\r\n") was called
}
"return all specified keys" in {
connection.readList returns List[String]("hola", null, null)
client.mget("a", "b", "c") mustEqual List[String]("hola", null, null)
connection.write("MGET a b c\r\n") was called
}
"return server info" in {
val sampleInfo = "res0: Any = \nredis_version:0.091\nconnected_clients:2\nconnected_slaves:0\nused_memory:3036\nchanges_since_last_save:0\nlast_save_time:1250440893\ntotal_connections_received:2\ntotal_commands_processed:0\nuptime_in_seconds:7\nuptime_in_days:0\n"
connection.readResponse returns sampleInfo
client.info mustEqual sampleInfo
connection.write("INFO\r\n") was called
}
"start monitor debug on the server" in {
connection.readBoolean returns true
client.monitor mustEqual true
connection.write("MONITOR\r\n") was called
}
"set a server as slave of a remote master" in {
connection.readBoolean returns true
client.slaveOf("localhost", 9999) mustEqual true
connection.write("SLAVEOF localhost 9999\r\n") was called
}
"set a server as master if no params sent" in {
connection.readBoolean returns true
client.slaveOf() mustEqual true
connection.write("SLAVEOF NO ONE\r\n") was called
}
"set a server as master" in {
connection.readBoolean returns true
client.setAsMaster mustEqual true
connection.write("SLAVEOF NO ONE\r\n") was called
}
"select the current db" in {
connection.readBoolean returns true
client.selectDb(3) mustEqual true
client.db mustEqual 3
connection.write("SELECT 3\r\n") was called
}
"flush the db" in {
connection.readBoolean returns true
client.flushDb mustEqual true
connection.write("FLUSHDB\r\n") was called
}
"flush all the dbs" in {
connection.readBoolean returns true
client.flushAll mustEqual true
connection.write("FLUSHALL\r\n") was called
}
"shutdown the db" in {
connection.readBoolean returns true
client.shutdown mustEqual true
connection.write("SHUTDOWN\r\n") was called
}
"move keys from one db to another" in {
connection.readBoolean returns true
client.move("a", 2) mustEqual true
connection.write("MOVE a 2\r\n") was called
}
"quit" in {
connection.disconnect returns true
client.quit mustEqual true
connection.write("QUIT\r\n") was called
connection.disconnect was called
}
"auth with the server" in {
connection.readBoolean returns true
client.auth("secret") mustEqual true
connection.write("AUTH secret\r\n") was called
}
}
}

View File

@ -0,0 +1,105 @@
import org.specs._
import com.redis._
import org.specs.mock.Mockito
import org.mockito.Mock._
import org.mockito.Mockito._
object OperationsSpec extends Specification with Mockito {
"Redis Client Operations" should {
var client: RedisTestClient = null
var connection: Connection = null
doBefore{
connection = mock[Connection]
client = new RedisTestClient(connection)
}
"set a key" in {
connection.readBoolean returns true
client.set("a", "b") mustEqual true
connection.write("SET a 1\r\nb\r\n") was called
}
"set a key with setKey" in {
connection.readBoolean returns true
client.setKey("a", "b") mustEqual true
connection.write("SET a 1\r\nb\r\n") was called
}
"set a key with expiration" in {
connection.readBoolean returns true
client.set("a", "b", 4) mustEqual true
connection.write("SET a 1\r\nb\r\n") was called
connection.write("EXPIRE a 4\r\n") was called
}
"expire a key" in {
connection.readBoolean returns true
client.expire("a", 4) mustEqual true
connection.write("EXPIRE a 4\r\n") was called
}
"get a key" in {
connection.readResponse returns "b"
client.get("a") mustEqual "b"
connection.write("GET a\r\n") was called
}
"get and set a key" in {
connection.readResponse returns "old"
client.getSet("a", "new") mustEqual "old"
connection.write("GETSET a 3\r\nnew\r\n") was called
}
"delete a key" in {
connection.readBoolean returns true
client.delete("a") mustEqual true
connection.write("DEL a\r\n") was called
}
"tell if a key exists" in {
connection.readBoolean returns true
client.exists("a") mustEqual true
connection.write("EXISTS a\r\n") was called
}
"tell if a key exists" in {
connection.readBoolean returns true
client.exists("a") mustEqual true
connection.write("EXISTS a\r\n") was called
}
"increment a value" in {
connection.readInt returns 1
client.incr("a") mustEqual 1
connection.write("INCR a\r\n") was called
}
"increment a value by N" in {
connection.readInt returns 27
client.incr("a", 23) mustEqual 27
connection.write("INCRBY a 23\r\n") was called
}
"decrement a value" in {
connection.readInt returns 0
client.decr("a") mustEqual 0
connection.write("DECR a\r\n") was called
}
"decrement a value by N" in {
connection.readInt returns 25
client.decr("a", 2) mustEqual 25
connection.write("DECRBY a 2\r\n") was called
}
"return type of key" in {
connection.readResponse returns "String"
client.getType("a") mustEqual "String"
connection.write("TYPE a\r\n") was called
}
}
}

View File

@ -0,0 +1,102 @@
import org.specs._
import com.redis._
import org.specs.mock.Mockito
import org.mockito.Mock._
import org.mockito.Mockito._
import org.mockito.Mockito.doNothing
object SetOperationsSpec extends Specification with Mockito {
"Redis Client Set Operations" should {
var client: RedisTestClient = null
var connection: Connection = null
doBefore{
connection = mock[Connection]
client = new RedisTestClient(connection)
}
"add a member to a set" in {
connection.readBoolean returns true
client.setAdd("set", "value") must beTrue
connection.write("SADD set 5\r\nvalue\r\n") was called
}
"remove an member from a set" in {
connection.readBoolean returns true
client.setDelete("set", "value") must beTrue
connection.write("SREM set 5\r\nvalue\r\n") was called
}
"return the number of elements in the set" in {
connection.readInt returns 5
client.setCount("set") mustEqual 5
connection.write("SCARD set\r\n") was called
}
"return all the members from a set" in {
val setResult = Set("one", "two", "three")
connection.readSet returns setResult
client.setMembers("set") mustEqual setResult
connection.write("SMEMBERS set\r\n") was called
}
"pop an element from the set" in {
connection.readString returns "one"
client.setPop("set") mustEqual "one"
connection.write("SPOP set\r\n") was called
}
"move an element from one set to another" in {
connection.readBoolean returns true
client.setMove("set", "toset", "value") mustEqual true
connection.write("SMOVE set toset value\r\n") was called
}
"tell if member exists on the set" in {
connection.readBoolean returns true
client.setMemberExists("set", "value") mustEqual true
connection.write("SISMEMBER set 5\r\nvalue\r\n") was called
}
"return the intersection between N sets" in {
val setResult = Set("one", "two", "three")
connection.readSet returns setResult
client.setIntersect("set", "otherset", "another") mustEqual setResult
connection.write("SINTER set otherset another\r\n") was called
}
"return the intersection between N sets and store it a new one" in {
connection.readBoolean returns true
client.setInterStore("set", "oneset", "twoset") mustEqual true
connection.write("SINTERSTORE set oneset twoset\r\n") was called
}
"return the difference between N sets" in {
val setResult = Set("one", "two", "three")
connection.readSet returns setResult
client.setDiff("set", "oneset", "twoset") mustEqual setResult
connection.write("SDIFF set oneset twoset\r\n") was called
}
"return the difference between N sets and store it in a new one" in {
connection.readBoolean returns true
client.setDiffStore("newset", "oneset", "twoset") mustEqual true
connection.write("SDIFFSTORE newset oneset twoset\r\n") was called
}
"return the union between N sets" in {
val setResult = Set("one", "two", "three")
connection.readSet returns setResult
client.setUnion("set", "oneset", "twoset") mustEqual setResult
connection.write("SUNION set oneset twoset\r\n") was called
}
"return the union between N sets and store it in a new one" in {
connection.readBoolean returns true
client.setUnionStore("set", "oneset", "twoset") mustEqual true
connection.write("SUNIONSTORE set oneset twoset\r\n") was called
}
}
}

View File

@ -0,0 +1,34 @@
import org.specs._
import com.redis._
import org.specs.mock.Mockito
import org.mockito.Mock._
import org.mockito.Mockito._
object SortOperationsSpec extends Specification with Mockito {
"Redis Client Sort Operations" should {
var client: RedisTestClient = null
var connection: Connection = null
doBefore{
connection = mock[Connection]
client = new RedisTestClient(connection)
}
"sort the contents of the specified key" in {
val listResult: List[String] = List("one", "two", "three")
connection.readList returns listResult
client.sort("set", "ALPHA DESC") mustEqual listResult
connection.write("SORT set ALPHA DESC\r\n") was called
}
"sort the contents of the specified key with default" in {
val listResult: List[String] = List("one", "two", "three")
connection.readList returns listResult
client.sort("set") mustEqual listResult
connection.write("SORT set\r\n") was called
}
}
}