diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/protocol/SerializedException.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/protocol/SerializedException.scala index c21fbf958c4..8e2ca678300 100644 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/protocol/SerializedException.scala +++ b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/internal/protocol/SerializedException.scala @@ -16,7 +16,7 @@ import io.circe._ */ case class SerializedException( name: String, - message: String, + message: Option[String], stackTrace: Seq[SerializedException.TraceElement], cause: Option[SerializedException] ) @@ -33,7 +33,7 @@ object SerializedException { ): SerializedException = SerializedException( name = name, - message = message, + message = Some(message), stackTrace = stackTrace, cause = Some(cause) ) @@ -45,18 +45,13 @@ object SerializedException { message: String, stackTrace: Seq[SerializedException.TraceElement] ): SerializedException = - SerializedException( + new SerializedException( name = name, - message = message, + message = Some(message), stackTrace = stackTrace, cause = None ) - /** Creates a [[SerializedException]] from a [[Throwable]]. - */ - def apply(throwable: Throwable): SerializedException = - fromException(throwable) - /** Encodes a JVM [[Throwable]] as [[SerializedException]]. */ def fromException(throwable: Throwable): SerializedException = { @@ -66,7 +61,7 @@ object SerializedException { else Option(throwable.getCause).map(fromException) SerializedException( name = Option(clazz.getCanonicalName).getOrElse(clazz.getName), - message = throwable.getMessage, + message = Option(throwable.getMessage), stackTrace = throwable.getStackTrace.toSeq.map(encodeStackTraceElement), cause = cause ) @@ -164,7 +159,7 @@ object SerializedException { ): Decoder.Result[SerializedException] = { for { name <- json.get[String](JsonFields.Name) - message <- json.get[String](JsonFields.Message) + message <- json.get[Option[String]](JsonFields.Message) stackTrace <- json.get[Seq[TraceElement]](JsonFields.StackTrace) cause <- json.getOrElse[Option[SerializedException]](JsonFields.Cause)(None) diff --git a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/DefaultLogMessageRendererSpec.scala b/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/DefaultLogMessageRendererSpec.scala new file mode 100644 index 00000000000..6d6294d321e --- /dev/null +++ b/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/DefaultLogMessageRendererSpec.scala @@ -0,0 +1,33 @@ +package org.enso.loggingservice.internal + +import org.enso.loggingservice.LogLevel +import org.enso.loggingservice.internal.protocol.{ + SerializedException, + WSLogMessage +} +import org.scalatest.OptionValues +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import java.time.Instant +import java.time.temporal.ChronoUnit + +class DefaultLogMessageRendererSpec + extends AnyWordSpec + with Matchers + with OptionValues { + + "DefaultLogMessageRenderer" should { + "render NullPointerException" in { + val renderer = new DefaultLogMessageRenderer(printExceptions = true) + val ts = Instant.now().truncatedTo(ChronoUnit.MILLIS) + + val exception = + SerializedException.fromException(new NullPointerException) + val message = + WSLogMessage(LogLevel.Trace, ts, "group", "message", Some(exception)) + + noException should be thrownBy renderer.render(message) + } + } +} diff --git a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/SerializedExceptionSpec.scala b/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/SerializedExceptionSpec.scala index 195d78fb571..c5773066c34 100644 --- a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/SerializedExceptionSpec.scala +++ b/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/SerializedExceptionSpec.scala @@ -10,6 +10,7 @@ class SerializedExceptionSpec extends AnyWordSpec with Matchers with OptionValues { + "SerializedException" should { "serialize and deserialize with nested causes" in { val cause = SerializedException( @@ -25,7 +26,7 @@ class SerializedExceptionSpec SerializedException.TraceElement("e2", "loc2"), SerializedException.TraceElement("e3", "loc3") ), - cause = cause + cause ) exception.asJson @@ -33,5 +34,15 @@ class SerializedExceptionSpec .toOption .value shouldEqual exception } + + "be created from NullPointerException" in { + val exception = new NullPointerException() + val result = SerializedException.fromException(exception) + + result.name shouldEqual exception.getClass.getName + result.message shouldEqual None + result.cause shouldEqual None + result.stackTrace shouldBe Symbol("nonEmpty") + } } } diff --git a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/WSLogMessageSpec.scala b/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/WSLogMessageSpec.scala index 8ee0efe832f..e19c5736b6a 100644 --- a/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/WSLogMessageSpec.scala +++ b/lib/scala/logging-service/src/test/scala/org/enso/loggingservice/internal/WSLogMessageSpec.scala @@ -14,17 +14,31 @@ import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec class WSLogMessageSpec extends AnyWordSpec with Matchers with OptionValues { + "WSLogMessage" should { "serialize and deserialize to the same thing" in { val ts = Instant.now().truncatedTo(ChronoUnit.MILLIS) val message1 = WSLogMessage(LogLevel.Trace, ts, "group", "message", None) message1.asJson.as[WSLogMessage].toOption.value shouldEqual message1 + noException should be thrownBy message1.asJson.noSpaces - val exception = SerializedException("name", "message", Seq(), None) + val exception = SerializedException("name", "message", Seq()) val message2 = WSLogMessage(LogLevel.Trace, ts, "group", "message", Some(exception)) message2.asJson.as[WSLogMessage].toOption.value shouldEqual message2 + noException should be thrownBy message2.asJson.noSpaces + } + + "serialize NullPointerException" in { + val ts = Instant.now().truncatedTo(ChronoUnit.MILLIS) + + val exception = + SerializedException.fromException(new NullPointerException) + val message = + WSLogMessage(LogLevel.Trace, ts, "group", "message", Some(exception)) + message.asJson.as[WSLogMessage].toOption.value shouldEqual message + noException should be thrownBy message.asJson.noSpaces } } }