From 600c9e4536986d7c1377e7046fb13cbe9722752d Mon Sep 17 00:00:00 2001 From: Chua Chee Seng Date: Sat, 11 Jun 2022 11:06:36 +0800 Subject: [PATCH 1/4] First step to enhance Prettifier to fail early when cyclic object is detected when prettifying objects nested in collection. --- .../main/scala/org/scalactic/Prettifier.scala | 59 ++++++++----------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala b/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala index 2451da40eb..e03a9c8b6b 100644 --- a/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala +++ b/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala @@ -183,51 +183,40 @@ object Prettifier { */ implicit val default: Prettifier = new Prettifier { - def apply(o: Any): String = { - try { + private def prettify(o: Any, processed: Set[Any]): String = + if (processed.contains(o)) + throw new StackOverflowError("Cyclic relationship detected, let's fail early!") + else o match { case null => "null" case aUnit: Unit => "<(), the Unit value>" case aString: String => "\"" + aString + "\"" case aStringWrapper: org.scalactic.ColCompatHelper.StringOps => "\"" + aStringWrapper.mkString + "\"" case aChar: Char => "\'" + aChar + "\'" - case Some(e) => "Some(" + apply(e) + ")" - case Success(e) => "Success(" + apply(e) + ")" - case Left(e) => "Left(" + apply(e) + ")" - case Right(e) => "Right(" + apply(e) + ")" + case Some(e) => "Some(" + prettify(e, processed) + ")" + case Success(e) => "Success(" + prettify(e, processed) + ")" + case Left(e) => "Left(" + prettify(e, processed) + ")" + case Right(e) => "Right(" + prettify(e, processed) + ")" case s: Symbol => "'" + s.name - case Good(e) => "Good(" + apply(e) + ")" - case Bad(e) => "Bad(" + apply(e) + ")" - case One(e) => "One(" + apply(e) + ")" - case many: Many[_] => "Many(" + many.toIterator.map(apply(_)).mkString(", ") + ")" - case anArray: Array[_] => "Array(" + (anArray map apply).mkString(", ") + ")" - case aWrappedArray: WrappedArray[_] => "Array(" + (aWrappedArray map apply).mkString(", ") + ")" - case anArrayOps if ArrayHelper.isArrayOps(anArrayOps) => "Array(" + (ArrayHelper.asArrayOps(anArrayOps) map apply).mkString(", ") + ")" + case Good(e) => "Good(" + prettify(e, processed) + ")" + case Bad(e) => "Bad(" + prettify(e, processed) + ")" + case One(e) => "One(" + prettify(e, processed) + ")" + case many: Many[_] => "Many(" + many.map(prettify(_, processed + many)).mkString(", ") + ")" + case anArray: Array[_] => "Array(" + anArray.map(prettify(_, processed + anArray)).mkString(", ") + ")" + case aWrappedArray: WrappedArray[_] => "Array(" + aWrappedArray.map(prettify(_, processed + aWrappedArray)).mkString(", ") + ")" + case anArrayOps if ArrayHelper.isArrayOps(anArrayOps) => "Array(" + ArrayHelper.asArrayOps(anArrayOps).map(prettify(_, processed + anArrayOps)).mkString(", ") + ")" case aGenMap: scala.collection.GenMap[_, _] => ColCompatHelper.className(aGenMap) + "(" + (aGenMap.toIterator.map { case (key, value) => // toIterator is needed for consistent ordering - apply(key) + " -> " + apply(value) + prettify(key, processed + aGenMap) + " -> " + prettify(value, processed + aGenMap) }).mkString(", ") + ")" case aGenTraversable: GenTraversable[_] => - val isSelf = - if (aGenTraversable.size == 1) { - aGenTraversable.head match { - case ref: AnyRef => ref eq aGenTraversable - case other => other == aGenTraversable - } - } - else - false - if (isSelf) - aGenTraversable.toString - else { val className = aGenTraversable.getClass.getName - if (className.startsWith("scala.xml.NodeSeq$") || className == "scala.xml.NodeBuffer") + if (className.startsWith("scala.xml.NodeSeq$") || className == "scala.xml.NodeBuffer" || className == "scala.xml.Elem") aGenTraversable.mkString else - ColCompatHelper.className(aGenTraversable) + "(" + aGenTraversable.toIterator.map(apply(_)).mkString(", ") + ")" // toIterator is needed for consistent ordering - } - + ColCompatHelper.className(aGenTraversable) + "(" + aGenTraversable.toIterator.map(prettify(_, processed + aGenTraversable)).mkString(", ") + ")" // toIterator is needed for consistent ordering + // SKIP-SCALATESTJS-START case javaCol: java.util.Collection[_] => // By default java collection follows http://download.java.net/jdk7/archive/b123/docs/api/java/util/AbstractCollection.html#toString() @@ -235,7 +224,7 @@ object Prettifier { import scala.collection.JavaConverters._ val theToString = javaCol.toString if (theToString.startsWith("[") && theToString.endsWith("]")) - "[" + javaCol.iterator().asScala.map(apply(_)).mkString(", ") + "]" + "[" + javaCol.iterator().asScala.map(prettify(_, processed + javaCol)).mkString(", ") + "]" else theToString case javaMap: java.util.Map[_, _] => @@ -245,13 +234,17 @@ object Prettifier { val theToString = javaMap.toString if (theToString.startsWith("{") && theToString.endsWith("}")) "{" + javaMap.entrySet.iterator.asScala.map { entry => - apply(entry.getKey) + "=" + apply(entry.getValue) + prettify(entry.getKey, processed + javaMap) + "=" + prettify(entry.getValue, processed + javaMap) }.mkString(", ") + "}" else theToString // SKIP-SCALATESTJS,NATIVE-END case anythingElse => anythingElse.toString - } + } + + def apply(o: Any): String = { + try { + prettify(o, Set.empty) } catch { // This is in case of crazy designs like the one for scala.xml.Node. We handle Node From 112ecfd6b2ad05022069da326aaf150404baabbb Mon Sep 17 00:00:00 2001 From: Chua Chee Seng Date: Sat, 11 Jun 2022 14:37:17 +0800 Subject: [PATCH 2/4] Added collection size limit suppport to Prettifier through java system property. --- .../main/scala/org/scalactic/Prettifier.scala | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala b/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala index e03a9c8b6b..65ff35e3bc 100644 --- a/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala +++ b/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala @@ -183,6 +183,9 @@ object Prettifier { */ implicit val default: Prettifier = new Prettifier { + + val colSizeLimit: Int = Option(System.getProperty("scalactic.prettifier.collection.size.limit")).map(_.toInt).getOrElse(0) + private def prettify(o: Any, processed: Set[Any]): String = if (processed.contains(o)) throw new StackOverflowError("Cyclic relationship detected, let's fail early!") @@ -201,13 +204,15 @@ object Prettifier { case Good(e) => "Good(" + prettify(e, processed) + ")" case Bad(e) => "Bad(" + prettify(e, processed) + ")" case One(e) => "One(" + prettify(e, processed) + ")" - case many: Many[_] => "Many(" + many.map(prettify(_, processed + many)).mkString(", ") + ")" - case anArray: Array[_] => "Array(" + anArray.map(prettify(_, processed + anArray)).mkString(", ") + ")" - case aWrappedArray: WrappedArray[_] => "Array(" + aWrappedArray.map(prettify(_, processed + aWrappedArray)).mkString(", ") + ")" - case anArrayOps if ArrayHelper.isArrayOps(anArrayOps) => "Array(" + ArrayHelper.asArrayOps(anArrayOps).map(prettify(_, processed + anArrayOps)).mkString(", ") + ")" + case many: Many[_] => "Many(" + (if (colSizeLimit > 0) many.toIterator.take(colSizeLimit) else many.toIterator).map(prettify(_, processed + many)).mkString(", ") + ")" + case anArray: Array[_] => "Array(" + (if (colSizeLimit > 0) anArray.take(colSizeLimit) else anArray).map(prettify(_, processed + anArray)).mkString(", ") + ")" + case aWrappedArray: WrappedArray[_] => "Array(" + (if (colSizeLimit > 0) aWrappedArray.take(colSizeLimit) else aWrappedArray).map(prettify(_, processed + aWrappedArray)).mkString(", ") + ")" + case a if ArrayHelper.isArrayOps(a) => + val anArrayOps = ArrayHelper.asArrayOps(a).iterator + "Array(" + (if (colSizeLimit > 0) anArrayOps.take(colSizeLimit) else anArrayOps).map(prettify(_, processed + anArrayOps)).mkString(", ") + ")" case aGenMap: scala.collection.GenMap[_, _] => ColCompatHelper.className(aGenMap) + "(" + - (aGenMap.toIterator.map { case (key, value) => // toIterator is needed for consistent ordering + ((if (colSizeLimit > 0) aGenMap.take(colSizeLimit) else aGenMap).toIterator.map { case (key, value) => // toIterator is needed for consistent ordering prettify(key, processed + aGenMap) + " -> " + prettify(value, processed + aGenMap) }).mkString(", ") + ")" case aGenTraversable: GenTraversable[_] => @@ -215,7 +220,7 @@ object Prettifier { if (className.startsWith("scala.xml.NodeSeq$") || className == "scala.xml.NodeBuffer" || className == "scala.xml.Elem") aGenTraversable.mkString else - ColCompatHelper.className(aGenTraversable) + "(" + aGenTraversable.toIterator.map(prettify(_, processed + aGenTraversable)).mkString(", ") + ")" // toIterator is needed for consistent ordering + ColCompatHelper.className(aGenTraversable) + "(" + (if (colSizeLimit > 0) aGenTraversable.take(colSizeLimit) else aGenTraversable).toIterator.map(prettify(_, processed + aGenTraversable)).mkString(", ") + ")" // toIterator is needed for consistent ordering // SKIP-SCALATESTJS-START case javaCol: java.util.Collection[_] => @@ -223,8 +228,10 @@ object Prettifier { // let's do our best to prettify its element when it is not overriden import scala.collection.JavaConverters._ val theToString = javaCol.toString - if (theToString.startsWith("[") && theToString.endsWith("]")) - "[" + javaCol.iterator().asScala.map(prettify(_, processed + javaCol)).mkString(", ") + "]" + if (theToString.startsWith("[") && theToString.endsWith("]")) { + val itr = javaCol.iterator().asScala + "[" + (if (colSizeLimit > 0) itr.take(colSizeLimit) else itr).map(prettify(_, processed + javaCol)).mkString(", ") + "]" + } else theToString case javaMap: java.util.Map[_, _] => @@ -232,10 +239,12 @@ object Prettifier { // let's do our best to prettify its element when it is not overriden import scala.collection.JavaConverters._ val theToString = javaMap.toString - if (theToString.startsWith("{") && theToString.endsWith("}")) - "{" + javaMap.entrySet.iterator.asScala.map { entry => + if (theToString.startsWith("{") && theToString.endsWith("}")) { + val itr = javaMap.entrySet.iterator.asScala + "{" + (if (colSizeLimit > 0) itr.take(colSizeLimit) else itr).map { entry => prettify(entry.getKey, processed + javaMap) + "=" + prettify(entry.getValue, processed + javaMap) }.mkString(", ") + "}" + } else theToString // SKIP-SCALATESTJS,NATIVE-END From 5b3f1b7fa53d87ec9532576dd09ca4bf19a02944 Mon Sep 17 00:00:00 2001 From: Chua Chee Seng Date: Sun, 12 Jun 2022 16:14:01 +0800 Subject: [PATCH 3/4] Pass the size limit to default prettifier through parameter. --- .../main/scala/org/scalactic/Prettifier.scala | 168 +++++++++--------- .../main/scala/org/scalactic/SizeLimit.scala | 21 +++ 2 files changed, 106 insertions(+), 83 deletions(-) create mode 100644 jvm/scalactic/src/main/scala/org/scalactic/SizeLimit.scala diff --git a/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala b/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala index 65ff35e3bc..1b01b11370 100644 --- a/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala +++ b/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala @@ -131,6 +131,90 @@ trait Prettifier extends Serializable { // I removed the extends (Any => String) } } +private[scalactic] class DefaultPrettifier(sizeLimit: SizeLimit) extends Prettifier { + + val colSizeLimit: Int = sizeLimit.value + + private def prettify(o: Any, processed: Set[Any]): String = + if (processed.contains(o)) + throw new StackOverflowError("Cyclic relationship detected, let's fail early!") + else + o match { + case null => "null" + case aUnit: Unit => "<(), the Unit value>" + case aString: String => "\"" + aString + "\"" + case aStringWrapper: org.scalactic.ColCompatHelper.StringOps => "\"" + aStringWrapper.mkString + "\"" + case aChar: Char => "\'" + aChar + "\'" + case Some(e) => "Some(" + prettify(e, processed) + ")" + case Success(e) => "Success(" + prettify(e, processed) + ")" + case Left(e) => "Left(" + prettify(e, processed) + ")" + case Right(e) => "Right(" + prettify(e, processed) + ")" + case s: Symbol => "'" + s.name + case Good(e) => "Good(" + prettify(e, processed) + ")" + case Bad(e) => "Bad(" + prettify(e, processed) + ")" + case One(e) => "One(" + prettify(e, processed) + ")" + case many: Many[_] => "Many(" + (if (colSizeLimit > 0) many.toIterator.take(colSizeLimit) else many.toIterator).map(prettify(_, processed + many)).mkString(", ") + ")" + case anArray: Array[_] => "Array(" + (if (colSizeLimit > 0) anArray.take(colSizeLimit) else anArray).map(prettify(_, processed + anArray)).mkString(", ") + ")" + case aWrappedArray: WrappedArray[_] => "Array(" + (if (colSizeLimit > 0) aWrappedArray.take(colSizeLimit) else aWrappedArray).map(prettify(_, processed + aWrappedArray)).mkString(", ") + ")" + case a if ArrayHelper.isArrayOps(a) => + val anArrayOps = ArrayHelper.asArrayOps(a).iterator + "Array(" + (if (colSizeLimit > 0) anArrayOps.take(colSizeLimit) else anArrayOps).map(prettify(_, processed + anArrayOps)).mkString(", ") + ")" + case aGenMap: scala.collection.GenMap[_, _] => + ColCompatHelper.className(aGenMap) + "(" + + ((if (colSizeLimit > 0) aGenMap.take(colSizeLimit) else aGenMap).toIterator.map { case (key, value) => // toIterator is needed for consistent ordering + prettify(key, processed + aGenMap) + " -> " + prettify(value, processed + aGenMap) + }).mkString(", ") + ")" + case aGenTraversable: GenTraversable[_] => + val className = aGenTraversable.getClass.getName + if (className.startsWith("scala.xml.NodeSeq$") || className == "scala.xml.NodeBuffer" || className == "scala.xml.Elem") + aGenTraversable.mkString + else + ColCompatHelper.className(aGenTraversable) + "(" + (if (colSizeLimit > 0) aGenTraversable.take(colSizeLimit) else aGenTraversable).toIterator.map(prettify(_, processed + aGenTraversable)).mkString(", ") + ")" // toIterator is needed for consistent ordering + + // SKIP-SCALATESTJS-START + case javaCol: java.util.Collection[_] => + // By default java collection follows http://download.java.net/jdk7/archive/b123/docs/api/java/util/AbstractCollection.html#toString() + // let's do our best to prettify its element when it is not overriden + import scala.collection.JavaConverters._ + val theToString = javaCol.toString + if (theToString.startsWith("[") && theToString.endsWith("]")) { + val itr = javaCol.iterator().asScala + "[" + (if (colSizeLimit > 0) itr.take(colSizeLimit) else itr).map(prettify(_, processed + javaCol)).mkString(", ") + "]" + } + else + theToString + case javaMap: java.util.Map[_, _] => + // By default java map follows http://download.java.net/jdk7/archive/b123/docs/api/java/util/AbstractMap.html#toString() + // let's do our best to prettify its element when it is not overriden + import scala.collection.JavaConverters._ + val theToString = javaMap.toString + if (theToString.startsWith("{") && theToString.endsWith("}")) { + val itr = javaMap.entrySet.iterator.asScala + "{" + (if (colSizeLimit > 0) itr.take(colSizeLimit) else itr).map { entry => + prettify(entry.getKey, processed + javaMap) + "=" + prettify(entry.getValue, processed + javaMap) + }.mkString(", ") + "}" + } + else + theToString + // SKIP-SCALATESTJS,NATIVE-END + case anythingElse => anythingElse.toString + } + + def apply(o: Any): String = { + try { + prettify(o, Set.empty) + } + catch { + // This is in case of crazy designs like the one for scala.xml.Node. We handle Node + // specially above, but in case someone else creates a collection whose iterator + // returns itself, which will cause infinite recursion, at least we'll pop out and + // give them a string back. + case _: StackOverflowError => o.toString + } + } + +} + /** * Companion object for `Prettifier` that provides a default `Prettifier` implementation. */ @@ -181,89 +265,7 @@ object Prettifier { * For anything else, it returns the result of invoking `toString`. *

*/ - implicit val default: Prettifier = - new Prettifier { - - val colSizeLimit: Int = Option(System.getProperty("scalactic.prettifier.collection.size.limit")).map(_.toInt).getOrElse(0) - - private def prettify(o: Any, processed: Set[Any]): String = - if (processed.contains(o)) - throw new StackOverflowError("Cyclic relationship detected, let's fail early!") - else - o match { - case null => "null" - case aUnit: Unit => "<(), the Unit value>" - case aString: String => "\"" + aString + "\"" - case aStringWrapper: org.scalactic.ColCompatHelper.StringOps => "\"" + aStringWrapper.mkString + "\"" - case aChar: Char => "\'" + aChar + "\'" - case Some(e) => "Some(" + prettify(e, processed) + ")" - case Success(e) => "Success(" + prettify(e, processed) + ")" - case Left(e) => "Left(" + prettify(e, processed) + ")" - case Right(e) => "Right(" + prettify(e, processed) + ")" - case s: Symbol => "'" + s.name - case Good(e) => "Good(" + prettify(e, processed) + ")" - case Bad(e) => "Bad(" + prettify(e, processed) + ")" - case One(e) => "One(" + prettify(e, processed) + ")" - case many: Many[_] => "Many(" + (if (colSizeLimit > 0) many.toIterator.take(colSizeLimit) else many.toIterator).map(prettify(_, processed + many)).mkString(", ") + ")" - case anArray: Array[_] => "Array(" + (if (colSizeLimit > 0) anArray.take(colSizeLimit) else anArray).map(prettify(_, processed + anArray)).mkString(", ") + ")" - case aWrappedArray: WrappedArray[_] => "Array(" + (if (colSizeLimit > 0) aWrappedArray.take(colSizeLimit) else aWrappedArray).map(prettify(_, processed + aWrappedArray)).mkString(", ") + ")" - case a if ArrayHelper.isArrayOps(a) => - val anArrayOps = ArrayHelper.asArrayOps(a).iterator - "Array(" + (if (colSizeLimit > 0) anArrayOps.take(colSizeLimit) else anArrayOps).map(prettify(_, processed + anArrayOps)).mkString(", ") + ")" - case aGenMap: scala.collection.GenMap[_, _] => - ColCompatHelper.className(aGenMap) + "(" + - ((if (colSizeLimit > 0) aGenMap.take(colSizeLimit) else aGenMap).toIterator.map { case (key, value) => // toIterator is needed for consistent ordering - prettify(key, processed + aGenMap) + " -> " + prettify(value, processed + aGenMap) - }).mkString(", ") + ")" - case aGenTraversable: GenTraversable[_] => - val className = aGenTraversable.getClass.getName - if (className.startsWith("scala.xml.NodeSeq$") || className == "scala.xml.NodeBuffer" || className == "scala.xml.Elem") - aGenTraversable.mkString - else - ColCompatHelper.className(aGenTraversable) + "(" + (if (colSizeLimit > 0) aGenTraversable.take(colSizeLimit) else aGenTraversable).toIterator.map(prettify(_, processed + aGenTraversable)).mkString(", ") + ")" // toIterator is needed for consistent ordering - - // SKIP-SCALATESTJS-START - case javaCol: java.util.Collection[_] => - // By default java collection follows http://download.java.net/jdk7/archive/b123/docs/api/java/util/AbstractCollection.html#toString() - // let's do our best to prettify its element when it is not overriden - import scala.collection.JavaConverters._ - val theToString = javaCol.toString - if (theToString.startsWith("[") && theToString.endsWith("]")) { - val itr = javaCol.iterator().asScala - "[" + (if (colSizeLimit > 0) itr.take(colSizeLimit) else itr).map(prettify(_, processed + javaCol)).mkString(", ") + "]" - } - else - theToString - case javaMap: java.util.Map[_, _] => - // By default java map follows http://download.java.net/jdk7/archive/b123/docs/api/java/util/AbstractMap.html#toString() - // let's do our best to prettify its element when it is not overriden - import scala.collection.JavaConverters._ - val theToString = javaMap.toString - if (theToString.startsWith("{") && theToString.endsWith("}")) { - val itr = javaMap.entrySet.iterator.asScala - "{" + (if (colSizeLimit > 0) itr.take(colSizeLimit) else itr).map { entry => - prettify(entry.getKey, processed + javaMap) + "=" + prettify(entry.getValue, processed + javaMap) - }.mkString(", ") + "}" - } - else - theToString - // SKIP-SCALATESTJS,NATIVE-END - case anythingElse => anythingElse.toString - } - - def apply(o: Any): String = { - try { - prettify(o, Set.empty) - } - catch { - // This is in case of crazy designs like the one for scala.xml.Node. We handle Node - // specially above, but in case someone else creates a collection whose iterator - // returns itself, which will cause infinite recursion, at least we'll pop out and - // give them a string back. - case _: StackOverflowError => o.toString - } - } - } + implicit val default: Prettifier = new DefaultPrettifier(SizeLimit(0)) /** * A basic `Prettifier`. diff --git a/jvm/scalactic/src/main/scala/org/scalactic/SizeLimit.scala b/jvm/scalactic/src/main/scala/org/scalactic/SizeLimit.scala new file mode 100644 index 0000000000..b46dd929dd --- /dev/null +++ b/jvm/scalactic/src/main/scala/org/scalactic/SizeLimit.scala @@ -0,0 +1,21 @@ +/* + * Copyright 2001-2022 Artima, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.scalactic + +/** + * Size limit value class. + */ +case class SizeLimit(value: Int) \ No newline at end of file From 0065db6ef3682d19eacf558c9e3fac083c1c4c5d Mon Sep 17 00:00:00 2001 From: Chua Chee Seng Date: Mon, 13 Jun 2022 09:37:46 +0800 Subject: [PATCH 4/4] Added truncateAt to Prettifier companion object, use override approach for prettifyCollection. --- .../scala/org/scalactic/PrettifierSpec.scala | 5 + .../main/scala/org/scalactic/Prettifier.scala | 172 +++++++++++++----- 2 files changed, 128 insertions(+), 49 deletions(-) diff --git a/jvm/scalactic-test/src/test/scala/org/scalactic/PrettifierSpec.scala b/jvm/scalactic-test/src/test/scala/org/scalactic/PrettifierSpec.scala index 2c6f81a57e..fed0616419 100644 --- a/jvm/scalactic-test/src/test/scala/org/scalactic/PrettifierSpec.scala +++ b/jvm/scalactic-test/src/test/scala/org/scalactic/PrettifierSpec.scala @@ -397,6 +397,11 @@ class PrettifierSpec extends funspec.AnyFunSpec with matchers.should.Matchers { Prettifier.default(new Fred) shouldBe "It's Fred all the way down" } // SKIP-DOTTY-END + it("should truncate collection when used with Prettifier.truncateAt") { + val col = List(1, 2, 3) + val prettifier = Prettifier.truncateAt(SizeLimit(2)) + prettifier(col) shouldBe "List(1, 2, ...)" + } } } diff --git a/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala b/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala index 1b01b11370..625b73ea6c 100644 --- a/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala +++ b/jvm/scalactic/src/main/scala/org/scalactic/Prettifier.scala @@ -131,11 +131,58 @@ trait Prettifier extends Serializable { // I removed the extends (Any => String) } } -private[scalactic] class DefaultPrettifier(sizeLimit: SizeLimit) extends Prettifier { +private[scalactic] class DefaultPrettifier extends Prettifier { - val colSizeLimit: Int = sizeLimit.value + protected def prettifyCollection(o: Any, processed: Set[Any]): String = + o match { + case many: Many[_] => "Many(" + many.toIterator.map(prettify(_, processed + many)).mkString(", ") + ")" + case anArray: Array[_] => "Array(" + anArray.map(prettify(_, processed + anArray)).mkString(", ") + ")" + case aWrappedArray: WrappedArray[_] => "Array(" + aWrappedArray.map(prettify(_, processed + aWrappedArray)).mkString(", ") + ")" + case a if ArrayHelper.isArrayOps(a) => + val anArrayOps = ArrayHelper.asArrayOps(a).iterator + "Array(" + anArrayOps.map(prettify(_, processed + anArrayOps)).mkString(", ") + ")" + case aGenMap: scala.collection.GenMap[_, _] => + ColCompatHelper.className(aGenMap) + "(" + + (aGenMap.toIterator.map { case (key, value) => // toIterator is needed for consistent ordering + prettify(key, processed + aGenMap) + " -> " + prettify(value, processed + aGenMap) + }).mkString(", ") + ")" + case aGenTraversable: GenTraversable[_] => + val className = aGenTraversable.getClass.getName + if (className.startsWith("scala.xml.NodeSeq$") || className == "scala.xml.NodeBuffer" || className == "scala.xml.Elem") + aGenTraversable.mkString + else + ColCompatHelper.className(aGenTraversable) + "(" + aGenTraversable.toIterator.map(prettify(_, processed + aGenTraversable)).mkString(", ") + ")" // toIterator is needed for consistent ordering + + // SKIP-SCALATESTJS-START + case javaCol: java.util.Collection[_] => + // By default java collection follows http://download.java.net/jdk7/archive/b123/docs/api/java/util/AbstractCollection.html#toString() + // let's do our best to prettify its element when it is not overriden + import scala.collection.JavaConverters._ + val theToString = javaCol.toString + if (theToString.startsWith("[") && theToString.endsWith("]")) { + val itr = javaCol.iterator().asScala + "[" + itr.map(prettify(_, processed + javaCol)).mkString(", ") + "]" + } + else + theToString + case javaMap: java.util.Map[_, _] => + // By default java map follows http://download.java.net/jdk7/archive/b123/docs/api/java/util/AbstractMap.html#toString() + // let's do our best to prettify its element when it is not overriden + import scala.collection.JavaConverters._ + val theToString = javaMap.toString + if (theToString.startsWith("{") && theToString.endsWith("}")) { + val itr = javaMap.entrySet.iterator.asScala + "{" + itr.map { entry => + prettify(entry.getKey, processed + javaMap) + "=" + prettify(entry.getValue, processed + javaMap) + }.mkString(", ") + "}" + } + else + theToString + // SKIP-SCALATESTJS,NATIVE-END + case anythingElse => anythingElse.toString + } - private def prettify(o: Any, processed: Set[Any]): String = + protected def prettify(o: Any, processed: Set[Any]): String = if (processed.contains(o)) throw new StackOverflowError("Cyclic relationship detected, let's fail early!") else @@ -153,51 +200,7 @@ private[scalactic] class DefaultPrettifier(sizeLimit: SizeLimit) extends Prettif case Good(e) => "Good(" + prettify(e, processed) + ")" case Bad(e) => "Bad(" + prettify(e, processed) + ")" case One(e) => "One(" + prettify(e, processed) + ")" - case many: Many[_] => "Many(" + (if (colSizeLimit > 0) many.toIterator.take(colSizeLimit) else many.toIterator).map(prettify(_, processed + many)).mkString(", ") + ")" - case anArray: Array[_] => "Array(" + (if (colSizeLimit > 0) anArray.take(colSizeLimit) else anArray).map(prettify(_, processed + anArray)).mkString(", ") + ")" - case aWrappedArray: WrappedArray[_] => "Array(" + (if (colSizeLimit > 0) aWrappedArray.take(colSizeLimit) else aWrappedArray).map(prettify(_, processed + aWrappedArray)).mkString(", ") + ")" - case a if ArrayHelper.isArrayOps(a) => - val anArrayOps = ArrayHelper.asArrayOps(a).iterator - "Array(" + (if (colSizeLimit > 0) anArrayOps.take(colSizeLimit) else anArrayOps).map(prettify(_, processed + anArrayOps)).mkString(", ") + ")" - case aGenMap: scala.collection.GenMap[_, _] => - ColCompatHelper.className(aGenMap) + "(" + - ((if (colSizeLimit > 0) aGenMap.take(colSizeLimit) else aGenMap).toIterator.map { case (key, value) => // toIterator is needed for consistent ordering - prettify(key, processed + aGenMap) + " -> " + prettify(value, processed + aGenMap) - }).mkString(", ") + ")" - case aGenTraversable: GenTraversable[_] => - val className = aGenTraversable.getClass.getName - if (className.startsWith("scala.xml.NodeSeq$") || className == "scala.xml.NodeBuffer" || className == "scala.xml.Elem") - aGenTraversable.mkString - else - ColCompatHelper.className(aGenTraversable) + "(" + (if (colSizeLimit > 0) aGenTraversable.take(colSizeLimit) else aGenTraversable).toIterator.map(prettify(_, processed + aGenTraversable)).mkString(", ") + ")" // toIterator is needed for consistent ordering - - // SKIP-SCALATESTJS-START - case javaCol: java.util.Collection[_] => - // By default java collection follows http://download.java.net/jdk7/archive/b123/docs/api/java/util/AbstractCollection.html#toString() - // let's do our best to prettify its element when it is not overriden - import scala.collection.JavaConverters._ - val theToString = javaCol.toString - if (theToString.startsWith("[") && theToString.endsWith("]")) { - val itr = javaCol.iterator().asScala - "[" + (if (colSizeLimit > 0) itr.take(colSizeLimit) else itr).map(prettify(_, processed + javaCol)).mkString(", ") + "]" - } - else - theToString - case javaMap: java.util.Map[_, _] => - // By default java map follows http://download.java.net/jdk7/archive/b123/docs/api/java/util/AbstractMap.html#toString() - // let's do our best to prettify its element when it is not overriden - import scala.collection.JavaConverters._ - val theToString = javaMap.toString - if (theToString.startsWith("{") && theToString.endsWith("}")) { - val itr = javaMap.entrySet.iterator.asScala - "{" + (if (colSizeLimit > 0) itr.take(colSizeLimit) else itr).map { entry => - prettify(entry.getKey, processed + javaMap) + "=" + prettify(entry.getValue, processed + javaMap) - }.mkString(", ") + "}" - } - else - theToString - // SKIP-SCALATESTJS,NATIVE-END - case anythingElse => anythingElse.toString + case other => prettifyCollection(other, processed) } def apply(o: Any): String = { @@ -215,6 +218,72 @@ private[scalactic] class DefaultPrettifier(sizeLimit: SizeLimit) extends Prettif } +private[scalactic] class TruncatingPrettifier(sizeLimit: SizeLimit) extends DefaultPrettifier { + + private def dotDotDotIfTruncated(value: Boolean): String = + if (value) ", ..." else "" + + override protected def prettifyCollection(o: Any, processed: Set[Any]): String = { + o match { + case many: Many[_] => + val (taken, truncated) = if (many.size > sizeLimit.value) (many.toIterator.take(sizeLimit.value), true) else (many.toIterator, false) + "Many(" + taken.map(prettify(_, processed + many)).mkString(", ") + dotDotDotIfTruncated(truncated) + ")" + case anArray: Array[_] => + val (taken, truncated) = if (anArray.size > sizeLimit.value) (anArray.take(sizeLimit.value), true) else (anArray, false) + "Array(" + taken.map(prettify(_, processed + anArray)).mkString(", ") + dotDotDotIfTruncated(truncated) + ")" + case aWrappedArray: WrappedArray[_] => + val (taken, truncated) = if (aWrappedArray.size > sizeLimit.value) (aWrappedArray.take(sizeLimit.value), true) else (aWrappedArray, false) + "Array(" + taken.map(prettify(_, processed + aWrappedArray)).mkString(", ") + dotDotDotIfTruncated(truncated) + ")" + case a if ArrayHelper.isArrayOps(a) => + val anArrayOps = ArrayHelper.asArrayOps(a)// + val (taken, truncated) = if (anArrayOps.size > sizeLimit.value) (anArrayOps.iterator.take(sizeLimit.value), true) else (anArrayOps.iterator, false) + "Array(" + taken.map(prettify(_, processed + anArrayOps)).mkString(", ") + dotDotDotIfTruncated(truncated) + ")" + case aGenMap: scala.collection.GenMap[_, _] => + val (taken, truncated) = if (aGenMap.size > sizeLimit.value) (aGenMap.toIterator.take(sizeLimit.value), true) else (aGenMap.toIterator, false) + ColCompatHelper.className(aGenMap) + "(" + + (taken.map { case (key, value) => // toIterator is needed for consistent ordering + prettify(key, processed + aGenMap) + " -> " + prettify(value, processed + aGenMap) + }).mkString(", ") + dotDotDotIfTruncated(truncated) + ")" + case aGenTraversable: GenTraversable[_] => + val (taken, truncated) = if (aGenTraversable.size > sizeLimit.value) (aGenTraversable.take(sizeLimit.value), true) else (aGenTraversable, false) + val className = aGenTraversable.getClass.getName + if (className.startsWith("scala.xml.NodeSeq$") || className == "scala.xml.NodeBuffer" || className == "scala.xml.Elem") + aGenTraversable.mkString + else + ColCompatHelper.className(aGenTraversable) + "(" + taken.toIterator.map(prettify(_, processed + aGenTraversable)).mkString(", ") + dotDotDotIfTruncated(truncated) + ")" // toIterator is needed for consistent ordering + // SKIP-SCALATESTJS-START + case javaCol: java.util.Collection[_] => + // By default java collection follows http://download.java.net/jdk7/archive/b123/docs/api/java/util/AbstractCollection.html#toString() + // let's do our best to prettify its element when it is not overriden + import scala.collection.JavaConverters._ + val theToString = javaCol.toString + if (theToString.startsWith("[") && theToString.endsWith("]")) { + val itr = javaCol.iterator().asScala + val (taken, truncated) = if (javaCol.size > sizeLimit.value) (itr.take(sizeLimit.value), true) else (itr, false) + "[" + taken.map(prettify(_, processed + javaCol)).mkString(", ") + dotDotDotIfTruncated(truncated) + "]" + } + else + theToString + case javaMap: java.util.Map[_, _] => + // By default java map follows http://download.java.net/jdk7/archive/b123/docs/api/java/util/AbstractMap.html#toString() + // let's do our best to prettify its element when it is not overriden + import scala.collection.JavaConverters._ + val theToString = javaMap.toString + if (theToString.startsWith("{") && theToString.endsWith("}")) { + val itr = javaMap.entrySet.iterator.asScala + val (taken, truncated) = if (javaMap.size > sizeLimit.value) (itr.take(sizeLimit.value), true) else (itr, false) + "{" + taken.map { entry => + prettify(entry.getKey, processed + javaMap) + "=" + prettify(entry.getValue, processed + javaMap) + }.mkString(", ") + dotDotDotIfTruncated(truncated) + "}" + } + else + theToString + // SKIP-SCALATESTJS,NATIVE-END + case anythingElse => anythingElse.toString + } + } +} + /** * Companion object for `Prettifier` that provides a default `Prettifier` implementation. */ @@ -265,7 +334,12 @@ object Prettifier { * For anything else, it returns the result of invoking `toString`. *

*/ - implicit val default: Prettifier = new DefaultPrettifier(SizeLimit(0)) + implicit val default: Prettifier = new DefaultPrettifier() + + /** + * Create a default prettifier instance with collection size limit. + */ + def truncateAt(limit: SizeLimit): Prettifier = new TruncatingPrettifier(limit) /** * A basic `Prettifier`.