From b7c9a891e863cb814fd409684c3b655188f6d3fe Mon Sep 17 00:00:00 2001 From: Nils Homer Date: Wed, 20 Apr 2022 00:53:04 -0700 Subject: [PATCH 01/12] Add metrics sorter --- .../com/fulcrumgenomics/util/Metric.scala | 113 ++++++++++++++---- 1 file changed, 89 insertions(+), 24 deletions(-) diff --git a/src/main/scala/com/fulcrumgenomics/util/Metric.scala b/src/main/scala/com/fulcrumgenomics/util/Metric.scala index 62b0961ce..3a8e599d1 100644 --- a/src/main/scala/com/fulcrumgenomics/util/Metric.scala +++ b/src/main/scala/com/fulcrumgenomics/util/Metric.scala @@ -99,6 +99,36 @@ object Metric extends LazyLogging { } } + private[util] def build[T <: Metric](reflectiveBuilder: ReflectiveBuilder[T], + toArg: Int => String, + fail: (String, Option[Throwable]) => () + ): T = { + val names = Metric.names[T] + forloop(from = 0, until = names.length) { i => + reflectiveBuilder.argumentLookup.forField(names(i)) match { + case Some(arg) => + val value = { + val tmp = toArg(i) + if (tmp.isEmpty && arg.argumentType == classOf[Option[_]]) ReflectionUtil.SpecialEmptyOrNoneToken else tmp + } + + val argumentValue = ReflectionUtil.constructFromString(arg.argumentType, arg.unitType, value) match { + case Success(v) => v + case Failure(thr) => + fail(s"Could not construct value for column '${arg.name}' of type '${arg.typeDescription}' from '$value'", Some(thr)) + } + arg.value = argumentValue + case None => + fail(s"Did not have a field with name '${names(i)}'.", None) + } + } + + // build it. NB: if arguments are missing values, then an exception will be thrown here + // Also, we don't use the default "build()" method since if a collection or option is empty, it will be treated as + // missing. + reflectiveBuilder.build(reflectiveBuilder.argumentLookup.ordered.map(arg => arg.value getOrElse unreachable(s"Arguments not set: ${arg.name}"))) + } + /** Reads metrics from a set of lines. The first line should be the header with the field names. Each subsequent * line should be a single metric. */ def iterator[T <: Metric](lines: Iterator[String], source: Option[String] = None)(implicit tt: ru.TypeTag[T]): Iterator[T] = { @@ -122,33 +152,14 @@ object Metric extends LazyLogging { if (lines.isEmpty) fail(lineNumber=1, message="No header found") val parser = new DelimitedDataParser(lines=lines, delimiter=Delimiter, ignoreBlankLines=false, trimFields=true) - val names = parser.headers.toIndexedSeq val reflectiveBuilder = new ReflectiveBuilder(clazz) parser.zipWithIndex.map { case (row, rowIndex) => - forloop(from = 0, until = names.length) { i => - reflectiveBuilder.argumentLookup.forField(names(i)) match { - case Some(arg) => - val value = { - val tmp = row[String](i) - if (tmp.isEmpty && arg.argumentType == classOf[Option[_]]) ReflectionUtil.SpecialEmptyOrNoneToken else tmp - } - - val argumentValue = ReflectionUtil.constructFromString(arg.argumentType, arg.unitType, value) match { - case Success(v) => v - case Failure(thr) => - fail(lineNumber=rowIndex+2, message=s"Could not construct value for column '${arg.name}' of type '${arg.typeDescription}' from '$value'", Some(thr)) - } - arg.value = argumentValue - case None => - fail(lineNumber=rowIndex+2, message=s"Did not have a field with name '${names(i)}'.") - } - } - - // build it. NB: if arguments are missing values, then an exception will be thrown here - // Also, we don't use the default "build()" method since if a collection or option is empty, it will be treated as - // missing. - reflectiveBuilder.build(reflectiveBuilder.argumentLookup.ordered.map(arg => arg.value getOrElse unreachable(s"Arguments not set: ${arg.name}"))) + build( + reflectiveBuilder = reflectiveBuilder, + toArg = i => row[String](i), + fail = (message, throwable) => fail(rowIndex+2, message, throwable) + ) } } @@ -205,6 +216,60 @@ object Metric extends LazyLogging { def writer[T <: Metric](writer: Writer)(implicit tt: ru.TypeTag[T]): MetricWriter[T] = new MetricWriter[T](writer) } + +class MetricSorter[Key <: Ordered[Key], T <: Metric](maxObjectsInRam: Int = MetricSorter.MaxInMemory, + keyfunc: T => Key, + tmpDir: DirPath = Io.tmpDir + )(implicit tt: ru.TypeTag[T]) extends Sorter[T, Key]( + maxObjectsInRam = maxObjectsInRam, + codec = new MetricSorter.MetricSorterCodec[T](), + keyfunc = keyfunc, + tmpDir = tmpDir +) + +object MetricSorter { + /** The default maximum # of records to keep and sort in memory. */ + val MaxInMemory: Int = 1e6.toInt + + class MetricSorterCodec[T <: Metric]()(implicit tt: ru.TypeTag[T]) + extends Sorter.Codec[T] with LazyLogging { + private val clazz: Class[T] = ReflectionUtil.typeTagToClass[T] + private val reflectiveBuilder = new ReflectiveBuilder(clazz) + + private def fail(message: String, + throwable: Option[Throwable] = None): Unit = { + val fullMessage = s"For metric '${clazz.getSimpleName}'\n$message" + throwable.foreach { thr => + val stringWriter = new StringWriter + thr.printStackTrace(new PrintWriter(stringWriter)) + val banner = "#" * 80 + logger.debug(banner) + logger.debug(stringWriter.toString) + logger.debug(banner) + } + throw FailureException(message=Some(fullMessage)) + } + + /** Encode the metric into an array of bytes. */ + def encode(metric: T): Array[Byte] = metric.values.mkString(Metric.DelimiterAsString).getBytes + + /** Decode a metric from an array of bytes. */ + def decode(bs: Array[Byte], start: Int, length: Int): T = { + val fields = new String(bs.slice(from=start, until=start+length)).split(Metric.DelimiterAsString) + Metric.build( + reflectiveBuilder = reflectiveBuilder, + toArg = i => fields(i), + fail = fail + ) + + // build it. NB: if arguments are missing values, then an exception will be thrown here + // Also, we don't use the default "build()" method since if a collection or option is empty, it will be treated as + // missing. + reflectiveBuilder.build(reflectiveBuilder.argumentLookup.ordered.map(arg => arg.value getOrElse unreachable(s"Arguments not set: ${arg.name}"))) + } + } +} + /** * Base trait for metrics. * From 4d5bf04917babb5c5facc5354aa5d3c53ca7abcd Mon Sep 17 00:00:00 2001 From: Nils Homer Date: Wed, 20 Apr 2022 00:55:43 -0700 Subject: [PATCH 02/12] Update src/main/scala/com/fulcrumgenomics/util/Metric.scala --- src/main/scala/com/fulcrumgenomics/util/Metric.scala | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/scala/com/fulcrumgenomics/util/Metric.scala b/src/main/scala/com/fulcrumgenomics/util/Metric.scala index 3a8e599d1..38fd1e9d7 100644 --- a/src/main/scala/com/fulcrumgenomics/util/Metric.scala +++ b/src/main/scala/com/fulcrumgenomics/util/Metric.scala @@ -261,11 +261,6 @@ object MetricSorter { toArg = i => fields(i), fail = fail ) - - // build it. NB: if arguments are missing values, then an exception will be thrown here - // Also, we don't use the default "build()" method since if a collection or option is empty, it will be treated as - // missing. - reflectiveBuilder.build(reflectiveBuilder.argumentLookup.ordered.map(arg => arg.value getOrElse unreachable(s"Arguments not set: ${arg.name}"))) } } } From a392fba435802f409caf22b903c8b8a34ed2de85 Mon Sep 17 00:00:00 2001 From: Nils Homer Date: Wed, 20 Apr 2022 01:04:13 -0700 Subject: [PATCH 03/12] update fail --- .../com/fulcrumgenomics/util/Metric.scala | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/src/main/scala/com/fulcrumgenomics/util/Metric.scala b/src/main/scala/com/fulcrumgenomics/util/Metric.scala index 38fd1e9d7..a446ca7b7 100644 --- a/src/main/scala/com/fulcrumgenomics/util/Metric.scala +++ b/src/main/scala/com/fulcrumgenomics/util/Metric.scala @@ -129,28 +129,35 @@ object Metric extends LazyLogging { reflectiveBuilder.build(reflectiveBuilder.argumentLookup.ordered.map(arg => arg.value getOrElse unreachable(s"Arguments not set: ${arg.name}"))) } + private[util] def failReading[T <: Metric](clazz: Class[T], + message: String, + lineNumber: Option[Int] = None, + throwable: Option[Throwable] = None, + source: Option[String] = None): Unit = { + val sourceMessage = source.map("\nIn source: " + _).getOrElse("") + val prefix = lineNumber match { + case None => "For metric" + case Some(n) => s"On line #$n for metric" + } + val fullMessage = s"$prefix '${clazz.getSimpleName}'$sourceMessage\n$message" + throwable.foreach { thr => + val stringWriter = new StringWriter + thr.printStackTrace(new PrintWriter(stringWriter)) + val banner = "#" * 80 + logger.debug(banner) + logger.debug(stringWriter.toString) + logger.debug(banner) + } + throw FailureException(message=Some(fullMessage)) + } + /** Reads metrics from a set of lines. The first line should be the header with the field names. Each subsequent * line should be a single metric. */ def iterator[T <: Metric](lines: Iterator[String], source: Option[String] = None)(implicit tt: ru.TypeTag[T]): Iterator[T] = { val clazz: Class[T] = ReflectionUtil.typeTagToClass[T] - def fail(lineNumber: Int, - message: String, - throwable: Option[Throwable] = None): Unit = { - val sourceMessage = source.map("\nIn source: " + _).getOrElse("") - val fullMessage = s"On line #$lineNumber for metric '${clazz.getSimpleName}'$sourceMessage\n$message" - throwable.foreach { thr => - val stringWriter = new StringWriter - thr.printStackTrace(new PrintWriter(stringWriter)) - val banner = "#" * 80 - logger.debug(banner) - logger.debug(stringWriter.toString) - logger.debug(banner) - } - throw FailureException(message=Some(fullMessage)) - } - if (lines.isEmpty) fail(lineNumber=1, message="No header found") + if (lines.isEmpty) failReading(clazz=clazz, message="No header found", lineNumber=Some(1), source=source) val parser = new DelimitedDataParser(lines=lines, delimiter=Delimiter, ignoreBlankLines=false, trimFields=true) val reflectiveBuilder = new ReflectiveBuilder(clazz) @@ -158,7 +165,7 @@ object Metric extends LazyLogging { build( reflectiveBuilder = reflectiveBuilder, toArg = i => row[String](i), - fail = (message, throwable) => fail(rowIndex+2, message, throwable) + fail = (message, throwable) => failReading(clazz=clazz, message=message, lineNumber=Some(rowIndex+2), throwable=throwable, source=source) ) } } @@ -236,20 +243,6 @@ object MetricSorter { private val clazz: Class[T] = ReflectionUtil.typeTagToClass[T] private val reflectiveBuilder = new ReflectiveBuilder(clazz) - private def fail(message: String, - throwable: Option[Throwable] = None): Unit = { - val fullMessage = s"For metric '${clazz.getSimpleName}'\n$message" - throwable.foreach { thr => - val stringWriter = new StringWriter - thr.printStackTrace(new PrintWriter(stringWriter)) - val banner = "#" * 80 - logger.debug(banner) - logger.debug(stringWriter.toString) - logger.debug(banner) - } - throw FailureException(message=Some(fullMessage)) - } - /** Encode the metric into an array of bytes. */ def encode(metric: T): Array[Byte] = metric.values.mkString(Metric.DelimiterAsString).getBytes @@ -259,7 +252,7 @@ object MetricSorter { Metric.build( reflectiveBuilder = reflectiveBuilder, toArg = i => fields(i), - fail = fail + fail = (message, throwable) => Metric.failReading(clazz=clazz, message=message, throwable=throwable) ) } } From 392ba36a23ec6df07554f9860320a0ddebeed6c4 Mon Sep 17 00:00:00 2001 From: Nils Homer Date: Wed, 20 Apr 2022 01:07:13 -0700 Subject: [PATCH 04/12] Unit --- src/main/scala/com/fulcrumgenomics/util/Metric.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/com/fulcrumgenomics/util/Metric.scala b/src/main/scala/com/fulcrumgenomics/util/Metric.scala index a446ca7b7..a2b24865f 100644 --- a/src/main/scala/com/fulcrumgenomics/util/Metric.scala +++ b/src/main/scala/com/fulcrumgenomics/util/Metric.scala @@ -101,7 +101,7 @@ object Metric extends LazyLogging { private[util] def build[T <: Metric](reflectiveBuilder: ReflectiveBuilder[T], toArg: Int => String, - fail: (String, Option[Throwable]) => () + fail: (String, Option[Throwable]) => Unit ): T = { val names = Metric.names[T] forloop(from = 0, until = names.length) { i => From f66cb872a16e6ec1ca414c27ceaa4ae9b6ac9bb9 Mon Sep 17 00:00:00 2001 From: Nils Homer Date: Wed, 20 Apr 2022 01:17:00 -0700 Subject: [PATCH 05/12] type tag --- src/main/scala/com/fulcrumgenomics/util/Metric.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/com/fulcrumgenomics/util/Metric.scala b/src/main/scala/com/fulcrumgenomics/util/Metric.scala index a2b24865f..4217e5312 100644 --- a/src/main/scala/com/fulcrumgenomics/util/Metric.scala +++ b/src/main/scala/com/fulcrumgenomics/util/Metric.scala @@ -102,7 +102,7 @@ object Metric extends LazyLogging { private[util] def build[T <: Metric](reflectiveBuilder: ReflectiveBuilder[T], toArg: Int => String, fail: (String, Option[Throwable]) => Unit - ): T = { + )(implicit tt: ru.TypeTag[T]): T = { val names = Metric.names[T] forloop(from = 0, until = names.length) { i => reflectiveBuilder.argumentLookup.forField(names(i)) match { From 441f3d4c50fe1f6d7fa8e627a6888ab4ccd87bd2 Mon Sep 17 00:00:00 2001 From: Nils Homer Date: Wed, 20 Apr 2022 01:22:41 -0700 Subject: [PATCH 06/12] bug fix --- src/main/scala/com/fulcrumgenomics/util/Metric.scala | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/scala/com/fulcrumgenomics/util/Metric.scala b/src/main/scala/com/fulcrumgenomics/util/Metric.scala index 4217e5312..3f5f446b9 100644 --- a/src/main/scala/com/fulcrumgenomics/util/Metric.scala +++ b/src/main/scala/com/fulcrumgenomics/util/Metric.scala @@ -101,9 +101,10 @@ object Metric extends LazyLogging { private[util] def build[T <: Metric](reflectiveBuilder: ReflectiveBuilder[T], toArg: Int => String, + argNames: Option[Seq[String]] = None, fail: (String, Option[Throwable]) => Unit )(implicit tt: ru.TypeTag[T]): T = { - val names = Metric.names[T] + val names = argNames.getOrElse(Metric.names[T]) forloop(from = 0, until = names.length) { i => reflectiveBuilder.argumentLookup.forField(names(i)) match { case Some(arg) => @@ -154,17 +155,18 @@ object Metric extends LazyLogging { /** Reads metrics from a set of lines. The first line should be the header with the field names. Each subsequent * line should be a single metric. */ def iterator[T <: Metric](lines: Iterator[String], source: Option[String] = None)(implicit tt: ru.TypeTag[T]): Iterator[T] = { - val clazz: Class[T] = ReflectionUtil.typeTagToClass[T] - + val clazz: Class[T] = ReflectionUtil.typeTagToClass[T] if (lines.isEmpty) failReading(clazz=clazz, message="No header found", lineNumber=Some(1), source=source) - val parser = new DelimitedDataParser(lines=lines, delimiter=Delimiter, ignoreBlankLines=false, trimFields=true) + val parser = new DelimitedDataParser(lines=lines, delimiter=Delimiter, ignoreBlankLines=false, trimFields=true) + val names = parser.headers.toIndexedSeq val reflectiveBuilder = new ReflectiveBuilder(clazz) parser.zipWithIndex.map { case (row, rowIndex) => build( reflectiveBuilder = reflectiveBuilder, toArg = i => row[String](i), + argNames = Some(names), fail = (message, throwable) => failReading(clazz=clazz, message=message, lineNumber=Some(rowIndex+2), throwable=throwable, source=source) ) } From e78eeda266e9d49df1a3fa99d681d43b831a6d12 Mon Sep 17 00:00:00 2001 From: Nils Homer Date: Wed, 20 Apr 2022 16:53:30 -0700 Subject: [PATCH 07/12] cleanup --- .../com/fulcrumgenomics/util/Metric.scala | 114 ++------------- .../fulcrumgenomics/util/MetricBuilder.scala | 137 ++++++++++++++++++ .../fulcrumgenomics/util/MetricSorter.scala | 71 +++++++++ 3 files changed, 217 insertions(+), 105 deletions(-) create mode 100644 src/main/scala/com/fulcrumgenomics/util/MetricBuilder.scala create mode 100644 src/main/scala/com/fulcrumgenomics/util/MetricSorter.scala diff --git a/src/main/scala/com/fulcrumgenomics/util/Metric.scala b/src/main/scala/com/fulcrumgenomics/util/Metric.scala index 3f5f446b9..a76eeeb7d 100644 --- a/src/main/scala/com/fulcrumgenomics/util/Metric.scala +++ b/src/main/scala/com/fulcrumgenomics/util/Metric.scala @@ -25,24 +25,21 @@ package com.fulcrumgenomics.util -import com.fulcrumgenomics.cmdline.FgBioMain.FailureException -import com.fulcrumgenomics.commons.CommonsDef._ import com.fulcrumgenomics.commons.io.{Writer => CommonsWriter} import com.fulcrumgenomics.commons.reflect.{ReflectionUtil, ReflectiveBuilder} -import com.fulcrumgenomics.commons.util.{DelimitedDataParser, LazyLogging} +import com.fulcrumgenomics.commons.util.DelimitedDataParser import enumeratum.EnumEntry import htsjdk.samtools.util.Iso8601Date -import java.io.{PrintWriter, StringWriter, Writer} +import java.io.Writer import java.nio.file.Path import java.text.{DecimalFormat, NumberFormat, SimpleDateFormat} import java.util.Date import scala.collection.compat._ import scala.collection.concurrent.TrieMap import scala.reflect.runtime.{universe => ru} -import scala.util.{Failure, Success} -object Metric extends LazyLogging { +object Metric { val Delimiter: Char = '\t' val DelimiterAsString: String = s"$Delimiter" @@ -99,76 +96,17 @@ object Metric extends LazyLogging { } } - private[util] def build[T <: Metric](reflectiveBuilder: ReflectiveBuilder[T], - toArg: Int => String, - argNames: Option[Seq[String]] = None, - fail: (String, Option[Throwable]) => Unit - )(implicit tt: ru.TypeTag[T]): T = { - val names = argNames.getOrElse(Metric.names[T]) - forloop(from = 0, until = names.length) { i => - reflectiveBuilder.argumentLookup.forField(names(i)) match { - case Some(arg) => - val value = { - val tmp = toArg(i) - if (tmp.isEmpty && arg.argumentType == classOf[Option[_]]) ReflectionUtil.SpecialEmptyOrNoneToken else tmp - } - - val argumentValue = ReflectionUtil.constructFromString(arg.argumentType, arg.unitType, value) match { - case Success(v) => v - case Failure(thr) => - fail(s"Could not construct value for column '${arg.name}' of type '${arg.typeDescription}' from '$value'", Some(thr)) - } - arg.value = argumentValue - case None => - fail(s"Did not have a field with name '${names(i)}'.", None) - } - } - - // build it. NB: if arguments are missing values, then an exception will be thrown here - // Also, we don't use the default "build()" method since if a collection or option is empty, it will be treated as - // missing. - reflectiveBuilder.build(reflectiveBuilder.argumentLookup.ordered.map(arg => arg.value getOrElse unreachable(s"Arguments not set: ${arg.name}"))) - } - - private[util] def failReading[T <: Metric](clazz: Class[T], - message: String, - lineNumber: Option[Int] = None, - throwable: Option[Throwable] = None, - source: Option[String] = None): Unit = { - val sourceMessage = source.map("\nIn source: " + _).getOrElse("") - val prefix = lineNumber match { - case None => "For metric" - case Some(n) => s"On line #$n for metric" - } - val fullMessage = s"$prefix '${clazz.getSimpleName}'$sourceMessage\n$message" - throwable.foreach { thr => - val stringWriter = new StringWriter - thr.printStackTrace(new PrintWriter(stringWriter)) - val banner = "#" * 80 - logger.debug(banner) - logger.debug(stringWriter.toString) - logger.debug(banner) - } - throw FailureException(message=Some(fullMessage)) - } - /** Reads metrics from a set of lines. The first line should be the header with the field names. Each subsequent * line should be a single metric. */ def iterator[T <: Metric](lines: Iterator[String], source: Option[String] = None)(implicit tt: ru.TypeTag[T]): Iterator[T] = { - val clazz: Class[T] = ReflectionUtil.typeTagToClass[T] - - if (lines.isEmpty) failReading(clazz=clazz, message="No header found", lineNumber=Some(1), source=source) - val parser = new DelimitedDataParser(lines=lines, delimiter=Delimiter, ignoreBlankLines=false, trimFields=true) - val names = parser.headers.toIndexedSeq - val reflectiveBuilder = new ReflectiveBuilder(clazz) + val builder = new MetricBuilder[T](source=source)(tt) + if (lines.isEmpty) builder.fail(message="No header found", lineNumber=Some(1)) + val parser = new DelimitedDataParser(lines=lines, delimiter=Delimiter, ignoreBlankLines=false, trimFields=true) + val names = parser.headers.toIndexedSeq parser.zipWithIndex.map { case (row, rowIndex) => - build( - reflectiveBuilder = reflectiveBuilder, - toArg = i => row[String](i), - argNames = Some(names), - fail = (message, throwable) => failReading(clazz=clazz, message=message, lineNumber=Some(rowIndex+2), throwable=throwable, source=source) - ) + val argMap = names.zipWithIndex.map { case (name, i) => name -> row[String](i) }.toMap + builder.build(argMap=argMap, lineNumber=Some(rowIndex+2)) } } @@ -226,40 +164,6 @@ object Metric extends LazyLogging { } -class MetricSorter[Key <: Ordered[Key], T <: Metric](maxObjectsInRam: Int = MetricSorter.MaxInMemory, - keyfunc: T => Key, - tmpDir: DirPath = Io.tmpDir - )(implicit tt: ru.TypeTag[T]) extends Sorter[T, Key]( - maxObjectsInRam = maxObjectsInRam, - codec = new MetricSorter.MetricSorterCodec[T](), - keyfunc = keyfunc, - tmpDir = tmpDir -) - -object MetricSorter { - /** The default maximum # of records to keep and sort in memory. */ - val MaxInMemory: Int = 1e6.toInt - - class MetricSorterCodec[T <: Metric]()(implicit tt: ru.TypeTag[T]) - extends Sorter.Codec[T] with LazyLogging { - private val clazz: Class[T] = ReflectionUtil.typeTagToClass[T] - private val reflectiveBuilder = new ReflectiveBuilder(clazz) - - /** Encode the metric into an array of bytes. */ - def encode(metric: T): Array[Byte] = metric.values.mkString(Metric.DelimiterAsString).getBytes - - /** Decode a metric from an array of bytes. */ - def decode(bs: Array[Byte], start: Int, length: Int): T = { - val fields = new String(bs.slice(from=start, until=start+length)).split(Metric.DelimiterAsString) - Metric.build( - reflectiveBuilder = reflectiveBuilder, - toArg = i => fields(i), - fail = (message, throwable) => Metric.failReading(clazz=clazz, message=message, throwable=throwable) - ) - } - } -} - /** * Base trait for metrics. * diff --git a/src/main/scala/com/fulcrumgenomics/util/MetricBuilder.scala b/src/main/scala/com/fulcrumgenomics/util/MetricBuilder.scala new file mode 100644 index 000000000..52cd5994b --- /dev/null +++ b/src/main/scala/com/fulcrumgenomics/util/MetricBuilder.scala @@ -0,0 +1,137 @@ +/* + * The MIT License + * + * Copyright (c) 2022 Fulcrum Genomics + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package com.fulcrumgenomics.util + +import com.fulcrumgenomics.cmdline.FgBioMain.FailureException +import com.fulcrumgenomics.commons.CommonsDef.{forloop, unreachable} +import com.fulcrumgenomics.commons.reflect.{ReflectionUtil, ReflectiveBuilder} +import com.fulcrumgenomics.commons.util.LazyLogging + +import java.io.{PrintWriter, StringWriter} +import scala.reflect.runtime.{universe => ru} +import scala.util.{Failure, Success} + +/** Class for building metrics of type [[T]] + * + * @param source optionally, the source of reading (e.g. file) + * @tparam T the metric type + */ +class MetricBuilder[T <: Metric](source: Option[String] = None)(implicit tt: ru.TypeTag[T]) extends LazyLogging { + // The main reason why a builder is necessary is to cache some expensive reflective calls. + private val clazz: Class[T] = ReflectionUtil.typeTagToClass[T] + private val reflectiveBuilder = new ReflectiveBuilder(clazz) + private val names = Metric.names[T] + + /** Builds a metric from a delimited line + * + * @param line the line with delimited values + * @param delim the delimiter of the values + * @param lineNumber optionally, the line number when building a metric from a line in a file + * @return + */ + def build(line: String, delim: String = Metric.DelimiterAsString, lineNumber: Option[Int] = None): T = { + build(values = line.split(delim), lineNumber = lineNumber) + } + + /** Builds a metric from values for the complete set of metric fields + * + * @param values the values in the same order as the names defined in the class + * @param lineNumber optionally, the line number when building a metric from a line in a file + * @return + */ + def build(values: Iterable[String], lineNumber: Option[Int] = None): T = { + val vals = values.toIndexedSeq + if (names.length != vals.length) { + fail(message = f"Failed decoding: expected '${names.length}' fields, found '${vals.length}'.", lineNumber = lineNumber) + } + build(argMap = names.zip(values).toMap, lineNumber = lineNumber) + } + + /** Builds a metric of type [[T]] + * + * @param argMap map of field names to values. All required fields must be given. Can be in any order. + * @param lineNumber optionally, the line number when building a metric from a line in a file + * @return a new instance of type [[T]] + */ + def build(argMap: Map[String, String], lineNumber: Option[Int] = None): T = { + val names = argMap.keys.toIndexedSeq + forloop(from = 0, until = names.length) { i => + reflectiveBuilder.argumentLookup.forField(names(i)) match { + case Some(arg) => + val value = { + val tmp = argMap(names(i)) + if (tmp.isEmpty && arg.argumentType == classOf[Option[_]]) ReflectionUtil.SpecialEmptyOrNoneToken else tmp + } + + val argumentValue = ReflectionUtil.constructFromString(arg.argumentType, arg.unitType, value) match { + case Success(v) => v + case Failure(thr) => + fail( + message = s"Could not construct value for column '${arg.name}' of type '${arg.typeDescription}' from '$value'", + throwable = Some(thr), + lineNumber = lineNumber + ) + } + arg.value = argumentValue + case None => + fail( + message = s"Did not have a field with name '${names(i)}'.", + lineNumber = lineNumber + ) + } + } + + // build it. NB: if arguments are missing values, then an exception will be thrown here + // Also, we don't use the default "build()" method since if a collection or option is empty, it will be treated as + // missing. + reflectiveBuilder.build(reflectiveBuilder.argumentLookup.ordered.map(arg => arg.value getOrElse unreachable(s"Arguments not set: ${arg.name}"))) + } + + /** Logs the throwable, if given, and throws a [[FailureException]] with information about when reading metrics fails + * + * @param message the message to include in the exception thrown + * @param throwable optionally, a throwable that should be logged + * @param lineNumber optionally, the line number when building a metric from a line in a file + */ + def fail(message: String, throwable: Option[Throwable] = None, lineNumber: Option[Int] = None): Unit = { + throwable.foreach { thr => + val stringWriter = new StringWriter + thr.printStackTrace(new PrintWriter(stringWriter)) + val banner = "#" * 80 + logger.debug(banner) + logger.debug(stringWriter.toString) + logger.debug(banner) + } + val sourceMessage = source.map("\nIn source: " + _).getOrElse("") + val prefix = lineNumber match { + case None => "For metric" + case Some(n) => s"On line #$n for metric" + } + val fullMessage = s"$prefix '${clazz.getSimpleName}'$sourceMessage\n$message" + + throw FailureException(message = Some(fullMessage)) + } +} diff --git a/src/main/scala/com/fulcrumgenomics/util/MetricSorter.scala b/src/main/scala/com/fulcrumgenomics/util/MetricSorter.scala new file mode 100644 index 000000000..8e1c6b68a --- /dev/null +++ b/src/main/scala/com/fulcrumgenomics/util/MetricSorter.scala @@ -0,0 +1,71 @@ +/* + * The MIT License + * + * Copyright (c) 2022 Fulcrum Genomics + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package com.fulcrumgenomics.util + +import com.fulcrumgenomics.commons.CommonsDef.DirPath + +import scala.reflect.runtime.{universe => ru} + +/** Disk-backed metrics sorter + * + * @param maxObjectsInRam the maximum number of metrics to keep in memory before spilling to disk + * @param keyfunc method to convert a metric to an ordered key + * @param tmpDir the temporary directory in which to spill to disk + * @param codec the codec for encoding and decoding the metric + * @param tt the type tag for [[T]] + * @tparam Key the key to use for sorting metrics + * @tparam T the metric type + */ +class MetricSorter[Key <: Ordered[Key], T <: Metric](maxObjectsInRam: Int = MetricSorter.MaxInMemory, + keyfunc: T => Key, + tmpDir: DirPath = Io.tmpDir, + codec: Sorter.Codec[T] = new MetricSorter.MetricSorterCodec[T]() + )(implicit tt: ru.TypeTag[T]) extends Sorter[T, Key]( + maxObjectsInRam = maxObjectsInRam, + codec = codec, + keyfunc = keyfunc, + tmpDir = tmpDir +) + +object MetricSorter { + /** The default maximum # of records to keep and sort in memory. */ + val MaxInMemory: Int = 1e6.toInt + + /** The codec for encoding and decoding a metric */ + class MetricSorterCodec[T <: Metric]()(implicit tt: ru.TypeTag[T]) + extends Sorter.Codec[T] { + private val builder = new MetricBuilder[T]() + + /** Encode the metric into an array of bytes. */ + def encode(metric: T): Array[Byte] = metric.values.mkString(Metric.DelimiterAsString).getBytes + + /** Decode a metric from an array of bytes. */ + def decode(bs: Array[Byte], start: Int, length: Int): T = { + val fields = new String(bs.slice(from = start, until = start + length)).split(Metric.DelimiterAsString) + builder.build(fields) + } + } +} \ No newline at end of file From 9ad1eb13452b112dda6800755142a28c7cb546d8 Mon Sep 17 00:00:00 2001 From: Nils Homer Date: Wed, 20 Apr 2022 16:54:20 -0700 Subject: [PATCH 08/12] remove changed newline --- src/main/scala/com/fulcrumgenomics/util/Metric.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/scala/com/fulcrumgenomics/util/Metric.scala b/src/main/scala/com/fulcrumgenomics/util/Metric.scala index a76eeeb7d..52b52e6a2 100644 --- a/src/main/scala/com/fulcrumgenomics/util/Metric.scala +++ b/src/main/scala/com/fulcrumgenomics/util/Metric.scala @@ -163,7 +163,6 @@ object Metric { def writer[T <: Metric](writer: Writer)(implicit tt: ru.TypeTag[T]): MetricWriter[T] = new MetricWriter[T](writer) } - /** * Base trait for metrics. * From 8dcecfcee3870f9bfe93f44e07e692b7bbdfa0a9 Mon Sep 17 00:00:00 2001 From: Nils Homer Date: Wed, 20 Apr 2022 16:59:10 -0700 Subject: [PATCH 09/12] fix --- src/main/scala/com/fulcrumgenomics/util/MetricSorter.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/scala/com/fulcrumgenomics/util/MetricSorter.scala b/src/main/scala/com/fulcrumgenomics/util/MetricSorter.scala index 8e1c6b68a..02d710dc7 100644 --- a/src/main/scala/com/fulcrumgenomics/util/MetricSorter.scala +++ b/src/main/scala/com/fulcrumgenomics/util/MetricSorter.scala @@ -34,7 +34,6 @@ import scala.reflect.runtime.{universe => ru} * @param maxObjectsInRam the maximum number of metrics to keep in memory before spilling to disk * @param keyfunc method to convert a metric to an ordered key * @param tmpDir the temporary directory in which to spill to disk - * @param codec the codec for encoding and decoding the metric * @param tt the type tag for [[T]] * @tparam Key the key to use for sorting metrics * @tparam T the metric type @@ -42,10 +41,10 @@ import scala.reflect.runtime.{universe => ru} class MetricSorter[Key <: Ordered[Key], T <: Metric](maxObjectsInRam: Int = MetricSorter.MaxInMemory, keyfunc: T => Key, tmpDir: DirPath = Io.tmpDir, - codec: Sorter.Codec[T] = new MetricSorter.MetricSorterCodec[T]() + )(implicit tt: ru.TypeTag[T]) extends Sorter[T, Key]( maxObjectsInRam = maxObjectsInRam, - codec = codec, + codec = new MetricSorter.MetricSorterCodec[T](), keyfunc = keyfunc, tmpDir = tmpDir ) From b0b942e5969c3672e1521bab07b0f7a11ab53059 Mon Sep 17 00:00:00 2001 From: Nils Homer Date: Wed, 20 Apr 2022 17:03:36 -0700 Subject: [PATCH 10/12] fix --- src/main/scala/com/fulcrumgenomics/util/Metric.scala | 2 +- .../scala/com/fulcrumgenomics/util/MetricBuilder.scala | 10 +++++----- .../scala/com/fulcrumgenomics/util/MetricSorter.scala | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/scala/com/fulcrumgenomics/util/Metric.scala b/src/main/scala/com/fulcrumgenomics/util/Metric.scala index 52b52e6a2..4baae498a 100644 --- a/src/main/scala/com/fulcrumgenomics/util/Metric.scala +++ b/src/main/scala/com/fulcrumgenomics/util/Metric.scala @@ -106,7 +106,7 @@ object Metric { parser.zipWithIndex.map { case (row, rowIndex) => val argMap = names.zipWithIndex.map { case (name, i) => name -> row[String](i) }.toMap - builder.build(argMap=argMap, lineNumber=Some(rowIndex+2)) + builder.fromArgMap(argMap=argMap, lineNumber=Some(rowIndex+2)) } } diff --git a/src/main/scala/com/fulcrumgenomics/util/MetricBuilder.scala b/src/main/scala/com/fulcrumgenomics/util/MetricBuilder.scala index 52cd5994b..c3e23e46c 100644 --- a/src/main/scala/com/fulcrumgenomics/util/MetricBuilder.scala +++ b/src/main/scala/com/fulcrumgenomics/util/MetricBuilder.scala @@ -52,8 +52,8 @@ class MetricBuilder[T <: Metric](source: Option[String] = None)(implicit tt: ru. * @param lineNumber optionally, the line number when building a metric from a line in a file * @return */ - def build(line: String, delim: String = Metric.DelimiterAsString, lineNumber: Option[Int] = None): T = { - build(values = line.split(delim), lineNumber = lineNumber) + def fromLine(line: String, delim: String = Metric.DelimiterAsString, lineNumber: Option[Int] = None): T = { + fromValues(values = line.split(delim), lineNumber = lineNumber) } /** Builds a metric from values for the complete set of metric fields @@ -62,12 +62,12 @@ class MetricBuilder[T <: Metric](source: Option[String] = None)(implicit tt: ru. * @param lineNumber optionally, the line number when building a metric from a line in a file * @return */ - def build(values: Iterable[String], lineNumber: Option[Int] = None): T = { + def fromValues(values: Iterable[String], lineNumber: Option[Int] = None): T = { val vals = values.toIndexedSeq if (names.length != vals.length) { fail(message = f"Failed decoding: expected '${names.length}' fields, found '${vals.length}'.", lineNumber = lineNumber) } - build(argMap = names.zip(values).toMap, lineNumber = lineNumber) + fromArgMap(argMap = names.zip(values).toMap, lineNumber = lineNumber) } /** Builds a metric of type [[T]] @@ -76,7 +76,7 @@ class MetricBuilder[T <: Metric](source: Option[String] = None)(implicit tt: ru. * @param lineNumber optionally, the line number when building a metric from a line in a file * @return a new instance of type [[T]] */ - def build(argMap: Map[String, String], lineNumber: Option[Int] = None): T = { + def fromArgMap(argMap: Map[String, String], lineNumber: Option[Int] = None): T = { val names = argMap.keys.toIndexedSeq forloop(from = 0, until = names.length) { i => reflectiveBuilder.argumentLookup.forField(names(i)) match { diff --git a/src/main/scala/com/fulcrumgenomics/util/MetricSorter.scala b/src/main/scala/com/fulcrumgenomics/util/MetricSorter.scala index 02d710dc7..a1632a230 100644 --- a/src/main/scala/com/fulcrumgenomics/util/MetricSorter.scala +++ b/src/main/scala/com/fulcrumgenomics/util/MetricSorter.scala @@ -64,7 +64,7 @@ object MetricSorter { /** Decode a metric from an array of bytes. */ def decode(bs: Array[Byte], start: Int, length: Int): T = { val fields = new String(bs.slice(from = start, until = start + length)).split(Metric.DelimiterAsString) - builder.build(fields) + builder.fromValues(fields) } } } \ No newline at end of file From 3247e784d00a15fe60b0e6361b0b625e318bd6a2 Mon Sep 17 00:00:00 2001 From: Nils Homer Date: Thu, 21 Apr 2022 12:17:11 -0700 Subject: [PATCH 11/12] add MetricSorterTest --- .../util/MetricSorterTest.scala | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/test/scala/com/fulcrumgenomics/util/MetricSorterTest.scala diff --git a/src/test/scala/com/fulcrumgenomics/util/MetricSorterTest.scala b/src/test/scala/com/fulcrumgenomics/util/MetricSorterTest.scala new file mode 100644 index 000000000..02c0ac8a1 --- /dev/null +++ b/src/test/scala/com/fulcrumgenomics/util/MetricSorterTest.scala @@ -0,0 +1,70 @@ +/* + * The MIT License + * + * Copyright (c) 2022 Fulcrum Genomics + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package com.fulcrumgenomics.util + +import com.fulcrumgenomics.testing.UnitSpec + +case class MetricSorterTestMetric(name: String, count: Long) extends Metric with Ordered[MetricSorterTestMetric] { + override def compare(that: MetricSorterTestMetric): Int = { + var retval = this.count.compare(that.count) + if (retval == 0) retval = this.name.compare(that.name) + retval + } +} + +class MetricSorterTest extends UnitSpec { + + private val metrics = IndexedSeq( + MetricSorterTestMetric(name="foo", count=10), + MetricSorterTestMetric(name="foo", count=1), + MetricSorterTestMetric(name="bar", count=1), + MetricSorterTestMetric(name="foo", count=5), + MetricSorterTestMetric(name="roger", count=2), + MetricSorterTestMetric(name="nadal", count=2), + ) + + private val metricsSorted = metrics.sortBy(m => (m.count, m.name)) + + private case class Key() + + "MetricSorter" should "sort metrics in memory" in { + val sorter = new MetricSorter[MetricSorterTestMetric, MetricSorterTestMetric]( + maxObjectsInRam = 10000, + keyfunc = identity + ) + sorter ++= metrics + sorter.iterator.toSeq should contain theSameElementsInOrderAs metricsSorted + } + + it should "sort metrics after spilling to disk" in { + val sorter = new MetricSorter[MetricSorterTestMetric, MetricSorterTestMetric]( + maxObjectsInRam = 2, + keyfunc = identity + ) + sorter ++= metrics + sorter.iterator.toSeq should contain theSameElementsInOrderAs metricsSorted + } +} From 45b68d622cfd4264a7d7c4f9cb7704bbfb15ba7f Mon Sep 17 00:00:00 2001 From: Nils Homer Date: Thu, 21 Apr 2022 16:16:09 -0700 Subject: [PATCH 12/12] fix: relies on https://github.com/fulcrumgenomics/commons/pull/82 --- .../fulcrumgenomics/util/MetricBuilder.scala | 9 +++- .../util/MetricBuilderTest.scala | 43 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/test/scala/com/fulcrumgenomics/util/MetricBuilderTest.scala diff --git a/src/main/scala/com/fulcrumgenomics/util/MetricBuilder.scala b/src/main/scala/com/fulcrumgenomics/util/MetricBuilder.scala index c3e23e46c..2b12c139e 100644 --- a/src/main/scala/com/fulcrumgenomics/util/MetricBuilder.scala +++ b/src/main/scala/com/fulcrumgenomics/util/MetricBuilder.scala @@ -34,7 +34,9 @@ import java.io.{PrintWriter, StringWriter} import scala.reflect.runtime.{universe => ru} import scala.util.{Failure, Success} -/** Class for building metrics of type [[T]] +/** Class for building metrics of type [[T]]. + * + * This is not thread-safe. * * @param source optionally, the source of reading (e.g. file) * @tparam T the metric type @@ -77,6 +79,8 @@ class MetricBuilder[T <: Metric](source: Option[String] = None)(implicit tt: ru. * @return a new instance of type [[T]] */ def fromArgMap(argMap: Map[String, String], lineNumber: Option[Int] = None): T = { + reflectiveBuilder.reset() // reset the arguments to their initial values + val names = argMap.keys.toIndexedSeq forloop(from = 0, until = names.length) { i => reflectiveBuilder.argumentLookup.forField(names(i)) match { @@ -107,7 +111,8 @@ class MetricBuilder[T <: Metric](source: Option[String] = None)(implicit tt: ru. // build it. NB: if arguments are missing values, then an exception will be thrown here // Also, we don't use the default "build()" method since if a collection or option is empty, it will be treated as // missing. - reflectiveBuilder.build(reflectiveBuilder.argumentLookup.ordered.map(arg => arg.value getOrElse unreachable(s"Arguments not set: ${arg.name}"))) + val params = reflectiveBuilder.argumentLookup.ordered.map(arg => arg.value getOrElse unreachable(s"Arguments not set: ${arg.name}")) + reflectiveBuilder.build(params) } /** Logs the throwable, if given, and throws a [[FailureException]] with information about when reading metrics fails diff --git a/src/test/scala/com/fulcrumgenomics/util/MetricBuilderTest.scala b/src/test/scala/com/fulcrumgenomics/util/MetricBuilderTest.scala new file mode 100644 index 000000000..4c7dc21bd --- /dev/null +++ b/src/test/scala/com/fulcrumgenomics/util/MetricBuilderTest.scala @@ -0,0 +1,43 @@ +/* + * The MIT License + * + * Copyright (c) 2022 Fulcrum Genomics + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package com.fulcrumgenomics.util + +import com.fulcrumgenomics.testing.UnitSpec + + +case class MetricBuilderTestMetric(name: String, count: Long = 1) extends Metric + +class MetricBuilderTest extends UnitSpec { + private val builder = new MetricBuilder[MetricBuilderTestMetric]() + + "MetricBuilder.fromArgMap" should "build a metric from an argmap with all value specified" in { + builder.fromArgMap(Map("name" -> "foo", "count" -> "2")) shouldBe MetricBuilderTestMetric(name="foo", count=2) + } + + it should "build a metric from an argmap with only required values specified" in { + builder.fromArgMap(Map("name" -> "foo")) shouldBe MetricBuilderTestMetric(name="foo") + } +}