Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Scala Native multithreading #861

Merged
merged 3 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ final class JUnitReporter(
.append(ex.getMessage())
.toString()
)
trace(ex)
emitEvent(method, Status.Failure, new OptionalThrowable(ex))
}

Expand Down Expand Up @@ -268,7 +267,7 @@ final class JUnitReporter(
.toString()
}
private def formatTime(elapsedMillis: Double): String =
AnsiColors.c("%.2fs".format(elapsedMillis / 1000.0), AnsiColors.DarkGrey)
AnsiColors.c("%.3fs".format(elapsedMillis / 1000.0), AnsiColors.DarkGrey)
private val Trace = 0
private val Debug = 1
private val Info = 2
Expand Down
75 changes: 70 additions & 5 deletions munit/native/src/main/scala/munit/internal/PlatformCompat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,40 @@ import sbt.testing.EventHandler
import sbt.testing.Logger
import scala.concurrent.Await
import scala.concurrent.Awaitable
import scala.concurrent.Promise
import scala.concurrent.duration.Duration
import scala.concurrent.ExecutionContext
import java.util.concurrent.{
Executors,
ThreadFactory,
TimeoutException,
TimeUnit
}
import java.util.concurrent.atomic.AtomicInteger
import scala.scalanative.meta.LinktimeInfo.isMultithreadingEnabled

// Delay reachability of multithreading capability
// Would not force multithreading support unless explicitly configured by the user
// or if using threads in the tests
private object LazyMultithreadingSupport {
val sh = Executors.newSingleThreadScheduledExecutor(
new ThreadFactory {
val counter = new AtomicInteger
def threadNumber() = counter.incrementAndGet()
def newThread(r: Runnable) =
new Thread(r, s"munit-scheduler-${threadNumber()}") {
setDaemon(true)
setPriority(Thread.NORM_PRIORITY)
}
}
)
}
object PlatformCompat {
import LazyMultithreadingSupport._

def awaitResult[T](awaitable: Awaitable[T]): T = {
scalanative.runtime.loop()
if (!isMultithreadingEnabled)
Thread.`yield`() // invokes SN 0.4 scalanative.runtime.loop()
Await.result(awaitable, Duration.Inf)
}

Expand All @@ -26,18 +54,55 @@ object PlatformCompat {
task.execute(eventHandler, loggers)
Future.successful(())
}

def waitAtMost[T](
startFuture: () => Future[T],
duration: Duration,
ec: ExecutionContext
): Future[T] = {
startFuture()
if (!isMultithreadingEnabled) startFuture()
else {
val onComplete = Promise[T]()
val timeout =
if (duration.isFinite)
Some(
sh.schedule[Unit](
() =>
onComplete.tryFailure(
new TimeoutException(s"test timed out after $duration")
),
duration.toMillis,
TimeUnit.MILLISECONDS
)
)
else
None
ec.execute(new Runnable {
def run(): Unit = {
startFuture().onComplete { result =>
onComplete.tryComplete(result)
timeout.foreach(_.cancel(false))
}(ec)
}
})
onComplete.future
}
}

def setTimeout(ms: Int)(body: => Unit): () => Unit = {
Thread.sleep(ms)
body
if (!isMultithreadingEnabled) {
// Thread.sleep(ms)
Copy link
Collaborator

@valencik valencik Jan 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WojciechMazur
Why was this added but commented out?
Now it looks like if multithreading is disabled we ignore timeout durations and run body immediately?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what's the story of original Thread.sleep call but it makes no sense to have it. The goal of this method was to introduce timeouts, instead it was only slowing the execution, while there was nothing else running in the background.

body
() => ()
} else {
val scheduled = sh.schedule[Unit](
() => body,
ms,
TimeUnit.MILLISECONDS
)

() => ()
() => scheduled.cancel(false)
}
}

// Scala Native does not support looking up annotations at runtime.
Expand Down
2 changes: 1 addition & 1 deletion tests/shared/src/main/scala/munit/BaseFrameworkSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ abstract class BaseFrameworkSuite extends BaseSuite {
}
}
implicit val ec = munitExecutionContext
val elapsedTimePattern = Pattern.compile(" \\d+\\.\\d+s ?")
val elapsedTimePattern = Pattern.compile(" ? \\d+\\.\\d+s ?")
TestingConsole.out = out
TestingConsole.err = out
for {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,11 @@ object Issue285FrameworkSuite
| + issue-285-ok <elapsed time>
|beforeEach - issue-285-fail
|afterEach - issue-285-fail
|==> X munit.Issue285FrameworkSuite.issue-285-fail <elapsed time>java.util.concurrent.TimeoutException: test timed out after 5 milliseconds
|==> X munit.Issue285FrameworkSuite.issue-285-fail <elapsed time>java.util.concurrent.TimeoutException: test timed out after 5 milliseconds
|beforeEach - issue-285-ok
|afterEach - issue-285-ok
| + issue-285-ok-1 <elapsed time>
|afterAll
|""".stripMargin,
tags = Set(
// Skipped on JS/Native because we don't support
// `PlatformCompat.setTimeout` on Native and the test has stack traces
// on JS which fails the assertion (even if the behavior works as
// expected)
OnlyJVM
),
format = StdoutFormat
)
8 changes: 4 additions & 4 deletions tests/shared/src/main/scala/munit/SkippedFrameworkSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,13 @@ object SkippedFrameworkStdoutJVMSuite
|==> i munit.SkippedFrameworkSuite.pending.empty.ignored.comment PENDING comment ignored <elapsed time>
|==> i munit.SkippedFrameworkSuite.pending.successful.ignored PENDING ignored <elapsed time>
|==> i munit.SkippedFrameworkSuite.pending.successful.ignored.comment PENDING comment ignored <elapsed time>
|==> X munit.SkippedFrameworkSuite.pending.failed.not-ignored <elapsed time>munit.FailException: tests/shared/src/main/scala/munit/SkippedFrameworkSuite.scala:38 assertion failed
|==> X munit.SkippedFrameworkSuite.pending.failed.not-ignored <elapsed time>munit.FailException: tests/shared/src/main/scala/munit/SkippedFrameworkSuite.scala:38 assertion failed
|37: test("pending.failed.not-ignored".pending) {
|38: assert(false)
|39: }
| at munit.FunSuite.assert(FunSuite.scala:11)
| at munit.SkippedFrameworkSuite.$anonfun$new$21(SkippedFrameworkSuite.scala:38)
|==> X munit.SkippedFrameworkSuite.pending.failed.not-ignored.comment <elapsed time>munit.FailException: tests/shared/src/main/scala/munit/SkippedFrameworkSuite.scala:41 assertion failed
|==> X munit.SkippedFrameworkSuite.pending.failed.not-ignored.comment <elapsed time>munit.FailException: tests/shared/src/main/scala/munit/SkippedFrameworkSuite.scala:41 assertion failed
|40: test("pending.failed.not-ignored.comment".pending("comment")) {
|41: assert(false)
|42: }
Expand Down Expand Up @@ -199,14 +199,14 @@ object SkippedFrameworkStdoutJVMVerboseSuite
|munit.SkippedFrameworkSuite.pending.successful.ignored.comment started
|==> i munit.SkippedFrameworkSuite.pending.successful.ignored.comment PENDING comment ignored <elapsed time>
|munit.SkippedFrameworkSuite.pending.failed.not-ignored started
|==> X munit.SkippedFrameworkSuite.pending.failed.not-ignored <elapsed time>munit.FailException: tests/shared/src/main/scala/munit/SkippedFrameworkSuite.scala:38 assertion failed
|==> X munit.SkippedFrameworkSuite.pending.failed.not-ignored <elapsed time>munit.FailException: tests/shared/src/main/scala/munit/SkippedFrameworkSuite.scala:38 assertion failed
|37: test("pending.failed.not-ignored".pending) {
|38: assert(false)
|39: }
| at munit.FunSuite.assert(FunSuite.scala:11)
| at munit.SkippedFrameworkSuite.$anonfun$new$21(SkippedFrameworkSuite.scala:38)
|munit.SkippedFrameworkSuite.pending.failed.not-ignored.comment started
|==> X munit.SkippedFrameworkSuite.pending.failed.not-ignored.comment <elapsed time>munit.FailException: tests/shared/src/main/scala/munit/SkippedFrameworkSuite.scala:41 assertion failed
|==> X munit.SkippedFrameworkSuite.pending.failed.not-ignored.comment <elapsed time>munit.FailException: tests/shared/src/main/scala/munit/SkippedFrameworkSuite.scala:41 assertion failed
|40: test("pending.failed.not-ignored.comment".pending("comment")) {
|41: assert(false)
|42: }
Expand Down
Loading