Skip to content

Commit d0c10a3

Browse files
authored
Allow classes to be used as test suite (#393)
Fixes #173 Probably should be the default way of defining test suites going forward, since `class`es are more hermetic than `object`s as by default any local members are not accessible outside the `class` whereas an `object`s local members are global Needed to vendor a bit more code fro https://github.com/portable-scala/portable-scala-reflect to make `lookupInstantiatableClass` work in Scala 3
1 parent f7a10e8 commit d0c10a3

28 files changed

+93
-36
lines changed

readme.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ package test.utest.examples
161161

162162
import utest._
163163

164-
object HelloTests extends TestSuite{
164+
class HelloTests extends TestSuite{
165165
val tests = Tests{
166166
test("test1"){
167167
throw new Exception("test1")
@@ -205,6 +205,7 @@ Tests: 3, Passed: 1, Failed: 2
205205
The tests are run one at a time, and any tests that fail with an exception have
206206
their stack trace printed. If the number of tests is large, a separate
207207
results-summary and failures-summary will be shown after all tests have run.
208+
Tests can either be inside zero-parameter `class`es (as shown above) or static `object`s.
208209

209210
Nesting Tests
210211
-------------
@@ -220,7 +221,7 @@ package test.utest.examples
220221

221222
import utest._
222223

223-
object NestedTests extends TestSuite{
224+
class NestedTests extends TestSuite{
224225
val tests = Tests{
225226
val x = 1
226227
test("outer1"){

utest/src-3-jvm/utest/PortableScalaReflectExcerpts.scala

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,25 @@ object PortableScalaReflectExcerpts {
5656
c(clazz)
5757
}
5858

59+
private def isInstantiatableClass(clazz: Class[_]): Boolean = {
60+
/* A local class will have a non-null *enclosing* class, but a null
61+
* *declaring* class. For a top-level class, both are null, and for an
62+
* inner class (non-local), both are the same non-null class.
63+
*/
64+
def isLocalClass: Boolean =
65+
clazz.getEnclosingClass() != clazz.getDeclaringClass()
66+
67+
(clazz.getModifiers() & Modifier.ABSTRACT) == 0 &&
68+
clazz.getConstructors().length > 0 &&
69+
!isModuleClass(clazz) &&
70+
!isLocalClass
71+
}
72+
73+
def lookupInstantiatableClass(fqcn: String,
74+
loader: ClassLoader): Option[InstantiatableClass] = {
75+
load(fqcn, loader).filter(isInstantiatableClass).map(new InstantiatableClass(_))
76+
}
77+
5978
final class LoadableModuleClass private[PortableScalaReflectExcerpts] (val runtimeClass: Class[_]) {
6079
/** Loads the module instance and returns it.
6180
*
@@ -76,4 +95,32 @@ object PortableScalaReflectExcerpts {
7695
}
7796
}
7897
}
98+
99+
/** A wrapper for a class that can be instantiated.
100+
*
101+
* @param runtimeClass
102+
* The `java.lang.Class[_]` representing the class.
103+
*/
104+
final class InstantiatableClass (val runtimeClass: Class[_]) {
105+
106+
/** Instantiates this class using its zero-argument constructor.
107+
*
108+
* @throws java.lang.InstantiationException
109+
* (caused by a `NoSuchMethodException`)
110+
* If this class does not have a public zero-argument constructor.
111+
*/
112+
def newInstance(): Any = {
113+
try {
114+
runtimeClass.newInstance()
115+
} catch {
116+
case e: IllegalAccessException =>
117+
/* The constructor exists but is private; make it look like it does not
118+
* exist at all.
119+
*/
120+
throw new InstantiationException(runtimeClass.getName).initCause(
121+
new NoSuchMethodException(runtimeClass.getName + ".<init>()"))
122+
}
123+
}
124+
125+
}
79126
}

utest/src-js/utest/PlatformShims.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ object PlatformShims {
2222
def loadModule(name: String, loader: ClassLoader): Any = {
2323
Reflect
2424
.lookupLoadableModuleClass(name + "$", loader)
25+
.map(_.loadModule())
26+
.orElse(Reflect.lookupInstantiatableClass(name, loader).map(_.newInstance()))
2527
.getOrElse(throw new ClassNotFoundException(name))
26-
.loadModule()
2728
}
2829
}

utest/src-jvm/utest/PlatformShims.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ object PlatformShims extends PlatformShimsVersionSpecific {
1212
def loadModule(name: String, loader: ClassLoader): Any =
1313
Reflect
1414
.lookupLoadableModuleClass(name + "$", loader)
15+
.map(_.loadModule())
16+
.orElse(Reflect.lookupInstantiatableClass(name, loader).map(_.newInstance()))
1517
.getOrElse(throw new ClassNotFoundException(name))
16-
.loadModule()
1718
}

utest/src-native/utest/PlatformShims.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ object PlatformShims {
1818
def loadModule(name: String, loader: ClassLoader): Any = {
1919
Reflect
2020
.lookupLoadableModuleClass(name + "$")
21+
.map(_.loadModule())
22+
.orElse(Reflect.lookupInstantiatableClass(name).map(_.newInstance()))
2123
.getOrElse(throw new ClassNotFoundException(name))
22-
.loadModule()
2324
}
2425
}

utest/src/utest/runner/Fingerprint.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,9 @@ object Fingerprint extends SubclassFingerprint {
88
def isModule() = true
99
def requireNoArgConstructor() = true
1010
}
11+
12+
object ClassFingerprint extends SubclassFingerprint {
13+
def superclassName() = "utest.TestSuite"
14+
def isModule() = false
15+
def requireNoArgConstructor() = true
16+
}

utest/src/utest/runner/Framework.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class Framework extends sbt.testing.Framework with framework.Formatter {
5656
def startHeader(path: String) = DefaultFormatters.renderBanner("Running Tests" + path)
5757

5858

59-
final def fingerprints(): Array[sbt.testing.Fingerprint] = Array(Fingerprint)
59+
final def fingerprints(): Array[sbt.testing.Fingerprint] = Array(Fingerprint, ClassFingerprint)
6060

6161
final def runner(args: Array[String],
6262
remoteArgs: Array[String],

utest/test/src/test/utest/AfterEachOnFailureTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import utest.framework.ExecutionContext.RunNow
66
/**
77
* Put executor.utestAfterEach(path) into finally block to make sure it will be executed regardless of the test failing.
88
*/
9-
object AfterEachOnFailureTest extends TestSuite {
9+
class AfterEachOnFailureTest extends TestSuite{
1010

1111
private var res:SomeResource = _
1212

utest/test/src/test/utest/AssertsTests.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import utest._
88
* since it is the thing that is meant to be *testing* all the fancy uTest
99
* asserts, we can't assume they work.
1010
*/
11-
object AssertsTests extends utest.TestSuite{
11+
class AssertsTests extends utest.TestSuite{
1212

1313
implicit val colors: shaded.pprint.TPrintColors = shaded.pprint.TPrintColors.Colors
1414
def tests = Tests{

utest/test/src/test/utest/BeforeAfterAllFailureTest.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import utest._
44
import utest.framework.StackMarker
55

66

7-
object BeforeAfterAllFailureTest extends TestSuite {
7+
class BeforeAfterAllFailureTest extends TestSuite{
88

99
// Hide this fella inside the outer object, because we don't want uTest's own
1010
// test suite to discover him: we want to run him manually
11-
object AfterAllFailureTest extends TestSuite {
11+
object AfterAllFailureTest extends TestSuite{
1212

1313
override def utestAfterAll(): Unit = {
1414
throw new Exception("Failed After!")
@@ -26,7 +26,7 @@ object BeforeAfterAllFailureTest extends TestSuite {
2626

2727
// No tests for this fella because currently, error handling of test suite
2828
// initialization is paid done in the SBT logic, not in TestRunner
29-
object BeforeAllFailureTest extends TestSuite {
29+
class BeforeAllFailureTest extends TestSuite{
3030
throw new Exception("Failed Before!")
3131
val tests = Tests {
3232
test("test"){

0 commit comments

Comments
 (0)