From 588cbc358bd089ef85fffdaf634acd21f5edbb40 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 28 Jul 2025 09:03:35 +0800 Subject: [PATCH 1/2] . --- readme.md | 5 +++-- utest/src-js/utest/PlatformShims.scala | 3 ++- utest/src-jvm/utest/PlatformShims.scala | 3 ++- utest/src-native/utest/PlatformShims.scala | 3 ++- utest/src/utest/runner/Fingerprint.scala | 6 ++++++ utest/src/utest/runner/Framework.scala | 2 +- .../src/test/utest/AfterEachOnFailureTest.scala | 2 +- utest/test/src/test/utest/AssertsTests.scala | 2 +- .../src/test/utest/BeforeAfterAllFailureTest.scala | 6 +++--- .../test/utest/BeforeAfterEachFailureTests.scala | 2 +- utest/test/src/test/utest/ByNameTests.scala | 2 +- utest/test/src/test/utest/DisablePrint2Tests.scala | 2 +- utest/test/src/test/utest/DisablePrintTests.scala | 2 +- .../test/src/test/utest/FrameworkAsyncTests.scala | 2 +- utest/test/src/test/utest/FrameworkTests.scala | 2 +- utest/test/src/test/utest/FutureCrashTest.scala | 2 +- utest/test/src/test/utest/FutureTest.scala | 2 +- utest/test/src/test/utest/MergeTestsTest.scala | 2 +- utest/test/src/test/utest/QueryTests.scala | 2 +- utest/test/src/test/utest/RetryTests.scala | 14 +++++++------- .../test/utest/examples/BeforeAfterAllTest.scala | 4 ++-- .../test/utest/examples/BeforeAfterEachTests.scala | 2 +- .../test/src/test/utest/examples/HelloTests.scala | 2 +- .../test/src/test/utest/examples/NestedTests.scala | 2 +- .../test/utest/examples/SeparateSetupTests.scala | 2 +- .../test/utest/examples/SharedFixturesTests.scala | 2 +- .../src/test/utest/examples/TestPathTests.scala | 2 +- 27 files changed, 46 insertions(+), 36 deletions(-) diff --git a/readme.md b/readme.md index 99ae7253..3f24e4bf 100644 --- a/readme.md +++ b/readme.md @@ -157,7 +157,7 @@ package test.utest.examples import utest._ -object HelloTests extends TestSuite{ +class HelloTests extends TestSuite{ val tests = Tests{ test("test1"){ throw new Exception("test1") @@ -201,6 +201,7 @@ Tests: 3, Passed: 1, Failed: 2 The tests are run one at a time, and any tests that fail with an exception have their stack trace printed. If the number of tests is large, a separate results-summary and failures-summary will be shown after all tests have run. +Tests can either be inside zero-parameter `class`es (as shown above) or static `object`s. Nesting Tests ------------- @@ -216,7 +217,7 @@ package test.utest.examples import utest._ -object NestedTests extends TestSuite{ +class NestedTests extends TestSuite{ val tests = Tests{ val x = 1 test("outer1"){ diff --git a/utest/src-js/utest/PlatformShims.scala b/utest/src-js/utest/PlatformShims.scala index dd556308..cdd55621 100644 --- a/utest/src-js/utest/PlatformShims.scala +++ b/utest/src-js/utest/PlatformShims.scala @@ -22,7 +22,8 @@ object PlatformShims { def loadModule(name: String, loader: ClassLoader): Any = { Reflect .lookupLoadableModuleClass(name + "$", loader) + .map(_.loadModule()) + .orElse(Reflect.lookupInstantiatableClass(name, loader).map(_.newInstance())) .getOrElse(throw new ClassNotFoundException(name)) - .loadModule() } } diff --git a/utest/src-jvm/utest/PlatformShims.scala b/utest/src-jvm/utest/PlatformShims.scala index 5b6d86e9..b21cbb6b 100644 --- a/utest/src-jvm/utest/PlatformShims.scala +++ b/utest/src-jvm/utest/PlatformShims.scala @@ -12,6 +12,7 @@ object PlatformShims extends PlatformShimsVersionSpecific { def loadModule(name: String, loader: ClassLoader): Any = Reflect .lookupLoadableModuleClass(name + "$", loader) + .map(_.loadModule()) + .orElse(Reflect.lookupInstantiatableClass(name, loader).map(_.newInstance())) .getOrElse(throw new ClassNotFoundException(name)) - .loadModule() } diff --git a/utest/src-native/utest/PlatformShims.scala b/utest/src-native/utest/PlatformShims.scala index 8f076f04..fcc8e5b6 100644 --- a/utest/src-native/utest/PlatformShims.scala +++ b/utest/src-native/utest/PlatformShims.scala @@ -18,7 +18,8 @@ object PlatformShims { def loadModule(name: String, loader: ClassLoader): Any = { Reflect .lookupLoadableModuleClass(name + "$") + .map(_.loadModule()) + .orElse(Reflect.lookupInstantiatableClass(name).map(_.newInstance())) .getOrElse(throw new ClassNotFoundException(name)) - .loadModule() } } diff --git a/utest/src/utest/runner/Fingerprint.scala b/utest/src/utest/runner/Fingerprint.scala index ba1ec178..3b0f21f6 100644 --- a/utest/src/utest/runner/Fingerprint.scala +++ b/utest/src/utest/runner/Fingerprint.scala @@ -8,3 +8,9 @@ object Fingerprint extends SubclassFingerprint { def isModule() = true def requireNoArgConstructor() = true } + +object ClassFingerprint extends SubclassFingerprint { + def superclassName() = "utest.TestSuite" + def isModule() = false + def requireNoArgConstructor() = true +} diff --git a/utest/src/utest/runner/Framework.scala b/utest/src/utest/runner/Framework.scala index 10211688..355e5122 100644 --- a/utest/src/utest/runner/Framework.scala +++ b/utest/src/utest/runner/Framework.scala @@ -56,7 +56,7 @@ class Framework extends sbt.testing.Framework with framework.Formatter { def startHeader(path: String) = DefaultFormatters.renderBanner("Running Tests" + path) - final def fingerprints(): Array[sbt.testing.Fingerprint] = Array(Fingerprint) + final def fingerprints(): Array[sbt.testing.Fingerprint] = Array(Fingerprint, ClassFingerprint) final def runner(args: Array[String], remoteArgs: Array[String], diff --git a/utest/test/src/test/utest/AfterEachOnFailureTest.scala b/utest/test/src/test/utest/AfterEachOnFailureTest.scala index 9dde5ae6..6caa6421 100644 --- a/utest/test/src/test/utest/AfterEachOnFailureTest.scala +++ b/utest/test/src/test/utest/AfterEachOnFailureTest.scala @@ -6,7 +6,7 @@ import utest.framework.ExecutionContext.RunNow /** * Put executor.utestAfterEach(path) into finally block to make sure it will be executed regardless of the test failing. */ -object AfterEachOnFailureTest extends TestSuite { +class AfterEachOnFailureTest extends TestSuite{ private var res:SomeResource = _ diff --git a/utest/test/src/test/utest/AssertsTests.scala b/utest/test/src/test/utest/AssertsTests.scala index a5b3a868..efeb07d7 100644 --- a/utest/test/src/test/utest/AssertsTests.scala +++ b/utest/test/src/test/utest/AssertsTests.scala @@ -8,7 +8,7 @@ import utest._ * since it is the thing that is meant to be *testing* all the fancy uTest * asserts, we can't assume they work. */ -object AssertsTests extends utest.TestSuite{ +class AssertsTests extends utest.TestSuite{ implicit val colors: shaded.pprint.TPrintColors = shaded.pprint.TPrintColors.Colors def tests = Tests{ diff --git a/utest/test/src/test/utest/BeforeAfterAllFailureTest.scala b/utest/test/src/test/utest/BeforeAfterAllFailureTest.scala index 0a39f77d..362be0b6 100644 --- a/utest/test/src/test/utest/BeforeAfterAllFailureTest.scala +++ b/utest/test/src/test/utest/BeforeAfterAllFailureTest.scala @@ -4,11 +4,11 @@ import utest._ import utest.framework.StackMarker -object BeforeAfterAllFailureTest extends TestSuite { +class BeforeAfterAllFailureTest extends TestSuite{ // Hide this fella inside the outer object, because we don't want uTest's own // test suite to discover him: we want to run him manually - object AfterAllFailureTest extends TestSuite { + object AfterAllFailureTest extends TestSuite{ override def utestAfterAll(): Unit = { throw new Exception("Failed After!") @@ -26,7 +26,7 @@ object BeforeAfterAllFailureTest extends TestSuite { // No tests for this fella because currently, error handling of test suite // initialization is paid done in the SBT logic, not in TestRunner - object BeforeAllFailureTest extends TestSuite { + class BeforeAllFailureTest extends TestSuite{ throw new Exception("Failed Before!") val tests = Tests { test("test"){ diff --git a/utest/test/src/test/utest/BeforeAfterEachFailureTests.scala b/utest/test/src/test/utest/BeforeAfterEachFailureTests.scala index f6c0dc2b..243682d7 100644 --- a/utest/test/src/test/utest/BeforeAfterEachFailureTests.scala +++ b/utest/test/src/test/utest/BeforeAfterEachFailureTests.scala @@ -4,7 +4,7 @@ import utest._ import scala.concurrent.{Future, ExecutionContext} import concurrent.duration._ -object BeforeAfterEachFailureTests extends TestSuite { +class BeforeAfterEachFailureTests extends TestSuite{ implicit val ec: ExecutionContext = utest.framework.ExecutionContext.RunNow private var failNextBeforeEach = false private var failAfterEach = false diff --git a/utest/test/src/test/utest/ByNameTests.scala b/utest/test/src/test/utest/ByNameTests.scala index fb945a47..ad9800b8 100644 --- a/utest/test/src/test/utest/ByNameTests.scala +++ b/utest/test/src/test/utest/ByNameTests.scala @@ -1,6 +1,6 @@ package test.utest import utest._ -object ByNameTests extends utest.TestSuite { +class ByNameTests extends utest.TestSuite { case class X(dummy: Int = 0, x: Int = 0) def doAction(action: => Any): Unit = () val tests = Tests{ diff --git a/utest/test/src/test/utest/DisablePrint2Tests.scala b/utest/test/src/test/utest/DisablePrint2Tests.scala index e4c7824c..c82d3a37 100644 --- a/utest/test/src/test/utest/DisablePrint2Tests.scala +++ b/utest/test/src/test/utest/DisablePrint2Tests.scala @@ -2,7 +2,7 @@ package test.utest import utest._ import utest.framework.Formatter -object DisablePrint2Tests extends utest.TestSuite{ +class DisablePrint2Tests extends utest.TestSuite{ override def utestFormatter = new Formatter { override def formatSingle(path: Seq[String], res: utest.framework.Result) = None override def formatColor = false diff --git a/utest/test/src/test/utest/DisablePrintTests.scala b/utest/test/src/test/utest/DisablePrintTests.scala index 4d8a2642..a3fe3ba6 100644 --- a/utest/test/src/test/utest/DisablePrintTests.scala +++ b/utest/test/src/test/utest/DisablePrintTests.scala @@ -2,7 +2,7 @@ package test.utest import utest._ import utest.framework.{Formatter, HTree, Result, Tree} -object DisablePrintTests extends utest.TestSuite{ +class DisablePrintTests extends utest.TestSuite{ override def utestFormatter = new Formatter { override def formatSummary(topLevelName: String, results: HTree[String, Result]) = None override def formatColor = false diff --git a/utest/test/src/test/utest/FrameworkAsyncTests.scala b/utest/test/src/test/utest/FrameworkAsyncTests.scala index a7218310..f7a56ee7 100644 --- a/utest/test/src/test/utest/FrameworkAsyncTests.scala +++ b/utest/test/src/test/utest/FrameworkAsyncTests.scala @@ -4,7 +4,7 @@ import utest._ import scala.concurrent.{Future, ExecutionContext} import concurrent.duration._ -object FrameworkAsyncTests extends TestSuite { +class FrameworkAsyncTests extends TestSuite{ implicit val ec: ExecutionContext = utest.framework.ExecutionContext.RunNow private val isNative = sys.props("java.vm.name") == "Scala Native" diff --git a/utest/test/src/test/utest/FrameworkTests.scala b/utest/test/src/test/utest/FrameworkTests.scala index d3059476..2b2e65cd 100644 --- a/utest/test/src/test/utest/FrameworkTests.scala +++ b/utest/test/src/test/utest/FrameworkTests.scala @@ -10,7 +10,7 @@ import scala.util.Failure import utest.framework.ExecutionContext.RunNow -object FrameworkTests extends utest.TestSuite{ +class FrameworkTests extends utest.TestSuite{ override def utestBeforeEach(path: Seq[String]): Unit = println("RUN " + path.mkString(".")) override def utestAfterEach(path: Seq[String]): Unit = println("END " + path.mkString(".")) diff --git a/utest/test/src/test/utest/FutureCrashTest.scala b/utest/test/src/test/utest/FutureCrashTest.scala index 66d7c871..e22bcab0 100644 --- a/utest/test/src/test/utest/FutureCrashTest.scala +++ b/utest/test/src/test/utest/FutureCrashTest.scala @@ -2,7 +2,7 @@ package test.utest import utest._ import concurrent.{Future, ExecutionContext} -object FutureCrashTest extends TestSuite { +class FutureCrashTest extends TestSuite{ def wrapping[T](f: => T):T = { f } diff --git a/utest/test/src/test/utest/FutureTest.scala b/utest/test/src/test/utest/FutureTest.scala index a4c529bf..ec39c6c8 100644 --- a/utest/test/src/test/utest/FutureTest.scala +++ b/utest/test/src/test/utest/FutureTest.scala @@ -2,7 +2,7 @@ package test.utest import utest._ import concurrent.{Future, ExecutionContext} -object FutureTest extends TestSuite { +class FutureTest extends TestSuite{ implicit val ec: ExecutionContext = ExecutionContext.global @volatile var flag = false diff --git a/utest/test/src/test/utest/MergeTestsTest.scala b/utest/test/src/test/utest/MergeTestsTest.scala index ddeeb1aa..0469d2f9 100644 --- a/utest/test/src/test/utest/MergeTestsTest.scala +++ b/utest/test/src/test/utest/MergeTestsTest.scala @@ -20,7 +20,7 @@ abstract class MergeSubTests2 extends TestSuite { } } -object MergeTestsTest extends TestSuite { +class MergeTestsTest extends TestSuite{ val x = new MergeSubTests1 {} val y = new MergeSubTests2 {} diff --git a/utest/test/src/test/utest/QueryTests.scala b/utest/test/src/test/utest/QueryTests.scala index 7c7a86b7..bcf90904 100644 --- a/utest/test/src/test/utest/QueryTests.scala +++ b/utest/test/src/test/utest/QueryTests.scala @@ -6,7 +6,7 @@ import utest.TestQueryParser.parse import utest._ -object QueryTests extends utest.TestSuite{ +class QueryTests extends utest.TestSuite{ def check(a: Either[String, TestQueryParser#Trees], b: Either[String, TestQueryParser#Trees]) = { Predef.assert(a == b, a) diff --git a/utest/test/src/test/utest/RetryTests.scala b/utest/test/src/test/utest/RetryTests.scala index 71b567da..aa74fbec 100644 --- a/utest/test/src/test/utest/RetryTests.scala +++ b/utest/test/src/test/utest/RetryTests.scala @@ -9,7 +9,7 @@ class FlakyThing{ if (runs < 2) throw new Exception("Flaky!") } } -object SuiteRetryTests extends TestSuite with TestSuite.Retries{ +class SuiteRetryTests extends TestSuite with TestSuite.Retries{ override val utestRetryCount = 3 val flaky = new FlakyThing def tests = Tests{ @@ -19,7 +19,7 @@ object SuiteRetryTests extends TestSuite with TestSuite.Retries{ } } -object SuiteManualRetryTests extends utest.TestSuite{ +class SuiteManualRetryTests extends utest.TestSuite{ override def utestWrap(path: Seq[String], body: => Future[Any])(implicit ec: ExecutionContext): Future[Any] = { def rec(count: Int): Future[Any] = { utestBeforeEach(path) @@ -41,7 +41,7 @@ object SuiteManualRetryTests extends utest.TestSuite{ } } -object SuiteRetryBeforeEachTests extends TestSuite with TestSuite.Retries { +class SuiteRetryBeforeEachTests extends TestSuite with TestSuite.Retries { private var x = 0 override val utestRetryCount = 3 override def utestBeforeEach(path: Seq[String]): Unit = { @@ -57,7 +57,7 @@ object SuiteRetryBeforeEachTests extends TestSuite with TestSuite.Retries { } } -object SuiteRetryBeforeAllTests extends TestSuite with TestSuite.Retries { +class SuiteRetryBeforeAllTests extends TestSuite with TestSuite.Retries { override val utestRetryCount = 3 var x = 100 val flaky = new FlakyThing @@ -82,7 +82,7 @@ object SuiteRetryBeforeAllTests extends TestSuite with TestSuite.Retries { } } -object LocalRetryTests extends utest.TestSuite{ +class LocalRetryTests extends utest.TestSuite{ val flaky = new FlakyThing def tests = Tests{ test("hello") - retry(3){ @@ -91,7 +91,7 @@ object LocalRetryTests extends utest.TestSuite{ } } -object SuiteRetryBeforeEachFailedTests extends TestSuite with TestSuite.Retries { +class SuiteRetryBeforeEachFailedTests extends TestSuite with TestSuite.Retries { override val utestRetryCount = 3 override def utestBeforeEach(path: Seq[String]): Unit = { flaky.run() @@ -104,7 +104,7 @@ object SuiteRetryBeforeEachFailedTests extends TestSuite with TestSuite.Retries } } -object SuiteRetryAfterEachFailedTests extends TestSuite with TestSuite.Retries { +class SuiteRetryAfterEachFailedTests extends TestSuite with TestSuite.Retries { val flaky = new FlakyThing override val utestRetryCount = 1 override def utestAfterEach(path: Seq[String]): Unit = { diff --git a/utest/test/src/test/utest/examples/BeforeAfterAllTest.scala b/utest/test/src/test/utest/examples/BeforeAfterAllTest.scala index f12220c5..ecfffdcc 100644 --- a/utest/test/src/test/utest/examples/BeforeAfterAllTest.scala +++ b/utest/test/src/test/utest/examples/BeforeAfterAllTest.scala @@ -3,7 +3,7 @@ package test.utest.examples import utest._ import scala.concurrent.Future -object BeforeAfterAllSimpleTests extends TestSuite { +class BeforeAfterAllSimpleTests extends TestSuite{ println("on object body, aka: before all") override def utestAfterAll(): Unit = { @@ -22,7 +22,7 @@ object BeforeAfterAllSimpleTests extends TestSuite { } } -object BeforeAfterAllTests extends TestSuite { +class BeforeAfterAllTests extends TestSuite{ var x = 100 println(s"starting with x: $x") diff --git a/utest/test/src/test/utest/examples/BeforeAfterEachTests.scala b/utest/test/src/test/utest/examples/BeforeAfterEachTests.scala index 2b486d53..8fdaf909 100644 --- a/utest/test/src/test/utest/examples/BeforeAfterEachTests.scala +++ b/utest/test/src/test/utest/examples/BeforeAfterEachTests.scala @@ -2,7 +2,7 @@ package test.utest.examples import utest._ -object BeforeAfterEachTests extends TestSuite { +class BeforeAfterEachTests extends TestSuite{ var x = 0 override def utestBeforeEach(path: Seq[String]): Unit = { println(s"on before each [${path.mkString("=>")}] x: $x") diff --git a/utest/test/src/test/utest/examples/HelloTests.scala b/utest/test/src/test/utest/examples/HelloTests.scala index 616322c3..4555d7ad 100644 --- a/utest/test/src/test/utest/examples/HelloTests.scala +++ b/utest/test/src/test/utest/examples/HelloTests.scala @@ -1,7 +1,7 @@ package test.utest.examples import utest._ -object HelloTests extends TestSuite{ +class HelloTests extends TestSuite{ val tests = Tests{ test("test1"){ // throw new Exception("test1") diff --git a/utest/test/src/test/utest/examples/NestedTests.scala b/utest/test/src/test/utest/examples/NestedTests.scala index ed61bc7f..bec361c6 100644 --- a/utest/test/src/test/utest/examples/NestedTests.scala +++ b/utest/test/src/test/utest/examples/NestedTests.scala @@ -3,7 +3,7 @@ package test.utest.examples import utest._ -object NestedTests extends TestSuite{ +class NestedTests extends TestSuite{ val tests = Tests{ val x = 1 test("outer1"){ diff --git a/utest/test/src/test/utest/examples/SeparateSetupTests.scala b/utest/test/src/test/utest/examples/SeparateSetupTests.scala index e2ee8004..bb44aaac 100644 --- a/utest/test/src/test/utest/examples/SeparateSetupTests.scala +++ b/utest/test/src/test/utest/examples/SeparateSetupTests.scala @@ -2,7 +2,7 @@ package test.utest.examples import utest._ -object SeparateSetupTests extends TestSuite{ +class SeparateSetupTests extends TestSuite{ val tests = Tests{ var x = 0 test("outer1"){ diff --git a/utest/test/src/test/utest/examples/SharedFixturesTests.scala b/utest/test/src/test/utest/examples/SharedFixturesTests.scala index 3567a9ce..ab377513 100644 --- a/utest/test/src/test/utest/examples/SharedFixturesTests.scala +++ b/utest/test/src/test/utest/examples/SharedFixturesTests.scala @@ -2,7 +2,7 @@ package test.utest.examples import utest._ -object SharedFixturesTests extends TestSuite{ +class SharedFixturesTests extends TestSuite{ var x = 0 val tests = Tests{ test("outer1"){ diff --git a/utest/test/src/test/utest/examples/TestPathTests.scala b/utest/test/src/test/utest/examples/TestPathTests.scala index 3a789d73..71b020a9 100644 --- a/utest/test/src/test/utest/examples/TestPathTests.scala +++ b/utest/test/src/test/utest/examples/TestPathTests.scala @@ -2,7 +2,7 @@ package test.utest.examples import utest._ -object TestPathTests extends TestSuite{ +class TestPathTests extends TestSuite{ val tests = Tests{ test("testPath"){ test("foo"){ From 5a19447bfc25f7a844cc8ac25bd44b721107fee6 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 28 Jul 2025 09:51:21 +0800 Subject: [PATCH 2/2] . --- .../utest/PortableScalaReflectExcerpts.scala | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/utest/src-3-jvm/utest/PortableScalaReflectExcerpts.scala b/utest/src-3-jvm/utest/PortableScalaReflectExcerpts.scala index 0f72e870..69cd857e 100644 --- a/utest/src-3-jvm/utest/PortableScalaReflectExcerpts.scala +++ b/utest/src-3-jvm/utest/PortableScalaReflectExcerpts.scala @@ -56,6 +56,25 @@ object PortableScalaReflectExcerpts { c(clazz) } + private def isInstantiatableClass(clazz: Class[_]): Boolean = { + /* A local class will have a non-null *enclosing* class, but a null + * *declaring* class. For a top-level class, both are null, and for an + * inner class (non-local), both are the same non-null class. + */ + def isLocalClass: Boolean = + clazz.getEnclosingClass() != clazz.getDeclaringClass() + + (clazz.getModifiers() & Modifier.ABSTRACT) == 0 && + clazz.getConstructors().length > 0 && + !isModuleClass(clazz) && + !isLocalClass + } + + def lookupInstantiatableClass(fqcn: String, + loader: ClassLoader): Option[InstantiatableClass] = { + load(fqcn, loader).filter(isInstantiatableClass).map(new InstantiatableClass(_)) + } + final class LoadableModuleClass private[PortableScalaReflectExcerpts] (val runtimeClass: Class[_]) { /** Loads the module instance and returns it. * @@ -76,4 +95,32 @@ object PortableScalaReflectExcerpts { } } } + + /** A wrapper for a class that can be instantiated. + * + * @param runtimeClass + * The `java.lang.Class[_]` representing the class. + */ + final class InstantiatableClass (val runtimeClass: Class[_]) { + + /** Instantiates this class using its zero-argument constructor. + * + * @throws java.lang.InstantiationException + * (caused by a `NoSuchMethodException`) + * If this class does not have a public zero-argument constructor. + */ + def newInstance(): Any = { + try { + runtimeClass.newInstance() + } catch { + case e: IllegalAccessException => + /* The constructor exists but is private; make it look like it does not + * exist at all. + */ + throw new InstantiationException(runtimeClass.getName).initCause( + new NoSuchMethodException(runtimeClass.getName + ".()")) + } + } + + } }