diff --git a/core/shared/src/main/scala/sigma/serialization/CoreByteWriter.scala b/core/shared/src/main/scala/sigma/serialization/CoreByteWriter.scala index 302663ba92..37d1455ba3 100644 --- a/core/shared/src/main/scala/sigma/serialization/CoreByteWriter.scala +++ b/core/shared/src/main/scala/sigma/serialization/CoreByteWriter.scala @@ -3,155 +3,106 @@ package sigma.serialization import scorex.util.serialization.Writer.Aux import scorex.util.serialization.{VLQByteBufferWriter, Writer} import sigma.ast.SType -import sigma.serialization.CoreByteWriter.{Bits, CostLimitChecker, DataInfo, U, Vlq, ZigZag} +import sigma.serialization.CoreByteWriter._ /** Implementation of [[Writer]] provided by `sigma-core` module. * * @param w destination [[Writer]] to which all the call got delegated. - * @param checkCostLimitOpt callback to check if the cost limit at current writing position - * is reached. The callback will throw an exception if the limit is reached. - * Note, the cost of serialization is approximated using formula - * `this.length * CostPerByte + InitialCost` */ -class CoreByteWriter(val w: Writer, val checkCostLimitOpt: Option[CostLimitChecker]) extends Writer { +class CoreByteWriter(val w: Writer) extends Writer { type CH = w.CH - /** Check the current writer length against the cost limit. */ - @inline protected def checkCostLimit(): Unit = { - if (checkCostLimitOpt.isDefined) - checkCostLimitOpt.get(this.length()) - } - @inline override def length(): Int = w.length() @inline override def newWriter(): Aux[CH] = w.newWriter() @inline override def putChunk(chunk: CH): this.type = { - w.putChunk(chunk); - checkCostLimit() - this + w.putChunk(chunk); this } @inline override def result(): CH = w.result() - @inline def put(x: Byte): this.type = { - w.put(x); - checkCostLimit() - this + @inline override def put(x: Byte): this.type = { + w.put(x); this } @inline def put(x: Byte, info: DataInfo[Byte]): this.type = { - w.put(x); - checkCostLimit() - this + w.put(x); this } override def putUByte(x: Int): this.type = { super.putUByte(x) } + def putUByte(x: Int, info: DataInfo[U[Byte]]): this.type = { super.putUByte(x) } @inline def putBoolean(x: Boolean): this.type = { - w.putBoolean(x); - checkCostLimit() - this + w.putBoolean(x); this } @inline def putBoolean(x: Boolean, info: DataInfo[Boolean]): this.type = { - w.putBoolean(x); - checkCostLimit() - this + w.putBoolean(x); this } @inline def putShort(x: Short): this.type = { - w.putShort(x); - checkCostLimit() - this + w.putShort(x); this } @inline def putShort(x: Short, info: DataInfo[Short]): this.type = { - w.putShort(x); - checkCostLimit() - this + w.putShort(x); this } @inline def putUShort(x: Int): this.type = { - w.putUShort(x); - checkCostLimit() - this + w.putUShort(x); this } @inline def putUShort(x: Int, info: DataInfo[Vlq[U[Short]]]): this.type = { - w.putUShort(x); - checkCostLimit() - this + w.putUShort(x); this } @inline def putInt(x: Int): this.type = { - w.putInt(x); - checkCostLimit() - this + w.putInt(x); this } @inline def putInt(x: Int, info: DataInfo[Int]): this.type = { - w.putInt(x); - checkCostLimit() - this + w.putInt(x); this } @inline def putUInt(x: Long): this.type = { - w.putUInt(x); - checkCostLimit() - this + w.putUInt(x); this } @inline def putUInt(x: Long, info: DataInfo[Vlq[U[Int]]]): this.type = { - w.putUInt(x); - checkCostLimit() - this + w.putUInt(x); this } @inline def putLong(x: Long): this.type = { - w.putLong(x); - checkCostLimit() - this + w.putLong(x); this } @inline def putLong(x: Long, info: DataInfo[Vlq[ZigZag[Long]]]): this.type = { - w.putLong(x); - checkCostLimit() - this + w.putLong(x); this } @inline def putULong(x: Long): this.type = { - w.putULong(x); - checkCostLimit() - this + w.putULong(x); this } @inline def putULong(x: Long, info: DataInfo[Vlq[U[Long]]]): this.type = { - w.putULong(x); - checkCostLimit() - this + w.putULong(x); this } override def putBytes(xs: Array[Byte], offset: Int, length: Int): this.type = { - w.putBytes(xs, offset, length); - checkCostLimit() - this + w.putBytes(xs, offset, length); this } @inline def putBytes(xs: Array[Byte]): this.type = { - w.putBytes(xs); - checkCostLimit() - this + w.putBytes(xs); this } @inline def putBytes(xs: Array[Byte], info: DataInfo[Array[Byte]]): this.type = { - w.putBytes(xs); - checkCostLimit() - this + w.putBytes(xs); this } /** Put the two bytes of the big-endian representation of the Short value into the @@ -159,32 +110,25 @@ class CoreByteWriter(val w: Writer, val checkCostLimitOpt: Option[CostLimitCheck @inline def putShortBytes(value: Short): this.type = { w.put((value >> 8).toByte) w.put(value.toByte) - checkCostLimit() this } @inline def putBits(xs: Array[Boolean]): this.type = { - w.putBits(xs); - checkCostLimit() - this + w.putBits(xs); this } @inline def putBits(xs: Array[Boolean], info: DataInfo[Bits]): this.type = { - w.putBits(xs); - checkCostLimit() - this + w.putBits(xs); this } @inline def putOption[T](x: Option[T])(putValueC: (this.type, T) => Unit): this.type = { w.putOption(x) { (_, v) => putValueC(this, v) } - checkCostLimit() this } @inline def putShortString(s: String): this.type = { w.putShortString(s); - checkCostLimit() this } @@ -194,11 +138,11 @@ class CoreByteWriter(val w: Writer, val checkCostLimitOpt: Option[CostLimitCheck } @inline def putType[T <: SType](x: T): this.type = { - TypeSerializer.serialize(x, this); // the cost is checked in TypeSerializer + TypeSerializer.serialize(x, this) this } @inline def putType[T <: SType](x: T, info: DataInfo[SType]): this.type = { - TypeSerializer.serialize(x, this); // the cost is checked in TypeSerializer + TypeSerializer.serialize(x, this) this } @@ -207,9 +151,6 @@ class CoreByteWriter(val w: Writer, val checkCostLimitOpt: Option[CostLimitCheck object CoreByteWriter { import scala.language.implicitConversions - /** Callback type of cost limit checker. */ - type CostLimitChecker = Int => Unit - /** Format descriptor type family. */ trait FormatDescriptor[T] { /** Size formula associated with this format */ diff --git a/core/shared/src/main/scala/sigma/serialization/CoreSerializer.scala b/core/shared/src/main/scala/sigma/serialization/CoreSerializer.scala index 2ec3ee5c1a..938d3f22c1 100644 --- a/core/shared/src/main/scala/sigma/serialization/CoreSerializer.scala +++ b/core/shared/src/main/scala/sigma/serialization/CoreSerializer.scala @@ -66,7 +66,7 @@ object CoreSerializer { def startWriter(): CoreByteWriter = { val b = new ByteArrayBuilder() val wi = new VLQByteBufferWriter(b) - val w = new CoreByteWriter(wi, None) + val w = new CoreByteWriter(wi) w } diff --git a/data/shared/src/main/scala/sigma/ast/ErgoTree.scala b/data/shared/src/main/scala/sigma/ast/ErgoTree.scala index 68d69abd91..8d731e1c67 100644 --- a/data/shared/src/main/scala/sigma/ast/ErgoTree.scala +++ b/data/shared/src/main/scala/sigma/ast/ErgoTree.scala @@ -381,7 +381,7 @@ object ErgoTree { * */ def withSegregation(header: HeaderType, prop: SigmaPropValue): ErgoTree = { val constantStore = new ConstantStore() - val w = SigmaSerializer.startWriter(constantStore) + val w = SigmaSerializer.startWriter(Some(constantStore)) // serialize value and segregate constants into constantStore ValueSerializer.serialize(prop, w) val extractedConstants = constantStore.getAll diff --git a/data/shared/src/main/scala/sigma/ast/methods.scala b/data/shared/src/main/scala/sigma/ast/methods.scala index 262a3266dd..fd9024ee62 100644 --- a/data/shared/src/main/scala/sigma/ast/methods.scala +++ b/data/shared/src/main/scala/sigma/ast/methods.scala @@ -12,6 +12,7 @@ import sigma.data.{DataValueComparer, KeyValueColl, Nullable, RType, SigmaConsta import sigma.eval.{CostDetails, ErgoTreeEvaluator, TracedCost} import sigma.reflection.RClass import sigma.serialization.CoreByteWriter.ArgInfo +import sigma.serialization.{DataSerializer, SigmaSerializer} import sigma.utils.SparseArrayContainer import scala.annotation.unused @@ -1532,9 +1533,17 @@ case object SGlobalMethods extends MonoTypeMethods { */ def serialize_eval(mc: MethodCall, G: SigmaDslBuilder, value: SType#WrappedType) (implicit E: ErgoTreeEvaluator): Coll[Byte] = { - // TODO v6.0: accumulate cost - val t = Evaluation.stypeToRType(mc.args(0).tpe) - G.serialize(value)(t) + + val addFixedCostCallback = { (costInfo: OperationCostInfo[FixedCost]) => + E.addCost(costInfo) + } + val addPerItemCostCallback = { (info: OperationCostInfo[PerItemCost], nItems: Int) => + E.addSeqCostNoOp(info.costKind, nItems, info.opDesc) + } + val w = SigmaSerializer.startWriter(None, + Some(addFixedCostCallback), Some(addPerItemCostCallback)) + DataSerializer.serialize(value, mc.args(0).tpe, w) + Colls.fromArray(w.toBytes) } protected override def getMethods() = super.getMethods() ++ { diff --git a/data/shared/src/main/scala/sigma/serialization/ErgoTreeSerializer.scala b/data/shared/src/main/scala/sigma/serialization/ErgoTreeSerializer.scala index 43e41f91ff..ce7d1241c3 100644 --- a/data/shared/src/main/scala/sigma/serialization/ErgoTreeSerializer.scala +++ b/data/shared/src/main/scala/sigma/serialization/ErgoTreeSerializer.scala @@ -347,7 +347,7 @@ class ErgoTreeSerializer { val newVal = newVals(positions.indexOf(i)) // we need to get newVal's serialized constant value (see ProveDlogSerializer for example) val constantStore = new ConstantStore() - val valW = SigmaSerializer.startWriter(constantStore) + val valW = SigmaSerializer.startWriter(Some(constantStore)) valW.putValue(newVal) val newConsts = constantStore.getAll require(newConsts.length == 1) diff --git a/data/shared/src/main/scala/sigma/serialization/SigmaByteWriter.scala b/data/shared/src/main/scala/sigma/serialization/SigmaByteWriter.scala index 87b115e942..93e078e319 100644 --- a/data/shared/src/main/scala/sigma/serialization/SigmaByteWriter.scala +++ b/data/shared/src/main/scala/sigma/serialization/SigmaByteWriter.scala @@ -4,28 +4,52 @@ import scorex.util.serialization.Writer import sigma.ast.syntax._ import sigma.ast._ import sigma.serialization.CoreByteWriter.{ArgInfo, DataInfo, FormatDescriptor, SeqFmt} -import CoreByteWriter.CostLimitChecker +import SigmaByteWriter._ /** Implementation of [[Writer]] provided by `sigma-data` module. * - * @param w destination [[Writer]] to which all the call got delegated. - * @param constantExtractionStore optional store to segregate constants to while - * replacing them with placeholders. - * @param checkCostLimitOpt callback to check if the cost limit at current writing position - * is reached. The callback will throw an exception if the limit is reached. + * @param w destination [[Writer]] to which all the call got delegated. + * @param constantExtractionStore optional store to segregate constants to while + * replacing them with placeholders. + * @param addFixedCostCallbackOpt optional callback to accumulate fixed costs. + * @param addPerItemCostCallbackOpt optional callback to accumulate per-item costs. */ class SigmaByteWriter( override val w: Writer, val constantExtractionStore: Option[ConstantStore], - override val checkCostLimitOpt: Option[CostLimitChecker] -) - extends CoreByteWriter(w, checkCostLimitOpt) { + val addFixedCostCallbackOpt: Option[FixedCostCallback], + val addPerItemCostCallbackOpt: Option[PerItemCostCallback] +) extends CoreByteWriter(w) { import CoreByteWriter._ import ValueSerializer._ + /** Adds the given cost to the callback if it is defined. */ + @inline private def addFixedCost(cost: OperationCostInfo[FixedCost]): Unit = { + if (addFixedCostCallbackOpt.isDefined) + addFixedCostCallbackOpt.get(cost) + } + + /** Adds the given cost to the callback if it is defined. */ + @inline private def addPerItemCost(cost: OperationCostInfo[PerItemCost], nItems: Int): Unit = { + if (addPerItemCostCallbackOpt.isDefined) + addPerItemCostCallbackOpt.get(cost, nItems) + } + + override def putChunk(chunk: w.CH): SigmaByteWriter.this.type = { + val start = length() + super.putChunk(chunk) + addPerItemCost(PutChunkCost, length() - start) + this + } + + override def put(x: Byte): this.type = { + addFixedCost(PutByteCost) + super.put(x) + } + override def put(x: Byte, info: DataInfo[Byte]): this.type = { ValueSerializer.addArgInfo(info) - checkCostLimit() + addFixedCost(PutByteCost) w.put(x); this } @@ -34,90 +58,160 @@ class SigmaByteWriter( super.putUByte(x) } - @inline override def putBoolean(x: Boolean, info: DataInfo[Boolean]): this.type = { + override def putBoolean(x: Boolean): this.type = { + addFixedCost(PutByteCost) + super.putBoolean(x) + } + + override def putBoolean(x: Boolean, info: DataInfo[Boolean]): this.type = { ValueSerializer.addArgInfo(info) - w.putBoolean(x); - checkCostLimit() - this + addFixedCost(PutByteCost) + w.putBoolean(x); this + } + + override def putShort(x: Short): this.type = { + addFixedCost(PutSignedNumericCost) + super.putShort(x) } - @inline override def putShort(x: Short, info: DataInfo[Short]): this.type = { + override def putShort(x: Short, info: DataInfo[Short]): this.type = { ValueSerializer.addArgInfo(info) - w.putShort(x); - checkCostLimit() - this + addFixedCost(PutSignedNumericCost) + w.putShort(x); this + } + + override def putUShort(x: Int): this.type = { + addFixedCost(PutUnsignedNumericCost) + super.putUShort(x) } - @inline override def putUShort(x: Int, info: DataInfo[Vlq[U[Short]]]): this.type = { + override def putUShort(x: Int, info: DataInfo[Vlq[U[Short]]]): this.type = { ValueSerializer.addArgInfo(info) - w.putUShort(x); - checkCostLimit() - this + addFixedCost(PutUnsignedNumericCost) + w.putUShort(x); this } - @inline override def putInt(x: Int, info: DataInfo[Int]): this.type = { + override def putInt(x: Int): this.type = { + addFixedCost(PutSignedNumericCost) + super.putInt(x) + } + + override def putInt(x: Int, info: DataInfo[Int]): this.type = { ValueSerializer.addArgInfo(info) - w.putInt(x); - checkCostLimit() - this + addFixedCost(PutSignedNumericCost) + w.putInt(x); this } - @inline override def putUInt(x: Long, info: DataInfo[Vlq[U[Int]]]): this.type = { + override def putUInt(x: Long): SigmaByteWriter.this.type = { + super.putUInt(x) + } + + override def putUInt(x: Long, info: DataInfo[Vlq[U[Int]]]): this.type = { ValueSerializer.addArgInfo(info) - w.putUInt(x); - checkCostLimit() - this + addFixedCost(PutUnsignedNumericCost) + w.putUInt(x); this } - @inline override def putLong(x: Long, info: DataInfo[Vlq[ZigZag[Long]]]): this.type = { + override def putLong(x: Long): SigmaByteWriter.this.type = { + addFixedCost(PutSignedNumericCost) + super.putLong(x) + } + + override def putLong(x: Long, info: DataInfo[Vlq[ZigZag[Long]]]): this.type = { ValueSerializer.addArgInfo(info) - w.putLong(x); - checkCostLimit() - this + addFixedCost(PutSignedNumericCost) + w.putLong(x); this } - @inline override def putULong(x: Long, info: DataInfo[Vlq[U[Long]]]): this.type = { + override def putULong(x: Long): SigmaByteWriter.this.type = { + addFixedCost(PutUnsignedNumericCost) + super.putULong(x) + } + + override def putULong(x: Long, info: DataInfo[Vlq[U[Long]]]): this.type = { ValueSerializer.addArgInfo(info) - w.putULong(x); - checkCostLimit() - this + addFixedCost(PutUnsignedNumericCost) + w.putULong(x); this + } + + override def putBytes(xs: Array[Byte], offset: Int, length: Int): this.type = { + addPerItemCost(PutChunkCost, length) + super.putBytes(xs, offset, length) + } + + override def putBytes(xs: Array[Byte]): SigmaByteWriter.this.type = { + addPerItemCost(PutChunkCost, xs.length) + super.putBytes(xs) } - @inline override def putBytes(xs: Array[Byte], info: DataInfo[Array[Byte]]): this.type = { + override def putBytes(xs: Array[Byte], info: DataInfo[Array[Byte]]): this.type = { ValueSerializer.addArgInfo(info) - w.putBytes(xs); - checkCostLimit() - this + addPerItemCost(PutChunkCost, xs.length) + w.putBytes(xs); this + } + + /** Put the two bytes of the big-endian representation of the Short value into the + * writer. */ + override def putShortBytes(value: Short): SigmaByteWriter.this.type = { + addPerItemCost(PutChunkCost, 2) + super.putShortBytes(value) } - @inline override def putBits(xs: Array[Boolean], info: DataInfo[Bits]): this.type = { + override def putBits(xs: Array[Boolean]): SigmaByteWriter.this.type = { + addPerItemCost(PutChunkCost, xs.length) // number of bits + super.putBits(xs) + } + + override def putBits(xs: Array[Boolean], info: DataInfo[Bits]): this.type = { ValueSerializer.addArgInfo(info) - w.putBits(xs); - checkCostLimit() - this + addPerItemCost(PutChunkCost, xs.length) // number of bits + w.putBits(xs); this } - @inline override def putType[T <: SType](x: T, info: DataInfo[SType]): this.type = { + override def putOption[T](x: Option[T])(putValueC: (this.type, T) => Unit): this.type = { + addFixedCost(PutByteCost) // cost of option tag byte + super.putOption(x)(putValueC) + } + + override def putShortString(s: String): SigmaByteWriter.this.type = { + addPerItemCost(PutChunkCost, s.length) + super.putShortString(s) + } + + override def putType[T <: SType](x: T, info: DataInfo[SType]): this.type = { ValueSerializer.addArgInfo(info) - TypeSerializer.serialize(x, this); // the cost is checked in TypeSerializer + TypeSerializer.serialize(x, this); // the cost is added in TypeSerializer this } - @inline def putValue[T <: SType](x: Value[T]): this.type = { - ValueSerializer.serialize(x, this) // the cost is checked in ValueSerializer + /** Serializes the given expression using [[ValueSerializer]]. */ + def putValue[T <: SType](x: Value[T]): this.type = { + ValueSerializer.serialize(x, this) // the cost is added in ValueSerializer this } - @inline def putValue[T <: SType](x: Value[T], info: DataInfo[SValue]): this.type = { + + /** Serializes the given expression using [[ValueSerializer]]. + * @param x the ErgoTree expression to serialize + * @param info additional information about the data being serialized + */ + def putValue[T <: SType](x: Value[T], info: DataInfo[SValue]): this.type = { ValueSerializer.addArgInfo(info) - ValueSerializer.serialize(x, this); // the cost is checked in ValueSerializer + ValueSerializer.serialize(x, this); // the cost is added in ValueSerializer this } - @inline def putValues[T <: SType](xs: Seq[Value[T]]): this.type = { + + /** Serializes the given sequence of expressions using [[ValueSerializer]]. */ + def putValues[T <: SType](xs: Seq[Value[T]]): this.type = { putUInt(xs.length) xs.foreach(putValue(_)) this } - @inline def putValues[T <: SType](xs: Seq[Value[T]], info: DataInfo[Seq[SValue]], itemInfo: DataInfo[SValue]): this.type = { + + /** Serializes the given sequence of expressions using [[ValueSerializer]]. + * @param xs the sequence of ErgoTree expressions to serialize + * @param info additional information about the data being serialized + */ + def putValues[T <: SType](xs: Seq[Value[T]], info: DataInfo[Seq[SValue]], itemInfo: DataInfo[SValue]): this.type = { putUInt(xs.length, valuesLengthInfo) foreach("\\#items", xs) { x => putValue(x, itemInfo) @@ -127,6 +221,40 @@ class SigmaByteWriter( } object SigmaByteWriter { + + /** Callback to accumulate fixed costs. */ + type FixedCostCallback = OperationCostInfo[FixedCost] => Unit + + /** Callback to accumulate per-item costs (chunked cost). */ + type PerItemCostCallback = (OperationCostInfo[PerItemCost], Int) => Unit + + /** Cost of writing single byte without any encoding. + * This also include overhead of method calls. + * This is the minimal possible JitCost value + */ + val PutByteCost = OperationCostInfo(FixedCost(JitCost(1)), NamedDesc("SigmaByteWriter.put")) + + /** Cost of writing a signed numeric including: + * 1) allocation of VLQ buffer array (see putULong in [[scorex.util.serialization.VLQWriter]]) + * 2) VLQ encoding + * 3) overhead of method calls. + */ + val PutUnsignedNumericCost = OperationCostInfo(FixedCost(JitCost(3)), NamedDesc("SigmaByteWriter.putUNumeric")) + + /** Cost of writing a signed numeric including: + * 1) ZigZag encoding. + * 2) allocation of VLQ buffer array (see putULong in [[scorex.util.serialization.VLQWriter]]) + * 3) VLQ encoding + * 4) overhead of method calls. + */ + val PutSignedNumericCost = OperationCostInfo(FixedCost(JitCost(3)), NamedDesc("SigmaByteWriter.putNumeric")) + + /** Cost of writing a chunk of bytes: + * 1) method call overhead + * 2) 1 cost unit per byte + */ + val PutChunkCost = OperationCostInfo(PerItemCost(JitCost(3), JitCost(1), 1), NamedDesc("SigmaByteWriter.putChunk")) + implicit case object ValueFmt extends FormatDescriptor[SValue] { override def size: String = "[1, *]" override def toString: String = "Expr" diff --git a/data/shared/src/main/scala/sigma/serialization/SigmaSerializer.scala b/data/shared/src/main/scala/sigma/serialization/SigmaSerializer.scala index e842258f80..7da7ec1606 100644 --- a/data/shared/src/main/scala/sigma/serialization/SigmaSerializer.scala +++ b/data/shared/src/main/scala/sigma/serialization/SigmaSerializer.scala @@ -4,7 +4,7 @@ import java.nio.ByteBuffer import scorex.util.ByteArrayBuilder import scorex.util.serialization._ import sigma.data.SigmaConstants -import sigma.serialization.CoreByteWriter.CostLimitChecker +import sigma.serialization.SigmaByteWriter.{FixedCostCallback, PerItemCostCallback} import sigma.serialization.ValueCodes.OpCode object SigmaSerializer { @@ -52,14 +52,18 @@ object SigmaSerializer { def startWriter(): SigmaByteWriter = { val b = new ByteArrayBuilder() val wi = new VLQByteBufferWriter(b) - val w = new SigmaByteWriter(wi, constantExtractionStore = None, checkCostLimitOpt = None) + val w = new SigmaByteWriter(wi, constantExtractionStore = None, addFixedCostCallbackOpt = None, addPerItemCostCallbackOpt = None) w } - def startWriter(constantExtractionStore: ConstantStore, checkCostLimit: Option[CostLimitChecker] = None): SigmaByteWriter = { + def startWriter( + constantExtractionStore: Option[ConstantStore], + addFixedCostCallback: Option[FixedCostCallback] = None, + addPerItemCostCallback: Option[PerItemCostCallback] = None + ): SigmaByteWriter = { val b = new ByteArrayBuilder() val wi = new VLQByteBufferWriter(b) - val w = new SigmaByteWriter(wi, constantExtractionStore = Some(constantExtractionStore), checkCostLimit) + val w = new SigmaByteWriter(wi, constantExtractionStore = constantExtractionStore, addFixedCostCallback, addPerItemCostCallback) w } } diff --git a/sc/shared/src/test/scala/sigma/SigmaDslTesting.scala b/sc/shared/src/test/scala/sigma/SigmaDslTesting.scala index f14ba275ff..5e43eb18e1 100644 --- a/sc/shared/src/test/scala/sigma/SigmaDslTesting.scala +++ b/sc/shared/src/test/scala/sigma/SigmaDslTesting.scala @@ -255,13 +255,21 @@ class SigmaDslTesting extends AnyPropSpec fail( s"""Should succeed with the same value or fail with the same exception, but was: - |First result: $b1 - |Second result: $b2 + |First result: ${errorWithStack(b1)} + |Second result: ${errorWithStack(b2)} |Root cause: $cause |""".stripMargin) } } + private def errorWithStack[A](e: Try[A]): String = e match { + case Failure(t) => + val sw = new java.io.StringWriter + t.printStackTrace(new java.io.PrintWriter(sw)) + sw.toString + case _ => e.toString + } + /** Creates a new ErgoLikeContext using given [[CContext]] as template. * Copies most of the data from ctx and the missing data is taken from the args. * This is a helper method to be used in tests only.