From 979ff9e765e3c08501cbd00354a87013853fe796 Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 19 Jul 2016 15:01:30 -0400 Subject: Unit tests for core code and bug fixes --- src/main/scala/com/drivergrp/core/app.scala | 22 ++- src/main/scala/com/drivergrp/core/generators.scala | 32 +++- src/main/scala/com/drivergrp/core/logging.scala | 7 + src/main/scala/com/drivergrp/core/messages.scala | 2 +- src/main/scala/com/drivergrp/core/rest.scala | 4 +- src/main/scala/com/drivergrp/core/stats.scala | 3 +- src/main/scala/com/drivergrp/core/time.scala | 8 +- src/test/scala/com/drivergrp/core/CoreTest.scala | 30 +++ .../scala/com/drivergrp/core/GeneratorsTest.scala | 206 +++++++++++++++++++++ .../scala/com/drivergrp/core/MessagesTest.scala | 80 ++++++++ src/test/scala/com/drivergrp/core/RestTest.scala | 40 ++++ src/test/scala/com/drivergrp/core/StatsTest.scala | 43 +++++ src/test/scala/com/drivergrp/core/TimeTest.scala | 57 ++++++ 13 files changed, 508 insertions(+), 26 deletions(-) create mode 100644 src/test/scala/com/drivergrp/core/CoreTest.scala create mode 100644 src/test/scala/com/drivergrp/core/GeneratorsTest.scala create mode 100644 src/test/scala/com/drivergrp/core/MessagesTest.scala create mode 100644 src/test/scala/com/drivergrp/core/RestTest.scala create mode 100644 src/test/scala/com/drivergrp/core/StatsTest.scala create mode 100644 src/test/scala/com/drivergrp/core/TimeTest.scala (limited to 'src') 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) + } +} -- cgit v1.2.3