From 95cc7ab7e0734e36b6c59746abb8ea887efdd21f Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Sun, 21 Apr 2024 01:37:39 -0700 Subject: [PATCH] airspec (feature): Scala Native 0.5 support (Scala 3 only) (#3494) --- .github/workflows/release-airspec.yml | 2 +- .github/workflows/test.yml | 3 + .../wvlet/log/LogTimestampFormatter.scala | 2 +- .../scala-3/java/util/logging/Level.scala | 20 +++ .../scala-3/java/util/logging/Logger.scala | 120 ++++++++++++++++++ .../{scala => scala-3}/wvlet/log/LogEnv.scala | 0 .../wvlet/log/LogTimestampFormatter.scala | 43 +++++++ .../wvlet/log/LogTimestampFormatter.scala | 16 --- .../src/main/scala/wvlet/log/LogFormat.scala | 24 ++-- .../src/main/scala/wvlet/log/LogRecord.scala | 6 +- .../src/main/scala/wvlet/log/Logger.scala | 6 +- .../src/main/scala/wvlet/airspec/Compat.scala | 4 +- .../src/main/scala/wvlet/airspec/Compat.scala | 4 +- .../main/scala-3/wvlet/airspec/Compat.scala | 13 +- airspec/build.sbt | 10 +- .../main/scala/wvlet/airspec/AirSpec.scala | 4 +- .../main/scala/wvlet/airspec/CompatApi.scala | 2 + .../wvlet/airspec/runner/AirSpecLogger.scala | 17 ++- .../airspec/runner/AirSpecSbtRunner.scala | 8 +- .../wvlet/airspec/runner/AirSpecTask.scala | 6 +- airspec/src/test/scala/examples/RxTest.scala | 7 +- 21 files changed, 255 insertions(+), 62 deletions(-) create mode 100644 airframe-log/.native/src/main/scala-3/java/util/logging/Level.scala create mode 100644 airframe-log/.native/src/main/scala-3/java/util/logging/Logger.scala rename airframe-log/.native/src/main/{scala => scala-3}/wvlet/log/LogEnv.scala (100%) create mode 100644 airframe-log/.native/src/main/scala-3/wvlet/log/LogTimestampFormatter.scala delete mode 100644 airframe-log/.native/src/main/scala/wvlet/log/LogTimestampFormatter.scala diff --git a/.github/workflows/release-airspec.yml b/.github/workflows/release-airspec.yml index 5a9613f00c..2cd2c49923 100644 --- a/.github/workflows/release-airspec.yml +++ b/.github/workflows/release-airspec.yml @@ -28,7 +28,7 @@ jobs: env: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} run: | - ../sbt "+airspecJVM/publishSigned; +airspecJS/publishSigned" + ../sbt "+airspecJVM/publishSigned; +airspecJS/publishSigned; +airspecNative/publishSigned" working-directory: ./airspec - name: Release to Sonatype env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a81811a03..ce37f80f68 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -224,6 +224,9 @@ jobs: - name: Scala JVM and Scala.js Test run: ../sbt "++airspecJVM/test; ++airspecJS/test" working-directory: ./airspec + - name: Scala Native Test + run: ../sbt "++ 3; airspecNative/test" + working-directory: ./airspec - name: Publish Test Report uses: mikepenz/action-junit-report@v4 if: always() # always run even if the previous step fails diff --git a/airframe-log/.js/src/main/scala/wvlet/log/LogTimestampFormatter.scala b/airframe-log/.js/src/main/scala/wvlet/log/LogTimestampFormatter.scala index 2af28bc0ac..af7ee63ab3 100644 --- a/airframe-log/.js/src/main/scala/wvlet/log/LogTimestampFormatter.scala +++ b/airframe-log/.js/src/main/scala/wvlet/log/LogTimestampFormatter.scala @@ -3,7 +3,7 @@ package wvlet.log import scala.scalajs.js /** - * Use scalajs.js.Date to foramte timestamp + * Use scalajs.js.Date to foramt timestamps */ object LogTimestampFormatter { def formatTimestamp(timeMillis: Long): String = { diff --git a/airframe-log/.native/src/main/scala-3/java/util/logging/Level.scala b/airframe-log/.native/src/main/scala-3/java/util/logging/Level.scala new file mode 100644 index 0000000000..61ea700db9 --- /dev/null +++ b/airframe-log/.native/src/main/scala-3/java/util/logging/Level.scala @@ -0,0 +1,20 @@ +package java.util.logging + + +case class Level(name: String, value: Int) extends Ordered[Level] { + override def compare(other: Level): Int = value.compare(other.value) + def intValue(): Int = value + override def toString: String = name +} + +object Level: + val OFF = Level("OFF", 0) + val SEVERE = Level("SEVERE", 1000) + val WARNING = Level("WARNING", 900) + val INFO = Level("INFO", 800) + val CONFIG = Level("CONFIG", 700) + val FINE = Level("FINE", 500) + val FINER = Level("FINER", 400) + val FINEST = Level("FINEST", 300) + val ALL = Level("ALL", Integer.MIN_VALUE) + diff --git a/airframe-log/.native/src/main/scala-3/java/util/logging/Logger.scala b/airframe-log/.native/src/main/scala-3/java/util/logging/Logger.scala new file mode 100644 index 0000000000..5aac70e365 --- /dev/null +++ b/airframe-log/.native/src/main/scala-3/java/util/logging/Logger.scala @@ -0,0 +1,120 @@ +package java.util.logging + + +abstract class Handler extends AutoCloseable: + def publish(record: LogRecord): Unit + def flush(): Unit + +/** + * Implements java.util.logging.Logger interface, which is not avaialble + * in Scala Native + * @param name + */ +class Logger(parent: Option[Logger], name: String) { + private var handlers = List.empty[Handler] + private var useParentHandlers = true + private var level: Option[Level] = None + + def getName(): String = name + + def log(level: Level, msg: String): Unit = { + log(LogRecord(level, msg)) + } + + def log(record: LogRecord): Unit = { + if(isLoggable(record.getLevel())) { + if(record.getLoggerName() == null) { + record.setLoggerName(name) + } + if(parent.nonEmpty && useParentHandlers) then + getParent().log(record) + else + handlers.foreach { h => h.publish(record) } + } + } + + def isLoggable(level: Level): Boolean = { + val l = getLevel() + if(level.intValue() < l.intValue()) then false else true + } + + def getParent(): Logger = { + parent.getOrElse(null) + } + + def getLevel(): Level = { + level.orElse(parent.map(_.getLevel())).getOrElse(Level.INFO) + } + + def setLevel(newLevel: Level): Unit = { + level = Some(newLevel) + } + + def resetLogLevel(): Unit = { + level = None + } + + def setUseParentHandlers(useParentHandlers: Boolean): Unit = { + this.useParentHandlers = useParentHandlers + } + + def addHandler(h: Handler): Unit = { + handlers = h :: handlers + } + + def removeHandler(h: Handler): Unit = { + handlers = handlers.filter(_ != h) + } + + def getHandlers: Array[Handler] = handlers.toArray +} + +object Logger: + import scala.jdk.CollectionConverters.* + private val loggerTable = new java.util.concurrent.ConcurrentHashMap[String, Logger]().asScala + private val rootLogger = Logger(None, "") + + def getLogger(name: String): Logger = { + loggerTable.get(name) match { + case Some(logger) => logger + case None => + val logger = newLogger(name) + synchronized { + loggerTable.put(name, logger) + } + logger + } + } + + private def newLogger(name: String): Logger = { + name match { + case null | "" => rootLogger + case other => + val parentName = name.substring(0, name.lastIndexOf('.').max(0)) + val parentLogger = getLogger(parentName) + Logger(Some(parentLogger), name) + } + } + + +abstract class Formatter: + def format(record: LogRecord): String + + +class LogRecord(_level: Level, msg: String) extends Serializable: + private val millis = System.currentTimeMillis() + private var loggerName = "" + private var thrown: Throwable = null + + def getMessage(): String = msg + def getMillis(): Long = millis + def getLoggerName(): String = loggerName + def getLevel(): Level = _level + def getThrown(): Throwable = thrown + + def setLoggerName(name: String): Unit = { + this.loggerName = name + } + def setThrown(e: Throwable): Unit = { + thrown = e + } diff --git a/airframe-log/.native/src/main/scala/wvlet/log/LogEnv.scala b/airframe-log/.native/src/main/scala-3/wvlet/log/LogEnv.scala similarity index 100% rename from airframe-log/.native/src/main/scala/wvlet/log/LogEnv.scala rename to airframe-log/.native/src/main/scala-3/wvlet/log/LogEnv.scala diff --git a/airframe-log/.native/src/main/scala-3/wvlet/log/LogTimestampFormatter.scala b/airframe-log/.native/src/main/scala-3/wvlet/log/LogTimestampFormatter.scala new file mode 100644 index 0000000000..d3f060deef --- /dev/null +++ b/airframe-log/.native/src/main/scala-3/wvlet/log/LogTimestampFormatter.scala @@ -0,0 +1,43 @@ +package wvlet.log + +import scalanative.posix.time.* +import scalanative.unsafe.* +import scalanative.unsigned.* +import scalanative.libc.stdio.* +import scalanative.libc.string.* + +/** + * Use strftime to format timestamps in Scala Native + */ +object LogTimestampFormatter { + + private def format(pattern: CString, timeMillis: Long): String = { + Zone { + val ttPtr = alloc[time_t]() + !ttPtr = (timeMillis / 1000).toSize + val tmPtr = alloc[tm]() + localtime_r(ttPtr, tmPtr) + val bufSize = 26.toUSize + val buf: Ptr[Byte] = alloc[Byte](bufSize) + strftime(buf, bufSize, pattern, tmPtr) + val ms = timeMillis % 1000 + + val msBuf: Ptr[Byte] = alloc[Byte](3) + sprintf(msBuf, c"%03d", ms) + strcat(buf, msBuf) + + val tzBuf: Ptr[Byte] = alloc[Byte](5) + strftime(tzBuf, 5.toUSize, c"%z", tmPtr) + strcat(buf, tzBuf) + fromCString(buf) + } + } + + def formatTimestamp(timeMillis: Long): String = { + format(c"%Y-%m-%d %H:%M:%S.", timeMillis) + } + + def formatTimestampWithNoSpaace(timeMillis: Long): String = { + format(c"%Y-%m-%dT%H:%M:%S.", timeMillis) + } +} diff --git a/airframe-log/.native/src/main/scala/wvlet/log/LogTimestampFormatter.scala b/airframe-log/.native/src/main/scala/wvlet/log/LogTimestampFormatter.scala deleted file mode 100644 index de8bb89da4..0000000000 --- a/airframe-log/.native/src/main/scala/wvlet/log/LogTimestampFormatter.scala +++ /dev/null @@ -1,16 +0,0 @@ -package wvlet.log - -/** - * Use scalajs.js.Date to foramte timestamp - */ -object LogTimestampFormatter { - def formatTimestamp(timeMillis: Long): String = { - // TODO - "" - } - - def formatTimestampWithNoSpaace(timeMillis: Long): String = { - // TODO - "" - } -} diff --git a/airframe-log/src/main/scala/wvlet/log/LogFormat.scala b/airframe-log/src/main/scala/wvlet/log/LogFormat.scala index 067b3cb940..ff021a6e6c 100644 --- a/airframe-log/src/main/scala/wvlet/log/LogFormat.scala +++ b/airframe-log/src/main/scala/wvlet/log/LogFormat.scala @@ -105,11 +105,11 @@ object LogFormatter extends AnsiColorPalette { object TSVLogFormatter extends LogFormatter { override def formatLog(record: LogRecord): String = { val s = Seq.newBuilder[String] - s += formatTimestampWithNoSpaace(record.getMillis) + s += formatTimestampWithNoSpaace(record.getMillis()) s += record.level.toString s += currentThreadName s += record.leafLoggerName - s += record.getMessage + s += record.getMessage() val log = s.result().mkString("\t") record.cause match { @@ -128,7 +128,7 @@ object LogFormatter extends AnsiColorPalette { object SimpleLogFormatter extends LogFormatter { override def formatLog(r: LogRecord): String = { val log = - s"[${highlightLog(r.level, r.leafLoggerName)}] ${highlightLog(r.level, r.getMessage)}" + s"[${highlightLog(r.level, r.leafLoggerName)}] ${highlightLog(r.level, r.getMessage())}" appendStackTrace(log, r) } } @@ -140,7 +140,7 @@ object LogFormatter extends AnsiColorPalette { override def formatLog(r: LogRecord): String = { val logTag = highlightLog(r.level, r.level.name) val log = - f"${withColor(Console.BLUE, formatTimestamp(r.getMillis))} ${logTag}%14s [${withColor(Console.WHITE, r.leafLoggerName)}] ${highlightLog(r.level, r.getMessage)}" + f"${withColor(Console.BLUE, formatTimestamp(r.getMillis()))} ${logTag}%14s [${withColor(Console.WHITE, r.leafLoggerName)}] ${highlightLog(r.level, r.getMessage())}" appendStackTrace(log, r) } } @@ -157,10 +157,10 @@ object LogFormatter extends AnsiColorPalette { val logTag = highlightLog(r.level, r.level.name) val log = - f"${withColor(Console.BLUE, formatTimestamp(r.getMillis))} ${logTag}%14s [${withColor( + f"${withColor(Console.BLUE, formatTimestamp(r.getMillis()))} ${logTag}%14s [${withColor( Console.WHITE, r.leafLoggerName - )}] ${highlightLog(r.level, r.getMessage)} ${loc}" + )}] ${highlightLog(r.level, r.getMessage())} ${loc}" appendStackTrace(log, r) } } @@ -174,10 +174,10 @@ object LogFormatter extends AnsiColorPalette { val logTag = highlightLog(r.level, r.level.name) val log = - f"${withColor(Console.BLUE, formatTimestamp(r.getMillis))} [${withColor(BRIGHT_BLUE, currentThreadName)}] ${logTag}%14s [${withColor( + f"${withColor(Console.BLUE, formatTimestamp(r.getMillis()))} [${withColor(BRIGHT_BLUE, currentThreadName)}] ${logTag}%14s [${withColor( Console.WHITE, r.leafLoggerName - )}] ${highlightLog(r.level, r.getMessage)} ${loc}" + )}] ${highlightLog(r.level, r.getMessage())} ${loc}" appendStackTrace(log, r) } } @@ -193,7 +193,7 @@ object LogFormatter extends AnsiColorPalette { .getOrElse("") val log = - f"${formatTimestamp(r.getMillis)} ${r.level.name}%5s [${r.leafLoggerName}] ${r.getMessage} ${loc}" + f"${formatTimestamp(r.getMillis())} ${r.level.name}%5s [${r.leafLoggerName}] ${r.getMessage()} ${loc}" appendStackTrace(log, r, coloring = false) } } @@ -205,11 +205,11 @@ object LogFormatter extends AnsiColorPalette { override def formatLog(r: LogRecord): String = { val loc = r.source - .map(source => s" ${withColor(Console.BLUE, s"- ${r.getLoggerName}(${source.fileLoc})")}") + .map(source => s" ${withColor(Console.BLUE, s"- ${r.getLoggerName()}(${source.fileLoc})")}") .getOrElse("") val log = - s"[${highlightLog(r.level, r.level.name)}] ${highlightLog(r.level, r.getMessage)}$loc" + s"[${highlightLog(r.level, r.level.name)}] ${highlightLog(r.level, r.getMessage())}$loc" appendStackTrace(log, r) } } @@ -219,7 +219,7 @@ object LogFormatter extends AnsiColorPalette { */ object BareFormatter extends LogFormatter { override def formatLog(r: LogRecord): String = { - val m = r.getMessage + val m = r.getMessage() r.cause match { case Some(ex) => s"${m}\n${formatStacktrace(ex)}" diff --git a/airframe-log/src/main/scala/wvlet/log/LogRecord.scala b/airframe-log/src/main/scala/wvlet/log/LogRecord.scala index b54b5c983e..181970ae9f 100644 --- a/airframe-log/src/main/scala/wvlet/log/LogRecord.scala +++ b/airframe-log/src/main/scala/wvlet/log/LogRecord.scala @@ -17,8 +17,8 @@ import java.util.{logging => jl} object LogRecord { def apply(record: jl.LogRecord): LogRecord = { - val l = LogRecord(LogLevel(record.getLevel), None, record.getMessage, Option(record.getThrown)) - l.setLoggerName(record.getLoggerName) + val l = LogRecord(LogLevel(record.getLevel()), None, record.getMessage(), Option(record.getThrown())) + l.setLoggerName(record.getLoggerName()) l } @@ -40,7 +40,7 @@ case class LogRecord(level: LogLevel, source: Option[LogSource], message: String cause.foreach(setThrown(_)) def leafLoggerName: String = { - val name = getLoggerName + val name = getLoggerName() leafLoggerNameCache.getOrElseUpdate( name, { name match { diff --git a/airframe-log/src/main/scala/wvlet/log/Logger.scala b/airframe-log/src/main/scala/wvlet/log/Logger.scala index 7ce4141045..93f5d77dab 100644 --- a/airframe-log/src/main/scala/wvlet/log/Logger.scala +++ b/airframe-log/src/main/scala/wvlet/log/Logger.scala @@ -51,11 +51,11 @@ class Logger( if (l == null) { LogLevel.INFO } else { - val jlLevel = l.getLevel + val jlLevel = l.getLevel() if (jlLevel != null) { LogLevel(jlLevel) } else { - getLogLevelOf(l.getParent) + getLogLevelOf(l.getParent()) } } } @@ -77,7 +77,7 @@ class Logger( } def getParent: Option[Logger] = { - Option(wrapped.getParent).map(x => Logger(x.getName)) + Option(wrapped.getParent()).map(x => Logger(x.getName())) } def addHandler(h: jl.Handler): Unit = { diff --git a/airspec/.js/src/main/scala/wvlet/airspec/Compat.scala b/airspec/.js/src/main/scala/wvlet/airspec/Compat.scala index 92eabf455f..3ecc6c7956 100644 --- a/airspec/.js/src/main/scala/wvlet/airspec/Compat.scala +++ b/airspec/.js/src/main/scala/wvlet/airspec/Compat.scala @@ -27,7 +27,9 @@ import scala.util.Try /** */ private[airspec] object Compat extends CompatApi with LogSupport { - override def isScalaJs = true + override def isScalaJVM = false + override def isScalaJs = true + override def isScalaNative = false override private[airspec] val executionContext: ExecutionContext = org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.global diff --git a/airspec/.jvm/src/main/scala/wvlet/airspec/Compat.scala b/airspec/.jvm/src/main/scala/wvlet/airspec/Compat.scala index ce72cd176b..f17573a2fd 100644 --- a/airspec/.jvm/src/main/scala/wvlet/airspec/Compat.scala +++ b/airspec/.jvm/src/main/scala/wvlet/airspec/Compat.scala @@ -31,7 +31,9 @@ import scala.concurrent.ExecutionContext /** */ private[airspec] object Compat extends CompatApi with LogSupport { - override def isScalaJs = false + override def isScalaJVM = true + override def isScalaJs = false + override def isScalaNative = false override private[airspec] val executionContext: ExecutionContext = ExecutionContext.fromExecutorService(Executors.newCachedThreadPool(newDaemonThreadFactory("airspec-executor"))) diff --git a/airspec/.native/src/main/scala-3/wvlet/airspec/Compat.scala b/airspec/.native/src/main/scala-3/wvlet/airspec/Compat.scala index e3793237aa..7e0265a745 100644 --- a/airspec/.native/src/main/scala-3/wvlet/airspec/Compat.scala +++ b/airspec/.native/src/main/scala-3/wvlet/airspec/Compat.scala @@ -31,7 +31,9 @@ import scala.util.{Success, Failure} /** */ private[airspec] object Compat extends CompatApi with LogSupport: - override def isScalaJs = false + override def isScalaJVM = false + override def isScalaJs = false + override def isScalaNative = true override private[airspec] val executionContext: ExecutionContext = ExecutionContext.global @@ -42,7 +44,6 @@ private[airspec] object Compat extends CompatApi with LogSupport: } private[airspec] def getFingerprint(fullyQualifiedName: String, classLoader: ClassLoader): Option[Fingerprint] = - println(s"Checking class fingerprint for ${fullyQualifiedName}") Try(findCompanionObjectOf(fullyQualifiedName, classLoader)).toOption .flatMap { case Some(spec: AirSpecSpi) => @@ -51,17 +52,17 @@ private[airspec] object Compat extends CompatApi with LogSupport: None } .orElse { - Try(classLoader.loadClass(fullyQualifiedName)).toOption + scala.scalanative.reflect.Reflect + .lookupInstantiatableClass(fullyQualifiedName) .flatMap { x => - if classOf[AirSpec].isAssignableFrom(x) then Some(AirSpecClassFingerPrint) + if classOf[AirSpec].isAssignableFrom(x.runtimeClass) then Some(AirSpecClassFingerPrint) else None } } - private[airspec] def newInstanceOf(fullyQualifiedName: String, classLoader: ClassLoader): Option[Any] = { + private[airspec] def newInstanceOf(fullyQualifiedName: String, classLoader: ClassLoader): Option[Any] = val clsOpt = scala.scalanative.reflect.Reflect.lookupInstantiatableClass(fullyQualifiedName) clsOpt.map(_.newInstance()) - } private[airspec] def withLogScanner[U](block: => U): U = try diff --git a/airspec/build.sbt b/airspec/build.sbt index 93bee80c93..633263d2ad 100644 --- a/airspec/build.sbt +++ b/airspec/build.sbt @@ -1,13 +1,13 @@ // A short cut for publishing snapshots to Sonatype addCommandAlias( "publishSnapshots", - s"+airspecJVM/publish; +airspecJS/publish" + s"+airspecJVM/publish; +airspecJS/publish; +airspecNative/publish" ) // [Development purpose] publish all artifacts to the local repo addCommandAlias( "publishAllLocal", - s"+airspecJVM/publishLocal; +airspecJS/publishLocal;" + s"+airspecJVM/publishLocal; +airspecJS/publishLocal; +airspecNative/publishLocal" ) // Reload build.sbt on changes @@ -335,6 +335,7 @@ lazy val airspec = pomPostProcess := excludePomDependency(Seq("airspec-deps", "airspec_2.12", "airspec_2.13")) ) .jvmSettings( + airspecJVMBuildSettings, // Embed dependent project codes to make airspec a single jar Compile / packageBin / mappings ++= (airspecDeps.jvm / Compile / packageBin / mappings).value, Compile / packageSrc / mappings ++= (airspecDeps.jvm / Compile / packageSrc / mappings).value, @@ -353,6 +354,7 @@ lazy val airspec = } ) .jsSettings( + airspecJSBuildSettings, Compile / packageBin / mappings ++= (airspecDeps.js / Compile / packageBin / mappings).value .filter(x => x._2 != "JS_DEPENDENCIES"), Compile / packageSrc / mappings ++= (airspecDeps.js / Compile / packageSrc / mappings).value, @@ -364,12 +366,12 @@ lazy val airspec = ) ) .nativeSettings( + airspecNativeBuildSettings, // Embed dependent project codes to make airspec a single jar Compile / packageBin / mappings ++= (airspecDeps.native / Compile / packageBin / mappings).value, Compile / packageSrc / mappings ++= (airspecDeps.native / Compile / packageSrc / mappings).value, libraryDependencies ++= Seq( - "org.scala-sbt" % "test-interface" % "1.0", - //("org.portable-scala" %%% "portable-scala-reflect" % "1.1.2").cross(CrossVersion.for3Use2_13) + "org.scala-native" %%% "test-interface" % "0.5.1" ) ) // This should be Optional dependency, but using Provided dependency for bloop which doesn't support Optional. diff --git a/airspec/src/main/scala/wvlet/airspec/AirSpec.scala b/airspec/src/main/scala/wvlet/airspec/AirSpec.scala index f619dba2c8..052e9fc5da 100644 --- a/airspec/src/main/scala/wvlet/airspec/AirSpec.scala +++ b/airspec/src/main/scala/wvlet/airspec/AirSpec.scala @@ -119,7 +119,9 @@ private[airspec] trait AirSpecSpi extends AirSpecSpiCompat { protected def inCircleCI: Boolean = airspec.inCircleCI protected def inGitHubAction: Boolean = airspec.inGitHubAction - protected def isScalaJS: Boolean = compat.isScalaJs + protected def isScalaJVM: Boolean = compat.isScalaJVM + protected def isScalaJS: Boolean = compat.isScalaJs + protected def isScalaNative: Boolean = compat.isScalaNative protected def isScala2: Boolean = scalaMajorVersion == 2 protected def isScala3: Boolean = scalaMajorVersion == 3 diff --git a/airspec/src/main/scala/wvlet/airspec/CompatApi.scala b/airspec/src/main/scala/wvlet/airspec/CompatApi.scala index 668642ff8f..151c61d15e 100644 --- a/airspec/src/main/scala/wvlet/airspec/CompatApi.scala +++ b/airspec/src/main/scala/wvlet/airspec/CompatApi.scala @@ -22,7 +22,9 @@ import scala.concurrent.ExecutionContext * An interface for compatibility between Scala JVM and Scala.js */ trait CompatApi { + def isScalaJVM: Boolean def isScalaJs: Boolean + def isScalaNative: Boolean private[airspec] def executionContext: ExecutionContext private[airspec] def findCompanionObjectOf(fullyQualifiedName: String, classLoader: ClassLoader): Option[Any] diff --git a/airspec/src/main/scala/wvlet/airspec/runner/AirSpecLogger.scala b/airspec/src/main/scala/wvlet/airspec/runner/AirSpecLogger.scala index c1770cc205..8f2a3bf460 100644 --- a/airspec/src/main/scala/wvlet/airspec/runner/AirSpecLogger.scala +++ b/airspec/src/main/scala/wvlet/airspec/runner/AirSpecLogger.scala @@ -26,10 +26,13 @@ private[airspec] case class AirSpecEvent( taskDef: TaskDef, // If None, it's a spec testName: Option[String], - override val status: Status, - override val throwable: OptionalThrowable, + _status: Status, + _throwable: OptionalThrowable, durationNanos: Long ) extends Event { + override def status(): Status = _status + override def throwable(): OptionalThrowable = _throwable + override def fullyQualifiedName(): String = { testName.getOrElse(taskDef.fullyQualifiedName()) } @@ -94,7 +97,7 @@ private[airspec] class AirSpecLogger() extends AnsiColorPalette { } def logEvent(e: AirSpecEvent, indentLevel: Int = 0, showTestName: Boolean = true): Unit = { - val (baseColor, showStackTraces) = e.status match { + val (baseColor, showStackTraces) = e.status() match { case Status.Success => (GREEN, false) case Status.Failure => (RED, false) // Do not show the stack trace for assertion failures case Status.Error => (RED, true) @@ -122,10 +125,10 @@ private[airspec] class AirSpecLogger() extends AnsiColorPalette { s"${withColor(GRAY, " <")} ${elapsedTime}" } } - val tail = e.status match { + val tail = e.status() match { case Status.Success => "" - case _ if e.throwable.isDefined() => - val ex = e.throwable.get() + case _ if e.throwable().isDefined() => + val ex = e.throwable().get() ex match { case se: AirSpecFailureBase => s" ${statusLabel(se.statusLabel)}: ${withColor(baseColor, se.message)} ${errorLocation(se)}" @@ -138,7 +141,7 @@ private[airspec] class AirSpecLogger() extends AnsiColorPalette { info(s"${indent(indentLevel)}${prefix}${tail}") if (showStackTraces) { - val ex = wvlet.airspec.compat.findCause(e.throwable.get()) + val ex = wvlet.airspec.compat.findCause(e.throwable().get()) val stackTrace = LogFormatter.formatStacktrace(ex) error(stackTrace) } diff --git a/airspec/src/main/scala/wvlet/airspec/runner/AirSpecSbtRunner.scala b/airspec/src/main/scala/wvlet/airspec/runner/AirSpecSbtRunner.scala index a1a0b4ecb6..88fe347baf 100644 --- a/airspec/src/main/scala/wvlet/airspec/runner/AirSpecSbtRunner.scala +++ b/airspec/src/main/scala/wvlet/airspec/runner/AirSpecSbtRunner.scala @@ -20,13 +20,15 @@ import wvlet.log.{LogSupport, Logger, LogLevel} import scala.util.matching.Regex /** - * AirSpecRunner receives a list of TaskDefs from sbt, then create AirSpecTasks to execute. + * d AirSpecRunner receives a list of TaskDefs from sbt, then create AirSpecTasks to execute. */ -private[airspec] class AirSpecSbtRunner(config: AirSpecConfig, val remoteArgs: Array[String], classLoader: ClassLoader) +private[airspec] class AirSpecSbtRunner(config: AirSpecConfig, _remoteArgs: Array[String], classLoader: ClassLoader) extends sbt.testing.Runner { + private lazy val taskLogger = new AirSpecLogger() - override def args: Array[String] = config.args + override def args: Array[String] = config.args + override def remoteArgs(): Array[String] = _remoteArgs override def tasks(taskDefs: Array[TaskDef]): Array[Task] = { taskDefs diff --git a/airspec/src/main/scala/wvlet/airspec/runner/AirSpecTask.scala b/airspec/src/main/scala/wvlet/airspec/runner/AirSpecTask.scala index f3e59303b0..a5c2bbd9d2 100644 --- a/airspec/src/main/scala/wvlet/airspec/runner/AirSpecTask.scala +++ b/airspec/src/main/scala/wvlet/airspec/runner/AirSpecTask.scala @@ -27,12 +27,14 @@ import scala.concurrent.{Await, Promise} private[airspec] class AirSpecTask( config: AirSpecConfig, taskLogger: AirSpecLogger, - override val taskDef: TaskDef, + _taskDef: TaskDef, classLoader: ClassLoader ) extends sbt.testing.Task with LogSupport { override def tags(): Array[String] = Array.empty + override def taskDef(): TaskDef = _taskDef + /** * This method will be used only for Scala (JVM). This will delegate the task execution process to execute(handler, * logger, continuation) @@ -60,7 +62,7 @@ private[airspec] class AirSpecTask( ): Unit = { implicit val ec = wvlet.airspec.Compat.executionContext try { - new AirSpecTaskRunner(taskDef, config, taskLogger, eventHandler, classLoader).runTask + new AirSpecTaskRunner(taskDef(), config, taskLogger, eventHandler, classLoader).runTask .foreach(_ => continuation(Array.empty)) } catch { case e: Throwable => diff --git a/airspec/src/test/scala/examples/RxTest.scala b/airspec/src/test/scala/examples/RxTest.scala index 654cd7ea8f..2803df9652 100644 --- a/airspec/src/test/scala/examples/RxTest.scala +++ b/airspec/src/test/scala/examples/RxTest.scala @@ -22,10 +22,15 @@ class RxTest extends AirSpec { private val v = new AtomicBoolean(false) override def afterAll: Unit = { - v.get() shouldBe true + if (!isScalaNative) { + v.get() shouldBe true + } } test("return Rx") { + if (isScalaNative) { + skip("Skip Rx timer test for Scala Native") + } Rx.intervalMillis(10).map(_ => v.set(true)) }