aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorvlad <vlad@drivergrp.com>2016-07-19 15:01:30 -0400
committervlad <vlad@drivergrp.com>2016-07-19 15:01:30 -0400
commit979ff9e765e3c08501cbd00354a87013853fe796 (patch)
treec5d41ed99759c3bf97ba4ef9162aeb68ed4c29f8
parent8d45c2ec5e8abc63046c610109471cc3fa7bfaa2 (diff)
downloaddriver-core-979ff9e765e3c08501cbd00354a87013853fe796.tar.gz
driver-core-979ff9e765e3c08501cbd00354a87013853fe796.tar.bz2
driver-core-979ff9e765e3c08501cbd00354a87013853fe796.zip
Unit tests for core code and bug fixes
-rw-r--r--project/Build.scala8
-rw-r--r--src/main/scala/com/drivergrp/core/app.scala22
-rw-r--r--src/main/scala/com/drivergrp/core/generators.scala32
-rw-r--r--src/main/scala/com/drivergrp/core/logging.scala7
-rw-r--r--src/main/scala/com/drivergrp/core/messages.scala2
-rw-r--r--src/main/scala/com/drivergrp/core/rest.scala4
-rw-r--r--src/main/scala/com/drivergrp/core/stats.scala3
-rw-r--r--src/main/scala/com/drivergrp/core/time.scala8
-rw-r--r--src/test/scala/com/drivergrp/core/CoreTest.scala30
-rw-r--r--src/test/scala/com/drivergrp/core/GeneratorsTest.scala206
-rw-r--r--src/test/scala/com/drivergrp/core/MessagesTest.scala80
-rw-r--r--src/test/scala/com/drivergrp/core/RestTest.scala40
-rw-r--r--src/test/scala/com/drivergrp/core/StatsTest.scala43
-rw-r--r--src/test/scala/com/drivergrp/core/TimeTest.scala57
14 files changed, 510 insertions, 32 deletions
diff --git a/project/Build.scala b/project/Build.scala
index 9af8d04..c0f3e5e 100644
--- a/project/Build.scala
+++ b/project/Build.scala
@@ -14,11 +14,6 @@ object BuildSettings {
Wart.Overloading, Wart.DefaultArguments, Wart.ToString, Wart.Any, Wart.Throw)
)
- val acyclicSettings = Seq(
- autoCompilerPlugins := true,
- addCompilerPlugin("com.lihaoyi" %% "acyclic" % "0.1.4")
- )
-
val compileScalastyle = taskKey[Unit]("compileScalastyle")
val buildSettings = Defaults.coreDefaultSettings ++ Seq (
@@ -33,7 +28,7 @@ object BuildSettings {
fork in run := true,
compileScalastyle := (scalastyle in Compile).toTask("").value,
(compile in Compile) <<= ((compile in Compile) dependsOn compileScalastyle)
- ) ++ wartRemoverSettings ++ acyclicSettings ++ reformatOnCompileSettings
+ ) ++ wartRemoverSettings ++ reformatOnCompileSettings
}
object DriverBuild extends Build {
@@ -48,6 +43,7 @@ object DriverBuild extends Build {
"com.typesafe.akka" %% "akka-http-spray-json-experimental" % akkaHttpV,
"com.typesafe.akka" %% "akka-http-testkit" % akkaHttpV,
"org.scalatest" % "scalatest_2.11" % "2.2.1" % "test",
+ "org.mockito" % "mockito-core" % "1.9.5" % "test",
"com.typesafe.slick" %% "slick" % "3.0.0",
"com.typesafe" % "config" % "1.2.1",
"com.typesafe.scala-logging" %% "scala-logging" % "3.1.0",
diff --git a/src/main/scala/com/drivergrp/core/app.scala b/src/main/scala/com/drivergrp/core/app.scala
index 13663ab..4639fc0 100644
--- a/src/main/scala/com/drivergrp/core/app.scala
+++ b/src/main/scala/com/drivergrp/core/app.scala
@@ -52,14 +52,22 @@ object app {
}
protected def bindHttp(modules: Seq[Module]): Unit = {
- import SprayJsonSupport._
- import DefaultJsonProtocol._
-
val serviceTypes = modules.flatMap(_.routeTypes)
val swaggerService = new Swagger(actorSystem, serviceTypes, config)
val swaggerRoutes = swaggerService.routes ~ swaggerService.swaggerUI
+ val versionRt = versionRoute(version, buildNumber)
+
+ val _ = http.bindAndHandle(
+ route2HandlerFlow(logRequestResult("log")(modules.map(_.route).foldLeft(versionRt ~ swaggerRoutes)(_ ~ _))),
+ interface,
+ port)(materializer)
+ }
- val versionRoute = path("version") {
+ protected def versionRoute(version: String, buildNumber: Int) = {
+ import SprayJsonSupport._
+ import DefaultJsonProtocol._
+
+ path("version") {
complete(
Map(
"version" -> version,
@@ -67,12 +75,6 @@ object app {
"serverTime" -> time.currentTime().millis.toString
))
}
-
- val _ = http.bindAndHandle(
- route2HandlerFlow(
- logRequestResult("log")(modules.map(_.route).foldLeft(versionRoute ~ swaggerRoutes)(_ ~ _))),
- interface,
- port)(materializer)
}
/**
diff --git a/src/main/scala/com/drivergrp/core/generators.scala b/src/main/scala/com/drivergrp/core/generators.scala
index 6055cd0..a564374 100644
--- a/src/main/scala/com/drivergrp/core/generators.scala
+++ b/src/main/scala/com/drivergrp/core/generators.scala
@@ -12,12 +12,14 @@ object generators {
import random._
private val DefaultMaxLength = 100
+ private val StringLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ".toSet
- def nextId[T](): Id[T] = Id[T](nextLong())
+ def nextId[T](): Id[T] = Id[T](scala.math.abs(nextLong()))
def nextName[T](maxLength: Int = DefaultMaxLength): Name[T] = Name[T](nextString(maxLength))
- def nextString(maxLength: Int = DefaultMaxLength) = random.nextString(maxLength)
+ def nextString(maxLength: Int = DefaultMaxLength): String =
+ (oneOf[Char](StringLetters) +: arrayOf(oneOf[Char](StringLetters), maxLength - 1)).mkString
def nextOption[T](value: => T): Option[T] = if (nextBoolean) Option(value) else None
@@ -27,23 +29,35 @@ object generators {
def nextTime(): Time = Time(math.abs(nextLong() % System.currentTimeMillis))
- def nextTimeRange(): TimeRange = TimeRange(nextTime(), nextTime())
+ def nextTimeRange(): TimeRange = {
+ val oneTime = nextTime()
+ val anotherTime = nextTime()
+
+ TimeRange(Time(scala.math.min(oneTime.millis, anotherTime.millis)),
+ Time(scala.math.max(oneTime.millis, anotherTime.millis)))
+ }
def nextBigDecimal(multiplier: Double = 1000000.00, precision: Int = 2): BigDecimal =
BigDecimal(multiplier * nextDouble, new MathContext(precision))
- def oneOf[T](items: Seq[T]): T = items(nextInt(items.size))
+ def oneOf[T](items: T*): T = oneOf(items.toSet)
+
+ def oneOf[T](items: Set[T]): T = items.toSeq(nextInt(items.size))
def arrayOf[T: ClassTag](generator: => T, maxLength: Int = DefaultMaxLength): Array[T] =
- Array.fill(maxLength)(generator)
+ Array.fill(nextInt(maxLength))(generator)
- def seqOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Seq[T] = Seq.fill(maxLength)(generator)
+ def seqOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Seq[T] =
+ Seq.fill(nextInt(maxLength))(generator)
- def vectorOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Vector[T] = Vector.fill(maxLength)(generator)
+ def vectorOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Vector[T] =
+ Vector.fill(nextInt(maxLength))(generator)
- def listOf[T](generator: => T, maxLength: Int = DefaultMaxLength): List[T] = List.fill(maxLength)(generator)
+ def listOf[T](generator: => T, maxLength: Int = DefaultMaxLength): List[T] =
+ List.fill(nextInt(maxLength))(generator)
- def setOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Set[T] = seqOf(generator, maxLength).toSet
+ def setOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Set[T] =
+ seqOf(generator, nextInt(maxLength)).toSet
def mapOf[K, V](maxLength: Int, keyGenerator: => K, valueGenerator: => V): Map[K, V] =
seqOf(nextPair(keyGenerator, valueGenerator), maxLength).toMap
diff --git a/src/main/scala/com/drivergrp/core/logging.scala b/src/main/scala/com/drivergrp/core/logging.scala
index a4557e0..126c670 100644
--- a/src/main/scala/com/drivergrp/core/logging.scala
+++ b/src/main/scala/com/drivergrp/core/logging.scala
@@ -35,6 +35,13 @@ object logging {
def debug(marker: Marker, message: String, args: AnyRef*): Unit
}
+ /**
+ * Logger implementation which uses `com.typesafe.scalalogging.Logger` on the back.
+ * It redefines the meaning of logging levels to fit to the Driver infrastructure design,
+ * and as using error and warn, debug and trace was always confusing and mostly done wrong.
+ *
+ * @param scalaLogging com.typesafe.scalalogging.Logger which logging will be delegated to
+ */
class TypesafeScalaLogger(scalaLogging: com.typesafe.scalalogging.Logger) extends Logger {
def fatal(message: String): Unit = scalaLogging.error(message)
diff --git a/src/main/scala/com/drivergrp/core/messages.scala b/src/main/scala/com/drivergrp/core/messages.scala
index a2d6cbd..3a97401 100644
--- a/src/main/scala/com/drivergrp/core/messages.scala
+++ b/src/main/scala/com/drivergrp/core/messages.scala
@@ -31,7 +31,7 @@ object messages {
map.get(key) match {
case Some(message) => message
case None =>
- log.error(s"Message with key $key not found for locale " + locale.getDisplayName)
+ log.error(s"Message with key '$key' not found for locale '${locale.getLanguage}'")
key
}
}
diff --git a/src/main/scala/com/drivergrp/core/rest.scala b/src/main/scala/com/drivergrp/core/rest.scala
index adbf716..b7edc5b 100644
--- a/src/main/scala/com/drivergrp/core/rest.scala
+++ b/src/main/scala/com/drivergrp/core/rest.scala
@@ -121,11 +121,11 @@ object rest {
/**
* "Unwraps" a `OptionT[Future, T]` and runs the inner route after future
* completion with the future's value as an extraction of type `Try[T]`.
+ * Copied akka-http code with added `.run` call on `OptionT`.
*/
def onComplete[T](optionT: OptionT[Future, T]): Directive1[Try[Option[T]]] =
Directive { inner ⇒ ctx ⇒
- import ctx.executionContext
- optionT.run.fast.transformWith(t ⇒ inner(Tuple1(t))(ctx))
+ optionT.run.fast.transformWith(t ⇒ inner(Tuple1(t))(ctx))(ctx.executionContext)
}
}
diff --git a/src/main/scala/com/drivergrp/core/stats.scala b/src/main/scala/com/drivergrp/core/stats.scala
index 904fcc5..cd77f7a 100644
--- a/src/main/scala/com/drivergrp/core/stats.scala
+++ b/src/main/scala/com/drivergrp/core/stats.scala
@@ -36,7 +36,8 @@ object stats {
class LogStats(log: Logger) extends Stats {
def recordStats(keys: StatsKeys, interval: TimeRange, value: BigDecimal): Unit = {
- log.audit(s"${keys.mkString(".")}(${interval.start.millis}-${interval.end.millis})=${value.toString}")
+ val valueString = value.bigDecimal.toPlainString
+ log.audit(s"${keys.mkString(".")}(${interval.start.millis}-${interval.end.millis})=$valueString")
}
}
}
diff --git a/src/main/scala/com/drivergrp/core/time.scala b/src/main/scala/com/drivergrp/core/time.scala
index dfa63c8..ebe0071 100644
--- a/src/main/scala/com/drivergrp/core/time.scala
+++ b/src/main/scala/com/drivergrp/core/time.scala
@@ -3,7 +3,7 @@ package com.drivergrp.core
import java.text.SimpleDateFormat
import java.util.{Calendar, Date, GregorianCalendar}
-import scala.concurrent.duration.Duration
+import scala.concurrent.duration._
object time {
@@ -25,10 +25,12 @@ object time {
def isAfter(anotherTime: Time): Boolean = millis > anotherTime.millis
- def advanceBy(duration: Duration): Time = Time(millis + duration.length)
+ def advanceBy(duration: Duration): Time = Time(millis + duration.toMillis)
}
- final case class TimeRange(start: Time, end: Time)
+ final case class TimeRange(start: Time, end: Time) {
+ def duration: Duration = FiniteDuration(end.millis - start.millis, MILLISECONDS)
+ }
implicit def timeOrdering: Ordering[Time] = Ordering.by(_.millis)
diff --git a/src/test/scala/com/drivergrp/core/CoreTest.scala b/src/test/scala/com/drivergrp/core/CoreTest.scala
new file mode 100644
index 0000000..005cda5
--- /dev/null
+++ b/src/test/scala/com/drivergrp/core/CoreTest.scala
@@ -0,0 +1,30 @@
+package com.drivergrp.core
+
+import java.io.ByteArrayOutputStream
+
+import org.scalatest.mock.MockitoSugar
+import org.scalatest.{FlatSpec, Matchers}
+import org.mockito.Mockito._
+
+class CoreTest extends FlatSpec with Matchers with MockitoSugar {
+
+ "'make' function" should "allow initialization for objects" in {
+
+ val createdAndInitializedValue = make(new ByteArrayOutputStream(128)) { baos =>
+ baos.write(Array(1.toByte, 1.toByte, 0.toByte))
+ }
+
+ createdAndInitializedValue.toByteArray should be(Array(1.toByte, 1.toByte, 0.toByte))
+ }
+
+ "'using' function" should "call close after performing action on resource" in {
+
+ val baos = mock[ByteArrayOutputStream]
+
+ using(baos /* usually new ByteArrayOutputStream(128) */ ) { baos =>
+ baos.write(Array(1.toByte, 1.toByte, 0.toByte))
+ }
+
+ verify(baos).close()
+ }
+}
diff --git a/src/test/scala/com/drivergrp/core/GeneratorsTest.scala b/src/test/scala/com/drivergrp/core/GeneratorsTest.scala
new file mode 100644
index 0000000..9332e7f
--- /dev/null
+++ b/src/test/scala/com/drivergrp/core/GeneratorsTest.scala
@@ -0,0 +1,206 @@
+package com.drivergrp.core
+
+import org.scalatest.{Assertions, FlatSpec, Matchers}
+
+class GeneratorsTest extends FlatSpec with Matchers with Assertions {
+ import generators._
+
+ "Generators" should "be able to generate com.drivergrp.core.Id identifiers" in {
+
+ val generatedId1 = nextId[String]()
+ val generatedId2 = nextId[String]()
+ val generatedId3 = nextId[Long]()
+
+ generatedId1 should be >= 0L
+ generatedId2 should be >= 0L
+ generatedId3 should be >= 0L
+ generatedId1 should not be generatedId2
+ generatedId2 should !==(generatedId3)
+ }
+
+ it should "be able to generate com.drivergrp.core.Name names" in {
+
+ nextName[String]() should not be nextName[String]()
+ nextName[String]().length should be >= 0
+
+ val fixedLengthName = nextName[String](10)
+ fixedLengthName.length should be <= 10
+ assert(!fixedLengthName.exists(_.isControl))
+ }
+
+ it should "be able to generate strings" in {
+
+ nextString() should not be nextString()
+ nextString().length should be >= 0
+
+ val fixedLengthString = nextString(20)
+ fixedLengthString.length should be <= 20
+ assert(!fixedLengthString.exists(_.isControl))
+ }
+
+ it should "be able to generate options which are sometimes have values and sometimes not" in {
+
+ val generatedOption = nextOption("2")
+
+ generatedOption should not contain "1"
+ assert(generatedOption === Some("2") || generatedOption === None)
+ }
+
+ it should "be able to generate a pair of two generated values" in {
+
+ val constantPair = nextPair("foo", 1L)
+ constantPair._1 should be("foo")
+ constantPair._2 should be(1L)
+
+ val generatedPair = nextPair(nextId[Int](), nextName[Int]())
+
+ generatedPair._1 should be > 0L
+ generatedPair._2.length should be > 0
+
+ nextPair(nextId[Int](), nextName[Int]()) should not be
+ nextPair(nextId[Int](), nextName[Int]())
+ }
+
+ it should "be able to generate a triad of two generated values" in {
+
+ val constantTriad = nextTriad("foo", "bar", 1L)
+ constantTriad._1 should be("foo")
+ constantTriad._2 should be("bar")
+ constantTriad._3 should be(1L)
+
+ val generatedTriad = nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal())
+
+ generatedTriad._1 should be > 0L
+ generatedTriad._2.length should be > 0
+ generatedTriad._3 should be >= BigDecimal(0.00)
+
+ nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal()) should not be
+ nextTriad(nextId[Int](), nextName[Int](), nextBigDecimal())
+ }
+
+ it should "be able to generate a time value" in {
+
+ val generatedTime = nextTime()
+ val currentTime = System.currentTimeMillis()
+
+ generatedTime.millis should be >= 0L
+ generatedTime.millis should be <= currentTime
+ }
+
+ it should "be able to generate a time range value" in {
+
+ val generatedTimeRange = nextTimeRange()
+ val currentTime = System.currentTimeMillis()
+
+ generatedTimeRange.start.millis should be >= 0L
+ generatedTimeRange.start.millis should be <= currentTime
+ generatedTimeRange.end.millis should be >= 0L
+ generatedTimeRange.end.millis should be <= currentTime
+ generatedTimeRange.start.millis should be <= generatedTimeRange.end.millis
+ }
+
+ it should "be able to generate a BigDecimal value" in {
+
+ val defaultGeneratedBigDecimal = nextBigDecimal()
+
+ defaultGeneratedBigDecimal should be >= BigDecimal(0.00)
+ defaultGeneratedBigDecimal should be <= BigDecimal(1000000.00)
+ defaultGeneratedBigDecimal.precision should be(2)
+
+ val unitIntervalBigDecimal = nextBigDecimal(1.00, 8)
+
+ unitIntervalBigDecimal should be >= BigDecimal(0.00)
+ unitIntervalBigDecimal should be <= BigDecimal(1.00)
+ unitIntervalBigDecimal.precision should be(8)
+ }
+
+ it should "be able to generate a specific value from a set of values" in {
+
+ val possibleOptions = Set(1, 3, 5, 123, 0, 9)
+
+ val pick1 = generators.oneOf(possibleOptions)
+ val pick2 = generators.oneOf(possibleOptions)
+ val pick3 = generators.oneOf(possibleOptions)
+
+ possibleOptions should contain(pick1)
+ possibleOptions should contain(pick2)
+ possibleOptions should contain(pick3)
+
+ val pick4 = generators.oneOf(1, 3, 5, 123, 0, 9)
+ val pick5 = generators.oneOf(1, 3, 5, 123, 0, 9)
+ val pick6 = generators.oneOf(1, 3, 5, 123, 0, 9)
+
+ possibleOptions should contain(pick4)
+ possibleOptions should contain(pick5)
+ possibleOptions should contain(pick6)
+
+ Set(pick1, pick2, pick3, pick4, pick5, pick6).size should be >= 1
+ }
+
+ it should "be able to generate array with values generated by generators" in {
+
+ val arrayOfTimes = arrayOf(nextTime(), 16)
+ arrayOfTimes.length should be <= 16
+
+ val arrayOfBigDecimals = arrayOf(nextBigDecimal(), 8)
+ arrayOfBigDecimals.length should be <= 8
+ }
+
+ it should "be able to generate seq with values generated by generators" in {
+
+ val seqOfTimes = seqOf(nextTime(), 16)
+ seqOfTimes.size should be <= 16
+
+ val seqOfBigDecimals = seqOf(nextBigDecimal(), 8)
+ seqOfBigDecimals.size should be <= 8
+ }
+
+ it should "be able to generate vector with values generated by generators" in {
+
+ val vectorOfTimes = vectorOf(nextTime(), 16)
+ vectorOfTimes.size should be <= 16
+
+ val vectorOfStrings = seqOf(nextString(), 8)
+ vectorOfStrings.size should be <= 8
+ }
+
+ it should "be able to generate list with values generated by generators" in {
+
+ val listOfTimes = listOf(nextTime(), 16)
+ listOfTimes.size should be <= 16
+
+ val listOfBigDecimals = seqOf(nextBigDecimal(), 8)
+ listOfBigDecimals.size should be <= 8
+ }
+
+ it should "be able to generate set with values generated by generators" in {
+
+ val setOfTimes = vectorOf(nextTime(), 16)
+ setOfTimes.size should be <= 16
+
+ val setOfBigDecimals = seqOf(nextBigDecimal(), 8)
+ setOfBigDecimals.size should be <= 8
+ }
+
+ it should "be able to generate maps with keys and values generated by generators" in {
+
+ val generatedConstantMap = mapOf(10, "key", 123)
+ generatedConstantMap.size should be <= 1
+ assert(generatedConstantMap.keys.forall(_ == "key"))
+ assert(generatedConstantMap.values.forall(_ == 123))
+
+ val generatedMap = mapOf(10, nextString(10), nextBigDecimal())
+ assert(generatedMap.keys.forall(_.length <= 10))
+ assert(generatedMap.values.forall(_ >= BigDecimal(0.00)))
+ }
+
+ it should "compose deeply" in {
+
+ val generatedNestedMap = mapOf(10, nextString(10), nextPair(nextBigDecimal(), nextOption(123)))
+
+ generatedNestedMap.size should be <= 10
+ generatedNestedMap.keySet.size should be <= 10
+ generatedNestedMap.values.size should be <= 10
+ assert(generatedNestedMap.values.forall(value => !value._2.exists(_ != 123)))
+ }
+}
diff --git a/src/test/scala/com/drivergrp/core/MessagesTest.scala b/src/test/scala/com/drivergrp/core/MessagesTest.scala
new file mode 100644
index 0000000..21fe30a
--- /dev/null
+++ b/src/test/scala/com/drivergrp/core/MessagesTest.scala
@@ -0,0 +1,80 @@
+package com.drivergrp.core
+
+import java.util.Locale
+
+import com.drivergrp.core.logging.Logger
+import com.drivergrp.core.messages.Messages
+import com.typesafe.config.{ConfigException, ConfigFactory}
+import org.mockito.Mockito._
+import org.scalatest.mock.MockitoSugar
+import org.scalatest.{FlatSpec, Matchers}
+
+import scala.collection.JavaConversions._
+
+class MessagesTest extends FlatSpec with Matchers with MockitoSugar {
+
+ val englishLocaleMessages =
+ Map("en.greeting" -> "Hello {0}!", "en.greetingFullName" -> "Hello {0} {1} {2}!", "en.hello" -> "Hello world!")
+
+ "Messages" should "read messages from config and format with parameters" in {
+
+ val log = mock[Logger]
+ val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages)
+
+ val messages = Messages.messages(messagesConfig, log, Locale.US)
+
+ messages("hello") should be("Hello world!")
+ messages("greeting", "Homer") should be("Hello Homer!")
+ messages("greetingFullName", "Homer", "J", "Simpson") should be("Hello Homer J Simpson!")
+ }
+
+ it should "be able to read messages for different locales" in {
+
+ val log = mock[Logger]
+
+ val messagesConfig = ConfigFactory.parseMap(
+ englishLocaleMessages ++ Map(
+ "zh.hello" -> "你好,世界!",
+ "zh.greeting" -> "你好,{0}!",
+ "zh.greetingFullName" -> "你好,{0} {1} {2}!"
+ ))
+
+ val englishMessages = Messages.messages(messagesConfig, log, Locale.US)
+ val englishMessagesToo = Messages.messages(messagesConfig, log, Locale.ENGLISH)
+ val chineseMessages = Messages.messages(messagesConfig, log, Locale.CHINESE)
+
+ englishMessages("hello") should be("Hello world!")
+ englishMessages("greeting", "Homer") should be("Hello Homer!")
+ englishMessages("greetingFullName", "Homer", "J", "Simpson") should be("Hello Homer J Simpson!")
+
+ englishMessagesToo("hello") should be(englishMessages("hello"))
+ englishMessagesToo("greeting", "Homer") should be(englishMessages("greeting", "Homer"))
+ englishMessagesToo("greetingFullName", "Homer", "J", "Simpson") should be(
+ englishMessages("greetingFullName", "Homer", "J", "Simpson"))
+
+ chineseMessages("hello") should be("你好,世界!")
+ chineseMessages("greeting", "Homer") should be("你好,Homer!")
+ chineseMessages("greetingFullName", "Homer", "J", "Simpson") should be("你好,Homer J Simpson!")
+ }
+
+ it should "raise exception when locale is not available" in {
+
+ val log = mock[Logger]
+ val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages)
+
+ an[ConfigException.Missing] should be thrownBy
+ Messages.messages(messagesConfig, log, Locale.GERMAN)
+ }
+
+ it should "log a problem, when there is no message for key" in {
+
+ val log = mock[Logger]
+ val messagesConfig = ConfigFactory.parseMap(englishLocaleMessages)
+
+ val messages = Messages.messages(messagesConfig, log, Locale.US)
+
+ messages("howdy") should be("howdy")
+
+ verify(log).error(s"Message with key 'howdy' not found for locale 'en'")
+ }
+}
diff --git a/src/test/scala/com/drivergrp/core/RestTest.scala b/src/test/scala/com/drivergrp/core/RestTest.scala
new file mode 100644
index 0000000..68be55c
--- /dev/null
+++ b/src/test/scala/com/drivergrp/core/RestTest.scala
@@ -0,0 +1,40 @@
+package com.drivergrp.core
+
+import com.drivergrp.core.time.provider.SystemTimeProvider
+import org.scalatest.{FlatSpec, Matchers}
+
+class RestTest extends FlatSpec with Matchers {
+
+ "Json format for Id" should "read and write correct JSON" in {
+
+ val referenceId = Id[String](1312L)
+
+ val writtenJson = com.drivergrp.core.rest.basicFormats.idFormat.write(referenceId)
+ writtenJson.prettyPrint should be("1312")
+
+ val parsedId = com.drivergrp.core.rest.basicFormats.idFormat.read(writtenJson)
+ parsedId should be(referenceId)
+ }
+
+ "Json format for Name" should "read and write correct JSON" in {
+
+ val referenceName = Name[String]("Homer")
+
+ val writtenJson = com.drivergrp.core.rest.basicFormats.nameFormat.write(referenceName)
+ writtenJson.prettyPrint should be("\"Homer\"")
+
+ val parsedName = com.drivergrp.core.rest.basicFormats.nameFormat.read(writtenJson)
+ parsedName should be(referenceName)
+ }
+
+ "Json format for Time" should "read and write correct JSON" in {
+
+ val referenceTime = new SystemTimeProvider().currentTime()
+
+ val writtenJson = com.drivergrp.core.rest.basicFormats.timeFormat.write(referenceTime)
+ writtenJson.prettyPrint should be("{\n \"timestamp\": " + referenceTime.millis + "\n}")
+
+ val parsedTime = com.drivergrp.core.rest.basicFormats.timeFormat.read(writtenJson)
+ parsedTime should be(referenceTime)
+ }
+}
diff --git a/src/test/scala/com/drivergrp/core/StatsTest.scala b/src/test/scala/com/drivergrp/core/StatsTest.scala
new file mode 100644
index 0000000..c4f449b
--- /dev/null
+++ b/src/test/scala/com/drivergrp/core/StatsTest.scala
@@ -0,0 +1,43 @@
+package com.drivergrp.core
+
+import com.drivergrp.core.logging.Logger
+import com.drivergrp.core.stats.LogStats
+import com.drivergrp.core.time.{Time, TimeRange}
+import org.scalatest.mock.MockitoSugar
+import org.scalatest.{FlatSpec, Matchers}
+import org.mockito.Mockito._
+
+class StatsTest extends FlatSpec with Matchers with MockitoSugar {
+
+ "Stats" should "format and store all recorded data" in {
+
+ val log = mock[Logger]
+ val stats = new LogStats(log)
+
+ stats.recordStats(Seq(), TimeRange(Time(2L), Time(5L)), BigDecimal(123.324))
+ verify(log).audit(s"(2-5)=123.324")
+
+ stats.recordStats("stat", TimeRange(Time(5L), Time(5L)), BigDecimal(333L))
+ verify(log).audit(s"stat(5-5)=333")
+
+ stats.recordStats("stat", Time(934L), 123)
+ verify(log).audit(s"stat(934-934)=123")
+
+ stats.recordStats("stat", Time(0L), 123)
+ verify(log).audit(s"stat(0-0)=123")
+ }
+
+ it should "format BigDecimal with all precision digits" in {
+
+ val log = mock[Logger]
+ val stats = new LogStats(log)
+
+ stats.recordStats(Seq("root", "group", "stat", "substat"),
+ TimeRange(Time(1467381889834L), Time(1468937089834L)),
+ BigDecimal(3.333333333333333))
+ verify(log).audit(s"root.group.stat.substat(1467381889834-1468937089834)=3.333333333333333")
+
+ stats.recordStats("stat", Time(1233L), BigDecimal(0.00000000000000000000001))
+ verify(log).audit(s"stat(1233-1233)=0.000000000000000000000010")
+ }
+}
diff --git a/src/test/scala/com/drivergrp/core/TimeTest.scala b/src/test/scala/com/drivergrp/core/TimeTest.scala
new file mode 100644
index 0000000..ad390c8
--- /dev/null
+++ b/src/test/scala/com/drivergrp/core/TimeTest.scala
@@ -0,0 +1,57 @@
+package com.drivergrp.core
+
+import com.drivergrp.core.time.{Time, _}
+import org.scalatest.{FlatSpec, Matchers}
+
+import scala.concurrent.duration._
+
+class TimeTest extends FlatSpec with Matchers {
+
+ "Time" should "have correct methods to compare" in {
+
+ Time(234L).isAfter(Time(123L)) should be(true)
+ Time(123L).isAfter(Time(123L)) should be(false)
+ Time(123L).isAfter(Time(234L)) should be(false)
+
+ Time(234L).isBefore(Time(123L)) should be(false)
+ Time(123L).isBefore(Time(123L)) should be(false)
+ Time(123L).isBefore(Time(234L)) should be(true)
+ }
+
+ it should "not modify time" in {
+
+ Time(234L).millis should be(234L)
+ }
+
+ it should "support arithmetic with scala.concurrent.duration" in {
+
+ Time(123L).advanceBy(0 minutes).millis should be(123L)
+ Time(123L).advanceBy(1 second).millis should be(123L + Second)
+ Time(123L).advanceBy(4 days).millis should be(123L + 4 * Days)
+ }
+
+ it should "have ordering defined correctly" in {
+
+ Seq(Time(321L), Time(123L), Time(231L)).sorted should
+ contain theSameElementsInOrderAs Seq(Time(123L), Time(231L), Time(321L))
+ }
+
+ it should "reset to the start of the period, e.g. month" in {
+
+ startOfMonth(Time(1468937089834L)) should be(Time(1467381889834L))
+ startOfMonth(Time(1467381889834L)) should be(Time(1467381889834L)) // idempotent
+ }
+
+ it should "have correct textual representations" in {
+
+ textualDate(Time(1468937089834L)) should be("July 19, 2016")
+ textualTime(Time(1468937089834L)) should be("Jul 19, 2016 10:04:49 AM")
+ }
+
+ "TimeRange" should "have duration defined as a difference of start and end times" in {
+
+ TimeRange(Time(321L), Time(432L)).duration should be(111.milliseconds)
+ TimeRange(Time(432L), Time(321L)).duration should be((-111).milliseconds)
+ TimeRange(Time(333L), Time(333L)).duration should be(0.milliseconds)
+ }
+}