From feaaa3400183929fbe8e6b41e2b4f99cb86eaa07 Mon Sep 17 00:00:00 2001 From: Yosuke Mizutani Date: Wed, 29 Apr 2015 13:35:20 +0900 Subject: [PATCH 1/3] implement BytesSpec --- .../redismock/MockListOperations.scala | 103 ++++++--------- .../mogproject/redismock/MockOperations.scala | 2 +- .../redismock/MockStringOperations.scala | 122 ++++++++---------- .../mogproject/redismock/entity/Bytes.scala | 34 +++-- .../mogproject/redismock/entity/Value.scala | 21 +-- .../redismock/storage/Storage.scala | 6 + .../redismock/util/TTLTrieMap.scala | 6 +- .../redismock/entity/BytesSpec.scala | 79 ++++++++++++ 8 files changed, 203 insertions(+), 170 deletions(-) create mode 100644 src/test/scala/com/github/mogproject/redismock/entity/BytesSpec.scala diff --git a/src/main/scala/com/github/mogproject/redismock/MockListOperations.scala b/src/main/scala/com/github/mogproject/redismock/MockListOperations.scala index 0e2d119..b361ea6 100644 --- a/src/main/scala/com/github/mogproject/redismock/MockListOperations.scala +++ b/src/main/scala/com/github/mogproject/redismock/MockListOperations.scala @@ -14,55 +14,38 @@ trait MockListOperations extends ListOperations with MockOperations with Storage // helper functions // - private[this] def set(key: Any, rawValue: Traversable[Bytes])(implicit format: Format): Unit = { - currentDB.update(Key(key), ListValue(rawValue)) + private def setRaw(key: Any, rawValue: Traversable[Bytes])(implicit format: Format): Unit = { + currentDB.update(Key(key), ListValue(rawValue.toVector)) } - private[this] def getRaw(key: Any)(implicit format: Format): Option[LIST.DataType] = - currentDB.get(Key(format.apply(key))).map(_.as(LIST)) + private def getRaw(key: Any)(implicit format: Format): Option[LIST.DataType] = currentDB.get(Key(key)).map(_.as(LIST)) - private[this] def getRawOrEmpty(key: Any)(implicit format: Format): LIST.DataType = + private def getRawOrEmpty(key: Any)(implicit format: Format): LIST.DataType = getRaw(key).getOrElse(Vector.empty[Bytes]) // LPUSH (Variadic: >= 2.4) // add values to the head of the list stored at key - override def lpush(key: Any, value: Any, values: Any*)(implicit format: Format): Option[Long] = { - currentDB.synchronized { - val v = (value :: values.toList).map(Bytes.apply).toVector ++ getRawOrEmpty(key) - set(key, v) - Some(v.size) - } + override def lpush(key: Any, value: Any, values: Any*)(implicit format: Format): Option[Long] = withDB { + val v = (value :: values.toList).map(Bytes.apply).toVector ++ getRawOrEmpty(key) + setRaw(key, v) + Some(v.size) } // LPUSHX (Variadic: >= 2.4) - // add value to the tail of the list stored at key - override def lpushx(key: Any, value: Any)(implicit format: Format): Option[Long] = { - currentDB.synchronized { - val v = Vector(Bytes(value)) ++ getRawOrEmpty(key) - set(key, v) - Some(v.size) - } - } + // add value to the head of the list stored at key + override def lpushx(key: Any, value: Any)(implicit format: Format): Option[Long] = lpush(key, value) // RPUSH (Variadic: >= 2.4) // add values to the tail of the list stored at key - override def rpush(key: Any, value: Any, values: Any*)(implicit format: Format): Option[Long] = { - currentDB.synchronized { - val v = getRawOrEmpty(key) ++ (value :: values.toList).map(Bytes.apply).toVector - set(key, v) - Some(v.size) - } + override def rpush(key: Any, value: Any, values: Any*)(implicit format: Format): Option[Long] = withDB { + val v = getRawOrEmpty(key) ++ (value :: values.toList).map(Bytes.apply).toVector + setRaw(key, v) + Some(v.size) } // RPUSHX (Variadic: >= 2.4) // add value to the tail of the list stored at key - override def rpushx(key: Any, value: Any)(implicit format: Format): Option[Long] = { - currentDB.synchronized { - val v = getRawOrEmpty(key) ++ Vector(Bytes(value)) - set(key, v) - Some(v.size) - } - } + override def rpushx(key: Any, value: Any)(implicit format: Format): Option[Long] = rpush(key, value) // LLEN // return the length of the list stored at the specified key. @@ -75,19 +58,14 @@ trait MockListOperations extends ListOperations with MockOperations with Storage // Start and end are zero-based indexes. override def lrange[A](key: Any, start: Int, end: Int)(implicit format: Format, parse: Parse[A]): Option[List[Option[A]]] = { getRaw(key) map { - _.sliceFromTo(start, end).map(bs => Some(bs.parse(parse))).toList + _.sliceFromTo(start, end).map(_.parseOption(parse)).toList } } // LTRIM // Trim an existing list so that it will contain only the specified range of elements specified. - override def ltrim(key: Any, start: Int, end: Int)(implicit format: Format): Boolean = currentDB.synchronized { - getRaw(key) match { - case Some(xs) => - set(key, xs.slice(start, end + 1)) - true - case None => false - } + override def ltrim(key: Any, start: Int, end: Int)(implicit format: Format): Boolean = withDB { + getRaw(key).map { xs => setRaw(key, xs.slice(start, end + 1))}.isDefined } // LINDEX @@ -99,16 +77,10 @@ trait MockListOperations extends ListOperations with MockOperations with Storage // LSET // set the list element at index with the new value. Out of range indexes will generate an error override def lset(key: Any, index: Int, value: Any)(implicit format: Format): Boolean = - getRaw(key) match { - case Some(xs) => - if (index < 0 || xs.length <= index) { - throw new IndexOutOfBoundsException("index out of range") - } - set(key, xs.updated(index, Bytes(value))) - true - case _ => - false - } + getRaw(key).map { xs => + if (index < 0 || xs.length <= index) {throw new IndexOutOfBoundsException("index out of range")} + setRaw(key, xs.updated(index, Bytes(value))) + }.isDefined // LREM // Removes the first count occurrences of elements equal to value from the list stored at key. @@ -124,7 +96,7 @@ trait MockListOperations extends ListOperations with MockOperations with Storage case _ => sofar.reverse ::: xs } - currentDB.synchronized { + withDB { for { xs <- getRaw(key) } yield { @@ -133,7 +105,7 @@ trait MockListOperations extends ListOperations with MockOperations with Storage case _ if count == 0 => f(xs.toList, xs.length, Bytes(value), Nil) case _ => f(xs.toList, count, Bytes(value), Nil) }).toVector - set(key, ys) + setRaw(key, ys) xs.length - ys.length } } @@ -141,41 +113,40 @@ trait MockListOperations extends ListOperations with MockOperations with Storage // LPOP // atomically return and remove the first (LPOP) or last (RPOP) element of the list - override def lpop[A](key: Any)(implicit format: Format, parse: Parse[A]): Option[A] = currentDB.synchronized { + override def lpop[A](key: Any)(implicit format: Format, parse: Parse[A]): Option[A] = withDB { for { v <- getRaw(key) x <- v.headOption } yield { - set(key, v.tail) + setRaw(key, v.tail) x.parse(parse) } } // RPOP // atomically return and remove the first (LPOP) or last (RPOP) element of the list - override def rpop[A](key: Any)(implicit format: Format, parse: Parse[A]): Option[A] = currentDB.synchronized { + override def rpop[A](key: Any)(implicit format: Format, parse: Parse[A]): Option[A] = withDB { for { v <- getRaw(key) x <- v.lastOption } yield { - set(key, v.init) + setRaw(key, v.init) x.parse(parse) } } // RPOPLPUSH // TBD - override def rpoplpush[A](srcKey: Any, dstKey: Any)(implicit format: Format, parse: Parse[A]): Option[A] = - currentDB.synchronized { - for { - v <- getRaw(srcKey) - x <- v.lastOption - } yield { - set(srcKey, v.init) - lpushx(dstKey, x.toArray) - x.parse(parse) - } + override def rpoplpush[A](srcKey: Any, dstKey: Any)(implicit format: Format, parse: Parse[A]): Option[A] = withDB { + for { + v <- getRaw(srcKey) + x <- v.lastOption + } yield { + setRaw(srcKey, v.init) + lpushx(dstKey, x.toArray) + x.parse(parse) } + } override def brpoplpush[A](srcKey: Any, dstKey: Any, timeoutInSeconds: Int)(implicit format: Format, parse: Parse[A]): Option[A] = { @tailrec diff --git a/src/main/scala/com/github/mogproject/redismock/MockOperations.scala b/src/main/scala/com/github/mogproject/redismock/MockOperations.scala index 79d1b6e..1482b6a 100644 --- a/src/main/scala/com/github/mogproject/redismock/MockOperations.scala +++ b/src/main/scala/com/github/mogproject/redismock/MockOperations.scala @@ -37,7 +37,7 @@ trait MockOperations extends Operations with Storage { self: Redis => override def keys[A](pattern: Any = "*")(implicit format: Format, parse: Parse[A]): Option[List[Option[A]]] = { val r = StringUtil.globToRegex(pattern.toString) println(r) - Some(currentDB.keys.withFilter(k => k.k.toString.matches(r)).map(k => k.k.parseOption(parse)).toList) + Some(currentDB.keys.withFilter(k => k.k.parse().matches(r)).map(k => k.k.parseOption(parse)).toList) } // RANDKEY diff --git a/src/main/scala/com/github/mogproject/redismock/MockStringOperations.scala b/src/main/scala/com/github/mogproject/redismock/MockStringOperations.scala index 32f3a69..39eea7a 100644 --- a/src/main/scala/com/github/mogproject/redismock/MockStringOperations.scala +++ b/src/main/scala/com/github/mogproject/redismock/MockStringOperations.scala @@ -2,6 +2,7 @@ package com.github.mogproject.redismock import com.github.mogproject.redismock.entity.{Bytes, StringValue, Key, STRING} import com.github.mogproject.redismock.storage.Storage +import com.github.mogproject.redismock.util.ops._ import com.redis._ import com.redis.serialization.Format import com.redis.serialization.Parse @@ -15,33 +16,31 @@ trait MockStringOperations extends StringOperations with MockOperations with Sto // helper functions // - private def setRaw(key: Any, value: Bytes, ttl: Option[Long])(implicit format: Format): Boolean = { + private def setRaw(key: Any, value: Bytes)(implicit format: Format): Unit = + currentDB.update(Key(key), StringValue(value)) + + private def setRaw(key: Any, value: Bytes, ttl: Option[Long])(implicit format: Format): Unit = currentDB.update(Key(key), StringValue(value), ttl) - true - } - // private[this] def setRaw(key: Any, value: Any, ttl: Option[Long])(implicit format: Format): Boolean = { - // currentDB.update(Key(key), StringValue(value), ttl) - // true - // } + private def getRaw(key: Any)(implicit format: Format): Option[Bytes] = + currentDB.get(Key(key)).map(_.as(STRING)) - private[this] def getRaw(key: Any)(implicit format: Format): Option[Bytes] = - currentDB.get(Key(format.apply(key))).map(_.as(STRING)) + private def getRawOrEmpty(key: Any)(implicit format: Format): Bytes = + getRaw(key).getOrElse(Bytes.empty) - private[this] def getRawOrEmpty(key: Any)(implicit format: Format): Bytes = getRaw(key).getOrElse(Bytes.empty) + private def getTime(time: SecondsOrMillis): Long = time match { + case Seconds(v) => v * 1000L + case Millis(v) => v + } // operations for Byte /** Convert Byte object to unsigned Int */ private[this] def b2ui(b: Byte): Int = (256 + b) % 256 - /** Return resized n-array padding with zero */ - private[this] def resizeByteArray(a: Seq[Byte], n: Int): Seq[Byte] = - a.take(n) ++ Array.fill[Byte](n - a.length)(0) - - def bitBinOp(op: (Int, Int) => Int)(a: Seq[Byte], b: Seq[Byte]): Seq[Byte] = { + def bitBinOp(op: (Int, Int) => Int)(a: Bytes, b: Bytes): Bytes = { val n = math.max(a.length, b.length) - resizeByteArray(a, n) zip resizeByteArray(b, n) map { case (x, y) => op(x, y).toByte } + Bytes(a.resized(n).zip(b.resized(n)) map { case (x, y) => op(x, y).toByte}) } /** Count number of 1-bit */ @@ -56,9 +55,8 @@ trait MockStringOperations extends StringOperations with MockOperations with Sto // SET KEY (key, value) // sets the key with the specified value. - override def set(key: Any, value: Any)(implicit format: Format): Boolean = { - setRaw(key, Bytes(value), None) - } + override def set(key: Any, value: Any)(implicit format: Format): Boolean = + true whenTrue withDB {setRaw(key, Bytes(value), None)} // SET key value [EX seconds] [PX milliseconds] [NX|XX] // set the string value of a key @@ -82,15 +80,7 @@ trait MockStringOperations extends StringOperations with MockOperations with Sto } override def set(key: Any, value: Any, onlyIfExists: Boolean, time: SecondsOrMillis): Boolean = { - if (!exists(key) ^ onlyIfExists) { - val t = time match { - case Seconds(v) => v * 1000L - case Millis(v) => v - } - setRaw(key, Bytes(value), Some(t)) - } else { - false - } + (!exists(key) ^ onlyIfExists) whenTrue {setRaw(key, Bytes(value), Some(getTime(time)))} } // GET (key) @@ -100,11 +90,7 @@ trait MockStringOperations extends StringOperations with MockOperations with Sto // GETSET (key, value) // is an atomic set this value and return the old value command. override def getset[A](key: Any, value: Any)(implicit format: Format, parse: Parse[A]): Option[A] = - currentDB synchronized { - val ret = get(key) - set(key, value) - ret - } + get(key) <| { _ => set(key, value)} // SETNX (key, value) // sets the value for the specified key, only if the key is not there. @@ -114,7 +100,7 @@ trait MockStringOperations extends StringOperations with MockOperations with Sto psetex(key, expiry * 1000L, value) override def psetex(key: Any, expiryInMillis: Long, value: Any)(implicit format: Format): Boolean = - setRaw(key, Bytes(value), Some(expiryInMillis)) + true whenTrue setRaw(key, Bytes(value), Some(expiryInMillis)) // INCR (key) // increments the specified key by 1 @@ -122,7 +108,7 @@ trait MockStringOperations extends StringOperations with MockOperations with Sto // INCR (key, increment) // increments the specified key by increment - override def incrby(key: Any, increment: Int)(implicit format: Format): Option[Long] = currentDB.synchronized { + override def incrby(key: Any, increment: Int)(implicit format: Format): Option[Long] = withDB { val n = get(key).map { v => Try(v.toLong).getOrElse(throw new RuntimeException("ERR value is not an integer or out of range")) }.getOrElse(0L) + increment @@ -130,14 +116,13 @@ trait MockStringOperations extends StringOperations with MockOperations with Sto Some(n) } - override def incrbyfloat(key: Any, increment: Float)(implicit format: Format): Option[Float] = - currentDB.synchronized { - val n = get(key).map { v => - Try(v.toFloat).getOrElse(throw new RuntimeException("ERR value is not a valid float")) - }.getOrElse(0.0f) + increment - set(key, n) - Some(n) - } + override def incrbyfloat(key: Any, increment: Float)(implicit format: Format): Option[Float] = withDB { + val n = get(key).map { v => + Try(v.toFloat).getOrElse(throw new RuntimeException("ERR value is not a valid float")) + }.getOrElse(0.0f) + increment + set(key, n) + Some(n) + } // DECR (key) // decrements the specified key by 1 @@ -163,36 +148,34 @@ trait MockStringOperations extends StringOperations with MockOperations with Sto // SETRANGE key offset value // Overwrites part of the string stored at key, starting at the specified offset, // for the entire length of value. - override def setrange(key: Any, offset: Int, value: Any)(implicit format: Format): Option[Long] = - currentDB.synchronized { - val a = getRawOrEmpty(key) - val b = format(value) - val c = a.take(offset) ++ Array.fill(math.max(0, offset - a.length))(0.toByte) ++ b ++ a.drop(offset + b.length) - set(key, c) - Some(c.length) - } + override def setrange(key: Any, offset: Int, value: Any)(implicit format: Format): Option[Long] = { + val a = getRawOrEmpty(key) + val b = format(value) + val c = a.take(offset) ++ Bytes.fill(math.max(0, offset - a.length))(0.toByte) ++ b ++ a.drop(offset + b.length) + setRaw(key, c) + Some(c.length) + } // GETRANGE key start end // Returns the substring of the string value stored at key, determined by the offsets // start and end (both are inclusive). override def getrange[A](key: Any, start: Int, end: Int)(implicit format: Format, parse: Parse[A]): Option[A] = { getRaw(key).map { x => - def f(n: Int): Int = if (n < 0) x.value.length + n else n + def f(n: Int): Int = if (n < 0) x.length + n else n x.slice(f(start), f(end) + 1).parse(parse) } } // STRLEN key // gets the length of the value associated with the key - override def strlen(key: Any)(implicit format: Format): Option[Long] = - Some(getRaw(key).map(_.length.toLong).getOrElse(0L)) + override def strlen(key: Any)(implicit format: Format): Option[Long] = Some(getRawOrEmpty(key).length) // APPEND KEY (key, value) // appends the key value with the specified value. override def append(key: Any, value: Any)(implicit format: Format): Option[Long] = { val xs = getRawOrEmpty(key) val ys = xs ++ format(value) - set(key, ys) + setRaw(key, ys) Some(ys.length) } @@ -221,7 +204,7 @@ trait MockStringOperations extends StringOperations with MockOperations with Sto val v = getRawOrEmpty(key) val old = (b2ui(v.getOrElse(n)) >> m) & 1 val w: Bytes = v.updated(n, (b2ui(v.getOrElse(n)) & (0xff ^ (1 << m)) | (x << m)).toByte, 0) - setRaw(key, w, None) + setRaw(key, w) Some(old) } @@ -239,19 +222,18 @@ trait MockStringOperations extends StringOperations with MockOperations with Sto // BITOP OR destkey srckey1 srckey2 srckey3 ... srckeyN // BITOP XOR destkey srckey1 srckey2 srckey3 ... srckeyN // BITOP NOT destkey srckey - override def bitop(op: String, destKey: Any, srcKeys: Any*)(implicit format: Format): Option[Int] = - currentDB.synchronized { - val result: Seq[Byte] = op.toUpperCase match { - case "AND" => srcKeys.view.map(getRawOrEmpty(_).value).reduceLeft(bitBinOp(_ & _)) - case "OR" => srcKeys.view.map(getRawOrEmpty(_).value).reduceLeft(bitBinOp(_ | _)) - case "XOR" => srcKeys.view.map(getRawOrEmpty(_).value).reduceLeft(bitBinOp(_ ^ _)) - case "NOT" => getRawOrEmpty(srcKeys(0)).value.map(x => (~x).toByte) - } - if (result.isEmpty) { - del(destKey) - } else { - setRaw(destKey, Bytes(result), None) - } - Some(result.length) + override def bitop(op: String, destKey: Any, srcKeys: Any*)(implicit format: Format): Option[Int] = { + val result: Bytes = op.toUpperCase match { + case "AND" => srcKeys.view.map(getRawOrEmpty).reduceLeft(bitBinOp(_ & _)) + case "OR" => srcKeys.view.map(getRawOrEmpty).reduceLeft(bitBinOp(_ | _)) + case "XOR" => srcKeys.view.map(getRawOrEmpty).reduceLeft(bitBinOp(_ ^ _)) + case "NOT" => Bytes(getRawOrEmpty(srcKeys(0)).map(x => (~x).toByte)) } + if (result.isEmpty) { + del(destKey) + } else { + setRaw(destKey, result) + } + Some(result.length) + } } diff --git a/src/main/scala/com/github/mogproject/redismock/entity/Bytes.scala b/src/main/scala/com/github/mogproject/redismock/entity/Bytes.scala index a086462..a23dbee 100644 --- a/src/main/scala/com/github/mogproject/redismock/entity/Bytes.scala +++ b/src/main/scala/com/github/mogproject/redismock/entity/Bytes.scala @@ -1,25 +1,32 @@ package com.github.mogproject.redismock.entity import com.redis.serialization.{Format, Parse} +import com.redis.serialization.Parse.parseDefault import scala.collection.immutable.VectorBuilder -import scala.collection.{IndexedSeqLike, mutable} +import scala.collection.mutable import scala.util.Try + /** * data should be stored as vector to compare accurately * @param value value binary */ -case class Bytes(value: Vector[Byte]) extends IndexedSeqLike[Byte, Bytes] { - - def newBuilder: mutable.Builder[Byte, Bytes] = new BytesBuilder +case class Bytes(value: Vector[Byte]) + extends scala.collection.AbstractSeq[Byte] + with scala.collection.immutable.IndexedSeq[Byte] + with scala.collection.TraversableLike[Byte, Bytes] + with scala.collection.IndexedSeqLike[Byte, Bytes] + with scala.Serializable { - lazy val seq: IndexedSeq[Byte] = value + override def newBuilder: mutable.Builder[Byte, Bytes] = new BytesBuilder - lazy val length: Int = value.length +// override val seq: IndexedSeq[Byte] = value +// + override val length: Int = value.length - def parse[A](parse: Parse[A]): A = parse(value.toArray) + def parse[A](parse: Parse[A] = parseDefault): A = parse(value.toArray) - def parseOption[A](parse: Parse[A]): Option[A] = Try(parse(value.toArray)).toOption + def parseOption[A](parse: Parse[A] = parseDefault): Option[A] = Try(this.parse(parse)).toOption def ++(bs: Bytes): Bytes = Bytes(value ++ bs.value) @@ -42,6 +49,9 @@ case class Bytes(value: Vector[Byte]) extends IndexedSeqLike[Byte, Bytes] { } } + /** Return n-length Bytes padding with elem */ + def resized(n: Int, elem: => Byte = 0.toByte): Bytes = take(n) ++ Bytes.fill(n - length)(elem) + override def equals(other: Any): Boolean = canEqual(other) && (other match { case that: Bytes => this.value == that.value case _ => false @@ -49,7 +59,7 @@ case class Bytes(value: Vector[Byte]) extends IndexedSeqLike[Byte, Bytes] { override def hashCode: Int = value.hashCode - override def toString = new String(value.toArray) + def newString = new String(value.toArray) } /** @@ -57,10 +67,10 @@ case class Bytes(value: Vector[Byte]) extends IndexedSeqLike[Byte, Bytes] { */ object Bytes { -// def newBuilder[A]: mutable.Builder[Byte, Bytes] = new BytesBuilder - def apply(v: Traversable[Byte]): Bytes = new Bytes(v.toVector) + def apply(xs: Int*): Bytes = new Bytes(xs.map(_.toByte).toVector) + def apply(v: Any)(implicit format: Format): Bytes = new Bytes(format(v).toVector) def fill(n: Int)(elem: => Byte): Bytes = Bytes(Vector.fill(n)(elem)) @@ -73,7 +83,7 @@ final class BytesBuilder() extends mutable.Builder[Byte, Bytes] { private val builder = new VectorBuilder[Byte] - def += (elem: Byte): this.type = { + def +=(elem: Byte): this.type = { builder += elem this } diff --git a/src/main/scala/com/github/mogproject/redismock/entity/Value.scala b/src/main/scala/com/github/mogproject/redismock/entity/Value.scala index 290cb52..06e681a 100644 --- a/src/main/scala/com/github/mogproject/redismock/entity/Value.scala +++ b/src/main/scala/com/github/mogproject/redismock/entity/Value.scala @@ -27,23 +27,10 @@ case class SetValue(value: SET.DataType) extends Value { val valueType = SET } case class HashValue(value: HASH.DataType) extends Value { val valueType = HASH } -object StringValue { - def apply(value: Any)(implicit format: Format): StringValue = apply(value) -} - -object ListValue { - def apply(value: Traversable[Array[Byte]]): ListValue = new ListValue(value.map(Bytes.apply).toVector) +object StringValue - def apply(value: Traversable[Any])(implicit format: Format): ListValue = apply(value.map(format.apply)) -} +object ListValue -object SetValue { - def apply(value: Set[Any])(implicit format: Format): ListValue = apply(value.map(format.apply)) - - def apply(value: Traversable[Any])(implicit format: Format): ListValue = apply(value.map(format.apply)) -} +object SetValue -object HashValue { - def apply(value: Map[Any, Any])(implicit format: Format): HashValue = - new HashValue(value.map { case (k, v) => Bytes(k) -> Bytes(v) }) -} \ No newline at end of file +object HashValue diff --git a/src/main/scala/com/github/mogproject/redismock/storage/Storage.scala b/src/main/scala/com/github/mogproject/redismock/storage/Storage.scala index a749d0b..4cd53db 100644 --- a/src/main/scala/com/github/mogproject/redismock/storage/Storage.scala +++ b/src/main/scala/com/github/mogproject/redismock/storage/Storage.scala @@ -20,6 +20,12 @@ trait Storage { * get specified database in the current node */ def getDB(db: Int): Database = currentNode.getOrElseUpdate(db, new Database) + + /** + * syntax sugar for executing atomic tasks with current DB + * @param thunk atomic tasks + */ + def withDB[A](thunk: => A): A = { currentDB.synchronized(thunk) } } object Storage { diff --git a/src/main/scala/com/github/mogproject/redismock/util/TTLTrieMap.scala b/src/main/scala/com/github/mogproject/redismock/util/TTLTrieMap.scala index 0493392..a82d501 100644 --- a/src/main/scala/com/github/mogproject/redismock/util/TTLTrieMap.scala +++ b/src/main/scala/com/github/mogproject/redismock/util/TTLTrieMap.scala @@ -43,8 +43,8 @@ class TTLTrieMap[K, V] { * @param timestamp timestamp to be expired in millis * @return true if the key exists */ - def updateExpireAt(key: K, timestamp: Long): Boolean = withTransaction { t => - t.contains(key) whenTrue t.expireAt.update(key, timestamp) + def updateExpireAt(key: K, timestamp: Long): Boolean = synchronized { + contains(key) whenTrue expireAt.update(key, timestamp) } /** @@ -168,8 +168,6 @@ class TTLTrieMap[K, V] { f } - def withTransaction[A](thunk: TTLTrieMap[K, V] => A): A = synchronized(thunk(this)) - override def equals(other: Any): Boolean = other match { case that: TTLTrieMap[K, V] => withTruncate()(this.store == that.store && this.expireAt == that.expireAt) case _ => false diff --git a/src/test/scala/com/github/mogproject/redismock/entity/BytesSpec.scala b/src/test/scala/com/github/mogproject/redismock/entity/BytesSpec.scala new file mode 100644 index 0000000..79d49fd --- /dev/null +++ b/src/test/scala/com/github/mogproject/redismock/entity/BytesSpec.scala @@ -0,0 +1,79 @@ +package com.github.mogproject.redismock.entity + +import org.scalatest.FunSpec +import org.scalatest.BeforeAndAfterEach +import org.scalatest.BeforeAndAfterAll +import org.scalatest.Matchers +import org.scalatest.prop.GeneratorDrivenPropertyChecks + + +class BytesSpec extends FunSpec +with Matchers +with GeneratorDrivenPropertyChecks +with BeforeAndAfterEach +with BeforeAndAfterAll { + describe("Bytes#seq") { + it("should return sequence of Byte") { + Bytes(1, 2, 3, -1, 0).seq shouldBe Bytes(1, 2, 3, -1, 0) + } + it("should describe its whole data") { + forAll { xs: Seq[Byte] => + Bytes(xs).seq shouldBe Bytes(xs) + } + } + } + + describe("Bytes#length") { + it("should return length of data") { + Bytes(1, 2, 3, -1, 0).length shouldBe 5 + } + } + + describe("Bytes#equals") { + it("should return true when the data is same") { + Bytes(1, 2, 3, -1, 0).equals(Bytes(1, 2, 3, -1, 0)) shouldBe true + Bytes(1, 2, 3, -1, 0).equals(Seq[Byte](1, 2, 3, -1, 0)) shouldBe false + Bytes(1, 2, 3, -1, 0).equals(1.23) shouldBe false + } + } + + describe("Bytes#fiil") { + it("should fill value of the specified number") { + Bytes.fill(-1)(3.toByte) shouldBe Bytes.empty + Bytes.fill(0)(3.toByte) shouldBe Bytes.empty + Bytes.fill(1)(3.toByte) shouldBe Bytes(3) + Bytes.fill(5)(3.toByte) shouldBe Bytes(3, 3, 3, 3, 3) + } + } + + describe("Bytes#apply") { + it("should construct with Vector[Byte]") { + Bytes.apply(Vector.empty[Byte]) shouldBe Bytes.empty + Bytes.apply(Vector[Byte](1.toByte)) shouldBe Bytes(1) + Bytes.apply(Vector[Byte](1.toByte, 2.toByte, 3.toByte)) shouldBe Bytes(1, 2, 3) + } + it("should construct with empty") { + Bytes.apply() shouldBe Bytes.empty + } + it("should construct with Int") { + Bytes.apply(1) shouldBe Bytes(1) + Bytes.apply(1, 2, 3) shouldBe Bytes(1, 2, 3) + Bytes.apply(1, 2, 3.toByte) shouldBe Bytes(1, 2, 3) + } + it("should construct with Byte") { + Bytes.apply(1.toByte) shouldBe Bytes(1) + Bytes.apply(1.toByte, 2.toByte, 3.toByte) shouldBe Bytes(1, 2, 3) + } + } + + describe("ByteBuilder") { + it("should build Bytes") { + val b = new BytesBuilder + b += 1 + b.clear() + b += 2 + b ++= Seq(3, 4) + b.result() shouldBe Bytes(2, 3, 4) + } + } +} From 0844ccf948e86df70770ffd8743c9df62844fbd4 Mon Sep 17 00:00:00 2001 From: Yosuke Mizutani Date: Wed, 29 Apr 2015 14:05:56 +0900 Subject: [PATCH 2/3] add tests to BytesSpec --- .../com/github/mogproject/redismock/entity/Bytes.scala | 2 -- .../github/mogproject/redismock/entity/BytesSpec.scala | 8 ++++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/scala/com/github/mogproject/redismock/entity/Bytes.scala b/src/main/scala/com/github/mogproject/redismock/entity/Bytes.scala index a23dbee..72d4240 100644 --- a/src/main/scala/com/github/mogproject/redismock/entity/Bytes.scala +++ b/src/main/scala/com/github/mogproject/redismock/entity/Bytes.scala @@ -20,8 +20,6 @@ case class Bytes(value: Vector[Byte]) override def newBuilder: mutable.Builder[Byte, Bytes] = new BytesBuilder -// override val seq: IndexedSeq[Byte] = value -// override val length: Int = value.length def parse[A](parse: Parse[A] = parseDefault): A = parse(value.toArray) diff --git a/src/test/scala/com/github/mogproject/redismock/entity/BytesSpec.scala b/src/test/scala/com/github/mogproject/redismock/entity/BytesSpec.scala index 79d49fd..088b148 100644 --- a/src/test/scala/com/github/mogproject/redismock/entity/BytesSpec.scala +++ b/src/test/scala/com/github/mogproject/redismock/entity/BytesSpec.scala @@ -46,6 +46,14 @@ with BeforeAndAfterAll { } } + describe("Bytes#newString") { + it("should make string from bytes") { + Bytes.empty.newString shouldBe "" + Bytes(97).newString shouldBe "a" + Bytes(97, 98, 99, 100, 101).newString shouldBe "abcde" + } + } + describe("Bytes#apply") { it("should construct with Vector[Byte]") { Bytes.apply(Vector.empty[Byte]) shouldBe Bytes.empty From d327c1277571fa3070ee0a21b6ac905bd6a832eb Mon Sep 17 00:00:00 2001 From: Yosuke Mizutani Date: Wed, 29 Apr 2015 17:49:52 +0900 Subject: [PATCH 3/3] add tests for util.ops closes #24 --- .../redismock/util/ops/BooleanOpsSpec.scala | 33 +++++++++++++++++++ .../redismock/util/ops/IdOpsSpec.scala | 21 ++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/test/scala/com/github/mogproject/redismock/util/ops/BooleanOpsSpec.scala create mode 100644 src/test/scala/com/github/mogproject/redismock/util/ops/IdOpsSpec.scala diff --git a/src/test/scala/com/github/mogproject/redismock/util/ops/BooleanOpsSpec.scala b/src/test/scala/com/github/mogproject/redismock/util/ops/BooleanOpsSpec.scala new file mode 100644 index 0000000..4b96424 --- /dev/null +++ b/src/test/scala/com/github/mogproject/redismock/util/ops/BooleanOpsSpec.scala @@ -0,0 +1,33 @@ +package com.github.mogproject.redismock.util.ops + +import org.scalatest.{Matchers, FunSpec} + +class BooleanOpsSpec extends FunSpec with Matchers { + + describe("BooleanOps#whenTrue") { + it("should return false with no side effect") { + var x = 0 + false whenTrue (x += 1) shouldBe false + x shouldBe 0 + } + it("should return true with side effect") { + var x = 0 + true whenTrue (x += 1) shouldBe true + x shouldBe 1 + } + } + + describe("BooleanOps#whenFalse") { + it("should return false with side effect") { + var x = 0 + false whenFalse (x += 1) shouldBe false + x shouldBe 1 + } + it("should return true with no side effect") { + var x = 0 + true whenFalse (x += 1) shouldBe true + x shouldBe 0 + } + } + +} diff --git a/src/test/scala/com/github/mogproject/redismock/util/ops/IdOpsSpec.scala b/src/test/scala/com/github/mogproject/redismock/util/ops/IdOpsSpec.scala new file mode 100644 index 0000000..556c134 --- /dev/null +++ b/src/test/scala/com/github/mogproject/redismock/util/ops/IdOpsSpec.scala @@ -0,0 +1,21 @@ +package com.github.mogproject.redismock.util.ops + +import org.scalatest.{Matchers, FunSpec} + +class IdOpsSpec extends FunSpec with Matchers { + + describe("IdOps#|>") { + it("should pipe operations") { + 3 |> (_ * 2) shouldBe 6 + 3 |> (_ * 2) |> (_ - 1) shouldBe 5 + } + } + + describe("IdOps#<|") { + it("should do with side effect and return the original value") { + true <| { x => assert(x) } shouldBe true + 123 <| { x => assert(x == 123) } shouldBe 123 + } + } + +}